Configurando o ponteiro pai git para um pai diferente

220

Se eu tenho um commit no passado que aponta para um pai, mas quero mudar o pai para o qual ele aponta, como eu faria isso?

dougvk
fonte

Respostas:

404

Usando git rebase. É o genérico "pegar commit (s) e colocá-los em um comando pai (base)" diferente no Git.

Algumas coisas a saber, no entanto:

  1. Como os SHAs de confirmação envolvem seus pais, quando você altera o pai de uma determinada confirmação, o SHA mudará - assim como os SHAs de todos os commits que vierem depois dele (mais recentes que ele) na linha de desenvolvimento.

  2. Se você estiver trabalhando com outras pessoas, e você já empurrou o commit em questão para onde eles o puxaram, a modificação do commit provavelmente é uma Bad Idea ™. Isso se deve ao número 1 e, portanto, à confusão resultante que os repositórios dos outros usuários encontrarão ao tentar descobrir o que aconteceu devido aos seus SHAs não mais corresponderem aos deles para as "mesmas" confirmações. (Consulte a seção "RECUPERANDO DO UPSTREAM REBASE" da página de manual vinculada para obter detalhes.)

Dito isto, se você estiver atualmente em uma ramificação com alguns commit que deseja mover para um novo pai, seria algo parecido com isto:

git rebase --onto <new-parent> <old-parent>

Isso vai passar tudo depois <old-parent> no ramo atual para sentar em cima de <new-parent>vez.

Âmbar
fonte
Estou usando o git rebase --onto para mudar de pais, mas parece que acabo com apenas um único commit. Eu fiz uma pergunta sobre ele: stackoverflow.com/questions/19181665/…
Eduardo Mello
7
Aha, então é para isso que --ontoé usado! A documentação sempre foi completamente obscura para mim, mesmo depois de ler esta resposta!
21915 Daniel C. Sobral
6
Esta linha: git rebase --onto <new-parent> <old-parent>contou-me tudo sobre como usar rebase --ontoque todas as outras perguntas e documentos que li até agora falharam.
jpmc26
3
Para mim, este comando deve ser:, rebase this commit on that oneou git rebase --on <new-parent> <commit>. Usar o pai antigo aqui não faz sentido para mim.
Samir Aguiar
1
@kopranb O pai antigo é importante quando você deseja descartar parte do caminho da cabeça para a cabeça remota. O caso que você está descrevendo é tratado por git rebase <new-parent>.
clacke
35

Se acontecer que você precisa evitar a reestruturação das confirmações subseqüentes (por exemplo, porque uma reescrita do histórico seria insustentável), use o git replace (disponível no Git 1.6.5 e posterior).

# …---o---A---o---o---…
#
# …---o---B---b---b---…
#
# We want to transplant B to be "on top of" A.
# The tree of descendants from B (and A) can be arbitrarily complex.

replace_first_parent() {
    old_parent=$(git rev-parse --verify "${1}^1") || return 1
    new_parent=$(git rev-parse --verify "${2}^0") || return 2
    new_commit=$(
      git cat-file commit "$1" |
      sed -e '1,/^$/s/^parent '"$old_parent"'$/parent '"$new_parent"'/' |
      git hash-object -t commit -w --stdin
    ) || return 3
    git replace "$1" "$new_commit"
}
replace_first_parent B A

# …---o---A---o---o---…
#          \
#           C---b---b---…
#
# C is the replacement for B.

Com a substituição acima estabelecida, quaisquer solicitações para o objeto B realmente retornarão o objeto C. O conteúdo de C é exatamente o mesmo que o conteúdo de B, exceto o primeiro pai (os mesmos pais (exceto o primeiro), a mesma árvore, mesma mensagem de confirmação).

As substituições estão ativas por padrão, mas podem ser desativadas usando a --no-replace-objectsopção git (antes do nome do comando) ou configurando a GIT_NO_REPLACE_OBJECTSvariável de ambiente. As substituições podem ser compartilhadas pressionando refs/replace/*(além do normal refs/heads/*).

Se você não gostar do commit-munging (feito com sed acima), poderá criar seu commit de substituição usando comandos de nível superior:

git checkout B~0
git reset --soft A
git commit -C B
git replace B HEAD
git checkout -

A grande diferença é que essa sequência não propaga os pais adicionais se B for um commit de mesclagem.

Chris Johnsen
fonte
E então, como você enviaria as alterações para um repositório existente? Parece que funciona localmente, mas git push não empurrar nada depois disso
Baptiste Wicht
2
@BaptisteWicht: As substituições são armazenadas em um conjunto separado de referências. Você precisará organizar para empurrar (e buscar) a refs/replace/hierarquia de referências. Se você precisar fazer isso apenas uma vez, poderá fazê-lo git push yourremote 'refs/replace/*'no repositório de origem e git fetch yourremote 'refs/replace/*:refs/replace/*'nos repositórios de destino. Se você precisar fazer isso várias vezes, poderá adicionar esses refspecs a uma remote.yourremote.pushvariável de configuração (no repositório de origem) e a uma remote.yourremote.fetchvariável de configuração (nos repositórios de destino).
31414 Chris Chrissen
3
Uma maneira mais simples de fazer isso é usar git replace --grafts. Não tenho certeza de quando isso foi adicionado, mas seu objetivo é criar uma substituição igual à confirmação B, mas com os pais especificados.
Paul Wagland
32

Observe que a alteração de uma confirmação no Git requer que todas as confirmações a seguir sejam alteradas. Isso é desencorajado se você publicou esta parte da história, e alguém pode ter construído seu trabalho na história que era antes da mudança.

A solução alternativa git rebasemencionada na resposta de Amber é usar o mecanismo de enxertos (consulte a definição de enxertos do Git no Git Glossary e a documentação do .git/info/graftsarquivo na documentação do Git Repository Layout ) para alterar o pai de um commit, verifique se ele corrigiu alguma coisa com algum visualizador de histórico ( gitk, git log --graph, etc.) e, em seguida, use git filter-branch(como descrito na seção "Exemplos" de sua página de manual) para torná-lo permanente (e em seguida remova o enxerto e, opcionalmente, remova os refs originais com backup git filter-branchou reclone o repositório):

echo "$ commit-id $ graft-id" >> .git / info / grafts
git filter-branch $ graft-id..HEAD

NOTA !!! Esta solução é diferente da solução rebase em que git rebaseseria rebase / transplante mudanças , enquanto solução de enxerto baseada seria commits simplesmente Reparent como é , não levando em conta as diferenças entre pai velho e novo pai!

Jakub Narębski
fonte
3
Pelo que entendi (de git.wiki.kernel.org/index.php/GraftPoint ), git replacesubstituiu os enxertos git (supondo que você tenha o git 1.6.5 ou posterior).
Alexander Bird
4
@ Thr4wn: Em grande parte verdade, mas se você deseja reescrever a história , enxertos + git-filter-branch (ou rebase interativa ou exportação rápida + por exemplo, reposicionador) é a maneira de fazê-lo. Se você deseja / precisa preservar a história , o git-replace é muito superior aos enxertos.
Jakub Narębski
@ JakubNarębski, você poderia explicar o que você quer dizer com reescrever vs preservar a história? Por que não consigo usar git-replace+ git-filter-branch? No meu teste, filter-branchparecia respeitar a substituição, refazendo a árvore inteira.
cdunn2001
1
@ cdunn2001: git filter-branchreescreve o histórico, respeitando os enxertos e substituições (mas no reescrito o histórico será discutido); push resultará em alterações não fasf-forward. git replacecria substituições transferíveis no local; push será rápido, mas você precisará pressionar refs/replacepara transferir substituições para ter o histórico corrigido. HTH
Jakub Narębski
23

Para esclarecer as respostas acima e conectar descaradamente meu próprio script:

Depende se você deseja "rebase" ou "reparent". Uma rebase , como sugerido por Amber , move-se por diferenças . Um reparador , como sugerido por Jakub e Chris , se desloca por instantâneos de toda a árvore. Se você deseja reparar, sugiro usar em git reparentvez de fazer o trabalho manualmente.

Comparação

Suponha que você tenha a imagem à esquerda e deseje que ela se pareça com a imagem à direita:

                   C'
                  /
A---B---C        A---B---C

O rebasing e o reparenting produzirão a mesma imagem, mas a definição de C'difere. Com git rebase --onto A B, C'não conterá nenhuma alteração introduzida por B. With git reparent -p A, C'será idêntico a C(exceto que Bnão estará na história).

Mark Lodato
fonte
1
+1 Eu experimentei o reparentscript; funciona lindamente; altamente recomendado . Também recomendo criar um novo branchrótulo e para checkoutesse ramo antes de chamar (como o rótulo do ramo será movido).
Rhubbarb
Também é importante notar que: (1) o nó original permanece intacto e (2) o novo nó não herda os filhos do nó original.
Rhubbarb
0

Definitivamente, a resposta de @ Jakub me ajudou algumas horas atrás, quando eu estava tentando exatamente a mesma coisa que o OP.
No entanto, git replace --graftagora é a solução mais fácil para enxertos. Além disso, um grande problema com essa solução foi que o ramo de filtro me soltou todos os ramos que não foram mesclados ao ramo da HEAD. Então, git filter-repofez o trabalho de forma perfeita e sem falhas.

$ git replace --graft <commit> <new parent commit>
$ git filter-repo --force

Para obter mais informações: consulte a seção "Histórico de novo enxerto" nos documentos

Teodoro
fonte