Verifica outra ramificação quando existem alterações não confirmadas no ramo atual

Na maioria das vezes quando tento fazer o checkout de outra ramificação existente, o Git não me permite se eu tiver algumas alterações não confirmadas na ramificação atual. Então, terei que confirmar ou ocultar essas alterações primeiro.

No entanto, ocasionalmente, o Git me permite fazer o checkout de outra ramificação sem cometer ou esconder essas alterações, e ele carregará essas alterações na ramificação que eu finalizo.

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

Notas preliminares

A observação aqui é que, depois que você começar a trabalhar no branch1 (esquecendo ou não percebendo que seria bom trocar para um branch2 branch diferente primeiro), você executa:

 git checkout branch2 

Às vezes Git diz “OK, você está na branch2 agora!” Às vezes, Git diz “Eu não posso fazer isso, eu perderia algumas de suas mudanças”.

Se o Git não permitir que você faça isso, você deve enviar suas alterações para salvá-las em algum lugar permanente. Você pode querer usar git stash para salvá-los; esta é uma das coisas para as quais foi projetada. Observe que git stash save na verdade significa “Confirme todas as alterações, mas em nenhuma ramificação, em seguida, remova-as de onde estou agora”. Isso permite alternar: agora você não tem alterações em andamento. Você pode então git stash apply los depois de mudar.

Você pode parar de ler aqui, se quiser!

Se o Git não permitir que você mude, você já tem um remédio: use git stash ou git commit ; ou, se suas alterações forem triviais para recriar, use git checkout -f para forçá-lo. Esta resposta é toda sobre quando o Git permite que você git checkout branch2 mesmo que você começou a fazer algumas alterações. Por que funciona às vezes , e não às outras vezes?

A regra aqui é simples de um jeito, e complicada / difícil de explicar em outro:

Você pode alternar ramificações com alterações não confirmadas na tree de trabalho se, e somente se, essa comutação não exigir a substituição dessas alterações.

Isto é – e, por favor, note que isto ainda é simplificado; há alguns casos de canto extremamente difíceis com git add s, git rm e tal – suponha que você esteja no branch1 . Um git checkout branch2 teria que fazer isso:

  • Para cada arquivo que está no branch1 e não no branch2 , remova esse arquivo.
  • Para cada arquivo que está em branch2 e não em branch1 , crie esse arquivo (com o conteúdo apropriado).
  • Para cada arquivo que esteja nos dois ramos, se a versão no branch2 for diferente, atualize a versão da tree de trabalho.

Cada uma dessas etapas pode atrapalhar algo em sua tree de trabalho:

  • Remover um arquivo é “seguro” se a versão na tree de trabalho for a mesma que a versão confirmada em branch1 ; é “inseguro” se você fez alterações.
  • Criar um arquivo da maneira que aparece no branch2 é “seguro” se não existir agora. 2 É “inseguro” se existe agora, mas tem o conteúdo “errado”.
  • E, claro, replace a versão da tree de trabalho de um arquivo por uma versão diferente é “seguro” se a versão da tree de trabalho já estiver confirmada no branch1 .

Criar uma nova ramificação ( git checkout -b newbranch ) é sempre considerado “seguro”: nenhum arquivo será adicionado, removido ou alterado na tree de trabalho como parte deste processo, e a área de índice / armazenamento temporário também é intocada. (Ressalva: é seguro criar uma nova ramificação sem mudar 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 mover para different-start-point Git aplicará as regras de segurança de checkout como de costume.)


1 Isso requer que definamos o que significa que um arquivo esteja em uma ramificação, o que, por sua vez, requer a definição adequada da ramificação da palavra. (Veja também O que exatamente queremos dizer com “branch”? ) Aqui, o que eu realmente quero dizer é o commit ao qual o branch-name resolve: um arquivo cujo caminho é P está em branch1 se git rev-parse branch1: P produz um jogo da velha. Esse arquivo não está no branch1 se você receber uma mensagem de erro. A existência do caminho P em seu índice ou tree de trabalho não é relevante ao responder a essa questão em particular. Assim, o segredo aqui é examinar o resultado do git rev-parse em cada branch-name : path . Isso falha porque o arquivo está “no máximo” um ramo ou nos fornece dois IDs de hash. Se os dois IDs de hash forem iguais , o arquivo será o mesmo em ambos os ramos. 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 principal ideia aqui é que os arquivos em commits são congelados para sempre. Arquivos que você irá editar obviamente não estão congelados. Estamos, pelo menos inicialmente, olhando apenas para as incompatibilidades 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 se livrar e estão no commit para o qual você irá mudar. Isso leva às complicações remanescentes, já que os arquivos também podem existir no índice e / ou na tree de trabalho, sem ter que existir esses dois commits congelados específicos com os quais estamos trabalhando.

2 Pode ser considerado “classificado como seguro” se já existir com o “conteúdo correto”, de modo que o Git não tenha que criá-lo depois de tudo. 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 seria aplicado a um arquivo modificado que fosse modificado para corresponder à ramificação de alternar para. Novamente, 1.8.5.4 apenas diz que “seria sobrescrito”. Veja o final das notas técnicas também: minha memory pode estar com defeito, já que eu não acho que as regras de read-tree tenham mudado desde que comecei a usar o Git na versão 1.5.


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

Sim, de algumas maneiras. Em particular, você pode organizar uma alteração e, em seguida, “des-modificar” o arquivo da tree de trabalho. Aqui está um arquivo em dois ramos, diferente em 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 

Nesse ponto, o arquivo da tree de trabalho é branch2 ao do branch2 , mesmo que estejamos no branch1 . Esta alteração não é testada para confirmação, que é o git status --short mostrado aqui:

 $ git status --short M inboth 

O espaço-then-M significa “modificado, mas não encenado” (ou, mais precisamente, a cópia da tree de trabalho difere da cópia de teste / índice).

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

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

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

Aqui, as cópias em etapas e em funcionamento combinavam com o que estava em branch2 , portanto, o checkout 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 da área de preparação agora (porque o checkout grava na área de preparação). Este é um pouco de um caso de canto. A mudança não se foi, mas o fato de que eu a encenou desapareceu.

Vamos organizar uma terceira variante do arquivo, diferente da cópia branch e, em seguida, definir a cópia de trabalho para corresponder à versão atual da filial:

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

Os dois M s aqui significam: o arquivo de teste difere do arquivo HEAD arquivo de trabalho da tree difere do arquivo de teste. A versão da tree de trabalho corresponde à versão branch1 (também conhecida como HEAD ):

 $ git diff HEAD $ 

Mas o git checkout não permite o checkout:

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

Vamos definir a versão branch2 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 àquela na branch2 , o arquivo branch2 não, portanto, um git checkout perderia essa cópia, e o git checkout será rejeitado.

Notas técnicas – apenas para os insanamente curiosos 🙂

O mecanismo de implementação subjacente para tudo isso é o índice do Git. O índice, também chamado de “área de preparação”, é onde você constrói o próximo commit: ele começa correspondendo ao commit atual, ie, o que você fez check-out agora, e então cada vez que você git add um arquivo, você substitui o commit. versão do índice com o que você tem em sua tree de trabalho.

Lembre-se, a tree de trabalho é onde você trabalha em seus arquivos. Aqui, eles têm sua forma normal, ao invés de algum formulário especial apenas útil para o Git, como em commits e no índice. Então você extrai um arquivo de um commit, através do índice e depois para dentro da work-tree. Depois de alterá-lo, você pode git add lo ao índice. Portanto, existem três locais para cada arquivo: o commit atual, o índice e a tree de trabalho.

Quando você executa git checkout branch2 , o que o Git faz por baixo das capas é comparar o commit da branch2 com o que quer que esteja no commit atual e no index agora. Qualquer arquivo que corresponda ao que está lá agora, o Git pode sair sozinho. Está tudo intocado. Qualquer arquivo que seja o mesmo em ambos os commits , o Git também pode sair sozinho – e esses são os que permitem que você mude de ramificação.

Muito do Git, incluindo commit-switching, é relativamente rápido por causa deste índice. O que está realmente no índice não é cada arquivo em si, mas sim o hash de cada arquivo. A cópia do arquivo em si é armazenada como o que o Git chama de object blob , no repository. Isso é semelhante a como os arquivos são armazenados em commits também: commits realmente não contêm os arquivos , eles apenas levam o Git ao hash ID de cada arquivo. Assim, o Git pode comparar IDs de hash – atualmente strings de 160 bytes – para decidir se as confirmações X e Y possuem o mesmo arquivo ou não. Ele pode, então, comparar esses IDs de hash ao ID de hash no índice também.

Isso é o que leva a todos os casos de canto excêntricos acima. Temos commits X e Y que ambos possuem path/to/name.txt arquivo path/to/name.txt , e nós temos uma input de índice para path/to/name.txt . Talvez todos os três hashes combinem. Talvez dois deles combinem e um não. Talvez todos os três sejam diferentes. E, também podemos ter another/file.txt que é apenas em X ou somente em Y e é ou não está no índice agora. Cada um desses vários casos requer sua própria consideração separada: o Git precisa copiar o arquivo de commit para index, ou removê-lo do index, para alternar de X para Y ? Nesse caso, também é necessário copiar o arquivo para a tree de trabalho ou removê-lo da tree de trabalho. E se esse for o caso, as versões de índice e tree de trabalho devem corresponder melhor a pelo menos uma das versões confirmadas; caso contrário, o Git estará destruindo alguns dados.

(As regras completas para tudo isso são descritas em, não a documentação do git checkout como você poderia esperar, mas sim a documentação do git read-tree , sob a seção intitulada “Two Tree Merge” .)

Você tem duas opções: esconder 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, em seguida, mescle suas alterações nela. Essa é uma das maiores coisas sobre o git: você pode criar um branch, commitar, buscar outras mudanças no branch em que você estava.

Você diz que não faz qualquer sentido, mas você está apenas fazendo isso para que você possa mesclá-los à vontade depois de fazer a atração. Obviamente, sua outra escolha é comprometer-se em sua cópia do ramo e, em seguida, fazer o pull. A presunção é que você não quer fazer isso (nesse caso, estou intrigado por não querer um ramo) ou tem medo de conflitos.

Se a nova ramificação contiver edições diferentes da ramificação atual para esse arquivo alterado específico, ela não permitirá alternar ramificações até que a alteração seja confirmada ou ocultada. Se o arquivo alterado for o mesmo em ambos os 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 acompanhados, 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ê gostaria de se mover entre as filiais enquanto fazia as alterações seria se estivesse realizando algumas experiências com o mestre, queria cometê-las, mas não para 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"