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
é --hard
mais 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 HEAD
costumava se referir a 2 operações atrás, ou seja, antes de 1. fazer o check-out newbranch
e 2. usado git reset
para 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 rebase
descartar os 3 commits após o primeiro exemplo? Isso ocorre porque, git rebase
sem argumentos, a --fork-point
opçã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 rebase
pode 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ê newbranch
como bifurcado no ramo upstream em uma revisão que inclui os 3 commits, depois reset --hard
reescreve 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-point
no rebase git e merge-base git docs.