update
This commit is contained in:
@@ -0,0 +1,473 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user