Monorepos und große Repositories#

In einem großen Projekt kann es sinnvoll sein, einzelne Komponenten in separaten Repositories zu pflegen. Manchmal schafft dies jedoch unnötige Komplexität, z.B. welche Versionen der Repositories miteinander kompatibel sind. In diesen Fällen kann es sinnvoll sein, alle Teile eines Projekts in einem monolithischen Repository oder Monorepo zu halten.

Definition#

  • In einem Monorepo enthält das Repository mehr als ein logisches Projekt (z.B. einen iOS-Client und eine Webanwendung).

  • Diese Projekte können unabhängig voneinander gebaut, getestet oder deployt werden.

  • Diese Projekte sind meist nur lose miteinander verbunden oder können auf andere Weise miteinander verbunden werden, z.B. über Tools zur Verwaltung von Abhängigkeiten.

  • Das Repository enthält viele Commits, Zweige und/oder Tags. Oder es enthält viele und/oder große Dateien.

Mit Tausenden von Commits von hunderten Autoren in tausenden von Dateien pro Monat ist das Linux-Kernel-Repository riesig.

Vor- und Nachteile#

Ein Vorteil von Monorepos kann sein, dass die Aufwände um zu bestimmen, welche Versionen des einen Projekts mit welchen Versionen des anderen Projekts kompatibel sind, deutlich verringert sein könnten. Dies ist zumindest immer dann der Fall, wenn alle Projekte eines Repository von nur einem Entwicklerteam bearbeitet werden. Dann empfiehlt sich, mit jedem Merge wieder eine lauffähige Version zu erhalten auch wenn die API zwischen den beiden Projekten geändert wurde.

Als Nachteil können sich jedoch Performance-Einbußen erweisen. Diese können z.B. entstehen durch:

eine große Anzahl an Commits

Da Git DAGs (directed acyclic graphs) verwendet, um die Historie eines Projekts darzustellen, werden alle Operationen, die diesen Graphen durchlaufen, also z.B. git log oder git blame, langsam werden.

eine große Anzahl von Git-Referenzen

Eine große Anzahl von Branches und Tags verlangsamen Git ebenfalls. Mit git ls-remote könnt ihr euch die Referenzen eines Repository anzeigen lassen und mit git gc werden lose Referenzen in einer einzigen Datei zusammengefasst.

Jede Operation, die den Commit-Verlauf eines Repositories durchlaufen und die einzelnen Referenzen berücksichtigen muss, wie z.B. bei git branch --contains <commit>, werden bei einem Repo mit vielen Referenzen langsam.

eine große Anzahl an versionierten Dateien

Der Index des Directory Cache (.git/index) wird von Git verwendet um zu ermitteln, ob die Datei verändert wurde. Dabei verlangsamen sich mit zunehmender Anzahl an Dateien viele Vorgänge, wie z.B. git status und git commit.

große Dateien

Große Dateien in einem Teilbaum oder einem Projekt verringern die Leistung des gesamten Repository.

Strategien für große Repositories#

Die Designziele von Git, die es so erfolgreich und beliebt gemacht haben, stehen manchmal im Widerspruch zu dem Wunsch, es auf eine Weise zu verwenden, für die es nicht konzipiert wurde. Dennoch gibt es eine Reihe von Strategien, die bei der Arbeit mit großen Repositories hilfreich sein können:

git clone --depth#

Auch wenn die Schwelle, ab der eine Historie als riesig eingestuft wird, ziemlich hoch ist, kann es immer noch mühsam sein, sie zu klonen. Dennoch können wir lange Historien nicht immer vermeiden, wenn sie aus rechtlichen oder regulatorischen Gründen beibehalten werden müssen.

Die Lösung für einen schnellen Clone eines solchen Repositories besteht darin, nur die jüngsten Revisionen zu kopieren. Mit der Shallow-Option von git clone könnt ihr nur die letzten N Commits der Historie abrufen, z.B. git clone --depth N REMOTE-URL.

Tipp

Auch Build-Systeme, die mit eurem Git-Repository verbunden sind, profitieren von solchen Shallow Clones!

Shallow Clones waren in Git bisher eher selten, da einige Operationen Anfangs kaum unterstützt wurden. Seit einiger Zeit (in den Versionen 1.9 und höher) könnt ihr jetzt sogar von einem Shallow Clone aus Pull- und Push-Vorgänge in Repositories durchführen.

git filter-branch#

Für große Repositories, in denen viele Binärdateien versehentlich übertragen wurden, oder alte Assets, die nicht mehr benötigt werden, ist git filter-branch eine gute Lösung um die gesamte Historie durchzugehen und Dateien nach vordefinierten Mustern herauszufiltern, zu ändern oder zu überspringen.

Es ist ein sehr leistungsfähiges Werkzeug, sobald ihr herausgefunden habt, wo euer Projektarchiv schwer ist. Es gibt auch Hilfsskripte, um große Objekte zu identifizieren: git filter-branch --tree-filter 'rm -rf /PATH/TO/BIG/ASSETS'.

Warnung

git filter-branch schreibt allerdings die gesamte Historie eures Projekts um, d.h., dass sich einerseits alle Commit-Hashes ändern und andererseits, dass jedes Teammitglied das aktualisierte Repository neu klonen muss.

git clone --branch#

Ihr könnt den Umfang der geklonten Historie auch begrenzen, indem ihr einen einzelnen Zweig klont, etwa mit git clone REMOTE-URL --branch BRANCH-NAME --single-branch FOLDER.

Dies kann nützlich sein, wenn ihr mit langlaufenden und abweichenden Zweigen arbeitet, oder wenn ihr viele Zweige habt und nur mit einigen davon arbeiten müsst. Wenn ihr jedoch nur eine wenige Zweige mit wenigen Unterschieden habt, werdet ihr damit jedoch wahrscheinlich keinen großen Unterschied feststellen.

Git LFS#

Git LFS ist eine Erweiterung, die Pointer auf große Dateien in eurem Repository speichert, anstatt die Dateien selbst; diese werden auf einem entfernten Server gespeichert, wodurch die Zeit für das Klonen eures Projektarchivs drastisch verkürzt wird. Git LFS greift dabei auf die nativen Push-, Pull-, Checkout- und Fetch-Operationen von Git zu, um die Objekte zu übertragen und zu ersetzen, d.h., dass ihr mit großen Dateien in eurem Repository wie gewohnt arbeiten könnt.

Ihr könnt Git LFS installieren mit

$ sudo apt install git-lfs
$ brew install git-lfs

Git LFS lässt sich mit git for windows mitinstallieren.

Anschließend könnt ihr Git LFS in eurem Repository installieren mit

$ git lfs install
Updated Git hooks.
Git LFS initialized.

Um nun Git LFS auf bestimmte Dateitypen anzuwenden, könnt ihr z.B. folgendes angeben:

$ git lfs track "*.pdf"
Tracking "*.pdf"

Dies erstellt in eurer .gitattributes-Datei folgende Zeile:

*.pdf filter=lfs diff=lfs merge=lfs -text

Schließlich sollter ihr die .gitattributes-Datei mit Git verwalten:

$ git add .gitattributes

git-sizer#

git-sizer berechnet verschiedene Metriken für ein lokales Git-Repository und kennzeichnet diejenigen, die euch Probleme oder Unannehmlichkeiten bereiten könnten, z.B.:

$ git-sizer
Processing blobs: 1903
Processing trees: 4126
Processing commits: 1055
Matching commits to trees: 1055
Processing annotated tags: 2
Processing references: 5
| Name                         | Value     | Level of concern               |
| ---------------------------- | --------- | ------------------------------ |
| Biggest objects              |           |                                |
| * Blobs                      |           |                                |
|   * Maximum size         [1] |  35.8 MiB | ***                            |

[1]  9fe7b8048891965e476aac0410e08e050fd21354 (refs/heads/main:docs/workspace/pandas/descriptive-statistics.ipynb)

Installation#

  1. Ruft die Releases-Seite auf und ladet die ZIP-Datei herunter, die eurer Plattform entspricht.

  2. Entpackt die Datei.

  3. Verschiebt die ausführbare Datei (git-sizer oder git-sizer.exe) in euren PATH.

$ brew install git-sizer

Git file system monitor (FSMonitor)#

git status und git add sind langsam, weil sie den gesamten Arbeitsbaum nach Änderungen durchsuchen müssen. Mit der Funktion git fsmonitor--daemon, die ab Git-Version 2.36 zur Verfügung steht, werden diese Befehle beschleunigt, indem der Umfang der Suche reduziert wird:

$ time git status
Auf Branch master
Ihr Branch ist auf demselben Stand wie 'origin/master'.
real    0m1,969s
user    0m0,237s
sys     0m1,257s
$ git config core.fsmonitor true
$ git config core.untrackedcache true
$ time git status
Auf Branch master
Ihr Branch ist auf demselben Stand wie 'origin/master'.
real    0m0,415s
user    0m0,171s
sys     0m0,675s
$ git fsmonitor--daemon status
fsmonitor-daemon beobachtet '/srv/jupyter/linux'

Scalar#

scalar, ein Repository-Management-Tool für große Repositories von Microsoft, ist seit Version 2.38 Teil der Git-Kerninstallation. Um es zu verwenden, könnt ihr entweder ein neues Repository mit scalar clone /path/to/repo klonen oder scalar auf einen bestehenden Klon mit scalar register /path/to/repo anwenden.

Weitere Optionen von scalar clone sind:

-b, --branch BRANCH

Branch, der nach dem Klonen ausgecheckt werden soll.

--full-clone

Vollständiges Arbeitsverzeichnis beim Klonen erstellen.

--single-branch

Lade nur Metadaten des Branches herunter, der ausgecheckt wird.

Mit scalar list könnt ihr sehen, welche Repositories derzeit von Scalar verfolgt werden und mit scalar unregister /path/to/repo wird das Repository aus dieser Liste entfernt.

Standardmäßig ist die Sparse-Checkout-Funktion aktiviert und es werden nur die Dateien im Stammverzeichnis des Git-Repositorys angezeigt. Verwendet git sparse-checkout set, um die Menge der Verzeichnisse zu erweitern, die ihr sehen möchtet, oder git sparse-checkout disable, um alle Dateien anzuzeigen. Wenn ihr nicht wisst, welche Verzeichnisse im Repository verfügbar sind, könnt ihr git ls-tree -d --name-only HEAD ausführen, um die Verzeichnisse im Stammverzeichnis zu ermitteln, oder git ls-tree -d --name-only HEAD /path/to/repo, um die Verzeichnisse in /path/to/repo zu ermitteln.

Siehe auch

git ls-tree

Um Sparse-Checkout nachträglich zu aktivieren, führt git sparse-checkout init --cone aus. Dadurch werden eure Sparse-Checkout-Patterns so initialisiert, dass sie nur mit den Dateien im Stammverzeichnis übereinstimmen.

Aktuell sind neben sparse-checkout noch die folgende Funktionen für scalar verfügbar:

Die Konfiguration von scalar wird aktualisiert, wenn neue Funktionen in Git eingeführt werden. Um sicherzustellen, dass ihr immer die neueste Konfiguration verwendet, solltet ihr scalar reconfigure /PATH/TO/REPO nach einer neuen Git-Version ausführen, um die Konfiguration eures Repositorys zu aktualisieren oder scalar reconfigure -a, um alle eure mit Scalar registrierten Repositories auf einmal zu aktualisieren.