Verfolgen Sie einen anderen Zweig, wenn im aktuellen Zweig noch nicht festgeschriebene Änderungen vorliegen

Die meiste Zeit, wenn ich versuche, einen anderen existierenden Zweig auszuprobieren, lässt Git mich nicht zu, wenn ich noch nicht festgeschriebene Änderungen am aktuellen Zweig habe. Also muss ich diese Änderungen zuerst vornehmen oder speichern.

Gelegentlich erlaubt es Git mir jedoch, einen anderen Zweig auszuprobieren, ohne diese Änderungen zu begehen oder zu speichern, und er wird diese Änderungen in die Zweigstelle übernehmen, die ich auschecke.

Was ist die Regel hier? Ist es wichtig, ob die Änderungen inszeniert oder nicht gesetzt sind? Die Änderungen in einen anderen Zweig zu tragen macht keinen Sinn für mich, warum lässt es das manchmal zu? Das heißt, ist es in einigen Situationen hilfreich?

Vorbemerkungen

Die Beobachtung hier ist, dass, nachdem Sie anfangen, in branch1 (vergessen oder nicht zu erkennen, dass es gut wäre, zuerst zu einem anderen branch2 zu wechseln), Sie branch2 ausführen:

 git checkout branch2 

Manchmal sagt Git “OK, du bist jetzt auf Zweig2!” Manchmal sagt Git: “Ich kann das nicht, ich würde einige deiner Änderungen verlieren.”

Wenn es Git nicht erlaubt, müssen Sie Ihre Änderungen bestätigen, um sie dauerhaft zu speichern. Vielleicht möchten Sie git stash , um sie zu speichern; Dies ist eines der Dinge, für die es entwickelt wurde. Beachten Sie, dass git stash save tatsächlich “Commit all the changes” bedeutet, aber auf keinem Zweig überhaupt, dann entferne sie von wo ich jetzt bin. ” Das ermöglicht den Wechsel: Sie haben jetzt keine Änderungen in Bearbeitung. Du kannst sie dann nach dem Umschalten git stash apply .

Sie können hier aufhören zu lesen, wenn Sie möchten!

Wenn Git dich nicht wechseln lässt, hast du bereits ein Problem: benutze git stash oder git commit ; oder, wenn Ihre Änderungen einfach neu zu erstellen sind, verwenden Sie git checkout -f , um es zu erzwingen. Bei dieser Antwort geht es darum, wann Git dich an git checkout branch2 , obwohl du anfing, einige Änderungen vorzunehmen. Warum funktioniert es manchmal und nicht zu anderen Zeiten?

Die Regel hier ist in einer Hinsicht einfach und in einer anderen kompliziert / schwer zu erklären:

Sie können Zweige mit nicht festgeschriebenen Änderungen im Arbeitsbaum wechseln, wenn und nur dann, wenn diese Änderungen nicht unnötig geändert werden müssen.

Das ist – und bitte beachten Sie, dass dies noch vereinfacht wird; Es gibt einige besonders schwierige Fälle mit inszenierten git addbranch1 , git rmbranch1 und dergleichen – angenommen, Sie sind auf branch1 . Ein git checkout branch2 müsste dies tun:

  • branch1 für jede Datei, die sich in branch1 und nicht in branch2 befindet, diese Datei.
  • branch2 für jede Datei, die sich in branch2 und nicht in branch1 befindet, diese Datei (mit dem entsprechenden Inhalt).
  • Wenn sich die Version in branch2 für jede Datei in beiden Zweigen unterscheidet, aktualisieren Sie die Version der Arbeitsbaumstruktur.

Jeder dieser Schritte könnte etwas in Ihrem Arbeitsbaum überlisten:

  • Das Entfernen einer Datei ist “sicher”, wenn die Version in der Baumstruktur mit der festgeschriebenen Version in branch1 ; Es ist “unsicher”, wenn Sie Änderungen vorgenommen haben.
  • Eine Datei so zu erstellen, wie sie in branch2 erscheint, ist “sicher”, wenn sie jetzt nicht existiert. 2 Es ist “unsicher”, wenn es jetzt existiert, aber den “falschen” Inhalt hat.
  • Und natürlich ist das Ersetzen der Arbeitsbaumversion einer Datei durch eine andere Version “sicher”, wenn die Arbeitsbaumversion bereits an branch1 .

Das Erstellen einer neuen Verzweigung ( git checkout -b newbranch ) wird immer als “sicher” betrachtet: Im Rahmen dieses processes werden keine Dateien hinzugefügt, entfernt oder geändert, und auch der Index / Staging-Bereich bleibt unberührt. (Achtung: es ist sicher, wenn man einen neuen Zweig erstellt, ohne den Startpunkt des neuen Zweiges zu ändern; wenn man aber ein anderes Argument hinzufügt, zB git checkout -b newbranch different-start-point , muss dies möglicherweise geändert werden, um zu einem different-start-point git checkout -b newbranch different-start-point zu wechseln different-start-point . Git wendet dann wie üblich die Sicherheitsregeln für die Kasse an.)


1 Dies erfordert, dass wir definieren, was es bedeutet, dass eine Datei in einer Verzweigung ist, was wiederum erfordert, dass der Wortzweig richtig definiert wird. (Siehe auch: Was genau meinen wir mit “branch”? ) Was ich hier wirklich meine, ist der Commit, zu dem der Branch-Name verrechnet wird: Eine Datei mit Pfad P ist in branch1 wenn git rev-parse branch1: P erzeugt Hash. Diese Datei befindet sich nicht in ” branch1 wenn Sie stattdessen eine Fehlermeldung erhalten. Die Existenz von Pfad P in Ihrem Index oder Arbeitsbaum ist für die Beantwortung dieser speziellen Frage nicht relevant. Daher besteht das Geheimnis darin, das Ergebnis von git rev-parse auf jedem branch-name : path zu untersuchen branch-name : path . Dies schlägt entweder fehl, weil die Datei höchstens in einem Zweig “in” ist, oder gibt uns zwei Hash-IDs. Wenn die beiden Hash-IDs identisch sind , ist die Datei in beiden Zweigen identisch. Es ist keine Änderung erforderlich. Wenn sich die Hash-IDs unterscheiden, ist die Datei in den beiden Zweigen unterschiedlich und muss geändert werden, um Zweige zu wechseln.

Der Schlüssel hier ist, dass Dateien in Commits für immer eingefroren sind. Dateien, die Sie bearbeiten, sind natürlich nicht eingefroren. Wir schauen, zumindest anfangs, nur auf die Diskrepanz zwischen zwei eingefrorenen Commits. Leider müssen wir – oder Git – auch mit Dateien umgehen, die nicht im Commit enthalten sind, von dem Sie wechseln und in das Commit, zu dem Sie wechseln werden. Dies führt zu den verbleibenden Komplikationen, da Dateien auch im Index und / oder im Arbeitsbaum existieren können, ohne dass diese beiden speziellen eingefrorenen Commits existieren müssen, mit denen wir arbeiten.

2 Es kann als “ungefährlich” betrachtet werden, wenn es bereits mit den “richtigen Inhalten” existiert, so dass Git es nicht unbedingt erstellen muss. Ich erinnere mich an einige Versionen von Git, die dies erlauben, aber das Testen zeigt, dass Git in Git 1.8.5.4 als “unsicher” gilt. Das gleiche Argument würde für eine modifizierte Datei gelten, die so modifiziert wird, dass sie mit der Verzweigung, die zu wechseln ist, übereinstimmt. Auch hier sagt 1.8.5.4 nur “würde überschrieben werden”. Siehe auch das Ende der technischen Hinweise: Mein Speicher ist möglicherweise errorshaft, da ich glaube, dass sich die Regeln für den Lese-Baum seit meiner ersten Verwendung von Git in Version 1.5 nicht geändert haben.


Ist es wichtig, ob die Änderungen inszeniert oder nicht gesetzt sind?

Ja, in gewisser Weise. Insbesondere können Sie eine Änderung durchführen und dann die Arbeitsbaumdatei “abändern”. Hier ist eine Datei in zwei Zweigen, die in branch1 und branch2 :

 $ git show branch1:inboth this file is in both branches $ git show branch2:inboth this file is in both branches but it has more stuff in branch2 now $ git checkout branch1 Switched to branch 'branch1' $ echo 'but it has more stuff in branch2 now' >> inboth 

Zu diesem Zeitpunkt stimmt die inboth mit der in branch2 , obwohl wir uns auf branch1 . Diese Änderung wird nicht für das git status --short , was der git status --short hier zeigt:

 $ git status --short M inboth 

Das Leerzeichen-dann-M bedeutet “modifiziert, aber nicht inszeniert” (genauer gesagt, die Arbeitsbaum-Kopie unterscheidet sich von der gestuften / Index-Kopie).

 $ git checkout branch2 error: Your local changes ... 

OK, nun lasst uns die Arbeitsbaumkopie inszenieren, von der wir bereits wissen, dass sie der Kopie in branch2 .

 $ git add inboth $ git status --short M inboth $ git checkout branch2 Switched to branch 'branch2' 

Hier entsprachen die inszenierten und arbeitenden Kopien dem, was in branch2 , so dass das Auschecken erlaubt war.

Lass uns einen anderen Schritt versuchen:

 $ git checkout branch1 Switched to branch 'branch1' $ cat inboth this file is in both branches 

Die Änderung, die ich vorgenommen habe, geht jetzt vom Staging-Bereich verloren (weil Checkout über den Staging-Bereich schreibt). Dies ist ein bisschen ein Eckfall. Die Veränderung ist nicht weg, aber die Tatsache, dass ich es inszeniert habe, ist weg.

Lassen Sie uns eine dritte Variante der Datei inszenieren, die sich von der Zweigniederlassung unterscheidet, und legen Sie die Arbeitskopie so fest, dass sie der aktuellen Zweigversion entspricht:

 $ echo 'staged version different from all' > inboth $ git add inboth $ git show branch1:inboth > inboth $ git status --short MM inboth 

Die zwei M s bedeuten hier: Die abgelegte Datei unterscheidet sich von der HEAD Datei und die Arbeitsbaumdatei unterscheidet sich von der gestuften Datei. Die Arbeitsbaum-Version stimmt mit der Version von branch1 (aka HEAD ) branch1 :

 $ git diff HEAD $ 

Aber git checkout erlaubt das Auschecken nicht:

 $ git checkout branch2 error: Your local changes ... 

Lassen Sie uns die Version von branch2 als Arbeitsversion branch2 :

 $ git show branch2:inboth > inboth $ git status --short MM inboth $ git diff HEAD diff --git a/inboth b/inboth index ecb07f7..aee20fb 100644 --- a/inboth +++ b/inboth @@ -1 +1,2 @@ this file is in both branches +but it has more stuff in branch2 now $ git diff branch2 -- inboth $ git checkout branch2 error: Your local changes ... 

Auch wenn die aktuelle Arbeitskopie mit der in der Datei branch2 , ist dies bei der gestaffelten Datei nicht der Fall, so dass ein git checkout diese Kopie verlieren würde, und der git checkout wird abgelehnt.

Technische Hinweise – nur für wahnsinnig Neugierige 🙂

Der zugrundeliegende Implementierungsmechanismus für all dies ist Git’s Index . Der Index, der auch als “Staging-Bereich” bezeichnet wird, ist der Ort, an dem Sie den nächsten Commit erstellen: Er beginnt mit dem aktuellen Commit, dh mit dem, was Sie jetzt ausgecheckt haben, und ersetzt dann jedes Mal, wenn git add eine Datei git add Index-Version mit allem, was Sie in Ihrem Arbeitsbaum haben.

Denken Sie daran, dass Sie in Ihrem Arbeitsbaum an Ihren Dateien arbeiten. Hier haben sie ihre normale Form und nicht irgendeine spezielle Nur-Nützlich-zu-Git-Form, wie sie es in Commits und im Index macht. Sie extrahieren also eine Datei von einem Commit über den Index und dann in den Arbeitsbaum. Nachdem Sie es git add Sie es dem Index hinzu. Es gibt also drei Stellen für jede Datei: das aktuelle Commit, den Index und den Arbeitsbaum.

Wenn Sie gut git checkout branch2 , dann tut Git unter dem git checkout branch2 den Vergleich zwischen dem Tipp-Commit von branch2 und dem aktuellen Commit und dem Index. Jede Datei, die zu dem passt, was jetzt da ist, kann Git alleine lassen. Es ist alles unberührt. Jede Datei, die in beiden Commits gleich ist , kann Git auch alleine lassen – und das sind diejenigen, die es Ihnen erlauben, zwischen Zweigen zu wechseln.

Ein Großteil von Git, einschließlich Commit-Switching, ist aufgrund dieses Index relativ schnell. Was eigentlich im Index steht, ist nicht jede Datei selbst, sondern der Hash jeder Datei. Die Kopie der Datei selbst wird in dem Repository gespeichert, in dem Git ein Blob-Objekt aufruft. Dies ist ähnlich wie die Dateien in Commits gespeichert werden: Commits enthalten nicht wirklich die Dateien , sie leiten Git nur zur Hash-ID jeder Datei. So kann Git Hash-IDs – derzeit 160 Byte lange Zeichenfolgen – vergleichen, um zu entscheiden, ob die Commits X und Y die gleiche Datei haben oder nicht. Er kann diese Hash-IDs dann auch mit der Hash-ID im Index vergleichen.

Dies führt zu allen oben genannten Fällen. Wir haben X und Y , die beide Dateipfad path/to/name.txt , und einen Indexeintrag für path/to/name.txt . Vielleicht stimmen alle drei Hashes überein. Vielleicht passen zwei von ihnen zusammen und einer nicht. Vielleicht sind alle drei verschieden. Und wir könnten auch eine another/file.txt , die nur in X oder nur in Y ist und jetzt im Index ist oder nicht. Jeder dieser verschiedenen Fälle erfordert eine eigene Überlegung: Muss Git die Datei von Commit zu Index kopieren oder aus Index entfernen, um von X zu Y zu wechseln? Wenn dies der Fall ist, muss auch die Datei in den Arbeitsbaum kopiert oder aus dem Arbeitsbaum entfernt werden. Und wenn das der Fall ist, sollten die Index- und Arbeitsbaumversionen besser mit mindestens einer der festgeschriebenen Versionen übereinstimmen; ansonsten wird Git einige Daten verraten.

(Die vollständigen Regeln für all das sind in der Dokumentation zu git checkout , wie Sie vielleicht erwarten würden, aber eher in der git read-tree Dokumentation, unter dem Abschnitt mit dem Titel “Two Tree Merge” .)

Sie haben zwei Möglichkeiten: Ihre Änderungen speichern:

 git stash 

dann später, um sie zurückzubekommen:

 git stash apply 

Oder setzen Sie Ihre Änderungen auf einen Zweig, damit Sie die Remote-Verzweigung abrufen und anschließend Ihre Änderungen zusammenführen können. Das ist einer der größten Vorteile von git: Sie können einen Zweig erstellen, sich an ihn binden und dann andere Änderungen in den Zweig holen, in dem Sie sich befanden.

Du sagst, es macht keinen Sinn, aber du machst es nur so, dass du sie nach Belieben zusammenfügen kannst. Offensichtlich ist Ihre andere Wahl, auf Ihrer Kopie des Zweigs festzulegen und dann den Zug zu machen. Die Vermutung ist, dass Sie das entweder nicht wollen (in diesem Fall bin ich verwirrt, dass Sie keine Filiale wollen) oder Sie haben Angst vor Konflikten.

Wenn die neue Verzweigung Bearbeitungen enthält, die sich von der aktuellen Verzweigung für die betreffende geänderte Datei unterscheiden, können Sie die Verzweigungen erst dann wechseln, wenn die Änderung festgeschrieben oder gespeichert wurde. Wenn die geänderte Datei in beiden Zweigen identisch ist (dh die festgeschriebene Version dieser Datei), können Sie frei wechseln.

Beispiel:

 $ echo 'hello world' > file.txt $ git add file.txt $ git commit -m "adding file.txt" $ git checkout -b experiment $ echo 'goodbye world' >> file.txt $ git add file.txt $ git commit -m "added text" # experiment now contains changes that master doesn't have # any future changes to this file will keep you from changing branches # until the changes are stashed or committed $ echo "and we're back" >> file.txt # making additional changes $ git checkout master error: Your local changes to the following files would be overwritten by checkout: file.txt Please, commit your changes or stash them before you can switch branches. Aborting 

Dies gilt sowohl für nicht verfolgte als auch für verfolgte Dateien. Hier ist ein Beispiel für eine nicht verfolgte Datei.

Beispiel:

 $ git checkout -b experimental # creates new branch 'experimental' $ echo 'hello world' > file.txt $ git add file.txt $ git commit -m "added file.txt" $ git checkout master # master does not have file.txt $ echo 'goodbye world' > file.txt $ git checkout experimental error: The following untracked working tree files would be overwritten by checkout: file.txt Please move or remove them before you can switch branches. Aborting 

Ein gutes Beispiel dafür, warum Sie sich während der Änderungen zwischen den Zweigen bewegen möchten, wäre, wenn Sie einige Experimente mit dem Master durchführen würden, die Sie übernehmen wollten, aber noch nicht beherrschen.

 $ echo 'experimental change' >> file.txt # change to existing tracked file # I want to save these, but not on master $ git checkout -b experiment M file.txt Switched to branch 'experiment' $ git add file.txt $ git commit -m "possible modification for file.txt"