强制推送后重新定位
Rebase after force push
我有一个特性分支 A。然后我开始开发依赖于 A 的第二个特性,所以我将我的新特性分支 B 建立在 A 上:
git checkout A
git checkout -B B
我在 B 上做了一些工作,所以现在在 B 上我有提交 1(来自 A)和新的提交 2。
我们公司总是尽可能地将一个 PR 的所有提交压缩在一起,所以有一次我强制推动 A,以便 A 只提交 1'。现在我想将 B 变基为 A(或 master,在 A 合并后),但由于我强制推送 A,git 尝试应用提交 1,这显然失败了。
2 种解决方法,但都不是很好:
使用git cherry-pick:
git checkout B
git checkout -B B2
git log // copy latest commit id
git checkout B
git reset --hard A
git cherry-pick <commit-id>
使用软重置:
git checkout B
git reset --soft HEAD~1
git stash
git reset --hard A
git stash pop
git commit -a -m "msg"
是否有 "git method" 来解决这个问题?我知道总是压缩提交可能不是最佳实践,但我无法改变。或者是否有更好的方法将一个分支建立在另一个分支之上?
最终,您会想要 git rebase --onto
。不过,有时您不需要做任何特别的事情。
设置
让我们画出你的初始情况:
...--A--B <-- master
\
C <-- feature/A
\
D <-- feature/B
也就是说,在一些主线上有一些提交(我在这里称之为 master
但它可能是 develop
或其他),然后在你的 feature/A
,然后在 feature/B
上提交一个。在您的 feature/B
上提交 D
的父项是您在 feature/B
和 feature/A
上的提交 C
。
稍后,您向 feature/A
添加了第二个提交,给出:
...--A--B <-- master
\
C--E <-- feature/A
\
D <-- feature/B
最终,feature/A
将合并到 master
,并且根据某些策略规则,您已经提交了一个新的提交 F
,它是 [=39= 的组合] 和 E
所以你现在有:
F <-- feature/A
/
...--A--B <-- master
\
C--E [abandoned]
\
D <-- feature/B
此时你想将 D
复制到某个新的提交 D'
中,它看起来与 D
与其父项的差异完全相同,但是 D'
的父级是 F
而不是 C
.
Git 提供了一种简单的方法来获得你想要的东西:
git checkout feature/B
git rebase --onto feature/A something-goes-here
问题出在 something-goes-here
部分。那里有什么?
正在复制一些提交
git rebase
命令本质上只是一系列git cherry-pick
命令,后面跟着一个b运行ch标签动作。正如您已经发现的那样,git cherry-pick
做您想要的:它复制提交。事实上,它可以复制多个提交(使用 Git 在内部调用 sequencer)。
也就是说,它将每个要复制的提交与提交的 parent 进行比较,以查看发生了什么变化。然后,它对 current 提交进行相同的更改,如果一切顺利,提交结果。
比如,我们先从这种情况说起。目前,我使用了一个新标签 saved-A
,以记住提交 E
,并且添加了名称 new-B
并在括号中添加了 HEAD
显示 current b运行ch 是 new-B
并且 current commit 是 commit F
:
F <-- feature/A, new-B (HEAD)
/
...--A--B <-- master
\
C--E <-- saved-A
\
D <-- feature/B
我们现在可以 运行 git cherry-pick feature/B
。我们告诉 Git:将提交 D
与其父 C
进行比较,然后在提交 F
处对我们现在所在的位置进行相同的更改,并提交结果。 如果一切顺利,我们得到:
D' <-- new-B (HEAD)
/
F <-- feature/A
/
...--A--B <-- master
\
C--E <-- saved-A
\
D <-- feature/B
我们现在需要做的就是将名称 feature/B
拉到指向提交 D'
,然后删除名称 new-B
:
D' <-- feature/B (HEAD)
/
F <-- feature/A
/
...--A--B <-- master
\
C--E <-- saved-A
\
D [abandoned]
同样,第一部分正是 git cherry-pick
所做的:复制一个提交。 last 部分是 git rebase
所做的:移动 b运行ch 标签,如 feature/B
.
这里的关键是 git rebase
复制了 一些 提交。 哪些?默认答案对你来说是错误的答案!
git rebase
的作用,简而言之
让我们看一张略有不同的图:
...--A--B <-- target
\
C--D--E <-- current (HEAD)
这里,我们是"on"b运行chcurrent
,即git status
会说on branch current
。 current
的tip commit 是commit E
: E
的hash ID 是name refs/heads/current
.
中存储的hash ID
如果我们现在运行:
git rebase target
Git 将 复制 提交 C-D-E
到新提交 C'-D'-E'
并将新提交放在 target
之上,然后移动b运行ch 名称,像这样:
C'-D'-E' <-- current (HEAD)
/
...--A--B <-- target
\
C--D--E [abandoned]
这通常是我们想要的。但是:git rebase
怎么知道复制C-D-E
却不知道复制A
呢?
答案是git rebase
使用Git的内部"list some commits"操作,git rev-list
,有一个停止点 . rebase 文档声称 git rebase
所做的是 运行:
git rev-list target..HEAD
这是一个善意的谎言:它足够接近,并且具有说明性。确切的细节比较棘手,我们稍后会讲到。现在,让我们看看 target..HEAD
的 target..
部分。这告诉 Git:不要列出您可以通过从目标开始并向后工作找到的任何提交。
因为target
命名提交B
,这意味着:不要复制提交B
。好吧,我们已经不打算复制提交 B
,所以没什么大不了的。但它也意味着:不要复制提交A
。为什么不?因为提交 B
指向回提交 A
。提交 A
在 b运行ches,target
和 current
上。所以我们会复制A
,但我们没有,因为它在不复制列表中。 before A
也有提交,但它们都在 不要复制 部分,所以 none被复制。
因此它的提交 C-D-E
被复制到这里:它们在要复制的列表中,并且不会因为在不复制列表中而停止。
所以,git rebase
的作用,简而言之,是这样的:
- 记住 b运行ch
HEAD
附加到哪个。
- 列出一些要复制的提交哈希 ID。
- 从当前 b运行ch 中分离
HEAD
。
- 复制列出的提交,一次一个,如同
git cherry-pick
。
- 移动
HEAD
附加的 b运行ch 名称,到我们现在所在的位置。
- 重新附加
HEAD
到移动的 b运行ch。
请注意,在第 4 步中可能会出错。特别是,复制提交,就像 git cherry-pick
一样——无论是否实际使用 git cherry-pick
——都可能有一个 合并冲突。如果是这样,rebase 会在中间停止,并带有一个分离的 HEAD。这就是了解第 3 步很重要的原因。但我们会把它留给其他问题和答案(以及关于 rebase 是否真的使用 cherry-pick 本身的细节:有时它使用,有时它伪造它)。
关于哪些提交被复制的真相
我们提到上面的 target..HEAD
是一个善意的谎言:一种简化,旨在更容易理解哪些提交被复制。现在是真相的时候了。
首先,git rebase
通常 完全忽略 合并提交。如果是合并(有两个或多个父项),则上面的 git rev-list
生成的任何提交都会被淘汰。只要您的列表中没有合并提交,这就没有关系。
其次,git rebase
也 省略了 补丁 ID 等效于 的提交.这使用了 git patch-id
程序。这里就不细说了,只是观察得到"some other commits"部分,Git其实要用git rev-list target...HEAD
,三个点。这会产生一个 对称差异 列表,其中的提交可以从 HEAD
到达但不是目标,并且还可以从 target
而不是 HEAD
到达。有关可达性的(更多)信息,请参阅 Think Like (a) Git。然后,rebase 命令对两个列表中的每个提交使用 git patch-id
(它是在内部生成的,因此它知道哪个提交哈希与哪个列表对应),并剔除那些具有匹配补丁 ID 的提交。这样做的效果是,如果提交 B
,例如 已经 与提交 D
相同(精挑细选),而不是复制 C-D-E
,我们只需复制 C-E
,得到:
C'-E' <-- current (HEAD)
/
...--A--B <-- target
\
C--D--E [abandoned]
因为提交 B
和 D
"do the same thing"。
最后,也是对我们来说最重要的,--onto
让我们可以使用不同的 target。
在上面的例子中,我们运行:
git rebase target
和 target
都是我们的 停止参数 对于 git rev-list stop..HEAD
和 我们的目标,因为 Git放份。但是我们可以 运行:
git rebase --onto target stop
现在 git rebase
将对 git rev-list
的 stop
部分使用我们的 stop 参数,同时继续使用我们的 目标 副本去向的参数。
所以,假设我们现在 this:
...--A--B <-- target
\
C <-- another
\
D--E <-- current (HEAD)
我们运行:
git rebase --onto target another
我们现在已经告诉 Git 我们的 rebase 的 stop 参数是 another
,它选择提交 C
。我们的 rebase 将在 another..HEAD
或 C..E
上使用 git rev-list
,这意味着要复制的提交列表将仅包含 D-E
.
该列表将通过 patch-id 和 no-merges 规则进一步过滤,但只要 B
与 不同 [=37] =],我们最终会得到:
D'-E' <-- current (HEAD)
/
...--A--B <-- target
\
C <-- another
\
D--E [abandoned]
也就是说,我们将只复制 可以从 current
访问的两个提交 D-E
,省略提交 C
可从 another
.
访问
综合起来
这是您在进行提交复制时的设置:
F <-- feature/A
/
...--A--B <-- master
\
C--E <-- saved-A
\
D <-- feature/B (HEAD)
请注意,我们添加了名称 saved-A
以记住要复制的内容 而不是 。我们不想复制提交 C
和 E
。无论如何我们都不会复制 E
,但这是记住 所有内容 不要复制的简单方法。
我们目前已经 feature/B
签出(提交 D
)。我们不需要创建名称 new-B
,因此我们没有这样做。现在我们只是 运行:
git rebase --onto feature/A saved-A
Git 现在将列出要复制的提交:当前 b运行ch、feature/B
、 除了 [=373= 之外的每个提交] saved-A
上的每个提交。这就是提交 D
.
Git 现在分离 HEAD,移动到提交 F
——我们的 --onto
目标——并复制 D
以产生 D'
。这完成了要复制的提交列表,因此成功将 D
复制到 D'
,Git 强制移动名称 feature/B
指向 D'
并重新附加 HEAD
,给我们:
D' <-- feature/B (HEAD)
/
F <-- feature/A
/
...--A--B <-- master
\
C--E <-- saved-A
\
D [abandoned]
这正是我们想要的。
我们现在可以删除名称 saved-A
。
如果你没有保存名字怎么办?
如果您已经变基 feature/A
但忘记将提交 E
的提交哈希 ID 保存在某处怎么办?
幸运的是,您没有保存E
或C
的散列ID。您可以:
- 使用
git log
或 找到它们
- 使用
git reflog
查找feature/A
用于命名的散列ID,或
- 做任何你想做的事来找到他们。
原始哈希 ID 有效,因此您只需 运行:
git rebase --onto feature/A <hash-ID-of-E-or-C>
找到哈希 ID 后。 (使用剪切和粘贴或类似方法获得正确的散列 ID;手动输入它,甚至是它的唯一前缀,很容易出错。)
Reflog 名称也有效,因此您通常可以这样做:
git rebase --onto feature/A feature/A@{1}
其中 feature/A@{1}
是当您 运行 git reflog feature/A
列出以前的哈希 ID 时,您将看到的提交 E
的哈希 ID 的引用日志名称feature/A
。 (feature/A@{2}
可能命名为 commit C
,这样也可以。)
关键是找到您要省略的提交,并使用带有 git rebase --onto
的提交。根据副本的去向设置 target,并设置停止点——the git rebase
documentation 调用 upstream 参数— 一个散列 ID 停止提交你不想要复制。
什么时候不需要特别的东西?
如果压缩后的提交与原始提交具有相同的补丁 ID,git rebase
的 将删除具有匹配补丁 ID 的提交 将完成所有工作你。这通常只会发生在你只有 one 提交被挤压合并到其他 b运行ch.
时
--onto
技巧总能奏效,所以您真的不必担心这种情况,但如果这种情况经常发生,很高兴知道。
我有一个特性分支 A。然后我开始开发依赖于 A 的第二个特性,所以我将我的新特性分支 B 建立在 A 上:
git checkout A
git checkout -B B
我在 B 上做了一些工作,所以现在在 B 上我有提交 1(来自 A)和新的提交 2。 我们公司总是尽可能地将一个 PR 的所有提交压缩在一起,所以有一次我强制推动 A,以便 A 只提交 1'。现在我想将 B 变基为 A(或 master,在 A 合并后),但由于我强制推送 A,git 尝试应用提交 1,这显然失败了。
2 种解决方法,但都不是很好:
使用git cherry-pick:
git checkout B
git checkout -B B2
git log // copy latest commit id
git checkout B
git reset --hard A
git cherry-pick <commit-id>
使用软重置:
git checkout B
git reset --soft HEAD~1
git stash
git reset --hard A
git stash pop
git commit -a -m "msg"
是否有 "git method" 来解决这个问题?我知道总是压缩提交可能不是最佳实践,但我无法改变。或者是否有更好的方法将一个分支建立在另一个分支之上?
最终,您会想要 git rebase --onto
。不过,有时您不需要做任何特别的事情。
设置
让我们画出你的初始情况:
...--A--B <-- master
\
C <-- feature/A
\
D <-- feature/B
也就是说,在一些主线上有一些提交(我在这里称之为 master
但它可能是 develop
或其他),然后在你的 feature/A
,然后在 feature/B
上提交一个。在您的 feature/B
上提交 D
的父项是您在 feature/B
和 feature/A
上的提交 C
。
稍后,您向 feature/A
添加了第二个提交,给出:
...--A--B <-- master
\
C--E <-- feature/A
\
D <-- feature/B
最终,feature/A
将合并到 master
,并且根据某些策略规则,您已经提交了一个新的提交 F
,它是 [=39= 的组合] 和 E
所以你现在有:
F <-- feature/A
/
...--A--B <-- master
\
C--E [abandoned]
\
D <-- feature/B
此时你想将 D
复制到某个新的提交 D'
中,它看起来与 D
与其父项的差异完全相同,但是 D'
的父级是 F
而不是 C
.
Git 提供了一种简单的方法来获得你想要的东西:
git checkout feature/B
git rebase --onto feature/A something-goes-here
问题出在 something-goes-here
部分。那里有什么?
正在复制一些提交
git rebase
命令本质上只是一系列git cherry-pick
命令,后面跟着一个b运行ch标签动作。正如您已经发现的那样,git cherry-pick
做您想要的:它复制提交。事实上,它可以复制多个提交(使用 Git 在内部调用 sequencer)。
也就是说,它将每个要复制的提交与提交的 parent 进行比较,以查看发生了什么变化。然后,它对 current 提交进行相同的更改,如果一切顺利,提交结果。
比如,我们先从这种情况说起。目前,我使用了一个新标签 saved-A
,以记住提交 E
,并且添加了名称 new-B
并在括号中添加了 HEAD
显示 current b运行ch 是 new-B
并且 current commit 是 commit F
:
F <-- feature/A, new-B (HEAD)
/
...--A--B <-- master
\
C--E <-- saved-A
\
D <-- feature/B
我们现在可以 运行 git cherry-pick feature/B
。我们告诉 Git:将提交 D
与其父 C
进行比较,然后在提交 F
处对我们现在所在的位置进行相同的更改,并提交结果。 如果一切顺利,我们得到:
D' <-- new-B (HEAD)
/
F <-- feature/A
/
...--A--B <-- master
\
C--E <-- saved-A
\
D <-- feature/B
我们现在需要做的就是将名称 feature/B
拉到指向提交 D'
,然后删除名称 new-B
:
D' <-- feature/B (HEAD)
/
F <-- feature/A
/
...--A--B <-- master
\
C--E <-- saved-A
\
D [abandoned]
同样,第一部分正是 git cherry-pick
所做的:复制一个提交。 last 部分是 git rebase
所做的:移动 b运行ch 标签,如 feature/B
.
这里的关键是 git rebase
复制了 一些 提交。 哪些?默认答案对你来说是错误的答案!
git rebase
的作用,简而言之
让我们看一张略有不同的图:
...--A--B <-- target
\
C--D--E <-- current (HEAD)
这里,我们是"on"b运行chcurrent
,即git status
会说on branch current
。 current
的tip commit 是commit E
: E
的hash ID 是name refs/heads/current
.
如果我们现在运行:
git rebase target
Git 将 复制 提交 C-D-E
到新提交 C'-D'-E'
并将新提交放在 target
之上,然后移动b运行ch 名称,像这样:
C'-D'-E' <-- current (HEAD)
/
...--A--B <-- target
\
C--D--E [abandoned]
这通常是我们想要的。但是:git rebase
怎么知道复制C-D-E
却不知道复制A
呢?
答案是git rebase
使用Git的内部"list some commits"操作,git rev-list
,有一个停止点 . rebase 文档声称 git rebase
所做的是 运行:
git rev-list target..HEAD
这是一个善意的谎言:它足够接近,并且具有说明性。确切的细节比较棘手,我们稍后会讲到。现在,让我们看看 target..HEAD
的 target..
部分。这告诉 Git:不要列出您可以通过从目标开始并向后工作找到的任何提交。
因为target
命名提交B
,这意味着:不要复制提交B
。好吧,我们已经不打算复制提交 B
,所以没什么大不了的。但它也意味着:不要复制提交A
。为什么不?因为提交 B
指向回提交 A
。提交 A
在 b运行ches,target
和 current
上。所以我们会复制A
,但我们没有,因为它在不复制列表中。 before A
也有提交,但它们都在 不要复制 部分,所以 none被复制。
因此它的提交 C-D-E
被复制到这里:它们在要复制的列表中,并且不会因为在不复制列表中而停止。
所以,git rebase
的作用,简而言之,是这样的:
- 记住 b运行ch
HEAD
附加到哪个。 - 列出一些要复制的提交哈希 ID。
- 从当前 b运行ch 中分离
HEAD
。 - 复制列出的提交,一次一个,如同
git cherry-pick
。 - 移动
HEAD
附加的 b运行ch 名称,到我们现在所在的位置。 - 重新附加
HEAD
到移动的 b运行ch。
请注意,在第 4 步中可能会出错。特别是,复制提交,就像 git cherry-pick
一样——无论是否实际使用 git cherry-pick
——都可能有一个 合并冲突。如果是这样,rebase 会在中间停止,并带有一个分离的 HEAD。这就是了解第 3 步很重要的原因。但我们会把它留给其他问题和答案(以及关于 rebase 是否真的使用 cherry-pick 本身的细节:有时它使用,有时它伪造它)。
关于哪些提交被复制的真相
我们提到上面的 target..HEAD
是一个善意的谎言:一种简化,旨在更容易理解哪些提交被复制。现在是真相的时候了。
首先,
git rebase
通常 完全忽略 合并提交。如果是合并(有两个或多个父项),则上面的git rev-list
生成的任何提交都会被淘汰。只要您的列表中没有合并提交,这就没有关系。其次,
git rebase
也 省略了 补丁 ID 等效于 的提交.这使用了git patch-id
程序。这里就不细说了,只是观察得到"some other commits"部分,Git其实要用git rev-list target...HEAD
,三个点。这会产生一个 对称差异 列表,其中的提交可以从HEAD
到达但不是目标,并且还可以从target
而不是HEAD
到达。有关可达性的(更多)信息,请参阅 Think Like (a) Git。然后,rebase 命令对两个列表中的每个提交使用git patch-id
(它是在内部生成的,因此它知道哪个提交哈希与哪个列表对应),并剔除那些具有匹配补丁 ID 的提交。这样做的效果是,如果提交B
,例如 已经 与提交D
相同(精挑细选),而不是复制C-D-E
,我们只需复制C-E
,得到:C'-E' <-- current (HEAD) / ...--A--B <-- target \ C--D--E [abandoned]
因为提交
B
和D
"do the same thing"。最后,也是对我们来说最重要的,
--onto
让我们可以使用不同的 target。
在上面的例子中,我们运行:
git rebase target
和 target
都是我们的 停止参数 对于 git rev-list stop..HEAD
和 我们的目标,因为 Git放份。但是我们可以 运行:
git rebase --onto target stop
现在 git rebase
将对 git rev-list
的 stop
部分使用我们的 stop 参数,同时继续使用我们的 目标 副本去向的参数。
所以,假设我们现在 this:
...--A--B <-- target
\
C <-- another
\
D--E <-- current (HEAD)
我们运行:
git rebase --onto target another
我们现在已经告诉 Git 我们的 rebase 的 stop 参数是 another
,它选择提交 C
。我们的 rebase 将在 another..HEAD
或 C..E
上使用 git rev-list
,这意味着要复制的提交列表将仅包含 D-E
.
该列表将通过 patch-id 和 no-merges 规则进一步过滤,但只要 B
与 不同 [=37] =],我们最终会得到:
D'-E' <-- current (HEAD)
/
...--A--B <-- target
\
C <-- another
\
D--E [abandoned]
也就是说,我们将只复制 可以从 current
访问的两个提交 D-E
,省略提交 C
可从 another
.
综合起来
这是您在进行提交复制时的设置:
F <-- feature/A
/
...--A--B <-- master
\
C--E <-- saved-A
\
D <-- feature/B (HEAD)
请注意,我们添加了名称 saved-A
以记住要复制的内容 而不是 。我们不想复制提交 C
和 E
。无论如何我们都不会复制 E
,但这是记住 所有内容 不要复制的简单方法。
我们目前已经 feature/B
签出(提交 D
)。我们不需要创建名称 new-B
,因此我们没有这样做。现在我们只是 运行:
git rebase --onto feature/A saved-A
Git 现在将列出要复制的提交:当前 b运行ch、feature/B
、 除了 [=373= 之外的每个提交] saved-A
上的每个提交。这就是提交 D
.
Git 现在分离 HEAD,移动到提交 F
——我们的 --onto
目标——并复制 D
以产生 D'
。这完成了要复制的提交列表,因此成功将 D
复制到 D'
,Git 强制移动名称 feature/B
指向 D'
并重新附加 HEAD
,给我们:
D' <-- feature/B (HEAD)
/
F <-- feature/A
/
...--A--B <-- master
\
C--E <-- saved-A
\
D [abandoned]
这正是我们想要的。
我们现在可以删除名称 saved-A
。
如果你没有保存名字怎么办?
如果您已经变基 feature/A
但忘记将提交 E
的提交哈希 ID 保存在某处怎么办?
幸运的是,您没有保存E
或C
的散列ID。您可以:
- 使用
git log
或 找到它们
- 使用
git reflog
查找feature/A
用于命名的散列ID,或 - 做任何你想做的事来找到他们。
原始哈希 ID 有效,因此您只需 运行:
git rebase --onto feature/A <hash-ID-of-E-or-C>
找到哈希 ID 后。 (使用剪切和粘贴或类似方法获得正确的散列 ID;手动输入它,甚至是它的唯一前缀,很容易出错。)
Reflog 名称也有效,因此您通常可以这样做:
git rebase --onto feature/A feature/A@{1}
其中 feature/A@{1}
是当您 运行 git reflog feature/A
列出以前的哈希 ID 时,您将看到的提交 E
的哈希 ID 的引用日志名称feature/A
。 (feature/A@{2}
可能命名为 commit C
,这样也可以。)
关键是找到您要省略的提交,并使用带有 git rebase --onto
的提交。根据副本的去向设置 target,并设置停止点——the git rebase
documentation 调用 upstream 参数— 一个散列 ID 停止提交你不想要复制。
什么时候不需要特别的东西?
如果压缩后的提交与原始提交具有相同的补丁 ID,git rebase
的 将删除具有匹配补丁 ID 的提交 将完成所有工作你。这通常只会发生在你只有 one 提交被挤压合并到其他 b运行ch.
--onto
技巧总能奏效,所以您真的不必担心这种情况,但如果这种情况经常发生,很高兴知道。