GitHub Stacked Pull Requests in Public Preview: ein Praxistest mit gh-stack

GitHub bietet native Stacked Pull Requests jetzt in Public Preview an. Ein Test mit der CLI-Extension gh-stack, plus was die Technik im Alltag wirklich bringt.

8 Min. Lesezeit

Ein grosser Pull Request ist für den Autor bequem und für den Reviewer eine Zumutung. Wer eine Funktion in einem Rutsch durchzieht, Datenmodell, API und UI in einem Commit-Berg, zwingt den Reviewer dazu, alles gleichzeitig zu verstehen, bevor er überhaupt ein sinnvolles Review abgeben kann.

Die naheliegende Lösung ist, die Änderung in mehrere kleine Pull Requests aufzuteilen, die aufeinander aufbauen. Genau das nennt man Stacked Pull Requests: PR 2 basiert auf PR 1, PR 3 auf PR 2, und so weiter. Jede Ebene lässt sich einzeln reviewen, ohne auf den Rest zu warten. Das Problem war bisher die Umsetzung. Ohne Werkzeugunterstützung bedeutet ein Stack ständiges manuelles Rebasing, falsche Base-Branches in der PR-Oberfläche und Merge-Reihenfolgen, die man sich selbst merken muss.

Tools wie Graphite, git-spice oder Sapling lösen das seit Jahren, aber als externe Schicht über GitHub. Seit dem 30. Juli 2026 bietet GitHub das nativ an: Stacked Pull Requests sind in Public Preview und rollen über die kommenden Tage für alle Repositories aus. Ich habe das anhand eines echten kleinen Projekts getestet, mit vier Pull Requests, die tatsächlich aufeinander aufgebaut und gemergt wurden.

Was Stacks im Alltag lösen

Der offensichtliche Vorteil ist die Diff-Grösse. Interessanter sind die zwei Effekte dahinter.

Der erste ist, dass der Autor nicht mehr blockiert ist. Ohne Stack hat man nach dem Öffnen eines PR zwei Möglichkeiten: warten, bis das Review durch ist, oder direkt weiterarbeiten und die neue Arbeit in denselben PR schieben, der dadurch wächst. Beides ist schlecht. Mit einem Stack legt man die nächste Ebene obendrauf und arbeitet weiter, während unten noch reviewt wird. Die Review-Latenz des Teams bestimmt nicht mehr das eigene Arbeitstempo.

Der zweite Effekt betrifft die Fehlersuche. Wenn eine Änderung in vier thematisch getrennten Commits auf main landet statt in einem grossen, lässt sich ein Fehlverhalten deutlich präziser zuordnen und auch gezielt zurücknehmen. Ein Revert trifft dann die Ebene, die den Fehler enthält, nicht die gesamte Funktion.

Dazu kommt ein organisatorischer Punkt, der oft untergeht: Verschiedene Ebenen brauchen oft verschiedene Reviewer. Die Datenbankmigration will jemand anderes sehen als die UI-Komponente. In einem einzelnen grossen PR müssen beide durch den kompletten Diff scrollen und hoffen, dass sie ihren Teil finden. In einem Stack bekommt jeder genau die Ebene, für die er zuständig ist, und beide können gleichzeitig arbeiten.

Bei der Wirkung auf die Review-Qualität lohnt sich Ehrlichkeit. Die verbreitete Empfehlung, einen PR unter ein paar hundert Zeilen zu halten, stammt aus der allgemeinen Code-Review-Literatur und nicht aus Messungen zu Stacks. Die Übersicht bei Awesome Code Reviews weist darauf hin, dass Studien zur Zerlegung von Änderungen zwar weniger falsch positive Befunde zeigten, aber nicht durchgängig mehr gefundene Defekte oder schnellere Reviews. Der belastbare Vorteil ist also weniger „bessere Reviews” als vielmehr weniger Blockade und bessere Zuordnung. Das ist immer noch viel wert, aber es ist eine andere Behauptung.

Die Extension gh-stack

Stacks lassen sich laut GitHub direkt über github.com, die mobile App oder Coding Agents wie Copilot bedienen. Getestet habe ich den Weg, der sich am ehesten in einen bestehenden Git-Workflow einfügt: die CLI-Extension.

gh extension install github/gh-stack

gh stack bringt eine eigene Befehlsgruppe mit: init legt einen neuen Stack an, add hängt eine weitere Ebene an, submit pusht alle Branches und erzeugt oder aktualisiert die zugehörigen Pull Requests, sync gleicht den lokalen Stand mit GitHub ab, merge führt den atomaren Merge aus.

Ein Stack mit vier Ebenen

Für den Test habe ich ein winziges Python-Projekt gebaut, wordstats, das Worthäufigkeiten in einer Textdatei zählt. Die Funktionalität wurde bewusst in vier Schichten zerlegt, jede auf der vorherigen aufbauend:

  1. Kernlogik: count_words() zählt Wörter mit einer regulären Textnormalisierung.
  2. CLI: ein argparse-Wrapper, der die Kernlogik über die Kommandozeile aufrufbar macht.
  3. Ein --top-Flag, das die Ausgabe auf die N häufigsten Wörter begrenzt.
  4. Tests für Kernlogik und CLI.

Der Stack entsteht mit init und wächst danach mit add:

gh stack init 01-core-counting
# ... core.py schreiben ...
gh stack add -Am "Core: Worthaeufigkeit zaehlen"

# ... cli.py schreiben ...
gh stack add -Am "CLI: wordstats <datei> gibt Worthaeufigkeiten aus" 02-cli-interface

# ... --top-Flag ergaenzen ...
gh stack add -Am "CLI: --top begrenzt die Ausgabe auf die N haeufigsten Woerter" 03-top-n-flag

# ... Tests schreiben ...
gh stack add -Am "Tests: core und CLI" 04-tests

Eine kleine Eigenheit dabei: Beim ersten add meldete gh stack, dass der Branch 01-core-counting noch keine Commits hatte, und hängte den Commit direkt dort an, statt einen neuen Branch zu erzeugen. Das ist kein Fehler, sondern der erwartete Fall bei init mit sofort folgendem add, aber die Meldung sollte man lesen, statt sie wegzuklicken.

gh stack view zeigt danach den lokalen Zustand als Baum:

● 04-tests (current)
│ cad75de · Tests: core und CLI

○ 03-top-n-flag
│ 31c657d · CLI: --top begrenzt die Ausgabe auf die N haeufigsten Woerter

○ 02-cli-interface
│ 3e4ab48 · CLI: wordstats <datei> gibt Worthaeufigkeiten aus

○ 01-core-counting
│ cc9e8db · Core: Worthaeufigkeit zaehlen

└ main

gh stack submit --auto --open pusht alle vier Branches und legt vier Pull Requests an, deren Base-Branch jeweils die vorherige Ebene ist, nicht main:

✓ Created PR #1 for 01-core-counting
✓ Created PR #2 for 02-cli-interface
✓ Created PR #3 for 03-top-n-flag
✓ Created PR #4 for 04-tests
✓ Stack created on GitHub with 4 PRs (stack #5)

Review-Isolation und die Stack-Map

Der eigentliche Nutzen zeigt sich beim Öffnen der einzelnen Pull Requests. PR #2 zeigt in der Kopfzeile ein Badge 2/4 und die Zeile dprinz wants to merge 1 commit into 01-core-counting from 02-cli-interface. Der Diff der PR beschränkt sich auf die CLI-Datei, obwohl inhaltlich schon die Kernlogik aus PR #1 mit im Branch steckt. GitHub zeigt nur die Differenz zur darunterliegenden Ebene an, nicht die kumulierte Änderung seit main.

Für den Reviewer bedeutet das: Wer PR #2 begutachtet, sieht ausschliesslich den CLI-Code, nicht noch einmal die bereits in PR #1 geprüfte Kernlogik. Mehrere Reviewer können unterschiedliche Ebenen gleichzeitig bearbeiten, ohne sich gegenseitig zu blockieren.

Partial Merge und automatisches Retargeting

Der Teil, der ohne native Unterstützung am meisten manuelle Arbeit verursacht, ist das Nachziehen der Base-Branches nach einem Merge. Ich habe das gezielt getestet, indem ich nur die unteren zwei Ebenen gemergt habe:

gh stack merge 2 --yes --squash
Merging #1, #2 into main via squash...
✓ Merged #1, #2 into main (a83468b)

PR #3 zeigte vor diesem Merge 03-top-n-flag gegen 02-cli-interface als Base. Direkt danach, ohne weiteres Zutun:

{"baseRefName": "main", "headRefName": "03-top-n-flag", "state": "OPEN"}

Der Base-Branch von PR #3 wurde automatisch auf main umgehängt. Ein gh stack sync rebast danach den lokalen Stand entsprechend:

✓ Rebased 03-top-n-flag onto main (adjusted for merged PR)
✓ Rebased 04-tests onto 03-top-n-flag (adjusted for merged PR)

Der Zusatz adjusted for merged PR ist wichtiger, als er aussieht. Genau an dieser Stelle liegt die klassische Falle beim manuellen Stapeln: Landet der untere PR als Squash-Commit auf main, sieht Git im Kind-Branch dieselben Änderungen als scheinbaren Konflikt wieder, obwohl sie inhaltlich schon drin sind. Dave Pacheco hat genau diese Prozedur beschrieben und braucht dafür von Hand drei aufeinanderfolgende Merges, unter anderem ein git merge -X ours auf den Squash-Commit. In meinem Test hat gh stack sync das nach einem Squash-Merge selbst aufgelöst.

Den Rest des Stacks konnte ich anschliessend in einem einzigen Befehl mergen:

gh stack merge 4 --yes --squash
Merging #3, #4 into main via squash...
✓ Merged #3, #4 into main (cc7bd1a)

Beide merge-Aufrufe sind laut Dokumentation atomar: Entweder werden alle eingeschlossenen Pull Requests gemergt oder keiner. Bestehende Branch Protections und Required Checks greifen dabei unverändert, gh stack merge umgeht sie nicht.

Einen Stack sinnvoll schneiden

Das Werkzeug nimmt einem die mechanische Arbeit ab, nicht die Entwurfsentscheidung. Ein schlecht geschnittener Stack ist mühsamer als ein einzelner grosser PR, weil der Reviewer dann Kontext über mehrere PRs hinweg zusammensuchen muss.

Der wichtigste Punkt: Nur stapeln, was tatsächlich voneinander abhängt. Zwei unabhängige Änderungen gehören in zwei unabhängige Pull Requests gegen main, nicht in einen Stack. Ein Stack drückt eine Reihenfolge aus. Wenn die Reihenfolge willkürlich ist, erzeugt man künstliche Abhängigkeiten und blockiert die obere Ebene ohne Grund.

Bewährt hat sich der Schnitt entlang der Architektur statt entlang der Dateien: erst das Datenmodell, dann die Geschäftslogik, dann die Schnittstelle nach aussen. Genau das ist im Beispiel oben passiert. Jede Ebene sollte für sich genommen einen sinnvollen Zustand herstellen und idealerweise für sich mergebar bleiben. Ein guter Test ist, ob man den Titel der Ebene in einem Satz sagen kann, ohne „und” zu benutzen.

Die Tiefe sollte man dabei im Rahmen halten. Drei bis fünf Ebenen sind gut überschaubar. Bei zehn Ebenen wird die Reihenfolge zum eigentlichen Problem, und die oberste Ebene wartet potenziell auf vier Reviews unter sich. Wenn ein Stack so tief wird, ist das meistens ein Hinweis darauf, dass unten ein eigenständiges Stück steckt, das früher hätte landen sollen.

Praktisch hilft ausserdem, den Stack früh zu pushen, auch als Draft. Die Reviewer sehen dann bereits die Struktur der geplanten Änderung, bevor die oberen Ebenen fertig sind. Das ist oft der Punkt, an dem sich ein falscher Schnitt noch billig korrigieren lässt, mit gh stack modify statt mit einem umgebauten Feature.

Und Ebenen, die schon reviewt und mergefähig sind, sollte man mergen, statt auf den kompletten Stack zu warten. Das ist der ganze Sinn der Übung. Ein Stack, der geschlossen als Block landet, hat gegenüber einem grossen PR nur noch den Vorteil der besseren Commit-Historie.

Grenzen der Preview

Ein paar Punkte relativieren den Komfort:

  • Merge-Queue-Unterstützung für Stacks rollt laut Changelog erst schrittweise in den folgenden Wochen aus. Wer bereits eine Merge Queue einsetzt, sollte vor dem produktiven Einsatz prüfen, ob sie mit gestapelten PRs im eigenen Repository schon zusammenspielt.
  • Es handelt sich um eine Public Preview. Verhalten und CLI-Kommandos können sich bis zur allgemeinen Verfügbarkeit noch ändern.
  • Getestet habe ich ausschliesslich den CLI-Weg über gh stack. Die Bedienung direkt über die GitHub-Weboberfläche oder die mobile App habe ich nicht geprüft.
  • Ein Stack bleibt an Git-Branches gebunden. Wer zwischendrin manuell mit git rebase oder git push --force in die Branches eingreift, riskiert eine Abweichung zwischen dem, was gh stack erwartet, und dem tatsächlichen Zustand auf GitHub. gh stack sync fängt das in der Praxis gut auf, ersetzt aber kein Verständnis dafür, was gerade im eigenen Stack passiert.

Der Einwand, der auch mit perfekter Werkzeugunterstützung bestehen bleibt, ist ein sozialer: Ein Stack ist für Aussenstehende schwerer zu lesen als ein einzelner PR. Wer nicht am Thema arbeitet und PR #3 von vier zugewiesen bekommt, muss erst verstehen, wo dieses Stück im Gesamtbild sitzt. Die Stack-Map hilft dabei, nimmt einem die Einordnung aber nicht ab. In einem Repository mit vielen unbeteiligten Mitlesern kostet das Aufmerksamkeit.

Ergebnis

Der Stack aus vier Pull Requests hat sich in der Praxis wie im Changelog beschrieben verhalten: eigenständige Diffs pro Ebene, ein sichtbarer Stack-Status in der PR-Oberfläche, automatisches Retargeting nach einem Teil-Merge und ein atomarer Merge über mehrere PRs hinweg. Nichts davon war vorher mit reinem Git unmöglich, aber es bedeutete disziplinierte manuelle Arbeit oder ein externes Tool.

Der grösste Unterschied zu einer manuell gepflegten Kette aus Feature-Branches ist nicht die Existenz des Stacks selbst, sondern dass GitHub sie kennt. Der Reviewer sieht sofort, wie viele Ebenen es gibt und wo eine bestimmte PR darin steht, ohne das erst aus Base-Branch und PR-Beschreibung zusammenzusuchen. Für kleinere Teams ohne Graphite-Lizenz ist das ein spürbarer Sprung, ohne dass ein zusätzliches Tool in den Workflow muss.

Meine Einschätzung nach dem Test: Der Nutzen steht und fällt damit, ob im Team tatsächlich zügig reviewt wird. Stacks nehmen einem das Warten auf Reviews als Blocker, sie ersetzen aber keine Review-Kultur. Wo PRs ohnehin tagelang liegen, wird aus einem Stack schnell eine tiefe Kette wartender Änderungen, und das ist unangenehmer als ein einzelner wartender PR.

Wer selbst testen will: Das komplette Beispielprojekt inklusive aller vier gemergten Pull Requests liegt unter dprinz/stacked-pull-requests.