Como faço para recuperar / sincronizar novamente após alguém enviar um rebase ou uma redefinição para um branch publicado?

88

Todos nós ouvimos que nunca se deve rebase um trabalho publicado, que é perigoso, etc. No entanto, não vi nenhuma receita postada sobre como lidar com a situação no caso de um rebase ser publicado.

Agora, observe que isso só é realmente viável se o repositório for clonado apenas por um grupo conhecido (de preferência pequeno) de pessoas, de modo que quem quer que faça o rebase ou redefinir possa notificar a todos que precisará prestar atenção na próxima vez que buscar(!).

Uma solução óbvia que eu vi funcionará se você não tiver nenhum commit local fooe for rebaseizado:

git fetch
git checkout foo
git reset --hard origin/foo

Isso simplesmente descartará o estado local de fooem favor de seu histórico de acordo com o repositório remoto.

Mas como alguém lida com a situação se tiver cometido mudanças locais substanciais naquele ramo?

Aristotle Pagaltzis
fonte
1 para a receita simples do caso. É ideal para sincronização pessoal entre máquinas, especialmente se elas tiverem sistemas operacionais diferentes. É algo que deve ser mencionado no manual.
Philip Oakley,
A receita ideal para sincronização pessoal é git pull --rebase && git push. Se você trabalhar masterapenas em , então isso quase infalivelmente fará a coisa certa para você, mesmo se você tiver rebaseado e empurrado na outra extremidade.
Aristóteles Pagaltzis,
Como estou sincronizando e desenvolvendo entre um PC e máquinas Linux, descobri que usar um novo branch para cada rebase / atualização funciona bem. Eu também uso a variante git reset --hard @{upstream}agora que sei que o encantamento refspec mágico para "esqueça o que eu tenho / tive, use o que eu obtive do controle remoto" Veja meu comentário final em stackoverflow.com/a/15284176/717355
Philip Oakley
Você será capaz, com Git2.0, de encontrar a origem antiga de seu branch (antes do branch upstream ser reescrito com a push -f): veja minha resposta abaixo
VonC

Respostas:

75

Voltar a sincronizar após um rebase forçado não é tão complicado na maioria dos casos.

git checkout foo
git branch old-foo origin/foo # BEFORE fetching!!
git fetch
git rebase --onto origin/foo old-foo foo
git branch -D old-foo

Ie. primeiro você configura um marcador para onde o branch remoto estava originalmente, então você o usa para reproduzir seus commits locais daquele ponto em diante no branch remoto rebaseizado.

Rebasing é como violência: se não resolve o seu problema, você só precisa de mais. ☺

Você pode fazer isso sem o marcador, é claro, se procurar o origin/fooID de confirmação pré-rebase e usá-lo.

É assim também que você lida com a situação em que se esqueceu de fazer um marcador antes de buscar. Nada está perdido - você só precisa verificar o reflog do branch remoto:

git reflog show origin/foo | awk '
    PRINT_NEXT==1 { print $1; exit }
    /fetch: forced-update/ { PRINT_NEXT=1 }'

Isso imprimirá o ID de confirmação que origin/fooapontou antes da busca mais recente que mudou seu histórico.

Você pode então simplesmente

git rebase --onto origin/foo $commit foo
Aristotle Pagaltzis
fonte
11
Nota rápida: eu acho que é bastante intuitivo, mas se você não conhece o awk bem ... aquele one-liner está apenas olhando para a saída da git reflog show origin/fooprimeira linha dizendo "fetch: force-update"; é isso que o git grava quando uma busca faz com que o branch remoto faça qualquer coisa, menos o avanço rápido. (Você também poderia fazer isso manualmente - a atualização forçada é provavelmente a coisa mais recente.)
Cascabel
2
Não é nada como violência. A violência ocasionalmente é divertida
Iolo
5
@iolo Verdade, rebasing é sempre divertido.
Dan Bechard,
1
Como a violência, quase sempre evita rebase. Mas tenha uma ideia de como.
Bob Stein
2
Bem, evite empurrar um rebase onde outros serão afetados.
Aristóteles Pagaltzis
11

Eu diria que a recuperação da seção upstream rebase da página do manual git-rebase cobre praticamente tudo isso.

Realmente não é diferente de recuperar de seu próprio rebase - você move um branch e rebase todos os branches que o tinham em seu histórico para sua nova posição.

Cascabel
fonte
4
Ah, é verdade. Mas embora agora eu entenda o que ele diz, eu não teria entendido antes, antes de descobrir isso sozinho. E não há receita de livro de receitas (talvez com razão nessa documentação). Também defenderei que chamar o “caso difícil” de difícil é FUD. Sugiro que a história reescrita é trivialmente administrável na escala da maior parte do desenvolvimento interno. A maneira supersticiosa com que esse assunto é sempre tratado me irrita.
Aristóteles Pagaltzis
4
@Aristotle: Você está certo ao dizer que é muito gerenciável, visto que todos os desenvolvedores sabem como usar o git, e que você pode comunicar-se efetivamente com todos os desenvolvedores. Em um mundo perfeito, esse seria o fim da história. Mas muitos projetos por aí são grandes o suficiente para que um rebase upstream seja realmente uma coisa assustadora. (E há lugares como meu local de trabalho, onde a maioria dos desenvolvedores nunca ouviu falar de um rebase.) Acho que a "superstição" é apenas uma forma de fornecer o conselho mais seguro e genérico possível. Ninguém quer ser aquele que causa um desastre no repo de outra pessoa.
Cascabel
2
Sim, entendo o motivo. E eu concordo totalmente com isso. Mas há um mundo de diferença entre “não tente fazer isso se você não entende as consequências” e “você nunca deve fazer isso porque é mau”, e só isso eu discordo. É sempre melhor instruir do que instilar medo.
Aristóteles Pagaltzis
@Aristotle: Concordo. Eu tento tender para o lado "certifique-se de que você sabe o que está fazendo", mas especialmente online, tento dar peso suficiente para que um visitante casual do Google perceba. Você está certo, muito provavelmente deveria ser atenuado.
Cascabel
11

Começando com git 1.9 / 2.0 Q1 2014, você não terá de marcar a sua origem ramo anterior antes rebasing-lo no ramo montante regravados, conforme descrito em Aristóteles Pagaltzis 's resposta :
Veja cometer 07d406b e cometer d96855f :

Depois de trabalhar no topicramo criado com git checkout -b topic origin/master, o histórico do ramo de rastreamento remoto origin/masterpode ter sido rebobinado e reconstruído, levando a um histórico desta forma:

                   o---B1
                  /
  ---o---o---B2--o---o---o---B (origin/master)
          \
           B3
            \
             Derived (topic)

onde origin/masterusado para apontar para commits B3, B2, B1e agora aponta para B, e seu topicramo foi iniciado em cima dela de volta quando origin/masterestava em B3.

Este modo usa o reflog de origin/masterpara encontrar B3como o ponto de bifurcação, de modo que o topicpossa ser realocado em cima do atualizadoorigin/master por:

$ fork_point=$(git merge-base --fork-point origin/master topic)
$ git rebase --onto origin/master $fork_point topic

É por isso que o git merge-basecomando tem uma nova opção:

--fork-point::

Encontre o ponto em que uma ramificação (ou qualquer histórico que leve a <commit>) bifurcou-se de outra ramificação (ou qualquer referência) <ref>.
Isso não apenas procura pelo ancestral comum dos dois commits, mas também leva em consideração o reflog de <ref>para ver se a história que levou a <commit>bifurcou-se de uma encarnação anterior do ramo<ref> .


O git pull --rebasecomando " " calcula o ponto de bifurcação da ramificação sendo realocada usando as entradas de reflog da baseramificação " " (normalmente uma ramificação de rastreamento remoto) em que o trabalho da ramificação foi baseado, a fim de lidar com o caso em que a "base" ramo foi rebobinado e reconstruído.

Por exemplo, se o histórico fosse assim:

  • a dica atual do basebranch " " está em B, mas fetch anterior observou que sua dica costumava ser B3e then B2and then B1 antes de chegar ao commit atual, e
  • o branch sendo rebaseado no topo da "base" mais recente é baseado em commit B3,

ele tenta encontrar B3atravessando a saída do " git rev-list --reflog base" (ou seja B, B1, B2, B3) até encontrar uma confirmação de que é um antepassado da ponta atual " Derived (topic)".

Internamente, temos get_merge_bases_many()que computar isso de uma só vez.
Queríamos uma base de mesclagem entre Derivedum commit de mesclagem fictício que resultaria da mesclagem de todas as dicas históricas de " base (origin/master)".
Quando existe tal confirmação, devemos obter um único resultado, que corresponda exatamente a uma das entradas de reflog de " base".


Git 2.1 (Q3 2014) irá adicionar tornar este recurso mais robusto para isso: consulte commit 1e0dacd de John Keeping ( johnkeeping)

lidar corretamente com o cenário em que temos a seguinte topologia:

    C --- D --- E  <- dev
   /
  B  <- master@{1}
 /
o --- B' --- C* --- D*  <- master

Onde:

  • B'é uma versão corrigida do Bque não é idêntica ao patch B;
  • C*e D*são idênticos ao patch Ce Drespectivamente e conflitam textualmente se aplicados na ordem errada;
  • Edepende textualmente D.

O resultado correto da git rebase master devé que Bé identificado como o ponto de bifurcação do deve master, de modo que C, D, Esão os commits que precisam ser repetidos para master; mas Ce Dsão idênticos ao patch com C*e D*e podem ser eliminados, de modo que o resultado final seja:

o --- B' --- C* --- D* --- E  <- dev

Se o ponto de bifurcação não for identificado, pegar Bum branch contendo B'resulta em um conflito e se os commits idênticos ao patch não forem identificados corretamente, então pegar Cum branch contendo D(ou equivalentemente D*) resulta em um conflito.


O " --fork-point" modo de " git rebase" regrediu quando o comando foi reescrito em C na era 2.20, que foi corrigido com Git 2.27 (Q2 2020).

Veja o commit f08132f (09 de dezembro de 2019) de Junio ​​C Hamano ( gitster) .
(Incorporado por Junio ​​C Hamano - gitster- no commit fb4175b , 27 de março de 2020)

rebase: --fork-pointcorreção de regressão

Assinado por: Alex Torok
[jc: reformulou a correção e usou os testes de Alex]
Assinado por: Junio ​​C Hamano

" git rebase --fork-point master" costumava funcionar bem, porque era chamado internamente de " git merge-base --fork-point" que sabia como lidar com refname curto e defini-lo como refname completo antes de chamar a get_fork_point()função subjacente .

Isso não é mais verdade depois que o comando foi reescrito em C, pois sua chamada interna feita diretamente para get_fork_point()não desativa uma referência curta.

Mova a lógica "dwim the refname para o refname completo" que é usada em "git merge-base" para a get_fork_point()função subjacente , de modo que o outro chamador da função na implementação de "git rebase" se comporte da mesma maneira para corrigir esta regressão.

VonC
fonte
1
Observe que um git push --force pode agora (git 1.8.5) ser feito de forma mais prudente: stackoverflow.com/a/18505634/6309
VonC