Esegui il checkout di un altro ramo in caso di modifiche senza commit sul ramo corrente


349

Il più delle volte quando provo a fare il checkout di un altro ramo esistente, Git non mi consente se ho alcune modifiche non confermate sul ramo corrente. Quindi dovrò prima impegnarmi o mettere da parte questi cambiamenti.

Tuttavia, di tanto in tanto Git mi consente di effettuare il checkout di un altro ramo senza eseguire il commit o lo stashing di tali modifiche e porterà tali modifiche al ramo che ho verificato.

Qual è la regola qui? Ha importanza se le modifiche sono in scena o non in scena? Portare le modifiche in un altro ramo non ha alcun senso per me, perché a volte Git lo consente? Cioè, è utile in alcune situazioni?

Risposte:


350

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 stashper salvarli; questa è una delle cose per cui è stata progettata. Si noti che git stash saveo git stash pusheffettivamente 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 applydopo 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 stasho git commit; oppure, se le tue modifiche sono banali da ricreare, usa git checkout -fper forzarle. Questa risposta riguarda quando Git ti consentirà git checkout branch2anche 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 rmdel genere - supponiamo che tu sia su branch1. A git checkout branch2dovrebbe fare questo:

  • Per ogni file che è in branch1e non è in branch2, 1 rimuovere quel file.
  • Per ogni file che è in branch2e 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-pointpotrebbe 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 branch1git rev-parse branch1:Pbranch1Pgit 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 branch1e 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 inbothcorrisponde a quello in branch2, anche se siamo attivi branch1. Questa modifica non viene gestita per il commit, che è ciò che git status --shortmostra 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 Ms qui significano: il file messo in scena differisce dal HEADfile 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 checkoutnon consentirà il pagamento:

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

Impostiamo la branch2versione 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 checkoutsi perderebbe quella copia e git checkoutverrà 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 addun 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 addall'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.txte 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.txtsolo 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 checkoutdocumentazione come ci si potrebbe aspettare, ma piuttosto nella git read-treedocumentazione, nella sezione intitolata "Two Tree Merge" .)


3
... c'è anche git checkout -m, che unisce il tuo worktree e l'indice cambia nella nuova cassa.
jill,

1
Grazie per questa eccellente spiegazione! Ma dove posso trovare le informazioni nei documenti ufficiali? O sono incompleti? In tal caso, qual è il riferimento autorevole per git (si spera diverso dal suo codice sorgente)?
massimo

1
(1) non è possibile e (2) il codice sorgente. Il problema principale è che Git è in continua evoluzione. Ad esempio, in questo momento, c'è una grande spinta per aumentare o abbandonare SHA-1 con o in favore di SHA-256. Questa particolare parte di Git è rimasta piuttosto stabile per molto tempo, tuttavia, e il meccanismo sottostante è semplice: Git confronta l'indice corrente con i commit correnti e target e decide quali file modificare (se presenti) in base al commit target , quindi verifica la "pulizia" dei file dell'albero di lavoro se è necessario sostituire la voce di indice.
torek,

6
Risposta breve: esiste una regola, ma è troppo ottuso per l'utente medio avere qualche speranza di comprensione e tanto meno ricordare, quindi invece di fare affidamento sullo strumento per comportarsi in modo intelligente, si dovrebbe invece fare affidamento sulla convenzione disciplinata di verificare solo quando la filiale attuale è impegnata e pulita. Non vedo come questo risponda alla domanda su quando sarebbe mai utile portare cambiamenti eccezionali su un altro ramo, ma potrei averlo perso perché faccio fatica a capirlo.
Neutrino

2
@HawkeyeParker: questa risposta ha subito numerose modifiche e non sono sicuro che nessuna di esse l'abbia migliorata molto, ma proverò ad aggiungere qualcosa su cosa significa che un file è "in un ramo". Alla fine questo sarà traballante perché la nozione di "ramo" qui non è definita correttamente in primo luogo, ma questo è ancora un altro elemento.
Torek,

50

Hai due possibilità: riporre le modifiche:

git stash

poi più tardi per riaverli:

git stash apply

oppure posiziona le modifiche su un ramo in modo da poter ottenere il ramo remoto e quindi unire le modifiche su di esso. Questa è una delle cose più belle di Git: puoi creare un ramo, impegnarti, quindi recuperare altre modifiche sul ramo in cui ti trovavi.

Dici che non ha alcun senso, ma lo stai facendo solo in modo da poterli unire a piacimento dopo aver fatto il tiro. Ovviamente la tua altra scelta è quella di impegnarti nella tua copia del ramo e poi fare il pull. La presunzione è che o non vuoi farlo (nel qual caso sono perplesso che non vuoi un ramo) o hai paura dei conflitti.


1
Non è il comando corretto git stash apply? qui i documenti.
Thomas8,

1
Proprio quello che stavo cercando, per passare temporaneamente a diversi rami, cercare qualcosa e tornare allo stesso stato del ramo su cui sto lavorando. Grazie Rob!
Naishta,

1
Sì, questo è il modo giusto per farlo. Apprezzo i dettagli nella risposta accettata, ma ciò rende le cose più difficili di quanto debbano essere.
Michael Leonard,

5
Inoltre, se non hai bisogno di mantenere lo stash in giro, puoi usarlo git stash pope eliminerà lo stash dal tuo elenco se si applica correttamente.
Michael Leonard,

1
uso migliore git stash pop, a meno che tu non abbia intenzione di tenere un registro degli stash nella cronologia dei repository
Damilola Olowookere,

14

Se il nuovo ramo contiene modifiche diverse dal ramo corrente per quel particolare file modificato, non ti consentirà di cambiare ramo finché la modifica non viene impegnata o nascosta. Se il file modificato è lo stesso su entrambi i rami (ovvero la versione di quel file impegnata), è possibile passare liberamente.

Esempio:

$ 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

Questo vale per file non tracciati e file tracciati. Ecco un esempio per un file non tracciato.

Esempio:

$ 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

Un buon esempio del motivo per cui vorresti spostarti tra i rami mentre facevi cambiamenti sarebbe se stavi eseguendo alcuni esperimenti sul master, volessi impegnarli, ma non padroneggiare ancora ...

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

In realtà non riesco ancora a capirlo. Nel tuo primo esempio, dopo aver aggiunto "e siamo tornati", si dice che la modifica locale verrà sovrascritta, quale modifica locale esattamente? "e siamo tornati"? Perché git non porta semplicemente questa modifica a master in modo che in master il file contenga "ciao mondo" e "e siamo tornati"
Xufeng,

Nel primo esempio il master ha solo "ciao mondo" impegnato. l'esperimento ha commesso "ciao mondo \ ngiorno addio". Affinché avvenga la modifica del ramo, file.txt deve essere modificato, il problema è che ci sono cambiamenti non confermati "ciao mondo \ nbuon saluto mondo \ n e siamo tornati".
Gordolio,

1

La risposta corretta è

git checkout -m origin/master

Unisce le modifiche dal ramo principale di origine con le modifiche locali anche senza commit.


0

Nel caso in cui non si desideri eseguire il commit di queste modifiche git reset --hard .

Successivamente puoi effettuare il checkout nel ramo desiderato, ma ricorda che le modifiche non impegnate andranno perse.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.