165 lines
5.0 KiB
Markdown
165 lines
5.0 KiB
Markdown
# Git-History kuerzen
|
|
|
|
Wenn ein Repository zu gross geworden ist und die alte Historie nicht mehr benoetigt wird, kann die Git-History ab einem bestimmten Commit abgeschnitten werden. Dabei wird die Historie **umgeschrieben**: Der Commit-Hash des neuen Anfangs und die Hashes aller folgenden Commits aendern sich.
|
|
|
|
> **Achtung:** Das ist eine destruktive Operation fuer die bisherige Historie. Vorher unbedingt ein Backup erstellen und mit allen anderen Personen abstimmen, die mit dem Repository arbeiten.
|
|
|
|
## Nicht verwechseln: Shallow Clone
|
|
|
|
Ein flacher Clone verbirgt alte Historie nur lokal:
|
|
|
|
```bash
|
|
git clone --shallow-since="2024-01-01" <REPOSITORY_URL>
|
|
```
|
|
|
|
Das Repository selbst wird dadurch nicht gekuerzt. Fuer eine dauerhafte Kuerzung muss die Historie umgeschrieben und anschliessend mit Force-Push auf das Remote uebertragen werden.
|
|
|
|
## Vorbereitung: Backup erstellen
|
|
|
|
Am sichersten arbeitet man mit einem Mirror-Clone:
|
|
|
|
```bash
|
|
git clone --mirror <REPOSITORY_URL> repository-backup.git
|
|
cd repository-backup.git
|
|
```
|
|
|
|
Den urspruenglichen Clone bzw. das Backup erst loeschen, wenn das Ergebnis geprueft wurde.
|
|
|
|
## `git-filter-repo` unter Arch Linux installieren
|
|
|
|
Das Arch-Paket heisst `git-filter-repo`:
|
|
|
|
```bash
|
|
sudo pacman -S git-filter-repo
|
|
```
|
|
|
|
Pruefen:
|
|
|
|
```bash
|
|
git filter-repo --version
|
|
```
|
|
|
|
Ein systemweites `pip install` funktioniert auf Arch Linux wegen PEP 668 normalerweise nicht. Die Paketinstallation ist vorzuziehen.
|
|
|
|
## Einen Commit zum neuen Root machen
|
|
|
|
Angenommen, dieser Commit soll der neue Anfang der Historie werden:
|
|
|
|
```text
|
|
464e39c1234567890abcdef1234567890abcdef
|
|
```
|
|
|
|
Dann werden seine Eltern entfernt:
|
|
|
|
```bash
|
|
git filter-repo --force \
|
|
--commit-callback 'if commit.original_id == b"464e39c1234567890abcdef1234567890abcdef": commit.parents = []'
|
|
```
|
|
|
|
`commit.original_id` enthaelt die vollstaendige 40-stellige Commit-ID. Eine kurze ID wie `464e39c` funktioniert mit `==` nicht. Alternativ kann ein eindeutiger Prefix so verwendet werden:
|
|
|
|
```bash
|
|
git filter-repo --force \
|
|
--commit-callback 'if commit.original_id.startswith(b"464e39c"): commit.parents = []'
|
|
```
|
|
|
|
Die vollstaendige ID kann vorher ermittelt werden:
|
|
|
|
```bash
|
|
git rev-parse 464e39c
|
|
```
|
|
|
|
Aus:
|
|
|
|
```text
|
|
A -- B -- C -- D -- E
|
|
```
|
|
|
|
wird dadurch sinngemaess:
|
|
|
|
```text
|
|
C' -- D' -- E'
|
|
```
|
|
|
|
Der Commit `C` bleibt inhaltlich und mit seinen Metadaten erhalten, bekommt aber wegen der entfernten Eltern einen neuen Hash. Auch `D` und `E` erhalten neue Hashes.
|
|
|
|
## Den Cutoff-Commit ebenfalls verwerfen
|
|
|
|
Wenn der Cutoff-Commit selbst nicht erhalten bleiben soll, kann ein neuer Root-Commit mit seinem Dateistand erstellt und die spaetere Historie darauf rebased werden:
|
|
|
|
```bash
|
|
git checkout --orphan temp <CUTOFF_COMMIT>
|
|
git commit -m "Start truncated history"
|
|
git rebase --onto temp <CUTOFF_COMMIT> main
|
|
git branch -D temp
|
|
```
|
|
|
|
Dabei ist `<CUTOFF_COMMIT>` der **letzte Commit, der verworfen werden soll**. Der neue Root-Commit enthaelt den Dateistand dieses Commits, ist aber ein neuer Commit. Bei Merge-Commits oder komplexen Branches kann die Rebase-Variante Konflikte verursachen. In solchen Faellen ist `git-filter-repo` in der Regel die bessere Wahl.
|
|
|
|
## Ergebnis pruefen
|
|
|
|
Vor dem Push die neue Historie kontrollieren:
|
|
|
|
```bash
|
|
git log --all --graph --oneline --decorate
|
|
git status
|
|
```
|
|
|
|
Der neue Root-Commit darf keine `parent`-Zeile haben:
|
|
|
|
```bash
|
|
git show --no-patch --pretty=raw <NEUER_ROOT_COMMIT>
|
|
```
|
|
|
|
Auch der aktuelle Dateistand sollte unveraendert sein. Einen Vergleich kann man beispielsweise vor dem Umschreiben speichern oder mit dem Backup pruefen.
|
|
|
|
## Remote wieder hinzufuegen
|
|
|
|
`git-filter-repo` entfernt das Remote oft absichtlich als Sicherheitsmassnahme. Deshalb zuerst pruefen:
|
|
|
|
```bash
|
|
git remote -v
|
|
```
|
|
|
|
Falls kein `origin` vorhanden ist:
|
|
|
|
```bash
|
|
git remote add origin <REPOSITORY_URL>
|
|
git ls-remote origin
|
|
```
|
|
|
|
`origin/main` ist eine Branch-Referenz und kein Remote-Name. Der Push verwendet daher `origin main`, nicht `origin/main`.
|
|
|
|
## Umschriebene Historie pushen
|
|
|
|
Nur den Hauptbranch pushen:
|
|
|
|
```bash
|
|
git push --force origin main
|
|
```
|
|
|
|
Alle lokalen Branches und Tags pushen:
|
|
|
|
```bash
|
|
git push --force --all origin
|
|
git push --force --tags origin
|
|
```
|
|
|
|
Das Remote kann den Push wegen Branch-Schutzregeln ablehnen. In diesem Fall muss der Schutz voruebergehend angepasst werden.
|
|
|
|
## Nacharbeiten und wichtige Folgen
|
|
|
|
- Alle Commit-Links, Pull Requests und Tags koennen durch die neuen Hashes ungueltig werden.
|
|
- Andere Clone enthalten weiterhin die alte Historie und muessen neu geklont oder manuell angepasst werden.
|
|
- Andere Branches oder Tags koennen die alte Historie weiterhin erreichbar machen.
|
|
- Ein Force-Push kann nicht ohne Weiteres rueckgaengig gemacht werden; das Backup aufbewahren.
|
|
|
|
Alte, nicht mehr erreichbare Objekte koennen nach der Pruefung lokal bereinigt werden:
|
|
|
|
```bash
|
|
git reflog expire --expire=now --all
|
|
git gc --prune=now
|
|
```
|
|
|
|
`git gc --aggressive` ist normalerweise nicht notwendig und kann bei grossen Repositories lange dauern. Auf einem Hosting-Dienst werden nicht mehr erreichbare Objekte eventuell erst spaeter durch dessen eigene Garbage Collection geloescht.
|