Kasa innego oddziału, gdy w bieżącym oddziale występują niezatwierdzone zmiany

349

Przez większość czasu, gdy próbuję wyewidencjonować inną istniejącą gałąź, Git nie pozwala mi, jeśli mam pewne niezatwierdzone zmiany w bieżącej gałęzi. Więc najpierw muszę zatwierdzić lub ukryć te zmiany.

Czasami jednak Git pozwala mi pobrać kolejną gałąź bez dokonywania lub ukrywania tych zmian i przenosi te zmiany do gałęzi, którą kasuję.

Jaka jest tutaj reguła? Czy ma znaczenie, czy zmiany są wprowadzane etapowo, czy nie? Przenoszenie zmian do innej gałęzi nie ma dla mnie żadnego sensu, dlaczego czasami git na to pozwala? Czy to jest pomocne w niektórych sytuacjach?

Xufeng
źródło

Odpowiedzi:

350

Uwagi wstępne

Obserwacja polega na tym, że po rozpoczęciu pracy branch1(zapominając lub nie wiedząc, że dobrze byłoby branch2najpierw przejść do innej gałęzi ), uruchomisz:

git checkout branch2

Czasami Git mówi „OK, jesteś teraz na branch2!” Czasami Git mówi: „Nie mogę tego zrobić, straciłbym niektóre z twoich zmian”.

Jeśli Git nie pozwoli ci tego zrobić, musisz zatwierdzić zmiany, aby zapisać je gdzieś na stałe. Możesz użyć ich git stashdo zapisania; jest to jedna z rzeczy, do których jest przeznaczona. Zauważ, że git stash savelub git stash pushfaktycznie oznacza „Zatwierdź wszystkie zmiany, ale w żadnym oddziale w ogóle, a następnie usuń je z miejsca, w którym teraz jestem”. Umożliwia to zmianę: teraz nie masz żadnych zmian w toku. Możesz je potem git stash applypo zmianie.

Pasek boczny: git stash saveto stara składnia; git stash pushzostał wprowadzony w Git w wersji 2.13, aby naprawić niektóre problemy z argumentami git stashi pozwolić na nowe opcje. Oba robią to samo, gdy są używane w podstawowy sposób.

Możesz przestać czytać tutaj, jeśli chcesz!

Jeśli Git nie pozwoli ci się zmienić, masz już lekarstwo: użyj git stashlub git commit; lub, jeśli zmiany są łatwe do odtworzenia, użyj, git checkout -faby je wymusić. Ta odpowiedź dotyczy tego, kiedy Git pozwoli ci, git checkout branch2mimo że zacząłeś wprowadzać zmiany. Dlaczego to działa czasami , a nie innym razem?

Zasada tutaj jest prosta w jeden sposób, a skomplikowana / trudna do wyjaśnienia w inny sposób:

Możesz przełączać gałęzie z niezatwierdzonymi zmianami w drzewie roboczym tylko wtedy, gdy wspomniane przełączanie nie wymaga usuwania tych zmian.

To znaczy - i proszę zauważyć, że jest to nadal uproszczone; są pewne wyjątkowo trudne skrzynie narożne ze scenami git adds, git rms i takimi - załóżmy, że jesteś włączony branch1. A git checkout branch2musiałby to zrobić:

  • Dla każdego pliku, który jest w, branch1a nie w branch2, 1 usuń ten plik.
  • Dla każdego pliku, który jest w branch2a nie w branch1utworzyć ten plik (z odpowiednią zawartością).
  • Jeśli dla każdego pliku w obu gałęziach branch2jest inna wersja , zaktualizuj działającą wersję drzewa.

Każdy z tych kroków może zablokować coś w twoim drzewie roboczym:

  • Usunięcie pliku jest „bezpieczne”, jeśli wersja w drzewie roboczym jest taka sama, jak wersja zatwierdzona w branch1; jest to „niebezpieczne”, jeśli dokonałeś zmian.
  • Utworzenie pliku tak, jak wygląda, branch2jest „bezpieczne”, jeśli nie istnieje teraz. 2 Jest „niebezpieczny”, jeśli już istnieje, ale ma „niewłaściwą” treść.
  • I oczywiście zastąpienie wersji drzewa roboczego inną wersją jest „bezpieczne”, jeśli wersja drzewa roboczego jest już zobowiązana branch1.

Utworzenie nowej gałęzi ( git checkout -b newbranch) jest zawsze uważane za „bezpieczne”: żadne pliki nie będą dodawane, usuwane ani zmieniane w drzewie roboczym w ramach tego procesu, a także indeks / obszar pomostowy pozostaje nietknięty. (Zastrzeżenie: jest bezpieczne podczas tworzenia nowej gałęzi bez zmiany punktu początkowego nowej gałęzi; ale jeśli dodasz inny argument, np. git checkout -b newbranch different-start-pointMoże to wymagać zmiany rzeczy, aby przejść do different-start-point. Git zastosuje reguły bezpieczeństwa przy kasie jak zwykle .)


1 Wymaga to zdefiniowania, co oznacza plik znajdujący się w gałęzi, co z kolei wymaga prawidłowego zdefiniowania gałęzi słowa . (Patrz również co dokładnie rozumiemy przez „oddział”? ) Oto, co mam na myśli to commit do którego postanawia branch-name: plik, którego ścieżka jest w razie produkuje hash. Ten plik nie jest w jeśli pojawi się komunikat o błędzie zamiast. Istnienie ścieżki w twoim indeksie lub drzewie roboczym nie ma znaczenia, gdy odpowiadasz na to konkretne pytanie. Zatem sekretem jest tutaj zbadanie wyniku każdego z nichP branch1git rev-parse branch1:Pbranch1Pgit rev-parsebranch-name:path. To się nie udaje, ponieważ plik znajduje się „w” co najwyżej jednej gałęzi lub daje nam dwa identyfikatory skrótu. Jeśli dwa identyfikatory skrótu są takie same , plik jest taki sam w obu gałęziach. Zmiana nie jest wymagana. Jeśli identyfikatory skrótów różnią się, plik jest inny w dwóch gałęziach i należy go zmienić, aby zmienić gałęzie.

Kluczowym pojęciem jest to, że pliki w commits są zamrażane na zawsze. Pliki, które będziesz edytować, oczywiście nie są zamrożone. Przynajmniej na początku patrzymy tylko na niedopasowania między dwoma zamrożonymi zobowiązaniami. Niestety, my - lub Git - mamy również do czynienia z plikami, które nie znajdują się w zatwierdzeniu, z którego chcesz się przełączyć, i w zatwierdzeniu, na które chcesz się przełączyć. Prowadzi to do pozostałych komplikacji, ponieważ pliki mogą również istnieć w indeksie i / lub w drzewie roboczym, bez konieczności istnienia tych dwóch zamrożonych zatwierdzeń, z którymi pracujemy.

2 Może być uważany za „trochę bezpieczny”, jeśli już istnieje z „właściwą treścią”, tak że Git nie musi go jednak tworzyć. Przywołuję przynajmniej niektóre wersje Gita, które na to pozwalają, ale testowanie właśnie teraz pokazuje, że jest to uważane za „niebezpieczne” w Git 1.8.5.4. Ten sam argument dotyczyłby zmodyfikowanego pliku, który akurat jest modyfikowany, aby pasował do gałęzi „przełącz się na”. Znowu 1.8.5.4 mówi tylko, że „zostanie zastąpiony”. Zobacz także koniec uwag technicznych: moja pamięć może być wadliwa, ponieważ nie sądzę, aby reguły drzewa odczytu zmieniły się, odkąd zacząłem używać Gita w wersji 1.5.


Czy ma znaczenie, czy zmiany są wprowadzane etapowo, czy nie?

Tak, pod pewnymi względami. W szczególności możesz wprowadzić zmiany, a następnie „odinstalować” plik drzewa roboczego. Oto plik w dwóch oddziałów, to różnią się branch1i 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

W tym momencie działający plik drzewa jest inbothzgodny z plikiem w branch2, mimo że jesteśmy włączeni branch1. Ta zmiana nie jest wprowadzana dla zatwierdzenia, co git status --shortpokazano tutaj:

$ git status --short
 M inboth

Spacja następnie M oznacza „zmodyfikowany, ale nie przemieszczany” (a ściślej mówiąc, kopia drzewa roboczego różni się od kopii przemieszczanej / indeksowanej).

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

OK, przygotujmy teraz kopię drzewa roboczego, w której wiemy, że pasuje również do kopii branch2.

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

Tutaj zarówno zainscenizowane, jak i robocze kopie pasowały do ​​tego, co było w branch2środku, więc kasa była dozwolona.

Spróbujmy kolejnego kroku:

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

Wprowadzona przeze mnie zmiana została teraz utracona z obszaru przeciwności (ponieważ kasa zapisuje przez obszar przeciwności). To trochę narożna sprawa. Zmiana nie ma, ale fakt, że miałem go wystawił, to już nie ma.

Zróbmy trzeci wariant pliku, inny niż kopia gałęzi, a następnie ustaw kopię roboczą, aby pasowała do bieżącej wersji gałęzi:

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

Te dwa Moznaczają tutaj: plik przemieszczany różni się od HEADpliku, a plik drzewa roboczego różni się od pliku przemieszczanego. Wersja drzewa roboczego jest zgodna z wersją branch1(aka HEAD):

$ git diff HEAD
$

Ale git checkoutnie zezwoli na kasę:

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

Ustawmy branch2wersję jako działającą:

$ 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 ...

Chociaż bieżąca kopia robocza jest zgodna z tą w branch2, plik przemieszczany nie, więc a git checkoutstraci tę kopię i git checkoutzostanie odrzucony.

Uwagi techniczne - tylko dla niesamowicie ciekawskich :-)

Podstawowym mechanizmem implementacji tego wszystkiego jest indeks Git . Indeks, zwany także „obszarem przejściowym”, jest miejscem, w którym budujesz następny zatwierdzenie: zaczyna się od bieżącego zatwierdzenia, tj. Niezależnie od tego, co wyewidencjonowałeś, a następnie za każdym razem, gdy tworzysz git addplik, zastępujesz wersję indeksu z tym, co masz w swoim drzewie roboczym.

Pamiętaj, że drzewo robocze to miejsce, w którym pracujesz nad plikami. Tutaj mają swoją normalną formę, a nie jakąś specjalną, użyteczną tylko dla Gita formę, taką jak w commits i indeksie. Tak więc wyodrębniasz plik z zatwierdzenia, poprzez indeks, a następnie do drzewa roboczego. Po zmianie, ty git adddo indeksu. Istnieją więc trzy miejsca dla każdego pliku: bieżący zatwierdzenie, indeks i drzewo robocze.

Po uruchomieniu git checkout branch2Git pod pokrywami porównuje zatwierdzenie końcówki z tym, branch2co jest teraz w bieżącym zatwierdzeniu i indeksie. Każdy plik, który pasuje do tego, co jest teraz, Git może zostawić w spokoju. Wszystko jest nietknięte. Każdy plik, który jest taki sam w obu zatwierdzeniach , Git może również zostawić w spokoju - i to one umożliwiają zmianę gałęzi.

Znaczna część Git, w tym przełączanie zmian , jest stosunkowo szybka z powodu tego indeksu. W indeksie nie znajduje się sam plik, ale skrót każdego pliku . Kopia samego pliku jest przechowywana w repozytorium jako to, co Git nazywa obiektem obiektu blob . Jest to podobne do sposobu przechowywania plików również w commits: commits tak naprawdę nie zawierają plików , po prostu prowadzą Git do identyfikatora skrótu każdego pliku. Dzięki temu Git może porównywać identyfikatory skrótów - obecnie ciągi o długości 160 bitów - aby zdecydować, czy zatwierdzenia X i Y mają ten sam plik, czy nie. Następnie może porównać te identyfikatory skrótu z identyfikatorem skrótu w indeksie.

To właśnie prowadzi do wszystkich powyższych przypadków narożników nieparzystych. Mamy zatwierdzenia X i Y, które oba mają plik path/to/name.txt, i mamy wpis indeksu dla path/to/name.txt. Może wszystkie trzy skróty pasują do siebie. Może dwa z nich pasują, a jeden nie. Może wszystkie trzy są różne. Możemy też mieć another/file.txtto tylko w X lub tylko w Y i jest teraz w indeksie lub nie. Każdy z tych różnych przypadków wymaga odrębnego rozpatrzenia: czy Git musi skopiować plik z zatwierdzenia do indeksu lub usunąć go z indeksu, aby przełączyć się z X na Y ? Jeśli tak, to również musiskopiuj plik do drzewa roboczego lub usuń go z drzewa roboczego. W takim przypadku wersje indeksu i drzewa roboczego lepiej pasują co najmniej do jednej z zatwierdzonych wersji; w przeciwnym razie Git zablokuje niektóre dane.

(Kompletne reguły tego wszystkiego są opisane w git checkoutdokumentacji nie w sposób, w jaki można się spodziewać, ale w git read-treedokumentacji, w części zatytułowanej „Scalanie dwóch drzew” .)

Trek
źródło
3
... istnieje również git checkout -mfunkcja, która łączy twoje środowisko pracy i indeksuje zmiany w nowej kasie.
jthill
1
Dzięki za to doskonałe wyjaśnienie! Ale gdzie mogę znaleźć informacje w oficjalnych dokumentach? A może są niekompletne? Jeśli tak, to jakie jest wiarygodne odniesienie do git (mam nadzieję, że inny niż jego kod źródłowy)?
maks.
1
(1) nie możesz i (2) kod źródłowy. Głównym problemem jest to, że Git stale się rozwija. Na przykład w tej chwili istnieje duży nacisk na ulepszenie lub porzucenie SHA-1 za pomocą lub na korzyść SHA-256. Ta szczególna część Git jest jednak dość stabilna przez długi czas, a podstawowy mechanizm jest prosty: Git porównuje bieżący indeks z bieżącymi i docelowymi zatwierdzeniami i decyduje, które pliki zmienić (jeśli w ogóle) na podstawie zatwierdzenia docelowego , a następnie testuje „czystość” plików drzewa roboczego, jeśli pozycja indeksu wymaga wymiany.
torek
6
Krótka odpowiedź: istnieje zasada, ale przeciętny użytkownik nie ma w ogóle nadziei na zrozumienie, nie mówiąc już o zapamiętywaniu, dlatego zamiast polegać na narzędziu do inteligentnego zachowania, powinieneś polegać na zdyscyplinowanej konwencji sprawdzania tylko, kiedy obecny oddział jest zaangażowany i czysty. Nie rozumiem, w jaki sposób odpowiada to na pytanie, kiedy przydałoby się przenieść wybitne zmiany do innej gałęzi, ale mogłem to przegapić, ponieważ staram się to zrozumieć.
Neutrino,
2
@HawkeyeParker: ta odpowiedź została poddana licznym edycjom i nie jestem pewien, czy któraś z nich znacznie ją poprawiła, ale spróbuję dodać coś o tym, co oznacza, że ​​plik może być „w gałęzi”. Ostatecznie będzie to chybotliwe, ponieważ pojęcie „gałąź” tutaj nie jest właściwie zdefiniowane w pierwszej kolejności, ale to kolejny element.
torek
50

Masz dwie możliwości: ukryć swoje zmiany:

git stash

a później, aby je odzyskać:

git stash apply

lub umieść zmiany w gałęzi, aby uzyskać gałąź zdalną, a następnie scalić z nią zmiany. To jedna z największych zalet git: możesz utworzyć gałąź, zatwierdzić ją, a następnie pobrać inne zmiany w gałęzi, w której byłeś.

Mówisz, że to nie ma żadnego sensu, ale robisz to tylko po to, abyś mógł je dowolnie połączyć po pociągnięciu. Oczywiście innym wyborem jest zatwierdzenie kopii oddziału, a następnie wykonanie ściągnięcia. Zakłada się, że albo nie chcesz tego zrobić (w tym przypadku jestem zaskoczony, że nie chcesz oddziału), albo boisz się konfliktów.

Obrabować
źródło
1
Czy to nie jest poprawne polecenie git stash apply? tutaj doktorzy.
Thomas8
1
Właśnie tego szukałem, aby tymczasowo przejść do różnych gałęzi, wyszukać coś i wrócić do tego samego stanu gałęzi, nad którym pracuję. Dzięki, Rob!
Naishta,
1
Tak, to jest właściwy sposób, aby to zrobić. Doceniam szczegół w przyjętej odpowiedzi, ale to sprawia, że ​​wszystko jest trudniejsze, niż musi być.
Michael Leonard,
5
Ponadto, jeśli nie masz potrzeby trzymania skrytki w pobliżu, możesz jej użyć, git stash popa jeśli z powodzeniem zastosuje, usunie skrytkę z listy.
Michael Leonard
1
lepsze wykorzystanie git stash pop, chyba że masz zamiar prowadzić dziennik zapasów w historii repo
Damilola Olowookere
14

Jeśli nowa gałąź zawiera zmiany, które różnią się od bieżącej gałęzi dla tego konkretnego zmienionego pliku, nie pozwoli ona na zmianę gałęzi, dopóki zmiana nie zostanie zatwierdzona lub zablokowana. Jeśli zmieniony plik jest taki sam w obu gałęziach (czyli zatwierdzonej wersji tego pliku), możesz swobodnie się przełączać.

Przykład:

$ 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

Dotyczy to zarówno nieśledzonych plików, jak i plików śledzonych. Oto przykład nieśledzonego pliku.

Przykład:

$ 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

Dobrym przykładem tego, dlaczego CHCESZ przemieszczać się między gałęziami, wprowadzając zmiany, byłoby, gdybyś przeprowadzał pewne eksperymenty na masterie, chciałem je zatwierdzić, ale jeszcze nie opanować ...

$ 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"
Gordolio
źródło
Właściwie wciąż tego nie rozumiem. W pierwszym przykładzie, po dodaniu „i wróciliśmy”, napisano, że zmiana lokalna zostanie nadpisana, jaka dokładnie zmiana lokalna? „i wróciliśmy”? Dlaczego git po prostu nie wprowadzi tej zmiany do master, aby w master plik zawierał „hello world” i „i wróciliśmy”
Xufeng
W pierwszym przykładzie mistrz popełnił tylko „witaj świecie”. w eksperymencie zostało popełnione polecenie „witaj świecie”. Aby zmiana gałęzi mogła nastąpić, plik.txt musi zostać zmodyfikowany, problem polega na tym, że wprowadzono niezatwierdzone zmiany „witaj świecie \ stary świat \ n i wróciliśmy”.
Gordolio
1

Poprawna odpowiedź to

git checkout -m origin/master

Łączy zmiany z głównej gałęzi pochodzenia z lokalnymi, nawet niezatwierdzonymi zmianami.

JD1731
źródło
0

Jeśli nie chcesz, aby te zmiany zostały w ogóle zatwierdzone, zrób to git reset --hard.

Następnie możesz przejść do wybranej gałęzi, ale pamiętaj, że niezatwierdzone zmiany zostaną utracone.

Kacpero
źródło