Fazer check-out de outra ramificação quando houver alterações não confirmadas na ramificação atual

349

Na maioria das vezes, quando tento fazer check-out de outra ramificação existente, o Git não me permite se houver alterações não confirmadas na ramificação atual. Portanto, terei que confirmar ou ocultar essas alterações primeiro.

No entanto, ocasionalmente, o Git permite que eu faça check-out de outra ramificação sem confirmar ou ocultar essas alterações, e carregará essas alterações para a ramificação que eu check-out.

Qual é a regra aqui? Importa se as alterações são preparadas ou não? Carregar as alterações para outro ramo não faz sentido para mim, por que o git permite isso às vezes? Ou seja, é útil em algumas situações?

Xufeng
fonte

Respostas:

350

Notas preliminares

A observação aqui é que, depois de começar a trabalhar branch1(esquecendo ou não percebendo que seria bom mudar para um ramo diferente branch2primeiro), você executa:

git checkout branch2

Às vezes, o Git diz "OK, você está no branch2 agora!" Às vezes, Git diz: "Não posso fazer isso, perderia algumas de suas alterações".

Se o Git não permitir, você deve confirmar suas alterações, salvá-las em algum lugar permanente. Você pode usar git stashpara salvá-los; essa é uma das coisas para as quais foi projetada. Observe que git stash saveou git stash pushrealmente significa "Confirme todas as alterações, mas em nenhuma ramificação, em seguida, remova-as de onde estou agora". Isso possibilita a troca: agora você não tem alterações em andamento. Você pode então git stash applyeles depois de mudar.

Barra lateral: git stash saveé a sintaxe antiga; git stash pushfoi introduzido no Git versão 2.13, para corrigir alguns problemas com os argumentos git stashe permitir novas opções. Ambos fazem a mesma coisa, quando usados ​​de maneira básica.

Você pode parar de ler aqui, se quiser!

Se o Git não permitir que você alterne, você já tem um remédio: use git stashor git commit; ou, se suas alterações forem triviais para recriar, use git checkout -fpara forçá-las. Esta resposta é sobre quando o Git permitirá git checkout branch2que você comece a fazer algumas alterações. Por que funciona algumas vezes , e não outras ?

A regra aqui é simples de uma maneira e complicada / difícil de explicar de outra:

Você pode alternar ramificações com alterações não confirmadas na árvore de trabalho se e somente se a referida alternância não exigir que essas alterações sejam prejudicadas.

Ou seja, e observe que isso ainda é simplificado; existem alguns casos de canto extra-difíceis com git adds, se git rme tal - suponha que você esteja branch1. A git checkout branch2teria que fazer o seguinte:

  • Para cada arquivo que está no branch1e não em branch2, 1 remove o arquivo.
  • Para cada arquivo que está no branch2e não em branch1, criar esse arquivo (com conteúdos apropriados).
  • Para cada arquivo que esteja nos dois ramos, se a versão branch2for diferente, atualize a versão da árvore de trabalho.

Cada uma dessas etapas pode prejudicar algo em sua árvore de trabalho:

  • A remoção de um arquivo é "segura" se a versão na árvore de trabalho for igual à versão confirmada branch1; é "inseguro" se você fez alterações.
  • Criar um arquivo como ele aparece branch2é "seguro" se ele não existir agora. 2 É "inseguro" se ele existe agora, mas tem o conteúdo "errado".
  • E, é claro, substituir a versão da árvore de trabalho de um arquivo por uma versão diferente é "seguro" se a versão da árvore de trabalho já estiver comprometida branch1.

Criar uma nova ramificação ( git checkout -b newbranch) é sempre considerado "seguro": nenhum arquivo será adicionado, removido ou alterado na árvore de trabalho como parte desse processo, e a área de indexação / preparação também é intocada. (Advertência: é seguro ao criar uma nova ramificação sem alterar o ponto de partida da nova ramificação; mas se você adicionar outro argumento, por exemplo git checkout -b newbranch different-start-point, isso pode ter que mudar as coisas, para mudar para different-start-point. O Git aplicará as regras de segurança do checkout normalmente. .)


1 Isso requer que definamos o que significa um arquivo estar em uma ramificação, o que, por sua vez, exige a definição correta da palavra ramificação . (Veja também o que exatamente queremos dizer com "ramo"? ) Aqui, o que eu realmente quero dizer é a comprometer-se a que os resolve-nome de ramo: um arquivo cujo caminho é é em se produz um hash. Esse arquivo não está em se você receber uma mensagem de erro. A existência de caminho no seu índice ou árvore de trabalho não é relevante ao responder a essa pergunta em particular. Assim, o segredo aqui é examinar o resultado de cadaP branch1git rev-parse branch1:Pbranch1Pgit rev-parsebranch-name:path. Isso falha porque o arquivo está "no" no máximo uma ramificação ou nos fornece dois IDs de hash. Se os dois IDs de hash forem iguais , o arquivo será o mesmo nas duas ramificações. Nenhuma mudança é necessária. Se os IDs de hash forem diferentes, o arquivo será diferente nas duas ramificações e deverá ser alterado para alternar as ramificações.

A noção principal aqui é que os arquivos em confirmações são congelados para sempre. Os arquivos que você editará obviamente não estão congelados. Estamos, pelo menos inicialmente, olhando apenas para as diferenças entre dois commits congelados. Infelizmente, nós - ou o Git - também temos que lidar com arquivos que não estão no commit do qual você vai mudar e estão no commit que você vai mudar. Isso leva às complicações restantes, pois os arquivos também podem existir no índice e / ou na árvore de trabalho, sem a necessidade de existir esses dois commits congelados específicos com os quais estamos trabalhando.

2 Pode ser considerado "seguro" se já existir com o "conteúdo correto", para que o Git não precise criá-lo, afinal. Lembro-me de pelo menos algumas versões do Git permitindo isso, mas o teste agora mostra que ele é considerado "inseguro" no Git 1.8.5.4. O mesmo argumento se aplicaria a um arquivo modificado que passou a ser modificado para corresponder à ramificação a ser alternada. Novamente, o 1.8.5.4 apenas diz que "seria substituído", no entanto. Veja também o final das notas técnicas: minha memória pode estar com defeito, pois não acho que as regras da árvore de leitura tenham sido alteradas desde que comecei a usar o Git na versão 1.5.


Importa se as alterações são preparadas ou não?

Sim, de certa forma. Em particular, você pode preparar uma alteração e "desmodificar" o arquivo da árvore de trabalho. Aqui está um arquivo em dois ramos, que é diferente em 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

Neste ponto, o arquivo da árvore de trabalho inbothcorresponde ao arquivo branch2, mesmo que estejamos ativando branch1. Essa mudança não é preparada para confirmação, que é o que git status --shortmostra aqui:

$ git status --short
 M inboth

O espaço então M significa "modificado, mas não preparado" (ou mais precisamente, a cópia da árvore de trabalho difere da cópia preparada / indexada).

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

OK, agora vamos preparar a cópia da árvore de trabalho, que já sabemos que também corresponde à cópia branch2.

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

Aqui, as cópias encenadas e em trabalho coincidiam com o que estava dentro branch2, então o check-out era permitido.

Vamos tentar outro passo:

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

A alteração que fiz foi perdida na área de preparação agora (porque o checkout grava na área de preparação). Este é um caso de esquina. A mudança não se foi, mas o fato de que eu tinha encenado isso, está desaparecido.

Vamos preparar uma terceira variante do arquivo, diferente da cópia de ramificação, e definir a cópia de trabalho para corresponder à versão atual da ramificação:

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

Os dois Maqui significam: o arquivo intermediário difere do HEADarquivo e o arquivo da árvore de trabalho difere do arquivo intermediário. A versão da árvore de trabalho corresponde à versão branch1(aka HEAD):

$ git diff HEAD
$

Mas git checkoutnão permitirá o checkout:

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

Vamos definir a branch2versão como a versão de trabalho:

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

Mesmo que a cópia de trabalho atual corresponda à cópia inserida branch2, o arquivo temporário não corresponde, portanto, a git checkoutperderia a cópia e ela git checkoutserá rejeitada.

Notas técnicas - apenas para os insanamente curiosos :-)

O mecanismo de implementação subjacente a tudo isso é o índice do Git . O índice, também chamado de "área de preparação", é onde você cria a próxima confirmação: ela começa a corresponder à confirmação atual, ou seja, o que você fez o check-out agora e, a cada vez que você cria git addum arquivo, substitui a versão do índice com o que você tem em sua árvore de trabalho.

Lembre -se de que a árvore de trabalho é onde você trabalha em seus arquivos. Aqui, eles têm sua forma normal, em vez de alguma forma especial útil apenas para o Git, como eles fazem nos commits e no índice. Então você extrai um arquivo de uma consolidação, através do índice, e depois na árvore de trabalho. Depois de alterá-lo, você git addé o índice. Portanto, existem três locais para cada arquivo: a confirmação atual, o índice e a árvore de trabalho.

Quando você executa git checkout branch2, o que o Git faz embaixo das capas é comparar a confirmação da dicabranch2 com o que estiver na confirmação atual e no índice agora. Qualquer arquivo que corresponda ao que existe agora, o Git pode deixar em paz. Tudo intocado. Qualquer arquivo que seja o mesmo nos dois commits , o Git também pode ser deixado em paz - e esses são os que permitem alternar ramificações.

Grande parte do Git, incluindo a troca de commit, é relativamente rápida por causa desse índice. O que realmente está no índice não é cada arquivo em si, mas o hash de cada arquivo . A cópia do arquivo em si é armazenada como o que o Git chama de objeto blob , no repositório. É semelhante à maneira como os arquivos são armazenados nas confirmações: confirmações não contêm os arquivos , eles apenas levam o Git ao ID de hash de cada arquivo. Portanto, o Git pode comparar IDs de hash - atualmente cadeias de caracteres de 160 bits - para decidir se as confirmações X e Y têm o mesmo arquivo ou não. Em seguida, ele também pode comparar esses IDs de hash com o ID de hash no índice.

É isso que leva a todos os casos de canto excêntricos acima. Temos confirmações X e Y que possuem arquivo path/to/name.txte temos uma entrada de índice para path/to/name.txt. Talvez todos os três hashes correspondam. Talvez dois deles combinem e um não. Talvez todos os três sejam diferentes. E também podemos ter another/file.txtisso apenas em X ou apenas em Y e está ou não está no índice agora. Cada um desses vários casos exige sua própria consideração separada: o Git precisa copiar o arquivo do commit para o índice, ou removê-lo do índice, para alternar de X para Y ? Nesse caso, também devecopie o arquivo para a árvore de trabalho ou remova-o da árvore de trabalho. E se for esse o caso, as versões do índice e da árvore de trabalho terão melhor correspondência com pelo menos uma das versões confirmadas; caso contrário, o Git estará bloqueando alguns dados.

(As regras completas para tudo isso estão descritas, não na git checkoutdocumentação que você pode esperar, mas na git read-treedocumentação, na seção intitulada "Mesclagem de duas árvores" .)

torek
fonte
3
... há também git checkout -m, que mescla sua árvore de trabalho e as alterações de índice no novo checkout.
jthill
11
Obrigado por esta excelente explicação! Mas onde posso encontrar as informações nos documentos oficiais? Ou eles estão incompletos? Em caso afirmativo, qual é a referência autorizada para o git (espero que não seja o código fonte)?
max
11
(1) você não pode, e (2) o código fonte. O principal problema é que o Git está em constante evolução. Por exemplo, agora, há um grande impulso para aumentar ou abandonar o SHA-1 com ou a favor do SHA-256. Porém, essa parte específica do Git é bastante estável há muito tempo, e o mecanismo subjacente é direto: o Git compara o índice atual com as confirmações atuais e de destino e decide quais arquivos serão alterados (se houver) com base na confirmação de destino. , em seguida, testa a "limpeza" dos arquivos da árvore de trabalho se a entrada do índice precisar ser substituída.
torek
6
Resposta curta: existe uma regra, mas é muito obtuso para o usuário médio ter alguma esperança de compreensão e muito menos lembrar; portanto, em vez de confiar na ferramenta para se comportar de maneira inteligível, você deve confiar na convenção disciplinada de verificar apenas quando o seu O ramo atual está comprometido e limpo. Não vejo como isso responde à questão de quando seria útil transportar mudanças pendentes para outro ramo, mas posso ter perdido isso porque luto para entendê-lo.
Neutrino
2
@HawkeyeParker: esta resposta passou por várias edições, e não tenho certeza se alguma delas melhorou muito, mas vou tentar adicionar algo sobre o que significa um arquivo estar "em um ramo". Em última análise, isso será instável porque a noção de "ramificação" aqui não está definida corretamente em primeiro lugar, mas esse é outro item.
Torek 17/05/19
50

Você tem duas opções: esconda suas alterações:

git stash

depois, para recuperá-los:

git stash apply

ou coloque suas alterações em uma ramificação para que você possa obter a ramificação remota e depois mesclar suas alterações. Essa é uma das melhores coisas do git: você pode criar um ramo, se comprometer com ele e buscar outras alterações no ramo em que estava.

Você diz que não faz sentido, mas só o faz para poder mesclá-los à vontade depois de fazer o puxão. Obviamente, sua outra opção é confirmar sua cópia do ramo e, em seguida, fazer o pull. A presunção é de que você não quer fazer isso (nesse caso, fico intrigado por não querer um ramo) ou tem medo de conflitos.

Roubar
fonte
11
Não é o comando correto git stash apply? aqui os documentos.
Thomas8
11
Exatamente o que eu estava procurando, mudar temporariamente para ramos diferentes, procurar algo e voltar ao mesmo estado do ramo em que estou trabalhando. Obrigado Rob!
Naishta 3/11
11
Sim, este é o caminho certo para fazer isso. Aprecio os detalhes na resposta aceita, mas isso está dificultando as coisas do que precisam.
Michael Leonard
5
Além disso, se você não tiver necessidade de manter o stash por perto, poderá usá git stash pop-lo e ele eliminará o stash da sua lista se for aplicado com êxito.
Michael Leonard
11
melhor uso git stash pop, a menos que você pretende manter um registro de esconderijos em sua história repo
Damilola Olowookere
14

Se a nova ramificação contiver edições diferentes da ramificação atual para esse arquivo alterado em particular, não será possível alternar ramificações até que a alteração seja confirmada ou oculta. Se o arquivo alterado for o mesmo nos dois ramos (ou seja, a versão confirmada desse arquivo), você poderá alternar livremente.

Exemplo:

$ 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

Isso vale para arquivos não rastreados, bem como arquivos rastreados. Aqui está um exemplo para um arquivo não rastreado.

Exemplo:

$ 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

Um bom exemplo de por que você desejaria mover-se entre os ramos enquanto fazia alterações seria se estivesse realizando algumas experiências no mestre, quisesse confirmá-las, mas ainda não o dominar ainda ...

$ 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
fonte
Na verdade, eu ainda não entendi direito. No seu primeiro exemplo, depois de adicionar "e voltamos", ele diz que a mudança local será substituída, que mudança local exatamente? "e voltamos"? Por git só não realizar esta mudança de dominar para que no mestre o arquivo contém "Olá mundo" e "E estamos de volta"
Xufeng
No primeiro exemplo, o mestre apenas confirmou 'olá mundo'. o experimento comprometeu-se 'olá mundo \ adeus mundo'. Para que a mudança de ramificação ocorra, o arquivo.txt precisa ser modificado, o problema é que existem alterações não confirmadas "olá mundo \ ngoodbye mundo \ e estamos de volta".
26514 Gordolio
1

A resposta correta é

git checkout -m origin/master

Ele mescla alterações da ramificação principal de origem com as alterações não confirmadas locais.

JD1731
fonte
0

Caso você não queira que essas alterações sejam confirmadas, faça git reset --hard.

Em seguida, você pode fazer o checkout no ramo desejado, mas lembre-se de que as alterações não confirmadas serão perdidas.

Kacpero
fonte