Mover a (s) confirmação (ões) mais recente (s) para uma nova ramificação com o Git

Eu gostaria de mover os últimos commits que eu cometi para masterizar em um novo branch e levar o master de volta para antes dos commits serem feitos. Infelizmente, meu Git-fu ainda não é forte o suficiente, alguma ajuda?

Ou seja, como eu posso ir disso

master A - B - C - D - E 

para isso?

 newbranch C - D - E / master A - B 

Movendo-se para um novo ramo

AVISO: Este método funciona porque você está criando uma nova ramificação com o primeiro comando: git branch newbranch . Se você quiser mover confirmações para uma ramificação existente, precisará mesclar suas alterações na ramificação existente antes de executar a git reset --hard HEAD~3 (consulte Movendo para uma ramificação existente abaixo). Se você não mesclar suas alterações primeiro, elas serão perdidas.

A menos que haja outras circunstâncias envolvidas, isso pode ser feito facilmente, ramificando e retrocedendo.

 # Note: Any changes not committed will be lost. git branch newbranch # Create a new branch, saving the desired commits git reset --hard HEAD~3 # Move master back by 3 commits (GONE from master) git checkout newbranch # Go to the new branch that still has the desired commits 

Mas tenha certeza de quantos commits voltar. Alternativamente, você pode, em vez de HEAD~3 , simplesmente fornecer o hash do commit (ou a referência como origem / master ) que você quer “reverter” para o branch master (/ current), por exemplo:

 git reset --hard a1b2c3d4 

* 1 Você estará “perdendo” commits da ramificação master, mas não se preocupe, você terá aqueles commits no newbranch!

ATENÇÃO: Com o Git versão 2.0 e posterior, se você posteriormente git rebase a nova ramificação na ramificação original ( master ), você pode precisar de uma opção explícita --no-fork-point durante o rebase para evitar perder as confirmações transportadas. Ter branch.autosetuprebase always definido torna isso mais provável. Veja a resposta de John Mellor para detalhes.

Movendo-se para um ramo existente

Se você quiser mover seus commits para uma ramificação existente , ficará assim:

 git checkout existingbranch git merge master git checkout master git reset --hard HEAD~3 # Go back 3 commits. You *will* lose uncommitted work. git checkout existingbranch 

Para aqueles que estão se perguntando por que funciona (como eu estava no começo):

Você quer voltar para C e mover D e E para o novo ramo. Aqui está o que parece no começo:

 ABCDE (HEAD) ↑ master 

Depois git branch newBranch :

  newBranch ↓ ABCDE (HEAD) ↑ master 

Após git reset --hard HEAD~2 :

  newBranch ↓ ABCDE (HEAD) ↑ master 

Como um ramo é apenas um ponteiro, o mestre apontou para o último commit. Quando você fez newBranch , você simplesmente fez um novo ponteiro para o último commit. Então, usando o git reset você moveu o ponteiro mestre de volta para dois commits. Mas desde que você não mudou newBranch , ele ainda aponta para o commit originalmente feito.

Em geral…

O método exposto pelo sykora é a melhor opção neste caso. Mas às vezes não é o mais fácil e não é um método geral. Para um método geral use git cherry-pick :

Para alcançar o que o OP quer, é um processo de duas etapas:

Passo 1 – Anote quais commits do master você quer em um newbranch

Executar

 git checkout master git log 

Observe os hashes de (digamos 3) commits que você quer no newbranch . Aqui vou usar:
C commit: 9aa1233
D commit: 453ac3d
E commit: 612ecb3

Nota: Você pode usar os primeiros sete caracteres ou todo o hash de confirmação

Passo 2 – Coloque-os no newbranch

 git checkout newbranch git cherry-pick 612ecb3 git cherry-pick 453ac3d git cherry-pick 9aa1233 

OU (no Git 1.7.2+, use intervalos)

 git checkout newbranch git cherry-pick 612ecb3~1..9aa1233 

git cherry-pick aplica esses três commits para newbranch.

Ainda outra maneira de fazer isso, usando apenas 2 comandos. Também mantém sua tree de trabalho atual intacta.

 git checkout -b newbranch # switch to a new branch git branch -f master HEAD~3 # make master point to some older commit 

Versão antiga – antes de aprender sobre git branch -f

 git checkout -b newbranch # switch to a new branch git push . +HEAD~3:master # make master point to some older commit 

Ser capaz de push para . é um bom truque para saber.

A maioria das respostas anteriores está perigosamente errada!

Não faça isso:

 git branch -t newbranch git reset --hard HEAD~3 git checkout newbranch 

Como na próxima vez que você rodar o git rebase (ou git pull --rebase ), esses 3 commits serão silenciosamente descartados do newbranch ! (veja a explicação abaixo)

Em vez disso, faça isso:

 git reset --keep HEAD~3 git checkout -t -b newbranch git cherry-pick ..HEAD@{2} 
  • Primeiro, ele descarta os 3 commits mais recentes ( --keep é como --hard , mas é mais seguro, pois falha ao invés de descartar as alterações não confirmadas).
  • Então bifurca-se de newbranch .
  • Então ele escolhe aqueles 3 commits de volta para newbranch . Como eles não são mais referenciados por um branch, ele faz isso usando o reflog do git: HEAD@{2} é o commit que o HEAD usou para se referir a 2 operações atrás, ie antes de nós 1. check out newbranch e 2. used git reset para descartar os 3 commits.

Atenção: o reflog é habilitado por padrão, mas se você o tiver desativado manualmente (por exemplo, usando um repository git “nu”), você não poderá recuperar os 3 commits após executar o git reset --keep HEAD~3

Uma alternativa que não depende do reflog é:

 # newbranch will omit the 3 most recent commits. git checkout -b newbranch HEAD~3 git branch --set-upstream-to=oldbranch # Cherry-picks the extra commits from oldbranch. git cherry-pick ..oldbranch # Discards the 3 most recent commits from oldbranch. git branch --force oldbranch oldbranch~3 

(se preferir, você pode escrever @{-1} – a ramificação anteriormente retirada – em vez de oldbranch ).


Explicação técnica

Por que o git rebase descartaria os 3 commits após o primeiro exemplo? É porque o git rebase sem argumentos habilita a opção --fork-point por padrão, que usa o reflog local para tentar ser robusto contra o branch upstream sendo forçado.

Suponha que você se ramificou de origem / mestre quando continha commits M1, M2, M3, e depois fez três commits:

 M1--M2--M3 <-- origin/master \ T1--T2--T3 <-- topic 

mas então alguém reescreve a história empurrando origem / mestre para remover M2:

 M1--M3' <-- origin/master \ M2--M3--T1--T2--T3 <-- topic 

Usando seu reflog local, o gase git rebase pode ver que você foi bifurcado a partir de uma encarnação anterior da origem / ramificação principal e, portanto, que os commits M2 e M3 não são realmente parte de sua ramificação de tópico. Por isso, é razoável supor que, uma vez que o M2 foi removido do branch upstream, você não o quer mais em sua ramificação de tópico, uma vez que o branch de tópicos é rebaseado:

 M1--M3' <-- origin/master \ T1'--T2'--T3' <-- topic (rebased) 

Esse comportamento faz sentido e geralmente é a coisa certa a fazer quando se está rebaixando.

Então, a razão pela qual os seguintes comandos falham:

 git branch -t newbranch git reset --hard HEAD~3 git checkout newbranch 

é porque eles deixam o reflog no estado errado. Git vê newbranch como tendo bifurcado o branch upstream em uma revisão que inclui os 3 commits, então o reset --hard reescreve o histórico do upstream para remover os commits, e então da próxima vez que você rodar git rebase ele os descartará como qualquer outro commit que foi removido do upstream.

Mas neste caso particular, queremos que esses 3 commits sejam considerados como parte do branch tópico. Para conseguir isso, precisamos extrair o upstream na revisão anterior que não inclui os 3 commits. Isso é o que minhas soluções sugeridas fazem, portanto, elas deixam o reflog no estado correto.

Para mais detalhes, veja a definição de --fork-point nos documentos git rebase e git merge-base .

Isso não “move” eles no sentido técnico, mas tem o mesmo efeito:

 A--B--C (branch-foo) \ ^-- I wanted them here! \ D--E--F--G (branch-bar) ^--^--^-- Opps wrong branch! While on branch-bar: $ git reset --hard D # remember the SHAs for E, F, G (or E and G for a range) A--B--C (branch-foo) \ \ D-(E--F--G) detached ^-- (branch-bar) Switch to branch-foo $ git cherry-pick E..G A--B--C--E'--F'--G' (branch-foo) \ E--F--G detached (This can be ignored) \ / D--H--I (branch-bar) Now you won't need to worry about the detached branch because it is basically like they are in the trash can waiting for the day it gets garbage collected. Eventually some time in the far future it will look like: A--B--C--E'--F'--G'--L--M--N--... (branch-foo) \ \ D--H--I--J--K--.... (branch-bar) 

Para fazer isso sem rewrite o histórico (ou seja, se você já enviou os commits):

 git checkout master git revert  git checkout -b new-branch git cherry-pick  

Ambos os ramos podem ser empurrados sem força!

Solução muito mais simples

E se:

  • Seu objective principal é reverter o master e
  • Você quer manter as alterações, mas não se importa particularmente com os commits individuais, e
  • Você ainda não empurrou e
  • Você quer que isso seja fácil e não complicado com ramos temporários e outras dores de cabeça

Então, o seguinte é muito mais simples (iniciando no branch master que possui commits errados):

 git reset HEAD~3 git stash git checkout newbranch git stash pop 

O que isto faz, por número de linha:

  1. Desfaz os últimos três commits (e suas mensagens) para master , mas deixa todos os arquivos de trabalho intactos
  2. Armazena todas as alterações do arquivo de trabalho, tornando a tree de trabalho master igual ao estado HEAD ~ 3
  3. Muda para um branch newbranch existente
  4. Retira os arquivos alterados do stash e para o seu diretório de trabalho

Agora você pode usar git add e git commit como faria normalmente. Todos os novos commits serão adicionados ao newbranch .

O que isso não faz:

  • Não deixa twigs temporários randoms bagunçando sua tree
  • Não preserva os commits e commits errados. Você precisará adicionar uma nova mensagem de commit a este novo commit.

O OP declarou que o objective era “levar o mestre de volta a antes que os commits fossem feitos” sem perder as alterações e essa solução faz isso.

Eu faço isso pelo menos uma vez por semana quando eu acidentalmente faço novos commits para master ao invés de develop . Normalmente eu tenho apenas um commit para rollback, em cujo caso usar o git reset HEAD^ na linha 1 é uma forma mais simples de reverter apenas um commit.

Teve apenas esta situação:

 Branch one: ABCDEFJLM \ (Merge) Branch two: GIKN 

Eu executei:

 git branch newbranch git reset --hard HEAD~8 git checkout newbranch 

Eu esperava que cometer eu seria a cabeça, mas comprometo L é agora …

Para ter certeza de pousar no lugar certo na história, é mais fácil trabalhar com o hash do commit

 git branch newbranch git reset --hard ######### git checkout newbranch 

1) Crie uma nova ramificação, que move todas as suas alterações para new_branch.

 git checkout -b new_branch 

2) Então volte para o ramo antigo.

 git checkout master 

3) fazer rebase git

 git rebase -i  

4) Em seguida, o editor aberto contém as últimas 3 informações de confirmação.

 ... pick  C pick  D pick  E ... 

5) Alterar pick para drop em todos os 3 commits. Em seguida, salve e feche o editor.

 ... drop  C drop  D drop  E ... 

6) Agora, os últimos 3 commits são removidos da ramificação atual ( master ). Agora empurre o ramo com força, com o sinal + antes do nome da ramificação.

 git push origin +master 

Você pode fazer isso é apenas 3 simples passo que eu usei.

1) faça uma nova ramificação onde você deseja enviar sua atualização recente.

git branch

2) Encontrar o ID de confirmação recente para confirmar no novo ramo.

git log

3) Copie essa nota de ID de commit que a lista de commits Most Recent ocorre no topo. então você pode encontrar o seu commit. você também encontra isso via mensagem.

git cherry-pick d34bcef232f6c...

você também pode fornecer alguns toques de id de confirmação.

git cherry-pick d34bcef...86d2aec

Agora seu trabalho feito. Se você escolheu o ID correto e o ramo correto, você terá sucesso. Então, antes disso, tenha cuidado. mais outro problema pode ocorrer.

Agora você pode empurrar seu código

git push