As confirmações do Git são duplicadas na mesma ramificação após fazer uma nova rebase

130

Entendo o cenário apresentado no Pro Git sobre The Perils of Rebasing . O autor basicamente explica como evitar confirmações duplicadas:

Não rebase as confirmações enviadas por push para um repositório público.

Vou lhe contar minha situação específica, porque acho que ela não se encaixa exatamente no cenário do Pro Git e ainda termino com confirmações duplicadas.

Digamos que eu tenha duas ramificações remotas com suas contrapartes locais:

origin/master    origin/dev
|                |
master           dev

Todos os quatro ramos contêm os mesmos commits e vou iniciar o desenvolvimento em dev:

origin/master : C1 C2 C3 C4
master        : C1 C2 C3 C4

origin/dev    : C1 C2 C3 C4
dev           : C1 C2 C3 C4

Após algumas confirmações, envio as alterações para origin/dev:

origin/master : C1 C2 C3 C4
master        : C1 C2 C3 C4

origin/dev    : C1 C2 C3 C4 C5 C6  # (2) git push
dev           : C1 C2 C3 C4 C5 C6  # (1) git checkout dev, git commit

Eu tenho que voltar masterpara fazer uma solução rápida:

origin/master : C1 C2 C3 C4 C7  # (2) git push
master        : C1 C2 C3 C4 C7  # (1) git checkout master, git commit

origin/dev    : C1 C2 C3 C4 C5 C6
dev           : C1 C2 C3 C4 C5 C6

E voltando a devrefazer as alterações para incluir a correção rápida no meu desenvolvimento atual:

origin/master : C1 C2 C3 C4 C7
master        : C1 C2 C3 C4 C7

origin/dev    : C1 C2 C3 C4 C5 C6
dev           : C1 C2 C3 C4 C7 C5' C6'  # git checkout dev, git rebase master

Se eu exibir o histórico de confirmações com o GitX / gitk, notarei que origin/devagora contém duas confirmações idênticas C5'e C6'diferentes do Git. Agora, se eu empurrar as alterações para origin/deveste é o resultado:

origin/master : C1 C2 C3 C4 C7
master        : C1 C2 C3 C4 C7

origin/dev    : C1 C2 C3 C4 C5 C6 C7 C5' C6'  # git push
dev           : C1 C2 C3 C4 C7 C5' C6'

Talvez eu não entenda completamente a explicação no Pro Git, então gostaria de saber duas coisas:

  1. Por que o Git duplica esses commits durante o rebasing? Existe uma razão específica para fazer isso, em vez de apenas aplicar C5e C6depois C7?
  2. Como posso evitar isso? Seria sensato fazê-lo?
elitalon
fonte

Respostas:

86

Você não deve usar rebase aqui, uma mesclagem simples será suficiente. O livro Pro Git que você vinculou explica basicamente essa situação exata. O funcionamento interno pode ser um pouco diferente, mas eis como eu o visualizo:

  • C5e C6são temporariamente retiradosdev
  • C7 é aplicado a dev
  • C5e C6são reproduzidos em cima C7, criando novos diffs e, portanto, novos commits

Então, no seu devramo, C5e C6efetivamente não existem mais: eles estão agora C5'e C6'. Quando você pressiona para origin/dev, o git vê C5'e C6'como novo confirma e o coloca no final da história. Na verdade, se você olhar para as diferenças entre C5e C5'em origin/dev, você vai notar que, embora o conteúdo é o mesmo, os números de linha são provavelmente diferentes - o que torna o hash do commit diferente.

Vou reafirmar a regra do Pro Git: nunca rebote os commit que já existiram em qualquer lugar, exceto no seu repositório local . Use mesclagem.

Justin ᚅᚔᚈᚄᚒᚔ
fonte
Eu tenho o mesmo problema, como posso corrigir meu histórico de ramificação remota agora, existe outra opção além de excluir a ramificação e recriá-la com a seleção de cereja?
Wazery
1
@xdsy: Jave uma olhada este e este .
Justin ᚅᚔᚈᚄᚒᚔ
2
Você diz que "C5 e C6 são temporariamente retirados do dev ... C7 é aplicado ao dev". Se for esse o caso, por que C5 e C6 aparecem antes de C7 na ordenação de confirmações na origem / dev?
KJ50 #
@ KJ50: Porque C5 e C6 já foram pressionados origin/dev. Quando devé rebased, seu histórico é modificado (C5 / C6 removido temporariamente e reaplicado após C7). O histórico de modificações de repositórios enviados é geralmente uma Really Bad Idea ™, a menos que você saiba o que está fazendo. Nesse caso simples, o problema poderia ser resolvido pressionando a força de um devpara o outro origin/devapós a reformulação e notificando qualquer outra pessoa que trabalhasse com origin/devisso, provavelmente eles estão prestes a ter um dia ruim. A melhor resposta, novamente, é "não faça isso ... use a mesclagem"
Justin #
3
Uma coisa a observar: o hash de C5 e C5 'é certamente diferente, mas não por causa dos números de linha, mas pelos dois fatos a seguir, dos quais qualquer um é suficiente para a diferença: 1) o hash de que estamos falando é o hash de toda a árvore de origem após a confirmação, não o hash da diferença delta, e, portanto, o C5 'contém o que vem do C7, enquanto o C5 não e 2) O pai do C5' é diferente do C5, e essas informações também é incluído no nó raiz de uma árvore de confirmação que afeta o resultado do hash.
Ozgur Murat
113

Resposta curta

Você omitiu o fato de executar git push, obteve o seguinte erro e prosseguiu com a execução git pull:

To [email protected]:username/test1.git
 ! [rejected]        dev -> dev (non-fast-forward)
error: failed to push some refs to '[email protected]:username/test1.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Apesar de o Git tentar ser útil, seu conselho sobre 'git pull' provavelmente não é o que você deseja fazer .

Se você é:

  • Trabalhando em um "ramo de recurso" ou "ramo desenvolvedor" sozinho , então você pode executar git push --forcepara atualizar o controle remoto com seus commits pós-REBASE ( como por resposta das user4405677 ).
  • Trabalhando em uma filial com vários desenvolvedores ao mesmo tempo, você provavelmente não deveria estar usandogit rebase em primeiro lugar. Para atualizar devcom as alterações de master, você deve, em vez de executar git rebase master dev, executar git merge masterenquanto estiver dev( conforme a resposta de Justin ).

Uma explicação um pouco mais longa

Cada hash de confirmação no Git é baseado em vários fatores, um dos quais é o hash da confirmação que vem antes dele.

Se você reordenar as confirmações, alterará os hashes de confirmação; rebasing (quando faz alguma coisa) mudará os hashes de confirmação. Com isso, o resultado da execução git rebase master dev, onde devestá fora de sincronia master, criará novas confirmações (e, portanto, hashes) com o mesmo conteúdo que as ativadas, devmas com as confirmações masterinseridas antes deles.

Você pode acabar em uma situação como essa de várias maneiras. Duas maneiras em que posso pensar:

  • Você pode ter confirmações nas masterquais deseja basear seu devtrabalho
  • Você pode ter confirmações devque já foram enviadas para um controle remoto, e depois proceder à alteração (reformular as mensagens de confirmação, reordenar confirmações, squash confirma, etc.)

Vamos entender melhor o que aconteceu - aqui está um exemplo:

Você tem um repositório:

2a2e220 (HEAD, master) C5
ab1bda4 C4
3cb46a9 C3
85f59ab C2
4516164 C1
0e783a3 C0

Conjunto inicial de confirmações lineares em um repositório

Em seguida, prossiga para alterar confirmações.

git rebase --interactive HEAD~3 # Three commits before where HEAD is pointing

(É aqui que você terá que aceitar minha palavra: existem várias maneiras de alterar confirmações no Git. Neste exemplo, mudei o horário de C3, mas você está inserindo novas confirmações, alterando mensagens de confirmação, reordenando confirmações, squashing comete em conjunto, etc.)

ba7688a (HEAD, master) C5
44085d5 C4
961390d C3
85f59ab C2
4516164 C1
0e783a3 C0

O mesmo confirma com novos hashes

É aqui que é importante notar que os hashes de confirmação são diferentes. Esse é um comportamento esperado, pois você alterou algo (qualquer coisa) sobre eles. Está tudo bem, MAS:

Um log gráfico mostrando que o mestre está fora de sincronia com o controle remoto

Tentar empurrar mostrará um erro (e sugerirá que você deve executar git pull).

$ git push origin master
To [email protected]:username/test1.git
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to '[email protected]:username/test1.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Se rodarmos git pull, vemos este log:

7df65f2 (HEAD, master) Merge branch 'master' of bitbucket.org:username/test1
ba7688a C5
44085d5 C4
961390d C3
2a2e220 (origin/master) C5
85f59ab C2
ab1bda4 C4
4516164 C1
3cb46a9 C3
0e783a3 C0

Ou, mostrado de outra maneira:

Um log gráfico mostrando uma confirmação de mesclagem

E agora temos confirmações duplicadas localmente. Se rodássemos, git pushnós os enviaríamos para o servidor.

Para evitar chegar a esse estágio, poderíamos ter corrido git push --force(onde corremos git pull). Isso teria enviado nossos commits com os novos hashes para o servidor sem problemas. Para corrigir o problema neste estágio, podemos redefinir para antes de executar git pull:

Olhe para o reflog ( git reflog) para ver o que o commit de hash foi antes de nós corremos git pull.

070e71d HEAD@{1}: pull: Merge made by the 'recursive' strategy.
ba7688a HEAD@{2}: rebase -i (finish): returning to refs/heads/master
ba7688a HEAD@{3}: rebase -i (pick): C5
44085d5 HEAD@{4}: rebase -i (pick): C4
961390d HEAD@{5}: commit (amend): C3
3cb46a9 HEAD@{6}: cherry-pick: fast-forward
85f59ab HEAD@{7}: rebase -i (start): checkout HEAD~~~
2a2e220 HEAD@{8}: rebase -i (finish): returning to refs/heads/master
2a2e220 HEAD@{9}: rebase -i (start): checkout refs/remotes/origin/master
2a2e220 HEAD@{10}: commit: C5
ab1bda4 HEAD@{11}: commit: C4
3cb46a9 HEAD@{12}: commit: C3
85f59ab HEAD@{13}: commit: C2
4516164 HEAD@{14}: commit: C1
0e783a3 HEAD@{15}: commit (initial): C0

Acima, vemos que ba7688aera o commit em que estávamos antes de executar git pull. Com esse hash de commit em mãos, podemos redefinir para that ( git reset --hard ba7688a) e depois executar git push --force.

E nós terminamos.

Mas espere, continuei baseando o trabalho nos commits duplicados

Se você não percebeu que as confirmações foram duplicadas e continuou a trabalhar em cima das confirmações duplicadas, você realmente fez uma bagunça. O tamanho da bagunça é proporcional ao número de confirmações que você tem sobre as duplicatas.

Como é isso:

3b959b4 (HEAD, master) C10
8f84379 C9
0110e93 C8
6c4a525 C7
630e7b4 C6
070e71d (origin/master) Merge branch 'master' of bitbucket.org:username/test1
ba7688a C5
44085d5 C4
961390d C3
2a2e220 C5
85f59ab C2
ab1bda4 C4
4516164 C1
3cb46a9 C3
0e783a3 C0

Log Git mostrando confirmações lineares sobre confirmações duplicadas

Ou, mostrado de outra maneira:

Um gráfico de log mostrando confirmações lineares sobre confirmações duplicadas

Nesse cenário, queremos remover as confirmações duplicadas, mas manter as confirmações que baseamos nelas - queremos manter C6 a C10. Como na maioria das coisas, existem várias maneiras de fazer isso:

Ou:

  • Crie uma nova ramificação na última confirmação duplicada 1 , cherry-pickcada confirmação (inclusive C6 a C10) nessa nova ramificação e trate essa nova ramificação como canônica.
  • Execute git rebase --interactive $commit, onde $commitestá a confirmação antes das confirmações duplicadas 2 . Aqui, podemos excluir completamente as linhas das duplicatas.

1 Não importa qual dos dois você escolher, ba7688aou 2a2e220funciona bem.

2 No exemplo seria 85f59ab.

TL; DR

Defina advice.pushNonFastForwardcomo false:

git config --global advice.pushNonFastForward false
Whymarrh
fonte
1
Não há problema em seguir o conselho "git pull ...", desde que a elipse oculte a opção "--rebase" (aka "-r"). ;-)
G. Sylvie Davies
4
Eu recomendaria usar git pushé --force-with-leasehoje em dia, como é um padrão melhor
Whymarrh
4
É essa resposta ou uma máquina do tempo. Obrigado!
21418 ZeMoon
Explicação muito interessante ... deparei-me com um problema semelhante que duplicava meu código de 5 a 6 vezes depois de tentar refazer repetidamente ... só para ter certeza de que o código está atualizado com o mestre ... mas toda vez que ele pressionava novo confirma minha ramificação, duplicando meu código também. Você pode me dizer se o push forçado (com opção de concessão) é seguro aqui, se eu for o único desenvolvedor trabalhando na minha filial? Ou fundir o mestre no meu, em vez disso, rebasear é a melhor maneira?
Dhruv Singhal
12

Acho que você pulou um detalhe importante ao descrever seus passos. Mais especificamente, sua última etapa, git pushno desenvolvimento , na verdade causaria um erro, pois normalmente você não pode enviar alterações não rápidas.

Como você fez git pullantes do último envio, que resultou em uma consolidação de mesclagem com C6 e C6 'como pais, e é por isso que ambos permanecerão listados no log. Um formato de log mais bonito pode ter tornado mais óbvio que são ramificações mescladas de confirmações duplicadas.

Ou você fez um git pull --rebase(ou sem explícito, --rebasese isso estiver implícito na sua configuração), que retirou os C5 e C6 originais de volta no seu desenvolvedor local (e re-redimensionou os seguintes para novos hashes, C7 'C5' 'C6' ').

Uma maneira de sair disso poderia ter sido git push -fforçar o empurrão quando deu o erro e limpar C5 C6 da origem, mas se alguém mais os tiver puxado antes de você os limpar, você terá muito mais problemas. basicamente todo mundo que possui C5 C6 precisaria executar etapas especiais para se livrar deles. É exatamente por isso que eles dizem que você nunca deve refazer nada que já tenha sido publicado. Ainda é possível se a publicação for dentro de uma equipe pequena.

user4405677
fonte
1
A omissão de git pullé crucial. Sua recomendação git push -f, embora perigosa, é provavelmente o que os leitores estão procurando.
Whymarrh
De fato. Quando escrevi a pergunta que realmente fiz git push --force, apenas para ver o que o Git faria. Eu aprendi muito sobre o Git desde então e hoje em dia rebasefaz parte do meu fluxo de trabalho normal. No entanto, git push --force-with-leaseevito substituir o trabalho de outra pessoa.
Elitalon
Usando --force-with-leaseé um bom padrão, eu vou deixar um comentário sob a minha resposta bem
Whymarrh
2

Eu descobri que, no meu caso, esse problema é consequência de um problema de configuração do Git. (Envolver e puxar)

Descrição do problema:

Sintomas: confirma duplicado na ramificação filha após a rebase, implicando várias mesclagens durante e após a rebase.

Fluxo de trabalho: Aqui estão as etapas do fluxo de trabalho que eu estava executando:

  • Trabalhe no "ramo de recursos" (filho de "ramo de desenvolvimento")
  • Confirmar e enviar alterações em "Recursos-ramificação"
  • Faça o checkout do "Develop-branch" (ramo principal dos recursos) e trabalhe com ele.
  • Confirmar e enviar alterações no "Develop-branch"
  • Faça o checkout "Features-branch" e retire as alterações do repositório (caso outra pessoa tenha comprometido o trabalho)
  • Rebase "Branch de recursos" para "Branch de desenvolvimento"
  • Força forçada das alterações no "Recurso-ramificação"

Como conseqüências desse fluxo de trabalho, a duplicação de todos os commits de "Feature-branch" desde a rebase anterior ... :-(

O problema ocorreu devido a mudanças nas ramificações filhas antes da rebase. A configuração padrão do Git é "mesclar". Isso está alterando os índices de confirmações executadas na ramificação filha.

A solução: no arquivo de configuração do Git, configure o pull para funcionar no modo rebase:

...
[pull]
    rebase = preserve
...

Espero que possa ajudar JN Grx

JN Gerbaux
fonte
1

Você pode ter extraído de um ramo remoto diferente do seu atual. Por exemplo, você pode ter saído do Master quando sua filial está desenvolvendo o rastreamento. O Git aplicará obedientemente cópias confirmadas duplicadas se extraídas de um ramo não rastreado.

Se isso acontecer, você pode fazer o seguinte:

git reset --hard HEAD~n

Onde n == <number of duplicate commits that shouldn't be there.>

Em seguida, verifique se você está puxando da ramificação correta e execute:

git pull upstream <correct remote branch> --rebase

Usar essa opção --rebasegarantirá que você não esteja adicionando confirmações estranhas, o que pode atrapalhar o histórico de confirmação.

Aqui está um pouco de mão para git rebase.

ScottyBlades
fonte