Большинство предыдущих ответов опасно неправильны!
НЕ делайте этого:
git branch -t newbranch
git reset --hard HEAD~3
git checkout newbranch
Поскольку в следующий раз, когда вы запустите git rebase
(или git pull --rebase
), эти 3 коммита будут автоматически отброшены из newbranch
! (см. пояснение ниже)
Вместо этого сделайте следующее:
git reset --keep HEAD~3
git checkout -t -b newbranch
git cherry-pick ..HEAD@{2}
- Сначала он отбрасывает 3 самых последних коммита (
--keep
походит на --hard
, но безопаснее, поскольку дает сбой, а не выбрасывает незафиксированные изменения).
- Затем он разветвляется
newbranch
.
- Затем он выбирает эти 3 коммита обратно на
newbranch
. Так как на них больше не ссылается ветвь, он делает это с помощью git's reflog : HEAD@{2}
- это коммит, который HEAD
использовал для ссылки на 2 операции назад, то есть до того, как мы 1. извлекли newbranch
и 2. использовали git reset
для сброса 3 коммитов.
Предупреждение: reflog включен по умолчанию, но если вы отключили его вручную (например, с помощью «чистого» хранилища git), вы не сможете вернуть 3 коммита после запуска git reset --keep HEAD~3
.
Альтернатива, которая не зависит от 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
(если хотите, вы можете написать @{-1}
- ранее извлеченную ветку - вместо oldbranch
).
Техническое объяснение
Зачем git rebase
отбрасывать 3 коммита после первого примера? Это связано с тем, что git rebase
без аргументов по умолчанию включает опцию --fork-point
, которая использует локальный журнал reflog, чтобы попытаться быть устойчивым к принудительному выталкиванию восходящей ветви.
Предположим, вы разветвились от origin / master, когда он содержал коммиты M1, M2, M3, а затем сделали три коммита самостоятельно:
M1--M2--M3 <-- origin/master
\
T1--T2--T3 <-- topic
но затем кто-то переписывает историю, принудительно нажимая origin / master, чтобы удалить M2:
M1--M3' <-- origin/master
\
M2--M3--T1--T2--T3 <-- topic
Используя ваш локальный reflog, git rebase
может видеть, что вы разветвились из более ранней инкарнации ветки origin / master, и, следовательно, что коммиты M2 и M3 на самом деле не являются частью вашей ветки тем. Следовательно, это разумно предполагает, что, поскольку M2 был удален из вышестоящей ветки, вы больше не захотите его в своей ветке тем, как только ветвь темы будет перебазирована:
M1--M3' <-- origin/master
\
T1'--T2'--T3' <-- topic (rebased)
Такое поведение имеет смысл, и, как правило, это правильное действие при перебазировании.
Итак, причина того, что следующие команды не работают:
git branch -t newbranch
git reset --hard HEAD~3
git checkout newbranch
потому что они оставляют reflog в неправильном состоянии. Git видит newbranch
как разветвленную ветвь вверх по течению в ревизии, которая включает 3 коммита, затем reset --hard
переписывает историю восходящего потока для удаления коммитов, и поэтому в следующий раз, когда вы запускаете git rebase
, он отбрасывает их, как и любой другой коммит, который был удален из апстрима.
Но в данном конкретном случае мы хотим, чтобы эти 3 коммита рассматривались как часть ветки темы. Чтобы достичь этого, нам нужно отключить апстрим в более ранней ревизии, которая не включает 3 коммита. Это то, что делают мои предложенные решения, поэтому они оба оставляют reflog в правильном состоянии.
Подробнее см. В определении --fork-point
в git rebase и git merge-base документы.