Большинство предыдущих ответов опасно неправильны!
Не делайте этого:
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 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
что он раздвоил ветку upstream в ревизии, которая включает в себя 3 коммита, затем reset --hard
переписывает историю апстрима для удаления коммитов, и поэтому при следующем запуске git rebase
он отбрасывает их, как и любой другой коммит, который был удален из апстрима.
Но в данном конкретном случае мы хотим, чтобы эти 3 коммита рассматривались как часть ветки темы. Чтобы достичь этого, нам нужно отключить апстрим в более ранней ревизии, которая не включает 3 коммита. Это то, что делают мои предложенные решения, поэтому они оба оставляют reflog в правильном состоянии.
Для получения дополнительной информации см. Определения --fork-point
в документах git rebase и git merge-base .