1 Punkte von GN⁺ 2 시간 전 | 1 Kommentare | Auf WhatsApp teilen
  • Der experimentelle Befehl git history, der in Git 2.54 und 2.55 eingeführt wurde, automatisiert das Bearbeiten früherer Commits und das Neuaufbauen abgeleiteter Branches, ohne den bestehenden Git-Workflow zu verändern
  • fixup fügt gestagte Änderungen in einen früheren Commit ein und rebased anschließend automatisch alle lokalen Branches, die von diesem Commit abgeleitet sind, auf die neuen Commit-Hashes
  • reword ändert die Nachricht eines früheren Commits, und split teilt die Änderungen eines Commits auf Hunk-Ebene in zwei Commits auf und baut danach nachfolgende Commits und Branches neu auf
  • Die drei Unterbefehle verhindern mit atomarer Verarbeitung, dass der Working Tree in einem kaputten Zwischenzustand zurückbleibt, indem sie Operationen mit möglichem Konflikt ablehnen, funktionieren jedoch nicht bei Historien mit Merge-Commits
  • Zwar bietet der Befehl weder die Konfliktbehandlung als erstklassiges Konzept, das rückgängig machen über ein Operations-Log noch die Modellierung der Working Copy als Commits wie jj, aber mehrere jj-artige Funktionen zur Bearbeitung der Historie lassen sich direkt mit der Git-Core-Distribution ohne separate Installation nutzen

Warum die Bearbeitung der bestehenden Git-Historie unbequem ist

  • Wenn mehrere Änderungen parallel bearbeitet werden, muss man ständig zwischen Branches und Commits wechseln, und während eines rebase -i kann der Working Tree in einen unvollständigen Zustand geraten
  • Die Git-Alternative jj wird oft als Lösung für dieses Problem genannt, doch selbst nach anderthalb Jahren mit Versuchen von jeweils einigen Tagen alle drei Monate kehrt man am Ende wieder zum bisherigen Git-Workflow zurück
  • git history ist ein experimenteller Befehl, der in der Git-Core-Distribution enthalten ist, sodass man ihn ohne Installation eines zusätzlichen Tools ausprobieren kann
    • Git 2.54 führte im April reword und split ein
    • Git 2.55 ergänzte im Juni fixup
  • Die Unterbefehle bestehen aus den drei Varianten fixup, reword und split

fixup: Früheren Commit und abgeleitete Branches gleichzeitig korrigieren

  • git history fixup korrigiert fehlerhafte Teile eines früheren Commits und rebased automatisch die Branches, die von diesem Commit abgeleitet wurden
  • Wie im üblichen Korrekturablauf werden Änderungen zunächst mit git add gestagt; führt man danach git history fixup <commit> aus, werden die gestagten Änderungen in den Ziel-Commit eingefügt
  • Das ähnelt einer Kombination aus git commit --fixup und einem Autosquash-Rebase, mit dem Unterschied, dass auch andere Branches, die den Ziel-Commit enthalten, aktualisiert werden
  • Während git rebase --update-refs nur Referenzen verschiebt, die im aktuellen Rebase-Bereich liegen, findet git history alle lokalen Branches, die vom Ziel-Commit abgeleitet sind, und schreibt sie neu
    • Falls nötig, lässt sich der Aktualisierungsbereich auf den aktuellen Branch beschränken
    • In Historien mit Merge-Commits funktioniert der Befehl nicht, weshalb er sich nicht für jede Git-Nutzungsweise eignet
  • Wenn Korrekturen in B eingefügt werden, entsteht B* mit neuem Hash; auch die darüberliegenden C und D werden als C* bzw. D* neu erzeugt
    • Die Branch-Spitzen von feat-1 und feat-2 werden ebenfalls auf die neuen Commits verschoben

Atomare Verarbeitung und Konfliktbegrenzung

  • Alle drei Unterbefehle werden atomar verarbeitet und hinterlassen den Working Tree nicht in einem halb kaputten Zustand
  • Die Atomarität wird dadurch gewährleistet, dass Operationen, die Konflikte auslösen könnten, von vornherein abgelehnt werden
  • Das ist funktional eingeschränkter als jj, das Konflikte als erstklassige Objekte behandelt, einen Konfliktzustand während eines Rebase beibehalten und später auflösen kann
  • Die Einschränkungen von git history sind bewusst so gesetzt, damit das Umschreiben der Historie nicht zu einer zustandserhaltenden Operation wird
    • Falls Git künftig erstklassige Konflikte unterstützt, könnten diese Einschränkungen aufgehoben werden

reword: Commit-Nachricht ohne Working Tree ändern

  • git history reword ändert die Nachricht eines früheren Commits und baut die darüberliegenden Commits und Branches automatisch neu auf
  • Führt man git history reword <commit> aus, öffnet sich im Editor die bisherige Nachricht; nach dem Bearbeiten und Speichern wird der nachfolgende Commit-Stack an die neuen Hashes angepasst neu aufgebaut
  • Das ist nützlich, wenn sich während iterativer Entwicklung das Design geändert hat und ältere Commit-Nachrichten an die aktuelle Struktur angepasst werden sollen
  • Abgesehen davon, dass statt des Tree-Inhalts nur die Commit-Nachricht geändert wird, funktioniert der Befehl wie fixup
  • reword verändert weder Index noch Working Tree, sondern nur den Commit-Graphen
    • Auch Commits auf nicht ausgecheckten Branches lassen sich ändern, ohne die aktuelle Arbeit zu stören
    • Wird die Nachricht von B geändert, ändert sich dessen Hash; der nachfolgende C wird daher als C* neu erzeugt, und auch feat-1 folgt dem neuen Commit

split: Einen Commit in zwei aufteilen

  • git history split teilt die Änderungen eines Commits anhand einer interaktiven Auswahl in zwei Commits auf
  • Führt man git history split <commit> aus, öffnet sich eine Eingabeaufforderung, in der die Änderungen des Ziel-Commits auf Hunk-Ebene ausgewählt werden
    • Die ausgewählten und beibehaltenen Hunks bilden den ersten Commit
    • Die übrigen Hunks kommen in den zweiten Commit
  • Das ähnelt git add -p, erfordert aber kein komplexes Verfahren mit einer Kombination aus git rebase
  • Wird B, das zwei nicht zusammenhängende Änderungen enthielt, aufgeteilt, entstehen B1 und B2, und der nachfolgende C wird darüber als C* neu aufgebaut
  • Unter den drei Befehlen ist dies der spezialisierteste Anwendungsfall, aber wenn ein Commit tatsächlich aufgeteilt werden muss, ist er nützlich

Funktionsunterschiede zu jj

1 Kommentare

 
GN⁺ 2 시간 전
Meinungen auf Hacker News
  • Git war für mich unbequem, aber nachdem ich die ersten drei Kapitel des kostenlos verfügbaren Buchs Pro Git gelesen hatte, wurde es besser. Es vermittelt ein hervorragendes mentales Modell der internen Funktionsweise von Git, und ob gut oder schlecht: Die Benutzeroberfläche von Git spiegelt seine interne Struktur direkt wider. Sobald ich das verstanden hatte, ergab alles Sinn.

    • Ich stimme nicht zu, dass die Benutzeroberfläche die interne Struktur direkt widerspiegelt. git checkout zum Beispiel verliert beim Wechseln von Branches keine Änderungen, die noch nicht in Git gelandet sind, ist aber beim Wiederherstellen einer anderen Version einer Datei darauf ausgelegt, Änderungen zurückzusetzen. Inwiefern spiegelt das die interne Struktur direkt wider?
      Unabhängig davon erschwert auch die Inkonsistenz der CLI die Nutzung von Git: Selbst wenn man weiß, was man intern tun möchte und welchen Befehl man braucht, ist es schwer, die genaue Kommandozeilen-Syntax zu finden.
    • Git ist nicht so verwirrend, wie sein Ruf nahelegt; man muss nur ein paar Stunden investieren, um es zu verstehen. Es ist schade, dass Leute es nicht lernen, obwohl es ein unverzichtbares Werkzeug für Software Engineers ist; als Profi reicht git commit -am “Changes” nicht aus.
    • Auch ich fand Git-Befehle verwirrend, aber nachdem ich ein YouTube-Video gesehen hatte, in dem Linus die internen Datenstrukturen von Git erklärt, wurde mir klar, dass es sehr einfach ist. Ich konnte leicht zuordnen, wie die Funktionen des Werkzeugs die Datenstrukturen manipulieren; eher sind es die höherstufigen Abstraktionen, die Git anbieten will, die die Sache verwirrender machen.
    • Nein. Die Hälfte der Git-Befehle ist Low-Level und die andere Hälfte High-Level, sie entsprechen also nicht 1:1 der internen Struktur.
    • Die User Experience von Mercurial war mit Abstand die beste, und man konnte es lernen, ohne extra Dokumentation zu lesen. Auch bei Git habe ich es wie Mercurial verwendet und mich meine ganze Laufbahn über damit beholfen, bei Problemen das lokale Repository zu löschen.
  • Es gibt git rebase --abort, und man kann vor dem Rebase auch ein Tag erstellen und mit git reset --hard $set_tag dorthin zurückkehren. Der vorherige Zustand verschwindet nicht, daher gibt es keinen Grund, Angst vor einem interaktiven Rebase zu haben.

    • Genau dafür lege ich oft Branches namens temp oder before-rebase an. Ich nutze sie wie Tags, aber Branches lassen sich bequemer verschieben, daher führe ich vor einem großen rebase -i normalerweise git branch before-rebase/some-feature aus.
      Tatsächlich musste ich git reset before-rebase kaum je verwenden, aber ich habe häufig git log -p before-rebase mit dem Branch nach dem Rebase verglichen, um unbeabsichtigte Änderungen beim Auflösen von Konflikten zu prüfen.
    • Man muss nicht einmal ein separates Sicherheits-Tag anlegen; das reflog übernimmt diese Rolle automatisch.
    • Nutzt du git reflog?
    • Einfacher als jj undo geht es nicht.
    • Stimmt. Nachdem ich git rebase eingegeben habe, gebe ich ungefähr in der Hälfte der Fälle direkt --abort ein.
  • Ich sehe mich selbst nicht als Coder oder Programmierer, aber Git zu lernen hat meinem Denken gefühlt eine Superkraft der Organisation gegeben. Ich nutze es für unzählige Zwecke: Websites, Designprojekte, Elektronik, Komposition, persönliche Wissensspeicher, Skripte zur Fernverwaltung, Konfigurationsverwaltung, Code-Schnipsel und mehr. Es ist nicht perfekt, aber ich empfehle allen, mit denen ich arbeite, es zu lernen.

    • Auch meine Fähigkeit zur Organisation hat sich deutlich verbessert, aber das ist eher ein Effekt von Versionsverwaltungssystemen allgemein als von Git selbst. Versionsverwaltungssysteme organisieren und bewahren das auf, was man erstellt, und erlauben es, jederzeit so viele Kopien herauszuziehen, wie man möchte; dadurch kann man sorgenfrei arbeiten.
      In David Allens Getting Things Done geht es darum, dass man ein vertrauenswürdiges System braucht, um Gedanken zu sammeln und zu organisieren. Für Aufgaben, Projekte und Listen nutze ich OmniFocus, aber beim Organisieren und Bewahren von Software und einigen Daten, die ich am Computer erstellt habe, vertraue ich Git.
    • Wie nutzt du Git zum Komponieren?
  • Ich mag diese Befehle theoretisch sehr und nutze auch die entsprechenden Befehle in jj ständig, konnte aber nicht herausfinden, wie git history die geänderten Commits signieren kann. Nachdem ich das Handbuch und den Git-Quellcode angesehen hatte, verschwinden alle Signaturen, wenn man einen bestehenden GPG-signierten Commit mit git history reword HEAD~ ändert.
    Weil ich die Commit-Signaturen beibehalten wollte, bin ich am Ende zum traditionellen git rebase -i zurückgekehrt.

    • Das wirkt wie eine große Lücke und scheint auch den Standardprozess zur Commit-Verarbeitung zu umgehen. Da auch das Handbuch git history als experimentelle Funktion bezeichnet, wäre es wohl einen Bugreport wert.
  • Auch ohne neuen Befehl habe ich seit über zehn Jahren die lokale Git-Commit-Historie ohne größere Schwierigkeiten massiv umgeschrieben. Ich bin überzeugt, dass Git und Rebase vor allem wegen eines User-Interface-Problems schwierig wirken: Man muss die exakten Kommandozeilenoptionen und deren Verhalten selbst herausfinden.
    Unter Windows war der Rebase-Dialog von TortoiseGit mächtig und zugleich leicht zu bedienen. Man klickt im Commit-Log mit der rechten Maustaste auf den Basis-Commit und wählt "Rebase onto this"; wie unter https://tortoisegit.org/docs/tortoisegit/tgit-dug-rebase.htm... sind das Umordnen, Aufteilen, Zusammenführen und Bearbeiten von Commits klar, und wenn man zwischendurch abbricht, kehrt alles in den ursprünglichen Zustand zurück.
    Da es allerdings als Shell-Erweiterung umgesetzt ist, gibt es zu viele Klicks; als eigenständige App mit Tabs wäre es besser. Das scheint aber ein Designprinzip zu sein, das von TortoiseSVN übernommen wurde. Git Extensions habe ich auch kurz ausprobiert, konnte aber häufig genutzte Funktionen nicht finden oder war bereits zu sehr an TortoiseGit gewöhnt, um dabei zu bleiben. Die Log-Ansicht sieht man unter https://tortoisegit.org/docs/tortoisegit/tgit-dug-showlog.ht....

    • Eines der Dinge, die ich nach dem Wechsel von Windows zu Linux vermisse, ist die integrierte Shell, in der jedes Programm Einträge zum Rechtsklickmenü von Dateien und Ordnern, Overlays wie die Status-Icons von TortoiseGit oder spezielle Objekte wie My Computer hinzufügen konnte. Meinem Verständnis nach erreichte das mit XP seinen Höhepunkt, und ab Vista änderte Microsoft es in eine Richtung, die Erweiterungen erschwerte.
      Allerdings verstehe ich bis heute nicht, warum irgendein Menü in VC++6 statt einer normalen Liste ein wie ein Ordner aussehendes Shell-Objekt erzeugte, das im Datei-öffnen-Dialog angezeigt wurde. Es war ein Assistent zum Hinzufügen von Komponenten zu einem Projekt, und während der Dialog geöffnet war, suchte man diesen Fake-Ordner unter My Computer.
    • Der bisherige Workflow war etwas umständlicher, und der neue Befehl ist tatsächlich bequemer. fixup ging bereits mit git commit --fixup oder davor mit git rebase -i, und reword war ebenfalls mit git rebase -i möglich.
      Offenbar hat sich genug Feedback angesammelt, dass das Bearbeiten einer Datei, um für jeden Commit die auszuführende Aktion festzulegen, unbequem ist, sodass ein höherstufiger Befehl ergänzt wurde, der die bestehende Funktionalität kapselt.
    • Unter den Git-User-Interfaces habe ich noch kein Tool gesehen, das an Magit herankommt. Es bietet zugleich tiefgehende Funktionen und ein vereinfachtes zustandsbehaftetes Interface.
      Bei Kommandozeilentools ist nach dem nächsten Befehl der Zugriff auf die vorherige Ausgabe erschwert. Wenn man einen in git log gesehenen Commit cherry-picken will, kann man den Hash nicht aus dem Gedächtnis eingeben, wenn er nicht mehr im Terminal steht; ein zustandsbehaftetes Interface kann dagegen mehrere Interaktionen parallel halten und frühere Informationen bewahren.
      Git hat sehr viele Zustandsinformationen, die man sich merken muss: den aktuellen Branch, den vorherigen Branch, geänderte Dateien, nicht gepushte Commits und mehr. Wenn man die umgebenden Werkzeuge wie Branch-Anzeige und Autovervollständigung in Shell-Plugins, mehrere Tabs und Fenster-Splits, Kopieren zwischen Tabs und Suche in der Autovervollständigung nicht beherrscht, kann die Git-Nutzung sehr unangenehm werden.
  • Die eigentliche Schwäche von jj sehe ich nur in der Git-Integration. Es formalisiert domänenspezifische Sprachen für File Sets, Revision Sets und Templates, nutzt sie konsistent in Konfiguration und CLI und stellt mächtige Konzepte als Interface bereit, ohne die interne Struktur direkt offenzulegen.
    Es ist aber so flexibel, dass die Git-Interoperabilität stark von Konventionen und Disziplin abhängt, und weil sich fast jeder jj-Zustand in Git ausdrücken lässt, kann man Remote-Repositories leicht beschädigen. Um das zu reparieren, muss man Git sehr gut verstehen.
    Für ein neues Einzelentwicklungsprojekt ist jj einen Versuch wert. Unter günstigen Bedingungen fühlt es sich an wie frühes Git im Vergleich zu Subversion; wenn man sich zuerst an die Eigenheiten von jj gewöhnt, ist man später auch etwas besser auf die schwierige Git-Integration vorbereitet. Kontext dazu gibt es unter https://news.ycombinator.com/item?id=48903054.

    • Grundsätzlich gibt es für jj auch Nicht-Git-Backends, aber als derzeit produktionsgeeignet ist ausdrücklich nur das Git-Backend gekennzeichnet. jj ohne Git-Integration ist noch eher Konzept als Realität.
      Ich mag jj und sein mentales Modell von Versionskontrolle und werde es weiter verwenden, aber weil ich gelegentlich auf Git hinuntersteigen muss, um Arbeit abzuschließen, fühlt es sich eher wie ein komfortabler Wrapper auf Git an als wie ein neues Versionsverwaltungssystem. Wenn Git Befehle ergänzt, die den jj-Workflow unterstützen, wird es schwer, noch einen Grund zu finden, jj separat zu installieren.
  • Die Art, wie git history fixup alle Nachfolger-Branches automatisch rebased, passt nicht zu meinem Workflow. Ich bewahre jede Version eines Feature-Branches als myfeature.1, myfeature.2 usw. auf, um bei verändertem Verhalten in einer neuen Version eine alte Version wiederzufinden; git reflog kann das nicht ersetzen.
    Wenn ein Problem zum Beispiel zwischen myfeature.58 und myfeature.59 entstanden ist und jedes Feature 15 Commits hat, kann man mit git range-diff myfeature.58~15..myfeature.58 myfeature.59~15..myfeature.59 die Unterschiede der beiden Versionen commitweise ansehen, selbst wenn die Basis-Commits unterschiedlich sind. Daher möchte ich keinen automatischen Rebase aller Branches, die einen bestimmten Commit enthalten, und auch der Ansatz, alte Versionen mit Tags zu markieren, passte für mich nicht gut.

  • Aktuelles Git hat drei häufig genutzte git rebase --interactive-Aufgaben als separate Befehle mit weniger Reibung umgesetzt; sie scheinen nur zu funktionieren, wenn es keine Konflikte gibt.

    • Ich bezweifle, dass git history wirklich nützlich ist. Ich bevorzuge es, Commits per interaktivem Rebase visuell zu verschieben, und möchte nicht versehentlich den Hash eines verwaisten Commits, der aus dem aktuellen lokalen Branch verschwunden ist, als Ziel für git history angeben.
  • Ich verstehe nicht, warum man sich so viel Mühe gibt, die Git-Historie perfekt aufzuräumen. Einzelne Commits liest man am Ende sowieso nicht noch einmal, also kann man vor dem Merge einfach alles squashen

    • Ich widerspreche entschieden. Wenn ich einen PR reviewe, lese ich zumindest die Commit-Historie, um den Ablauf der Änderungen nachzuvollziehen, und wenn sie voller "oops" und "fix" ist, lehne ich ihn sofort ab
      Commits erzählen die Geschichte der Arbeit, daher sollte man eine gut lesbare Commit-Historie liefern, damit Reviewer sich nicht zusätzlich abmühen müssen, die Absicht zu verstehen
    • Ich lese einzelne Commits häufig. In sorgfältig gepflegten Repositories ist das sehr nützlich, um Bugs oder Regressionen nachzuverfolgen und die Änderungsabsicht des Autors zu verstehen
    • Wenn man sich fragt, warum Code so geworden ist, hilft der Kontext des Commits, der diesen Code eingeführt hat, enorm. Wenn es ein riesiger Commit ist, der verschiedene Änderungen auf einmal enthält, sinkt dieser Wert deutlich
    • Das ist eine Sichtweise, an der man merkt, dass du nicht in einem großen Team arbeitest. Wenn ich aus dem Urlaub zurückkomme, lese ich als Erstes die Git-Historie, um zu sehen, was sich in der Zwischenzeit geändert hat, und auch wenn CI kaputt ist, ist sie nützlich; tatsächlich schaue ich mir die Historie in irgendeiner Form praktisch täglich an
      Wenn Codebasis und Funktionen sehr klein sind, kann man alles squashen, aber ich bevorzuge eine Vorgehensweise, bei der ich den tatsächlichen Änderungsverlauf sehen und Commits gezielt cherry-picken oder zurücksetzen kann
    • Eine sauber gepflegte Historie mit einem Commit pro logischer Änderung ist beim Bisecten von Regressionen äußerst nützlich
  • Ich hatte verstanden: Wenn aktuell D ausgecheckt ist und man B in B' ändert, lässt git rebase --update-refs das außerhalb des Rebase-Bereichs liegende E unverändert, erzeugt nur B' → C' → D' und behält das bestehende B → E bei
    Aktualisiert git history fixup dagegen auch E zu E'? Falls ja, frage ich mich, ob es mit dem seit Jahrzehnten in Fleisch und Blut übergegangenen git rebase ebenfalls eine Möglichkeit gibt, alle Nachkommen-Branches zu aktualisieren

    • Commit-Hashes berücksichtigen auch den Hash des Parent-Commits, daher kann E nicht unverändert bleiben. Ich habe --update-refs nicht selbst ausprobiert, aber der Dokumentation nach dürfte als Ergebnis der dritte Graph entstehen, also einschließlich E'; das gewünschte Verhalten ist damit bereits mit git rebase möglich
    • Siehe https://blog.hot-coffee.dev/en/blog/git_update_refs/