Sie holen sich das neueste main per Pull, und Git bricht ab mit CONFLICT (content): Merge conflict in checkout.js. Sie lösen den Konflikt, und im Log steht jetzt ein Commit namens Merge branch 'main' into feature/checkout, oben auf den drei Commits, die Sie tatsächlich geschrieben haben. Vier Commits, eine einzige echte Änderung. Ein Kollege, der diese Historie nächsten Monat liest, kann nicht erkennen, welcher welcher ist.
git merge und git rebase beantworten dieselbe Frage: Wie bringe ich meinen Branch auf den Stand der Änderungen, die anderswo gemacht wurden. Sie hinterlassen sehr unterschiedliche Spuren, und die falsche Wahl auf einem geteilten Branch richtet echten Schaden an.
Was merge und rebase im Commit-Graphen unterschiedlich machen
Angenommen, main hat die Commits D, E, F bekommen, während Sie an feature gearbeitet haben, das Ihre eigenen Commits A, B, C hat, abgezweigt von der alten Spitze von main:
| |
git merge main, ausgeführt von feature aus, erzeugt einen neuen Commit mit zwei Eltern: der Spitze von feature und der Spitze von main. An A, B oder C ändert sich nichts. Gleiche Hashes, gleiche Eltern, gleiche Stelle im Graphen:
| |
git rebase main, ebenfalls von feature aus ausgefĂĽhrt, macht das Gegenteil. Es lässt main unangetastet und spielt A, B und C einzeln oben auf F ab, wobei fĂĽr jeden ein neuer Commit entsteht (A’, B’, C’) mit neuen Hashes und neuen Eltern:
| |
Das Ergebnis sieht genau so aus, als hätten Sie von Anfang an von F abgezweigt und sich nie von main entfernt. Genau das ist der Reiz: eine lineare Historie, keine Merge-Commits im Log. Der Preis ist, dass A, B und C fĂĽr Git nicht mehr existieren. A’, B’ und C’ ersetzen sie, und jeder, der A, B oder C bereits per Pull geholt hatte, sitzt jetzt auf Commits, die nicht mehr zu Ihren passen.
Was ein Fast-Forward-Merge ist und wann Git ihn statt eines Merge-Commits nutzt
Ein Merge braucht nur dann einen Merge-Commit, wenn sich beide Branches bewegt haben. Wenn feature von F abgezweigt ist und main sich seitdem nicht bewegt hat, gibt es beim Zusammenführen von feature in main nichts zu vereinen: Git schiebt den main-Zeiger einfach bis C vor und stoppt. Das ist ein Fast-Forward, und es hinterlässt dieselbe flache Historie wie ein Rebase:
| |
Git erzeugt nur dann einen Merge-Commit, wenn beide Seiten Commits hinzugefügt haben, die die andere nicht hat. Manche Teams erzwingen trotzdem einen, damit jeder Feature-Branch eine sichtbare Markierung in main hinterlässt, an der Stelle, an der er integriert wurde:
| |
Wie Sie Ihre Commits mit interaktivem Rebase aufräumen
Rebase kann auch Ihre eigene Historie bearbeiten, ganz ohne einen anderen Branch. git rebase -i öffnet Ihre letzten N Commits in einem Editor, eine Zeile pro Commit:
| |
| |
Ändern Sie das Wort am Zeilenanfang, und Git handelt entsprechend, sobald das Rebase läuft:
| Befehl | Wirkung |
|---|---|
pick | Behält den Commit unverändert |
reword | Behält den Commit, ändert seine Nachricht |
squash | FĂĽgt ihn in den vorherigen Commit ein, kombiniert beide Nachrichten |
fixup | FĂĽgt ihn in den vorherigen Commit ein, verwirft diese Nachricht |
drop | Entfernt den Commit vollständig |
edit | Pausiert hier, damit Sie den Commit von Hand anpassen können |
Machen Sie aus dieser Liste squash e4f5a6b fix typo und fixup c7d8e9f WIP: still debugging, und Sie erhalten einen einzigen Commit, Add checkout validation, statt drei, die nur in der Reihenfolge Sinn ergeben, in der Sie sie geschrieben haben. Tun Sie das, bevor Sie den Pull Request öffnen, nicht nachdem jemand Ihren Branch schon per Pull geholt hat — der Squash schreibt den Hash jedes nachfolgenden Commits um, genau wie jedes andere Rebase.
Wenn Sie frĂĽhere Commits laufend korrigieren, schieben git commit --fixup <hash> und das anschlieĂźende git rebase -i --autosquash <base> jeden Fixup automatisch neben sein Ziel, sodass Sie die Todo-Liste nie von Hand umsortieren mĂĽssen.
Wie git pull --rebase Ihren Branch flach hält
Dieselbe Wahl taucht bei jedem Pull wieder auf. Ein einfaches git pull, auf einem Branch mit lokalen Commits, während das Remote weitergezogen ist, macht einen Fetch gefolgt von einem Merge. So bekommen Sie einen Merge branch 'main' into feature/checkout-Commit aus keinem anderen Grund als schlechtem Timing. git pull --rebase holt dieselben Commits und spielt Ihre lokalen oben drauf ab:
| |
Bei nicht committeten Änderungen weigert sich das Rebase zu starten. Fügen Sie --autostash hinzu, und Git stasht sie, macht das Rebase und wendet sie danach wieder an:
| |
Stellen Sie das einmal ein, statt das Flag jedes Mal zu tippen, pro Branch oder global:
| |
Das ist sicher, weil es nur Commits umschreibt, die ausschlieĂźlich auf Ihrem lokalen Branch existieren. Sobald das nicht mehr stimmt, ist Rebase nicht mehr sicher.
Die goldene Regel: rebasen Sie niemals Commits, die jemand anderes schon per Pull geholt hat
Rebase schreibt Commit-Hashes um. Wenn Sie einen Branch rebasen, dessen alte Commits jemand anderes bereits hat (er hat Ihren Branch per Pull geholt, oder es ist main, oder irgendetwas mit mehr als einem Mitwirkenden), sind seine Historie und Ihre sich jetzt uneinig darĂĽber, was dieser Branch ist. Git hat keine Möglichkeit zu wissen, dass Ihr A und sein A’ dieselbe logische Ă„nderung sind. Es sieht zwei unabhängige Commits, und der nächste Merge zwischen den beiden Kopien verdoppelt alles, was auseinandergelaufen ist.
Die Regel, die das verhindert: rebasen Sie nur Branches, auf denen sonst noch niemand aufgebaut hat. Ihr eigener Feature-Branch, bevor Sie jemandem einen Hash gegeben haben, auf dem er aufbauen kann, ist freies Terrain. Ein Branch, den Sie schon gepusht haben und auf den Sie Kollegen verwiesen haben, main, jeder langlebige geteilte Branch: Die werden gemerged, niemals rebased.
Einen bereits gepushten Branch zu rebasen erfordert einen erzwungenen Push, damit das Remote die umgeschriebene Historie akzeptiert:
| |
--force-with-lease verweigert den Push, wenn das Remote Commits hat, die Sie noch nicht per Fetch geholt haben — genau die Situation, in der jemand auf der alten Historie aufgebaut hat und ein nacktes --force seine Arbeit stillschweigend verwerfen würde. Nie ein nacktes --force, und nur auf einem Branch, bei dem Sie sicher sind, dass er allein Ihnen gehört.
Wie Sie sich von einem missglĂĽckten Rebase mit git reflog erholen
Ein Rebase auf der falschen Basis, oder ein interaktives Rebase, bei dem Sie den falschen Commit gedroppt haben, fĂĽhlt sich unwiederbringlich an: Die Commits, die Sie vor einer Minute noch hatten, tauchen in git log nicht mehr auf. Sie sind nicht verschwunden. git reflog protokolliert jede Position, auf die HEAD auf Ihrer Maschine gezeigt hat, Rebases eingeschlossen:
| |
| |
HEAD@{3} ist die Stelle, auf die Ihr Branch unmittelbar vor Beginn des Rebase gezeigt hat. Setzen Sie darauf zurĂĽck, und Sie sind wieder genau im vorherigen Zustand, gedroppte Commits eingeschlossen:
| |
Reflog-Einträge verfallen standardmäßig nach 90 Tagen, oder nach 30 Tagen bei Commits, auf die keine andere Referenz mehr zeigt. Es ist ein Sicherheitsnetz für die letzte missglückte Stunde, kein dauerhafter Speicher. Es ist außerdem rein lokal: Es wird nie gepusht und kann eine Umschreibung, die jemand anderes bereits per Pull geholt hat, nicht rückgängig machen.
Derselbe Instinkt (nach einer missglückten Änderung zu einem bekannten, funktionierenden Zustand zurückzukehren) gilt auch außerhalb der Git-Historie. Ein Kubernetes-Deployment führt aus demselben Grund seine eigene Rollout-Historie, und kubectl rollout undo macht für ein missglücktes Deployment das, was git reset --hard gegen einen Reflog-Eintrag für ein missglücktes Rebase macht.
Git rebase vs merge: was Sie wann nutzen
| Situation | Nutzen Sie |
|---|---|
Ihren lokalen, noch nicht gepushten Branch auf den Stand von main bringen | rebase (oder pull --rebase) |
| Ihre eigenen Commits aufräumen, bevor Sie einen Pull Request öffnen | rebase -i |
Einen fertigen Feature-Branch in main integrieren | merge (mit oder ohne --no-ff, je nach Konvention Ihres Teams) |
Der Branch ist geteilt, gepusht, oder ist main selbst | merge — niemals rebasen |
Sie müssen genau wissen, wann ein Feature in main gelandet ist | merge --no-ff — der Merge-Commit markiert es |
| Sie wollen eine lineare Historie ohne Merge-Commits im Log | rebase auf Ihrer Seite, bevor Sie den Pull Request öffnen |
Die Entscheidung hängt eigentlich davon ab, wer diese Commits schon gesehen hat. Nur Sie? Rebase, und halten Sie die Historie sauber. Mindestens eine weitere Person? Merge. Ein etwas unordentlicheres Log kostet Sie nichts; eine umgeschriebene geteilte Historie kostet jeden, der sie per Pull geholt hat, einen ganzen Nachmittag.
Wenn Ihr Team noch keine Konvention hat, fangen Sie hier an: pull.rebase = true lokal, rebase -i, um Ihre Commits vor dem Öffnen des Pull Requests aufzuräumen, dann ein einfacher Merge, um sie in main zu bringen. Damit bekommen Sie eine saubere Historie pro Branch, ohne je einen Commit umzuschreiben, von dem jemand anderes abhängt, CI eingeschlossen. Ein Workflow wie Docker-Image mit GitHub Actions bauen und veröffentlichen taggt jeden Build mit dem Commit-SHA, und diese SHAs müssen weiterhin existieren, nachdem die Pipeline gegen sie gelaufen ist. Setzen Sie git config --global pull.rebase true noch heute: Allein das entfernt die meisten zufälligen Merge-Commits aus Ihrem Log.