Mova os commit (s) mais recentes para um novo branch com Git

4985

Gostaria de mudar os últimos commits comprometidos em dominar para uma nova ramificação e retornar o master antes que esses commits fossem feitos. Infelizmente, meu Git-fu ainda não é forte o suficiente, ajuda?

Ou seja, como posso passar disso

master A - B - C - D - E

para isso?

newbranch     C - D - E
             /
master A - B 
Mark A. Nicolosi
fonte
114
Nota: Eu fiz a pergunta oposta aqui
Benjol
3
eddmann.com/posts/… este funciona #
Sagar Naliyapara
7
Os comentários aqui foram eliminados? Eu pergunto, porque durante a minha visita bimestral a essa pergunta, eu sempre rolar por esse comentário.
Tejas Kale

Respostas:

6452

Movendo para uma ramificação existente

Se você deseja mover suas confirmações para uma ramificação existente , será semelhante a:

git checkout existingbranch
git merge master         # Bring the commits here
git checkout master
git reset --keep HEAD~3  # Move master back by 3 commits.
git checkout existingbranch

A --keepopção preserva quaisquer alterações não confirmadas que você possa ter em arquivos não relacionados ou anula se essas alterações precisarem ser substituídas - da mesma forma que git checkoutocorre. Se for interrompido, git stashas alterações e tentativas serão repetidas ou usadas --hardpara perdê- las (mesmo de arquivos que não foram alterados entre as confirmações!)

Movendo para uma nova ramificação

Este método funciona criando uma nova ramificação com o primeiro comando ( git branch newbranch), mas sem mudar para ele. Em seguida, revertemos a ramificação atual (principal) e mudamos para a nova ramificação para continuar trabalhando.

git branch newbranch      # Create a new branch, containing all current commits
git reset --keep HEAD~3   # Move master back by 3 commits (Make sure you know how many commits you need to go back)
git checkout newbranch    # Go to the new branch that still has the desired commits
# Warning: after this it's not safe to do a rebase in newbranch without extra care.

Mas certifique-se de quantos commits para voltar. Como alternativa, em vez de HEAD~3, você pode simplesmente fornecer o hash do commit (ou a referência como origin/master) que deseja reverter, por exemplo:

git reset --keep a1b2c3d4

AVISO: Com o Git versão 2.0 e posterior, se você adiantar git rebasea nova ramificação na ramificação original ( master), poderá ser necessária uma --no-fork-pointopção explícita durante a reformulação para evitar perder as confirmações que você moveu da ramificação mestre. Tendo branch.autosetuprebase alwaysdefinido torna isso mais provável. Veja a resposta de John Mellor para obter detalhes.

Duc Filan
fonte
249
E, em particular, não tente voltar além do ponto em que você enviou por último o commit para outro repositório a partir do qual alguém poderia ter puxado.
Greg Hewgill 27/10/2009
107
Pensando se você pode explicar por que isso funciona. Para mim, você está criando um novo ramo, removendo três confirmações do ramo antigo em que ainda está e, em seguida, verificando o ramo que criou. Então, como os commits que você removeu aparecem magicamente no novo ramo?
Jonathan Dumaine
142
@ Jonathan Dumaine: Porque eu criei o novo ramo antes de remover os commits do ramo antigo. Eles ainda estão lá no novo ramo.
Sykora
86
filiais em git são apenas marcadores que apontam para commits na história, não há nada a ser clonado, criado ou excluído (exceto os marcadores)
knittl
218
Observe também: não faça isso com alterações não confirmadas em sua cópia de trabalho! Isso só me mordeu! :(
Adam Tuttle
1039

Para aqueles que se perguntam 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 a princípio:

A-B-C-D-E (HEAD)
        ↑
      master

Depois git branch newBranch:

    newBranch
        ↓
A-B-C-D-E (HEAD)
        ↑
      master

Depois git reset --hard HEAD~2:

    newBranch
        ↓
A-B-C-D-E (HEAD)
    ↑
  master

Como um ramo é apenas um ponteiro, o mestre apontou para o último commit. Quando você criou newBranch , você simplesmente criou um novo ponteiro para o último commit. Em seguida, usando git resetvocê moveu o ponteiro mestre para trás duas confirmações. Mas como você não moveu o newBranch , ele ainda aponta para o commit original.

Ryan Lundy
fonte
56
Eu também precisava fazer um git push origin master --forcepara a mudança aparecer no repositório principal.
Dženan
9
Esta resposta faz com que as confirmações sejam perdidas: da próxima vez que você git rebase, as 3 confirmações serão descartadas silenciosamente newbranch. Veja minha resposta para detalhes e alternativas mais seguras.
31716 John Mellor
14
@ John, isso é um absurdo. Rebasear sem saber o que você está fazendo faz com que os compromissos sejam perdidos. Se você perdeu commits, sinto muito por você, mas esta resposta não perdeu seus commits. Observe que origin/masternão aparece no diagrama acima. Se você pressionasse origin/mastere fizesse as alterações acima, com certeza, as coisas seriam engraçadas. Mas esse é o tipo de problema "Doutor, dói quando faço isso". E está fora do escopo para o que a pergunta original fez. Sugiro que você escreva sua própria pergunta para explorar seu cenário em vez de sequestrar este.
precisa saber é o seguinte
1
@ John, na sua resposta, você disse "Não faça isso! git branch -t newbranch". Volte e leia as respostas novamente. Ninguém sugeriu fazer isso.
Ryan Lundy
1
@ Kyralessa, claro, mas se você olhar para o diagrama da pergunta, é claro que eles querem newbranchse basear na masterfilial local existente . Depois de realizar a resposta aceita, quando o usuário fica em torno de correr git rebaseem newbranch, git vai lembrá-los que eles se esqueceu de definir o ramo upstream, então eles vão executar git branch --set-upstream-to=master, em seguida, git rebasee têm o mesmo problema. Eles também podem usar git branch -t newbranchem primeiro lugar.
precisa
455

Em geral...

O método exposto pelo sykora é a melhor opção nesse 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 deseja, é um processo de duas etapas:

Etapa 1 - Observe o que você confirma do mestre que deseja em um newbranch

Executar

git checkout master
git log

Observe os hashes de (digamos 3) confirmações que você deseja newbranch. Aqui vou usar:
C commit: 9aa1233
D commit: 453ac3d
E commit:612ecb3

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

Etapa 2 - Coloque-os no newbranch

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

OR (no Git 1.7.2+, use intervalos)

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

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

Ivan
fonte
13
O OP não estava tentando passar do master para o newbranch? Se você escolher cereja no mestre, você estará adicionando ao ramo mestre - adicionando confirmações que já possuía, de fato. E não recua nem a cabeça do galho para B. Ou há algo sutil e legal que eu não estou entendendo?
RaveTheTadpole
6
Isso funciona muito bem se você acidentalmente confirmar o ramo errado, não mestre, quando deveria ter criado um novo ramo de recurso.
julianc 27/02
5
+1 para uma abordagem útil em algumas situações. Isso é bom se você deseja extrair seus próprios commits (que são intercalados com outros) para um novo ramo.
Tyler V.
8
É melhor resposta. Dessa forma, você pode mover confirmações para qualquer ramificação.
skywinder
8
A ordem da colheita da cereja é importante?
precisa saber é
328

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

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 eu aprender sobregit 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 pushpara .é um truque bom saber.

aragaer
fonte
1
Diretório atual. Eu acho que isso funcionaria apenas se você estiver em um diretório superior.
Aragaer 28/03
1
O impulso local é indutor de sorriso, mas, refletindo, como é diferente git branch -faqui?
jthill
2
@GardardSexton .é atual diretor. O git pode enviar para REMOTES ou URLs GIT. path to local directoryé suportada sintaxe dos URLs do Git. Consulte a seção URLs do GIT em git help clone.
weakish
31
Não sei por que isso não é classificado como mais alto. Simples, sem o pequeno, mas potencial, risco de redefinição do git - difícil.
Godsmith
4
@ Godsmith Meu palpite é que as pessoas preferem três comandos simples a dois um pouco mais obscuros. Além disso, as respostas mais votadas recebem mais votos por natureza de serem exibidas primeiro.
JS_Riddler
323

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

Na próxima vez em que você executar git rebase(ou git pull --rebase) esses 3 commits serão descartados silenciosamente newbranch! (veja a explicação abaixo)

Em vez disso, faça o seguinte:

git reset --keep HEAD~3
git checkout -t -b newbranch
git cherry-pick ..HEAD@{2}
  • Primeiro, ele descarta os três commits mais recentes ( --keepé --hardmais seguro, pois falha ao invés de jogar fora as alterações não confirmadas).
  • Então bifurca-se newbranch.
  • Em seguida, escolhe os 3 commits de volta newbranch. Como eles não são mais referenciados por um ramo, ele faz isso usando o reflog do git : HEAD@{2}é o commit que HEADcostumava se referir a 2 operações atrás, ou seja, antes de 1. fazer o check-out newbranche 2. usado git resetpara descartar os 3 commit.

Aviso: o reflog está ativado por padrão, mas se você o desativou manualmente (por exemplo, usando um repositório git "bare"), não será possível recuperar os 3 commits após a execuçã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}- o ramo previamente retirado - em vez de oldbranch).


Explicação técnica

Por que git rebasedescartar os 3 commits após o primeiro exemplo? Isso ocorre porque, git rebasesem argumentos, a --fork-pointopção por padrão é ativada, que usa o reflog local para tentar ser robusto contra o desvio do ramo upstream.

Suponha que você ramificou origem / mestre quando ele continha confirmações M1, M2, M3 e, em seguida, fez três confirmações:

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

mas alguém reescreve o histórico pressionando origem / mestre para remover M2:

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

Usando seu reflog local, você git rebasepode ver que você se retirou de uma encarnação anterior da ramificação de origem / mestre e, portanto, as confirmações M2 e M3 não são realmente parte da ramificação de tópicos. Portanto, considera-se razoavelmente que, desde que o M2 foi removido da ramificação upstream, você não a deseja mais em sua ramificação de tópico, assim que a ramificação de tópico for rebaseada:

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

Esse comportamento faz sentido e geralmente é a coisa certa a se fazer quando se rebase.

Portanto, o motivo pelo 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. O Git vê newbranchcomo bifurcado no ramo upstream em uma revisão que inclui os 3 commits, depois reset --hardreescreve o histórico do upstream para remover os commits e, da próxima vez que você o executa git rebase, descarta-os como qualquer outro commit que foi removido do upstream.

Mas neste caso em particular, queremos que esses 3 commits sejam considerados como parte do ramo de tópicos. Para conseguir isso, precisamos extrair o upstream da revisão anterior que não inclui os 3 commits. É isso que minhas soluções sugeridas fazem, portanto, ambas deixam o reflog no estado correto.

Para mais detalhes, ver a definição de --fork-pointno rebase git e merge-base git docs.

John Mellor
fonte
15
Esta resposta diz "NÃO faça isso!" acima de algo que ninguém sugeriu fazer.
Ryan Lundy
4
A maioria das pessoas não reescreve a história publicada, especialmente em master. Então não, eles não estão perigosamente errados.
Walf
4
@ Kyralessa, a que -tvocê está se referindo git branchacontece implicitamente se você tiver git config --global branch.autosetuprebase alwaysdefinido. Mesmo se não, eu já expliquei a você que o mesmo problema ocorre se você configurar o rastreamento após executar esses comandos, como o OP provavelmente pretende fazer diante da pergunta deles.
John Mellor
2
@RockLee, sim, o modo geral de corrigir essas situações é criar um novo ramo (newbranch2) a partir de um ponto de partida seguro e escolher todos os commits que você deseja manter (de badnewbranch para newbranch2). A escolha da cereja fornecerá aos commits novos hashes, para que você possa refazer com segurança o newbranch2 (e agora pode excluir o badnewbranch).
John Mellor
2
@Walf, você entendeu errado: o git rebase foi projetado para ser robusto contra os upstreams que tiveram sua história reescrita. Infelizmente, os efeitos colaterais dessa robustez afetam a todos, mesmo que nem eles nem o seu montante reescrevam a história.
John Mellor
150

Solução muito mais simples usando o git stash

Aqui está uma solução muito mais simples para confirmações no ramo errado. Iniciando na ramificação masterque possui três confirmações incorretas:

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

Quando usar isso?

  • Se seu objetivo principal é reverter master
  • Você deseja manter as alterações no arquivo
  • Você não se importa com as mensagens nos commits equivocados
  • Você ainda não empurrou
  • Você quer que seja fácil de memorizar
  • Você não quer complicações como ramificações temporárias / novas, localização e cópia de hashes de confirmação e outras dores de cabeça

O que isso faz, pelo número da linha

  1. Desfaz as últimas três confirmações (e suas mensagens) mastere ainda mantém intactos todos os arquivos de trabalho
  2. Esconde todas as alterações do arquivo de trabalho, tornando a masterárvore de trabalho exatamente igual ao estado HEAD ~ 3
  3. Muda para uma ramificação existente newbranch
  4. Aplica as alterações ocultas ao seu diretório de trabalho e limpa a ocultação

Agora você pode usar git adde git commitcomo faria normalmente. Todas as novas confirmações serão adicionadas a newbranch.

O que isso não faz

  • Não deixa galhos temporários aleatórios bagunçando sua árvore
  • Ele não preserva as mensagens de confirmação incorretas; portanto , você precisará adicionar uma nova mensagem de confirmação a essa nova confirmação
  • Atualizar! Use a seta para cima para rolar pelo buffer de comando e reaplicar o commit anterior com sua mensagem de commit (obrigado @ARK)

Metas

O OP declarou que o objetivo era "recuperar o domínio antes que esses comprometimentos fossem feitos" sem perder as alterações e essa solução faz isso.

Faço isso pelo menos uma vez por semana quando acidentalmente faço novos commit em mastervez de develop. Normalmente, tenho apenas uma confirmação para reverter, caso em que o uso git reset HEAD^na linha 1 é uma maneira mais simples de reverter apenas uma confirmação.

Não faça isso se você empurrou as alterações do mestre para montante

Outra pessoa pode ter feito essas alterações. Se você estiver reescrevendo apenas o mestre local, não haverá impacto quando for promovido a montante, mas enviar um histórico reescrito para os colaboradores pode causar dores de cabeça.

bater
fonte
4
Obrigado, estou tão feliz por ler muito do passado / do que chegar até aqui, porque é um caso de uso bastante comum para mim também. Somos tão atípicos?
Jim Mack
6
Acho que somos totalmente típicos e "oops, que cometi dominar por engano" é o caso de uso mais comum para a necessidade de reverter um punhado ou menos de confirmações. Sorte que esta solução é tão simples que eu a memorizei agora.
Slam de
9
Essa deve ser a resposta aceita. É simples, fácil de entender e fácil de lembrar
Sina Madani 28/11
1
Eu acho que o esconderijo é necessário. Acabei de fazê-lo sem e funcionou bem.
A Campos
1
Você também pode recuperar facilmente suas mensagens de confirmação, se as tiver no histórico da CLI (linha de comando). Por acaso eu tinha os comandos git adde git commitque eu usava, então tudo o que eu precisava fazer era acertar a seta e entrar algumas vezes e explodir! Tudo estava de volta, mas agora no ramo certo.
Luke Gedeon
30

Isso não os "move" 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)
Sukima
fonte
1
Você não pode usar rebasepara a mesma coisa?
Bergi
Sim, você pode usar alternativamente rebasena ramificação desanexada no cenário acima.
Sukima 30/06
24

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

git checkout master
git revert <commitID(s)>
git checkout -b new-branch
git cherry-pick <commitID(s)>

Ambos os galhos podem ser empurrados sem força!

teh_senaus
fonte
Mas você precisa lidar com o cenário de reversão, que, dependendo da sua circunstância, pode ser muito mais complicado. Se você reverter uma confirmação na ramificação, o Git ainda verá essas confirmações como ocorridas; portanto, para desfazer isso, você deve reverter a reversão. Isso queima algumas pessoas, especialmente quando elas revertem uma mesclagem e tentam mesclar o ramo novamente, apenas para descobrir que o Git acredita que ele já foi fundido nesse ramo (o que é inteiramente verdade).
21716 Makoto
1
É por isso que escolho os commits no final, para um novo ramo. Dessa forma, o git os vê como novos commits, o que resolve seu problema.
Teh_senaus
Isso é mais perigoso do que parece à primeira vista, pois você está alterando o estado da história do repositório sem realmente entender as implicações desse estado.
21716 Makoto
5
Não sigo o seu argumento - o ponto desta resposta é que você não está mudando o histórico, simplesmente adicionando novos commits (que efetivamente desfazem as refazer as alterações). Esses novos commits podem ser enviados e mesclados normalmente.
Teh_senaus
13

Teve exatamente esta situação:

Branch one: A B C D E F     J   L M  
                       \ (Merge)
Branch two:             G I   K     N

Eu executei:

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

Eu esperava que o commit fosse o HEAD, mas o commit é L agora ...

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

git branch newbranch 
git reset --hard #########
git checkout newbranch
Darkglow
fonte
9

Como posso ir disso

A - B - C - D - E 
                |
                master

para isso?

A - B - C - D - E 
    |           |
    master      newbranch

Com dois comandos

  • git branch -m master newbranch

dando

A - B - C - D - E 
                |
                newbranch

e

  • mestre do ramo git B

dando

A - B - C - D - E
    |           |
    master      newbranch
Ivan
fonte
Sim, isso funciona e é bastante fácil. A GUI do Sourcetree está um pouco confusa sobre as alterações feitas no shell git, mas depois de uma busca, tudo fica bem novamente.
lars k.
Sim, eles são como na pergunta. O primeiro par de diagramas pretende ser equivalente aos da pergunta, apenas redesenhado da maneira que eu gostaria para o propósito de ilustração na resposta. Renomeie basicamente o ramo mestre como newbranch e crie um novo ramo mestre onde você desejar.
Ivan
Eu acredito que esta é a resposta certa.
Leonardo Herrera
4

Se você só precisa mover todos os seus commits não pressionados para uma nova ramificação , precisará,

  1. crie uma nova ramificação a partir da atual:git branch new-branch-name

  2. empurre seu novo ramo :git push origin new-branch-name

  3. reverta sua ramificação antiga (atual) para o último estado push / estável:git reset --hard origin/old-branch-name

Algumas pessoas também têm outras, em upstreamsvez de origin, elas devem usarupstream

Shamsul Arefin Sajib
fonte
3

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

git checkout -b new_branch

2) Volte ao ramo antigo.

git checkout master

3) Faça git rebase

git rebase -i <short-hash-of-B-commit>

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

...
pick <C's hash> C
pick <D's hash> D
pick <E's hash> E
...

5) Altere pickpara dropem todos esses 3 commits. Em seguida, salve e feche o editor.

...
drop <C's hash> C
drop <D's hash> D
drop <E's hash> E
...

6) Agora, as 3 últimas confirmações são removidas da ramificação atual ( master). Agora empurre o ramo vigorosamente, com +sinal antes do nome do ramo.

git push origin +master
rashok
fonte
2

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

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

git branch <branch name>

2) Encontre o ID de confirmação recente para confirmação na nova ramificação.

git log

3) Copie esse ID de confirmação, observe que a lista de confirmação Mais Recente ocorre no topo. para que você possa encontrar 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 está pronto. Se você escolheu a identificação correta e o ramo correto, terá sucesso. Portanto, antes disso, tenha cuidado. caso contrário, outro problema pode ocorrer.

Agora você pode enviar seu código

git push

Pankaj Kumar
fonte
0

Outra maneira de fazer isso:

[1] Renomeie o masterramo para o seu newbranch(supondo que você esteja no masterramo):

git branch -m newbranch

[2] Crie um masterbranch a partir do commit que você deseja:

git checkout -b master <seven_char_commit_id>

por exemplo, git checkout -b master a34bc22

Saikat
fonte