Git ist nicht in Ordnung
(billjings.com)- Git war als verteiltes Repository für Quellcode erfolgreich, aber die Verarbeitung verteilter Workflows ist eher eine später angefügte Lösung, deren Grenzen inzwischen sichtbar werden
- Git-Commits und -Branches können Nachfolge-Commits, Amend-Verlauf, Rebase-Verlauf und verworfene Zustände nicht selbst ausdrücken
- Bei Stacked PRs muss man nachfolgende PRs finden und mit erhaltenem Stack rebased halten, aber Git kann diese Beziehungen nur schwer zuverlässig erfassen
- Git hält veränderliche Zustände wie Staging, unstaged, Dateisystem und HEAD außerhalb von Commits und Branches, was Lernen und Nutzung komplexer macht
- In asynchronen Entwicklungs-Abläufen, in denen PRs vor dem Merge gemeinsam genutzt werden müssen, führt Gits rückwärtsgerichtetes unveränderliches Verlaufsmodell immer wieder zu Problemen
Die zwei Rollen von Git
- Git dient sowohl als verteiltes Repository für Quellcode als auch als Werkzeug für verteilte Workflows
- Als Code-Repository war es äußerst erfolgreich, aber seine Art, mit verteilten Workflows umzugehen, ist größtenteils eher eine nachträglich angebaute Lösung
- Asynchrone Entwicklung ist, wie es East River Source Control formuliert, fast eine Grundvoraussetzung; sie entsteht nicht nur bei Zusammenarbeit über Zeitzonen hinweg, sondern auch dann, wenn man mit zeitlichem Abstand mit sich selbst arbeitet
- jj ist ein Werkzeug, das die Grenzen von Git noch klarer sichtbar macht, und wer Git für ausreichend hält, wird jj wahrscheinlich nicht ernsthaft ausprobieren
Beziehungen, die Gits Grundmodell verfehlt
- Im Zentrum des Git-Denkens stehen Commits und Branches
- Ein Commit ist ein unveränderliches Objekt, das Quellcode und Verlauf enthält
- Ein Branch ist ein veränderlicher Zeiger mit Log
- Typische Git-Diagramme zeichnen Commits als
C1,C2,C3, wodurch Reihenfolge und Beziehungen klar erscheinen, aber die Namen echter Repository-Commits sind eher Hashes oder Nachrichten, sodass eine solche Reihenfolge im System selbst nicht existiert - Bezeichnungen wie
C2undC2’nach einem Rebase sind nur für Menschen leicht verständliche Erklärungen; Git weiß nicht, dass diese beiden Commits einander entsprechen - Um Nachfolge-Commits eines bestimmten Commits zu finden, muss man alle Branches durchsuchen und die Commits auf Pfaden ermitteln, die von diesem Commit ausgehen; das ist nicht einfach
In Git gibt es kein „C“
- Ein Git-Commit kann die folgenden Informationen nicht selbst kennen
-
Nachfolge-Commits
- der Änderungsverlauf, der nach einem Amend vom alten Commit zum neuen Commit weiterführt
-
Rebase-Verlauf
- ob dieser Commit verworfen wurde
- auch Branches haben Grenzen
- Branches besitzen zwar eine Art Verlaufskonzept, aber man kann kaum darauf vertrauen, dass sie 1:1 Codeänderungen entsprechen
- Branches haben keine Beziehungen untereinander; man kann zum Beispiel
wp/bugfixnicht zuverlässig vontrunkaus finden - Da es keinen Vorwärtsverweis von
trunkaufwp/bugfixgibt, handelt es sich auch nicht um eine erreichbare Beziehung - Git-Diagramme lassen für Menschen Reihenfolge und Zuordnung sichtbar erscheinen, können aber die tatsächlichen Fähigkeiten des Werkzeugs überzeichnen
-
Warum Stacked PRs schwierig sind
- Wenn man mit Menschen in anderen Zeitzonen zusammenarbeitet und vor dem Review nicht mergen will, muss man Arbeit wie in einer CPU pipelineartig organisieren
- Statt einen PR zu erstellen und bis zum Ende des Reviews zu warten, erstellt man einen zweiten PR auf dem ersten und stapelt weitere darauf; mehrere sequentielle PRs liegen dann gleichzeitig im Review. Das ist Stacked PR
- Git macht es schwer, eine Stacked-PR-Struktur zuverlässig zu handhaben
- Man erstellt einen Folge-PR wie
Refactor key entry codeaufFix key entry raceund muss nach einem Update durch Fetch vontrunkden Stack unter Erhalt seiner Struktur rebased halten - Git kennt Nachfolge-Commits nicht, daher ist
Refactor key entry codevonFix key entry raceaus nicht leicht zu erkennen - Ein Commit kann verworfen worden sein; selbst wenn man Folge-Commits sieht, ist schwer zu wissen, ob sie aktuell sind
- Branches werden wie die PRs selbst verwendet, lassen sich in diesem Ablauf aber leicht versehentlich überschreiben
- Man erstellt einen Folge-PR wie
- Stacking-Werkzeuge wie Graphite können diese Arbeit über Git leisten, aber sie erweitern weder Gits Commits noch seine Branches selbst
- Sie müssen ein separates Repository für Branch-Metadaten anlegen und mit Git synchronisieren
- Wenn Nutzer Git selbst direkt manipulieren, kann dieses Repository vom Git-Zustand abweichen
Veränderlicher Zustand liegt außerhalb der Commits
- Viele Probleme von Git folgen daraus, dass Veränderlichkeit (mutability) nicht direkt modelliert wird
- In Gits Bearbeitungs-Workflow gibt es getrennte Zustände außerhalb von Commits und Branches
- Staging oder der Index ist ein aus der Arbeitskopie erzeugter Quellcode-Snapshot, aus dem neue Commits erstellt werden
- Unstaged ist ein zweiter Diff, der den Unterschied zwischen Index und Dateisystem darstellt
- Das Dateisystem enthält den ausgecheckten Inhalt, zu dem staged und unstaged Änderungen hinzukommen
- HEAD ist die Stelle, an der neue Commits erzeugt werden
- Stash funktioniert wie ein separates Repository zum Speichern und Wiederherstellen von Staging- und unstaged-Änderungen
- Wenn man den Checkout auf einen anderen Commit oder Branch umstellt, versucht Git, das Dateisystem an die neue Position anzupassen und gleichzeitig die Diffs von Staging oder unstaged zu bewahren
- Dieser Prozess hat zwar andere Befehle, ähnelt aber anhand der Pfeilbeziehungen einer Rebase, bei der das Staging auf eine neue Basis verschoben wird
Warum sich nicht alles als Commit modellieren lässt
- Auch Staging und die Arbeitskopie haben klare Vorfahren und enthalten Quellcode; betrachtet man nur statische Zustände, lassen sie sich wie Commits ausdrücken
- Da Commit-IDs aber Hashes ihres Inhalts sind, würden sich die IDs ständig ändern, wenn Commits veränderlich wären
- Damit Staging und Arbeitskopie konsistent darauf zeigen können, „was sie sind“, müssten sie eher wie Branches als wie Commits behandelt werden, aber Branches haben die zuvor beschriebenen Grenzen
- Diese Komplexität führt zu echten Problemen
- Git zu lernen und zu benutzen wird schwieriger, weil dasselbe Konzept auf beiden Seiten getrennt existiert
- Der Gesamtzustand des Repositorys unterscheidet sich stark von dem Zustand, den man per Clone erhält, wodurch Export unhandlich wird
- Asynchrone Abläufe, in denen sich Änderungssätze im Lauf der Zeit verändern, funktionieren nicht gut
- Das System auf der veränderlichen Seite kann Merges nicht ausdrücken, sodass reale Workflows mitunter nicht darstellbar sind
Fälle, in denen Git reale Workflows nicht ausdrücken kann
- Während man auf einem neuen Feature-Branch noch ohne Commit entwickelt, kann man einen Bug entdecken, der die Entwicklung auf dem Gerät behindert
- Wenn dieser Bug das neue Feature nicht blockiert, die Entwicklung aber lästig macht, kann man die Arbeit stashen, auf einen neuen Branch wechseln, Reproduktionstest und Fix erstellen und dafür einen PR öffnen
- Wenn man danach zum neuen Feature-Branch zurückkehrt, sind die Optionen begrenzt
- Man rebased
new-featureaufbugfix, obwohl keine echte Abhängigkeit besteht, und fährt mit dem Review fort - Während der Entwicklung nutzt man
new-featureüberbugfixrebased und macht diesen Rebase vor dem Einreichen des Branches wieder rückgängig
- Man rebased
- Mit Git lässt sich der Zustand „Der Bearbeitungsarbeitsbereich muss den gesamten Code des bugfix sowie den bereits committeten Code des neuen Features zusammen enthalten“ nicht ausdrücken
- Dieselbe Struktur zeigt sich auch bei schwierigeren Problemen wie Kompatibilitätstests mit noch nicht gemergten PRs
- Mit dem passenden Werkzeug, etwa Jujutsu megamerges, kann man mehrere PRs parallel pflegen und sie im Bearbeitungsbereich trotzdem gemeinsam nutzen
Git reicht nicht mehr aus
- Versionsverwaltungswerkzeuge der frühen 2000er waren schwer zu benutzen und zu verwalten, qualitativ uneinheitlich, und selbst Subversion galt weithin als schmerzhaft
- Damals war der Wunsch nach einer vollständigen lokalen Kopie des Repositorys nicht allgemein verbreitet, und auch lokale Branches waren kein universeller Wunsch
- Viele empfanden Dateisperren als lästig, aber manche hielten sie für nötig und fragten sogar, ob sich in Git einzelne Dateien oder Verzeichnisse sperren lassen
- Für Menschen, die verteilte Workflows wie in Open Source direkt erlebt hatten, wirkte DVCS wie ein Verband, der alte Wunden abdeckt
- Für Menschen, die heute sinnvoll verteilte Workflows nutzen, ist Gits rückwärtsgerichtetes unveränderliches Verlaufsmodell eine wiederkehrende Problemquelle
- Unternehmen wie Meta verwenden seit fast zehn Jahren interne Systeme, die Git deutlich voraus sind
- Der Trend „Jetzt fasst Claude Git stellvertretend an“ macht solche Alternativen nicht bedeutungslos
- Mit LLMs scheint selbst auf einem einzelnen Rechner mehr asynchrone Entwicklung durch Engineers stattzufinden als früher
1 Kommentare
Lobste.rs-Kommentare
Es wäre wohl gut gewesen zu zeigen, wie jj die im Artikel angesprochenen Probleme löst.
Für jj-Nutzer ist das vielleicht offensichtlich, aber wahrscheinlich sind sie nicht die Hauptzielgruppe des Artikels.
Die Funktionen, die im Artikel als Beleg dafür genannt werden, dass Git nicht in Ordnung ist, habe ich persönlich noch nie gebraucht.
Ich frage mich, ob nur ich das so sehe.
Ein wichtiger Aspekt bei Werkzeugen ist, dass sie Teil eines dynamischen Systems sind. Was ein Werkzeug ermöglicht, beeinflusst, „was ich glaube tun zu können“, und dieser Glaube verändert wiederum die Wahrnehmung des Werkzeugs und die Richtung seiner Weiterentwicklung.
Wenn ein Werkzeug den Status quo erschüttert, verändern sich auch die Vorstellungen und Erwartungen darüber, was möglich ist.
Klingt interessant, aber bei dem Diagramm wird mir schwindelig.
Zu der Aussage, dass die Lage heute nicht mehr so schlimm sei wie in den frühen 2000ern und dass die Grenzen der Versionsverwaltungssysteme vor Git ziemlich klar gewesen seien: Darcs kam vor Git heraus und hat einige Probleme der Snapshot-basierten Versionsverwaltung grundlegend gelöst.
Anfangs wurde es wegen schlechter Performance abgehängt, später wurde die Performance aber verbessert, und die Leute sind nicht noch einmal zurückgekommen, um nachzusehen. Es gibt auch andere Versionsverwaltungssysteme, die interessante Dinge machen, daher fände ich es gut, wenn man nicht so tut, als seien „wenn nicht Git, dann Jujutsu“ die einzigen Optionen. Diese Art von Argumentation sehe ich viel zu oft.
Das ist ebenfalls ein Problem des Datenmodells.
Wie geht jj damit um? https://www.billjings.com/posts/title/git-is-not-fine/RealityEx23.png
jj new A Bverwendet, kann der Working-Copy-Commit mehrere Eltern haben und verhält sich dadurch wie ein Merge-Commit.Dadurch landen die Änderungen beider Eltern im Working Copy, und man kann entweder auf diesem Merge weiterarbeiten oder einen der beiden Commits amendieren.
Ich bevorzuge weiterhin Git, und der Autor wirkt auf mich voreingenommen.
jj newausführen muss, kann mangitundjjzusammen verwenden.Git zeigt dann immer auf den Eltern-Commit, und der aktuelle
jj commiterscheint wie uncommittete Änderungen im Working Tree.So habe ich
jjgelernt. Für Dinge, diejjgut kann, wie Rebase-Verarbeitung oder das Verschieben von Trees, habe ichjjbenutzt, und für alltägliche Aufgaben, bei denen ich entweder noch keinenjj-Befehl kannte oder mir zuerst ein Git-Befehl wiegit blameeinfiel, habe ich weiter diegit-Befehle verwendet.Bis ich es tatsächlich täglich benutzt habe, war für mich nicht wirklich greifbar, warum
jjbesser sein soll; beim bloßen Lesen dachte ich eher: „Brauche ich diese Funktion wirklich unbedingt?“ oder „Das geht doch in Git schon“.Natürlich hat auch
jjNachteile. Wenn keine aktuelle.gitignorevorhanden ist, können Binärdateien versehentlich in einen Commit geraten. Immerhin warntjj, wenn man sehr große Dateien hinzufügt, aber kleinere Dateien können durchrutschen.Wenn beim Debuggen verfolgte Dateien oder Log-Dateien im aktuellen Verzeichnis liegen, können auch die mit hineingeraten; deshalb ist es sinnvoll, nach allen Tree-Manipulationen die gesamte Diffstat zu prüfen.
Problematisch kann das besonders dann werden, wenn man mit
jjeine Bisektion macht und dabei einen Commit testet, der älter ist als der Commit, der.gitignoreaktualisiert hat. Vielleicht sollte es für die Bisektion einen Read-only-Modus geben.