以前的大多数答案都是危险的错误!
不要这样做:
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来实现:是用于引用 2 个操作之前的HEAD@{2}
提交,即在我们 1. 签出和 2. 用于丢弃 3 个提交之前。HEAD``newbranch``git reset
警告:默认情况下,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
默认启用该选项,它使用本地引用日志来尝试对强制推送的上游分支保持鲁棒性。
假设您在 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
使用本地引用日志,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
是因为他们将重新记录置于错误的状态。Git 认为newbranch
在包含 3 个提交的修订版中分叉了上游分支,然后重写reset --hard
上游的历史记录以删除提交,因此下次运行时git rebase
它会像已从上游删除的任何其他提交一样丢弃它们。
但在这种特殊情况下,我们希望将这 3 个提交视为主题分支的一部分。为了实现这一点,我们需要在不包含 3 次提交的早期版本中分叉上游。这就是我建议的解决方案所做的,因此它们都使重新日志保持正确的状态。
有关更多详细信息,请参阅git rebase和git merge-base--fork-point
文档中的定义。