Note preliminari
L'osservazione qui è che, dopo aver iniziato a lavorare branch1
(dimenticando o non rendendosi conto che sarebbe bene passare prima a un ramo diverso branch2
), esegui:
git checkout branch2
A volte Git dice "OK, ora sei su branch2!" A volte, Git dice "Non posso farlo, perderei alcuni dei tuoi cambiamenti".
Se Git non ti consente di farlo, devi impegnare le tue modifiche per salvarle in un posto permanente. Potresti voler usare git stash
per salvarli; questa è una delle cose per cui è stata progettata. Si noti che git stash save
o git stash push
effettivamente significa "Commit tutti i cambiamenti, ma in nessun ramo del tutto, poi rimuoverle dal punto in cui sono ora." Ciò rende possibile il passaggio: ora non ci sono cambiamenti in corso. È quindi possibile git stash apply
dopo il passaggio.
Barra laterale: git stash save
è la vecchia sintassi; git stash push
è stato introdotto in Git versione 2.13, per risolvere alcuni problemi con gli argomentigit stash
e consentire nuove opzioni. Entrambi fanno la stessa cosa, se usati nei modi di base.
Puoi smettere di leggere qui, se vuoi!
Se Git non ti consente di cambiare, hai già un rimedio: usa git stash
o git commit
; oppure, se le tue modifiche sono banali da ricreare, usa git checkout -f
per forzarle. Questa risposta riguarda quando Git ti consentirà git checkout branch2
anche se hai iniziato ad apportare alcune modifiche. Perché a volte funziona e non altre volte?
La regola qui è semplice in un modo e complicata / difficile da spiegare in un altro:
È possibile cambiare i rami con modifiche senza commit nell'albero di lavoro se e solo se detta commutazione non richiede il clobbering di tali cambiamenti.
Questo è — e per favore nota che questo è ancora semplificato; ci sono alcuni casi angolari extra-difficili con scenografie git add
, scenografie e cose git rm
del genere - supponiamo che tu sia su branch1
. A git checkout branch2
dovrebbe fare questo:
- Per ogni file che è in
branch1
e non è in branch2
, 1 rimuovere quel file.
- Per ogni file che è in
branch2
e non è inbranch1
, creare quel file (con contenuti appropriati).
- Per ogni file che si trova in entrambi i rami, se la versione in
branch2
è diversa, aggiornare la versione dell'albero funzionante.
Ognuno di questi passaggi potrebbe ostruire qualcosa nel tuo albero di lavoro:
- La rimozione di un file è "sicura" se la versione nella struttura di lavoro è la stessa della versione di cui è stato eseguito il commit
branch1
; è "non sicuro" se hai apportato modifiche.
- La creazione di un file nel modo in cui appare
branch2
è "sicura" se non esiste ora. 2 È "pericoloso" se esiste ora ma ha contenuti "sbagliati".
- E, naturalmente, sostituire la versione dell'albero di lavoro di un file con una versione diversa è "sicuro" se la versione dell'albero di lavoro è già impegnata
branch1
.
La creazione di un nuovo ramo ( git checkout -b newbranch
) è sempre considerata "sicura": nessun file verrà aggiunto, rimosso o alterato nell'albero di lavoro come parte di questo processo e anche l'area di indice / stadiazione non viene toccata. (Avvertenza: è sicuro quando si crea un nuovo ramo senza cambiare il punto di partenza del nuovo ramo; ma se si aggiunge un altro argomento, ad esempio, git checkout -b newbranch different-start-point
potrebbe essere necessario cambiare le cose, passare a different-start-point
. Git applicherà quindi le regole di sicurezza del checkout come al solito .)
1 Ciò richiede che definiamo cosa significa che un file si trova in un ramo, che a sua volta richiede di definire correttamente la parola ramo . (Vedi anche Che cosa si intende per "succursale"? ) Ecco, quello che voglio dire è l'impegno a cui la succursale-nome viene risolto: un file il cui percorso è è in se produce un hash. Quel file non è presente se viene visualizzato un messaggio di errore. L'esistenza del percorso nel tuo indice o albero di lavoro non è rilevante quando rispondi a questa particolare domanda. Pertanto, il segreto qui è esaminare il risultato diP
branch1
git rev-parse branch1:P
branch1
P
git rev-parse
ciascunobranch-name:path
. Ciò non riesce perché il file è "in" al massimo in un ramo o ci fornisce due ID hash. Se i due ID hash sono uguali , il file è lo stesso in entrambi i rami. Non è richiesto alcun cambiamento. Se gli ID hash differiscono, il file è diverso nei due rami e deve essere cambiato per cambiare ramo.
L'idea chiave qui è che i file nei commit sono bloccati per sempre. I file si modifica sono ovviamente non congelato. Almeno inizialmente stiamo osservando solo le discrepanze tra due commit congelati. Sfortunatamente, anche noi, o Git, abbiamo a che fare con file che non sono presenti nel commit a cui stai per passare e che sono nel commit a cui stai per passare. Ciò porta alle restanti complicazioni, poiché i file possono anche esistere nell'indice e / o nell'albero di lavoro, senza che debbano esistere questi due particolari commit congelati con cui stiamo lavorando.
2 Potrebbe essere considerato "sicuro" se esiste già con il "giusto contenuto", quindi Git non deve crearlo dopo tutto. Ricordo almeno alcune versioni di Git che lo consentivano, ma i test appena mostrati lo considerano "non sicuro" in Git 1.8.5.4. Lo stesso argomento si applicherebbe a un file modificato che risulta modificato per corrispondere al ramo to-be-switch-to. Ancora una volta, 1.8.5.4 dice solo "verrebbe sovrascritto", però. Vedi anche la fine delle note tecniche: la mia memoria potrebbe essere difettosa perché non penso che le regole dell'albero di lettura siano cambiate da quando ho iniziato a usare Git alla versione 1.5. Qualcosa.
Ha importanza se le modifiche sono in scena o non in scena?
Sì, per certi versi. In particolare, è possibile effettuare una modifica, quindi "deselezionare" il file dell'albero di lavoro. Ecco un file in due rami, che è diverso in branch1
e 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
A questo punto, il file dell'albero di lavoro inboth
corrisponde a quello in branch2
, anche se siamo attivi branch1
. Questa modifica non viene gestita per il commit, che è ciò che git status --short
mostra qui:
$ git status --short
M inboth
Lo spazio-allora-M significa "modificato ma non in scena" (o più precisamente, la copia dell'albero di lavoro differisce dalla copia in scena / indice).
$ git checkout branch2
error: Your local changes ...
OK, ora mettiamo in scena la copia dell'albero di lavoro, che sappiamo già corrisponde anche alla copia branch2
.
$ git add inboth
$ git status --short
M inboth
$ git checkout branch2
Switched to branch 'branch2'
Qui le copie in scena e funzionanti corrispondevano entrambe a ciò che si trovava branch2
, quindi la cassa era consentita.
Proviamo un altro passo:
$ git checkout branch1
Switched to branch 'branch1'
$ cat inboth
this file is in both branches
La modifica che ho apportato ora viene persa dall'area di gestione temporanea (poiché il checkout scrive nell'area di gestione temporanea). Questo è un po 'un caso angolare. Il cambiamento non è andato, ma il fatto che l'ho messo in scena è sparito.
Mettiamo in scena una terza variante del file, diversa da una copia del ramo, quindi impostiamo la copia di lavoro in modo che corrisponda alla versione corrente del ramo:
$ echo 'staged version different from all' > inboth
$ git add inboth
$ git show branch1:inboth > inboth
$ git status --short
MM inboth
I due M
s qui significano: il file messo in scena differisce dal HEAD
file e il file dell'albero di lavoro differisce dal file messo in scena. La versione dell'albero di lavoro corrisponde alla versione branch1
(aka HEAD
):
$ git diff HEAD
$
Ma git checkout
non consentirà il pagamento:
$ git checkout branch2
error: Your local changes ...
Impostiamo la branch2
versione come versione funzionante:
$ 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 ...
Anche se la copia di lavoro corrente corrisponde a quella in branch2
, il file a fasi non lo fa, quindi git checkout
si perderebbe quella copia e git checkout
verrà rifiutata.
Note tecniche — solo per i follemente curiosi :-)
Il meccanismo di implementazione sottostante per tutto questo è l' indice di Git . L'indice, chiamato anche "area di gestione temporanea", è il punto in cui viene creato il commit successivo : inizia a corrispondere al commit corrente, ovvero a qualsiasi cosa sia stata estratta ora, quindi ogni volta che si esegue git add
un file, si sostituisce la versione dell'indice con qualunque cosa tu abbia nel tuo albero di lavoro.
Ricorda il albero di lavoro è il luogo in cui lavori sui tuoi file. Qui, hanno la loro forma normale, piuttosto che una speciale forma solo utile da Git come fanno nelle commit e nell'indice. Quindi si estrae un file da un commit, attraverso l'indice, e poi nella struttura di lavoro. Dopo averlo cambiato, lo fai git add
all'indice. Quindi ci sono in effetti tre posizioni per ogni file: il commit corrente, l'indice e l'albero di lavoro.
Quando corri git checkout branch2
, ciò che Git fa sotto le copertine è di confrontare il commit di puntabranch2
con qualsiasi cosa sia nel commit corrente e nell'indice ora. Qualsiasi file che corrisponda a ciò che è lì ora, Git può lasciare da solo. È tutto intatto. Qualsiasi file che è lo stesso in entrambi i commit , Git può anche lasciare da solo - e questi sono quelli che ti consentono di cambiare ramo.
Gran parte di Git, incluso il commit-switching, è relativamente veloce grazie a questo indice. Ciò che è effettivamente nell'indice non è ogni file stesso, ma piuttosto l' hash di ogni file . La copia del file stesso è memorizzata come ciò che Git chiama un oggetto BLOB , nel repository. Questo è simile al modo in cui i file sono memorizzati anche nei commit: i commit non contengono effettivamente i file , conducono semplicemente Git all'ID hash di ciascun file. Quindi Git può confrontare gli ID hash - attualmente stringhe lunghe 160 bit - per decidere se commettere X e Y hanno lo stesso file o meno. Può quindi confrontare anche questi ID hash con l'ID hash nell'indice.
Questo è ciò che porta a tutti i casi angolari dispari sopra. Abbiamo commesso X e Y che hanno entrambi file path/to/name.txt
e abbiamo una voce di indice per path/to/name.txt
. Forse tutti e tre gli hash corrispondono. Forse due di loro corrispondono e uno no. Forse tutti e tre sono diversi. E potremmo anche avere another/file.txt
solo in X o solo in Y e non è o non è nell'indice ora. Ognuno di questi vari casi richiede una propria considerazione separata: Git deve copiare il file da commit a indice o rimuoverlo da indice per passare da X a Y ? Se è così, anche deve copia il file nell'albero di lavoro o lo rimuove dall'albero di lavoro. E sein tal caso, le versioni dell'indice e dell'albero di lavoro devono corrispondere meglio ad almeno una delle versioni impegnate; altrimenti Git bloccherà alcuni dati.
(Le regole complete per tutto questo sono descritte in, non nella git checkout
documentazione come ci si potrebbe aspettare, ma piuttosto nella git read-tree
documentazione, nella sezione intitolata "Two Tree Merge" .)
git checkout -m
, che unisce il tuo worktree e l'indice cambia nella nuova cassa.