473 lines
10 KiB
Markdown
473 lines
10 KiB
Markdown
# Spickzettel: Gestapelten Feature-Branch nach einem Squash-Merge aktualisieren
|
|
|
|
Ausgangssituation:
|
|
|
|
```text
|
|
main: A----------------S
|
|
\ /
|
|
feature1: B------------C
|
|
\
|
|
feature2: D---E
|
|
```
|
|
|
|
- `feature1` wurde von `main` abgezweigt.
|
|
- `feature2` wurde von `feature1` abgezweigt.
|
|
- `feature1` wurde anschließend per **Squash-Merge** in `main` integriert.
|
|
- `feature2` enthält sowohl die Änderungen von `feature1` als auch eigene Änderungen.
|
|
- Jetzt soll `feature2` auf dem aktuellen `main` basieren und nur noch seine eigenen Änderungen zusätzlich enthalten.
|
|
|
|
Ziel:
|
|
|
|
```text
|
|
main: A----------------S
|
|
\
|
|
feature2: D'---E'
|
|
```
|
|
|
|
Die Commits `D` und `E` werden dabei als neue Commits `D'` und `E'` auf die neue Basis gesetzt.
|
|
|
|
---
|
|
|
|
## 1. Arbeitsstand prüfen
|
|
|
|
Zuerst sicherstellen, dass keine ungesicherten Änderungen vorhanden sind:
|
|
|
|
```bash
|
|
git status
|
|
```
|
|
|
|
Idealerweise lautet die Ausgabe sinngemäß:
|
|
|
|
```text
|
|
nothing to commit, working tree clean
|
|
```
|
|
|
|
Falls noch Änderungen vorhanden sind, entweder committen oder vorübergehend weglegen:
|
|
|
|
```bash
|
|
git stash push -m "temporäre Änderungen vor Rebase"
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Remote-Branches aktualisieren
|
|
|
|
```bash
|
|
git fetch origin
|
|
```
|
|
|
|
Damit werden beispielsweise diese Referenzen aktualisiert:
|
|
|
|
```text
|
|
origin/main
|
|
origin/feature1
|
|
origin/feature2
|
|
```
|
|
|
|
Wichtig: `origin/main` ist der aktuelle Stand des Remote-Branches. Er enthält jetzt den Squash-Commit von `feature1`.
|
|
|
|
---
|
|
|
|
## 3. Prüfen, auf welchem Branch man arbeitet
|
|
|
|
```bash
|
|
git switch feature2
|
|
```
|
|
|
|
Anschließend:
|
|
|
|
```bash
|
|
git status
|
|
```
|
|
|
|
Es sollte erkennbar sein, dass man sich auf `feature2` befindet.
|
|
|
|
Optional kann man sich die Historie grafisch ansehen:
|
|
|
|
```bash
|
|
git log --oneline --graph --decorate --all
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Den alten Basis-Commit identifizieren
|
|
|
|
Für den Rebase muss Git wissen:
|
|
|
|
> Ab welchem Commit beginnen die Änderungen, die ausschließlich zu `feature2` gehören?
|
|
|
|
Wenn der Branch `feature1` noch existiert und unverändert auf seinem ursprünglichen Stand steht, ist der Standardfall:
|
|
|
|
```bash
|
|
git rebase --onto origin/main origin/feature1 feature2
|
|
```
|
|
|
|
Das bedeutet:
|
|
|
|
```text
|
|
git rebase --onto <neue-Basis> <alte-Basis> <Branch>
|
|
```
|
|
|
|
Konkret:
|
|
|
|
- `origin/main`: neue Basis
|
|
- `origin/feature1`: alte Basis, die nicht erneut übernommen werden soll
|
|
- `feature2`: Branch, dessen eigene Commits neu aufgesetzt werden
|
|
|
|
Git nimmt also alle Commits, die in `feature2`, aber nicht in `feature1` enthalten sind, und wendet sie auf `origin/main` an.
|
|
|
|
Das ist in der Regel genau das gewünschte Verhalten.
|
|
|
|
---
|
|
|
|
## 5. Vorher ein Sicherheitsbackup anlegen
|
|
|
|
Ein Rebase schreibt die Branch-Historie um. Deshalb ist ein Backup empfehlenswert:
|
|
|
|
```bash
|
|
git switch feature2
|
|
git branch backup/feature2-vor-rebase
|
|
```
|
|
|
|
Falls etwas schiefgeht, kann man jederzeit auf diesen Stand zurückgreifen:
|
|
|
|
```bash
|
|
git switch backup/feature2-vor-rebase
|
|
```
|
|
|
|
Oder den ursprünglichen Stand wiederherstellen:
|
|
|
|
```bash
|
|
git switch feature2
|
|
git reset --hard backup/feature2-vor-rebase
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Rebase durchführen
|
|
|
|
Mit weiterhin ausgechecktem `feature2`:
|
|
|
|
```bash
|
|
git rebase --onto origin/main origin/feature1
|
|
```
|
|
|
|
Die Kurzform funktioniert hier, weil Git den aktuell ausgecheckten Branch verwendet.
|
|
|
|
Alternativ explizit:
|
|
|
|
```bash
|
|
git rebase --onto origin/main origin/feature1 feature2
|
|
```
|
|
|
|
Nach dem erfolgreichen Rebase sieht die Historie ungefähr so aus:
|
|
|
|
```text
|
|
main: A----------------S
|
|
\
|
|
feature2: D'---E'
|
|
```
|
|
|
|
Die Änderungen aus `feature1` werden nicht noch einmal als eigene Commits übernommen. Sie sind bereits im Squash-Commit auf `main` enthalten.
|
|
|
|
---
|
|
|
|
# Falls Konflikte auftreten
|
|
|
|
Konflikte sind bei diesem Vorgang möglich, insbesondere wenn:
|
|
|
|
- der Squash-Merge in GitLab manuell angepasst wurde,
|
|
- `feature2` dieselben Dateien wie `feature1` weiter verändert,
|
|
- sich `main` seit dem Merge weiterentwickelt hat.
|
|
|
|
Git zeigt die betroffenen Dateien an:
|
|
|
|
```bash
|
|
git status
|
|
```
|
|
|
|
Die Konfliktmarkierungen sehen beispielsweise so aus:
|
|
|
|
```text
|
|
<<<<<<< HEAD
|
|
Stand von main
|
|
=======
|
|
Stand aus feature2
|
|
>>>>>>> <commit>
|
|
```
|
|
|
|
Die Datei muss manuell in den gewünschten Endzustand gebracht werden. Danach:
|
|
|
|
```bash
|
|
git add pfad/zur/datei
|
|
```
|
|
|
|
Wenn alle Konflikte des aktuellen Rebase-Schritts gelöst sind:
|
|
|
|
```bash
|
|
git rebase --continue
|
|
```
|
|
|
|
Git kann anschließend weitere Konflikte melden. Dann wiederholt man:
|
|
|
|
```bash
|
|
git status
|
|
# Dateien bearbeiten
|
|
git add <dateien>
|
|
git rebase --continue
|
|
```
|
|
|
|
## Einen Rebase abbrechen
|
|
|
|
Wenn der Rebase nicht sinnvoll fortgesetzt werden kann:
|
|
|
|
```bash
|
|
git rebase --abort
|
|
```
|
|
|
|
Damit wird `feature2` auf den Zustand vor Beginn des Rebases zurückgesetzt.
|
|
|
|
---
|
|
|
|
# 7. Ergebnis überprüfen
|
|
|
|
Nach dem Rebase zunächst die Historie ansehen:
|
|
|
|
```bash
|
|
git log --oneline --graph --decorate -20
|
|
```
|
|
|
|
Außerdem prüfen, welche Änderungen `feature2` gegenüber `main` enthält:
|
|
|
|
```bash
|
|
git diff origin/main...feature2
|
|
```
|
|
|
|
Das sollte im Wesentlichen nur die Änderungen des zweiten Features zeigen.
|
|
|
|
Auch die Commit-Liste kann geprüft werden:
|
|
|
|
```bash
|
|
git log --oneline origin/main..feature2
|
|
```
|
|
|
|
Dort sollten nur die eigenen Commits von `feature2` auftauchen, nicht mehr die ursprünglichen Commits von `feature1`.
|
|
|
|
Zusätzlich:
|
|
|
|
```bash
|
|
git status
|
|
```
|
|
|
|
---
|
|
|
|
# 8. Den umgeschriebenen Branch pushen
|
|
|
|
Da ein Rebase die Commit-IDs ändert, ist ein normaler Push meist nicht möglich. Verwenden Sie:
|
|
|
|
```bash
|
|
git push --force-with-lease origin feature2
|
|
```
|
|
|
|
Nicht bevorzugt:
|
|
|
|
```bash
|
|
git push --force origin feature2
|
|
```
|
|
|
|
`--force-with-lease` ist sicherer, weil Git den Push ablehnt, wenn zwischenzeitlich jemand anderes neue Änderungen auf `origin/feature2` veröffentlicht hat.
|
|
|
|
Danach sollte der Merge Request von `feature2` nach `main` aktualisiert sein. Im Diff sollten nun normalerweise nur noch die Änderungen des zweiten Features erscheinen.
|
|
|
|
---
|
|
|
|
# Der wichtigste Standardbefehl
|
|
|
|
Wenn `feature1` noch existiert und auf dem alten Stand steht:
|
|
|
|
```bash
|
|
git fetch origin
|
|
git switch feature2
|
|
git branch backup/feature2-vor-rebase
|
|
git rebase --onto origin/main origin/feature1
|
|
git push --force-with-lease origin feature2
|
|
```
|
|
|
|
Dabei sollte man den Rebase-Schritt nicht blind überspringen, sondern Konflikte und den anschließenden Diff prüfen.
|
|
|
|
---
|
|
|
|
# Wenn `feature1` bereits gelöscht wurde
|
|
|
|
Wenn der Remote-Branch `feature1` nach dem Merge gelöscht wurde, funktioniert dieser Befehl nicht mehr:
|
|
|
|
```bash
|
|
git rebase --onto origin/main origin/feature1
|
|
```
|
|
|
|
Dann braucht man den alten Stand von `feature1`, also den Commit, auf dem `feature2` ursprünglich basierte.
|
|
|
|
## Möglichkeit A: Lokale Referenz vorhanden
|
|
|
|
Prüfen:
|
|
|
|
```bash
|
|
git branch -a
|
|
```
|
|
|
|
Vielleicht existiert noch ein lokaler Branch:
|
|
|
|
```text
|
|
feature1
|
|
```
|
|
|
|
Dann kann man verwenden:
|
|
|
|
```bash
|
|
git rebase --onto origin/main feature1 feature2
|
|
```
|
|
|
|
## Möglichkeit B: Commit aus der Historie verwenden
|
|
|
|
Mit der Historie:
|
|
|
|
```bash
|
|
git log --oneline --graph --all
|
|
```
|
|
|
|
den letzten Commit von `feature1` vor den eigenen `feature2`-Commits suchen.
|
|
|
|
Angenommen, dieser Commit heißt:
|
|
|
|
```text
|
|
abc1234
|
|
```
|
|
|
|
Dann:
|
|
|
|
```bash
|
|
git rebase --onto origin/main abc1234 feature2
|
|
```
|
|
|
|
Bedeutung:
|
|
|
|
> Alle Commits nach `abc1234`, die zu `feature2` gehören, auf `origin/main` setzen.
|
|
|
|
## Möglichkeit C: Reflog verwenden
|
|
|
|
Wenn der Commit nicht mehr über die normale Historie sichtbar ist:
|
|
|
|
```bash
|
|
git reflog
|
|
```
|
|
|
|
Dort kann man nach dem ursprünglichen Abzweigpunkt oder dem früheren Stand von `feature1` suchen.
|
|
|
|
---
|
|
|
|
# Variante ohne `rebase --onto`: eigene Commits per Cherry-Pick übernehmen
|
|
|
|
Wenn die Branch-Historie unklar ist, kann man auch einen neuen Branch auf Basis von `main` erstellen und nur die eigenen `feature2`-Commits übernehmen.
|
|
|
|
Beispiel:
|
|
|
|
```bash
|
|
git fetch origin
|
|
git switch -c feature2-neu origin/main
|
|
git cherry-pick <commit-D>^..<commit-E>
|
|
```
|
|
|
|
Dabei werden die eigenen Commits von `feature2` einzeln übernommen.
|
|
|
|
Danach kann der neue Branch entweder als neuer Merge Request verwendet oder nach sorgfältiger Prüfung an die Stelle von `feature2` gepusht werden.
|
|
|
|
Diese Variante ist nützlich, wenn:
|
|
|
|
- `feature1` gelöscht wurde,
|
|
- die ursprüngliche Branch-Basis nicht mehr eindeutig ist,
|
|
- `feature2` nur wenige eigene Commits enthält.
|
|
|
|
---
|
|
|
|
# Was man nicht tun sollte
|
|
|
|
Nach einem Squash-Merge von `feature1` ist dieser Ablauf problematisch:
|
|
|
|
```bash
|
|
git switch feature2
|
|
git merge origin/main
|
|
```
|
|
|
|
Das kann funktionieren, muss aber nicht. Git sieht die ursprünglichen Commits von `feature1` weiterhin in der Historie von `feature2`, während `main` einen neuen Squash-Commit enthält. Dadurch können Änderungen doppelt betrachtet oder Konflikte erzeugt werden.
|
|
|
|
Für diesen speziellen Fall ist besser:
|
|
|
|
```bash
|
|
git rebase --onto origin/main origin/feature1
|
|
```
|
|
|
|
---
|
|
|
|
# Alternative: `feature1` nicht squashen
|
|
|
|
Wenn häufig gestapelte Branches verwendet werden, kann es sinnvoll sein, den übergeordneten Branch `feature1` nicht zu squashen, sondern per normalem Merge zu integrieren.
|
|
|
|
Dann bleibt die Commit-Abstammung erhalten:
|
|
|
|
```text
|
|
main: A---------M
|
|
\ /
|
|
feature1: B-----C
|
|
\
|
|
feature2: D---E
|
|
```
|
|
|
|
Git kann in diesem Fall besser erkennen, dass `B` und `C` bereits in `main` enthalten sind.
|
|
|
|
Eine mögliche Teamregel wäre daher:
|
|
|
|
- unabhängige Feature-Branches: Squash-Merge erlaubt
|
|
- voneinander abhängige oder gestapelte Feature-Branches: normaler Merge ohne Squash oder anschließend gezielter Rebase
|
|
|
|
---
|
|
|
|
# Zusammenfassung
|
|
|
|
Für Ihren Fall ist der typische Ablauf:
|
|
|
|
```bash
|
|
git status
|
|
git fetch origin
|
|
git switch feature2
|
|
git branch backup/feature2-vor-rebase
|
|
git rebase --onto origin/main origin/feature1
|
|
```
|
|
|
|
Bei Konflikten:
|
|
|
|
```bash
|
|
# Dateien bearbeiten
|
|
git add <dateien>
|
|
git rebase --continue
|
|
```
|
|
|
|
Bei Abbruch:
|
|
|
|
```bash
|
|
git rebase --abort
|
|
```
|
|
|
|
Nach erfolgreichem Rebase prüfen:
|
|
|
|
```bash
|
|
git diff origin/main...feature2
|
|
git log --oneline origin/main..feature2
|
|
```
|
|
|
|
Dann pushen:
|
|
|
|
```bash
|
|
git push --force-with-lease origin feature2
|
|
```
|
|
|
|
Der entscheidende Punkt ist: **Nach einem Squash-Merge muss ein davon abgeleiteter Branch auf die neue `main`-Basis neu aufgebaut werden.** Ein einfacher Merge von `main` in den alten gestapelten Branch ist dafür nicht die zuverlässigsten Variante. |