在我变基然后同步之后,PR 中的历史记录加倍并显示不相关的更改
After I rebase and then sync, history doubles in the PR and shows unrelated changes
我今天早些时候问了这个问题,现在我尝试了更多的事情,我可能会更好地理解。
情况:
- 我有一个 F# 的分支
- 我有一个本地
master
和一个本地 workbranch
,两者也在我的 fork 中。
- workbranch有PR
- Workbranch 更改了 7 个文件
- 一个 PR 被合并到 upstream master 中,对我自己的工作有用,所以我想 rebase 我的分支
- 我的 workbranch 落后 master 1 个提交。我想要最后一次提交,它有 26 个更改的文件
- 我喜欢看到我的变化,就像在 master 的领导下一样(因此:rebase)
我执行的命令:
git checkout master
git fetch upstream
git merge upstream/master
git push origin/master
git checkout workbranch // up-to-date with origin
git rebase master
git rebase --continue // after solving merge conflict
git pull . // not sure why there were changes to be pulled, was this where I went wrong?
git push .
这之后,我看到的结果是:
- 今天早些时候的 21 次提交
- 1 次来自 master 的提交,其中包含 26 个更改的文件
- 另外 8 次提交与之前的 21 次提交完全相同
- 此 rebase 的合并提交
- diff 显示 33 个文件已更改
在 github.com 的分支比较概述中,在我的分支中,我看到:
- 今天早些时候的 21 次提交
- 另外 8 次提交如上
- 变基的合并提交
- 此处的 diff 显示 7 个文件已更改
我希望看到的是我的原始提交,每个只有一次,没有重复,合并提交,并且应该没有来自master的提交。
我怀疑是pull-before-push的问题
这是正常的。请注意,这不是 理想的 ,但很正常——这是一些人完全避免使用 git rebase
的原因之一。
Long:为什么会这样
首先记住什么是 Git 提交是什么:
每次提交都会存储所有文件的快照,以及一些元数据:提交人的姓名和电子邮件地址、date-and-time 戳记等。
每个提交都有一个唯一的编号。这个数字不是一个简单的计数——它不是 1、2、3 等等——而是一个又大又丑的 random-looking(但根本不是随机的)哈希 ID。哈希 ID 是两个 Git 可以判断它们是否都有提交的方式,因为此哈希 ID 在 every Git 中的计算方式相同。如果他们的 Git 有提交而你没有,那么你的 Git 在其数据库中没有提交编号。如果您的 Git 有提交而他们没有,那么您的 Git 在其数据库中有提交编号(和提交),而他们没有。
此外,Git 存储库中的 历史记录 只是一组提交。 Git 通过在每个提交中存储提交的 parent 提交的提交编号(哈希 ID)来解决这个问题,或者对于合并提交,父提交(复数)。这些是在此提交之前的提交。
如果我们忽略合并提交,我们会变得简单,backwards-looking 提交链,我们可以这样绘制:
... <-F <-G <-H
这里的 H
代表链中 last 提交的实际哈希 ID。在 H
的元数据中,Git 存储了提交 G
的实际哈希 ID。所以通过读取 H
的内容,Git 可以找到 G
的提交号,这让 Git 读取包含 G
的提交号F
,依此类推。
A branch name in Git 简单地保存 last 提交的提交号——丑陋的大哈希 ID在链中。因此,如果您的分支 master
具有上述提交,我们可以这样绘制:
...--F--G--H <-- master
我们真的不需要 backwards-pointing 箭头 在 提交之间,只要我们记住它们总是指向后方,因为任何提交中的任何内容都不可能变了。这包括父提交的哈希 ID。
分支名称和其他名称,例如 remote-tracking 名称,但是,移动。所以我们会画出他们的箭头来提醒我们。
绘制您的设置
我们可以这样画出你的初始情况:
...--F <-- master, origin/master, upstream/master
\
G--H <-- workbranch, origin/workbranch
现在我们更新我们的 upstream/master
,其中有一些新的提交,它们具有自己唯一的哈希 ID:
git checkout master
git fetch upstream
这给了我们:
I--J <-- upstream/master
/
...--F <-- master (HEAD), origin/master
\
G--H <-- workbranch, origin/workbranch
git checkout
步骤确保我们的 当前 分支是 master
,即我们正在处理提交 F
。这就是为什么我们在这里将特殊名称 HEAD
附加到分支名称 master
。
接下来,我们 Git 移动 我们的名字 master
指向我们刚刚从 upstream
获得的最后一个新提交:
git merge upstream/master
产生:
I--J <-- master (HEAD), upstream/master
/
...--F <-- origin/master
\
G--H <-- workbranch
请注意 master
现在如何指向现有提交 J
。 origin
上的 Git 甚至还没有提交 I-J
,我们对其 master
的记忆,在我们的 origin/master
中,仍然指向提交 F
.
最后,我们运行:
git push origin master # note: not origin/master
我们 Git 在 origin
呼叫 Git。这就是为什么这是 origin master
而不是 origin/master
:我们想在 origin
调用 Git,并根据 our[=446= 发送提交] master
,这也是为什么最后一部分是master
而不是origin/master
的原因。因此,我们将提交 I-J
(我们通过 upstream
的 master
从 upstream
获得)发送到 origin
,并要求 origin
设置 他们的 master
指向提交 J
.
假设他们服从,这就是我们在这个过程结束时在本地拥有的:
I--J <-- master (HEAD), origin/master, upstream/master
/
...--F
\
G--H <-- workbranch, origin/workbranch
请注意,在所有这些过程中,没有 提交 发生变化。整个过程是关于从其他存储库(位于 upstream
的存储库)提交到特定的 Git 存储库(我们的,以及位于 origin
的存储库),并更新我们的分支名称(master
) 和 Git 中 origin
中的名称 master
(我们的 Git 在我们的 origin/master
中保留了记忆)。
(这一切都非常令人困惑:需要很长时间才能习惯所有重复。我发现将每个存储库视为不同的“人”会有所帮助:Upstream 先生了解提交 I-J
, 然后我们了解他们,然后我们告诉 Origin 先生。)
Rebase 假装更改提交
为了 git rebase
完成它的工作,它必须 假装 更改提交。这实际上是不可能的。相反,rebase 获取现有提交并使用它们进行 new 提交,这些提交略有不同,因此具有不同的提交编号。
让我们 re-draw commi 后没有 up-kink 的最终情况F
。我们可以随心所欲地绘制图表,只要我们可以从名称到提交,然后遵循内部 backwards-pointing 箭头。 git log --graph
命令绘制了一个图,其中较新的提交朝向图的 top,但对于 Whosebug,我更喜欢将较新的提交绘制在右侧。
...--F--I--J <-- master (HEAD), origin/master, upstream/master
\
G--H <-- workbranch, origin/workbranch
我们想要做的是假装我们从提交 J
进行提交 G
。当然,我们没有,但是git rebase
可以:
- 使用 Git 的 分离 HEAD 模式提取提交
J
到 work-tree;
- 使用
git cherry-pick
复制提交G
到这里;
- 再次使用
git cherry-pick
复制提交H
;最后
- 强制使用名称
workbranch
来标识 last-copied 提交。
变基操作在 git cherry-pick
的每个步骤中都可能遇到障碍,看起来您的操作曾经遇到过一次。
我们首先告诉 Git 提取提交 H
并在此处附加 HEAD
。这就是 git rebase
决定复制哪些提交的方式:它将查看 HEAD
。所以我们 运行:
git checkout workbranch
这给了我们:
...--F--I--J <-- master, origin/master, upstream/master
\
G--H <-- workbranch (HEAD), origin/workbranch
同样,提交没有改变,但我们现在正在处理从提交H
.
中提取的文件
然后我们运行:
git rebase master
Git 现在列出 workbranch
上不在 master
上的提交的原始哈希 ID。请注意,master
包含提交 ...-F-I-J
,结束于 J
,而 workbranch
包含提交 ...-F-G-H
,结束于 H
。 F
s 和更早的提交被取消了,I-J
提交根本不在 workbranch
上,所以这里要复制的提交列表只是 G
和H
.
(在你的情况下,有两个以上的提交要复制,但结果应该很清楚。)
接下来,因为我们说过 git rebase master
,Git 对提交 J
:
进行特殊的 detached-HEAD 模式检出
...--F--I--J <-- HEAD, master, origin/master, upstream/master
\
G--H <-- workbranch, origin/workbranch
现在 Git 使用 git cherry-pick
(或或多或少等价的东西,取决于你的 Git 年份和你传递给 git rebase
的标志)来复制 更改 在提交 G
中所做的更改,现在 HEAD
所在的位置。如果一切顺利,Git 会自行进行新的提交。要记住它是 G
的副本,我们将其称为 G'
:
G' <-- HEAD
/
...--F--I--J <-- master, origin/master, upstream/master
\
G--H <-- workbranch, origin/workbranch
rebase 命令继续复制剩余的提交,给出:
G'-H' <-- HEAD
/
...--F--I--J <-- master, origin/master, upstream/master
\
G--H <-- workbranch, origin/workbranch
现在所有提交都已成功复制(或者您已修复它们并根据需要使用 git rebase --continue
),Git 将名称 workbranch
拉到指向 H'
提交,并且 re-attaches HEAD
:
G'-H' <-- workbranch (HEAD)
/
...--F--I--J <-- master, origin/master, upstream/master
\
G--H <-- origin/workbranch
它 似乎 两个现有的提交似乎以某种方式移动了,因为新的提交具有相同的作者和 time-stamp 以及日志消息等等。提交 numbers 有什么不同,但谁真的会 look 看那些又大又丑的哈希 ID?
我们的 Git 故意 忘记了 workbranch
曾经指向提交 H
。相反,我们的 workbranch
现在指向 new-and-improved 提交 H'
。但是请注意,我们的 Git 记得 origin
处的 Git 有 他们的 workbranch
记住现有的提交 H
.
git push
假设我们现在 Git 调用 他们的 Git,在 origin
,并发送提交 G'-H'
给他们:
git push origin workbranch
他们会将 G'
和 H'
放入他们自己的存储库中,至少是暂时的,然后考虑我们的要求让他们更改 他们的 名称workbranch
指向提交 H'
。但是现在,他们会说不。
当我们礼貌地要求他们将他们的 workbranch
从他们的(和我们的)H
移到我们的(现在也是他们的)H'
时,他们说 no 因为如果他们这样做,他们会忘记如何找到提交 H
。他们不知道 H'
是 new-and-improved 替代 H
。他们只知道,如果按照我们的要求去做,他们就会忘记 H
。他们将没有仍然可以找到的名字 H
.
所以,他们说不。
git pull
如果你现在 运行 git pull origin workbranch
,甚至 git pull
没有参数,你现在可以 Git 调用他们的 Git 并询问他们关于 他们的 workbranch
。他们会说:哦,当然,我的 workbranch
,它上面有这两个非常好的提交 G
和 H
,你喜欢吗? 如果您的 Git 已经扔掉了旧的 G-H
,它会占用这些副本。如果没有——你的 Git 肯定还有它们,因为你的 origin/workbranch
一直记得它们——你的 Git 说它已经有了它们,但无论如何谢谢,现在你的 Git 知道他们的 workbranch
指向提交 H
。因此,如果需要,您的 Git 会更新您的 origin/workbranch
(不需要,因为您的 origin/workbranch
已经还记得 H
):
G'-H' <-- workbranch (HEAD)
/
...--F--I--J <-- master, origin/master, upstream/master
\
G--H <-- origin/workbranch
和现在 你的 Git 运行任何git pull
的后半部分。
git pull
命令实际上由运行ning tw组成 Git 命令:
第一个命令总是git fetch
。这就是让您的 Git 调用他们的 Git 并询问他们的 workbranch
的步骤(也许还有他们的其他分支,具体取决于您 运行 git fetch
).此步骤带来了他们拥有的任何提交,而您没有,而您的 Git 将需要这些提交。然后,您的 Git 会在必要时更新您的 origin/*
名称。
第二个命令默认为git merge
。他们所说的任何提交上的合并 运行s 是 他们的 分支的最后一次提交。
所以在这里,你的 Git 运行s git merge
在提交 H
的哈希 ID 上——他们的 workbranch
,这是你的 origin/workbranch
.所以你的 Git 现在 合并 你的提交 H'
与共享提交 H
:
G'-H'-M <-- workbranch (HEAD)
/ /
...--F--I--J <- / -- master, origin/master, upstream/master
\ /
G-------H <-- origin/workbranch
您制作的那些副本,改进和丢弃旧的 G-H
,仍然存在。旧的 G-H
也仍然存在。新的合并提交 M
将 两个分支 合并在一起。一个分支包含您认为已经摆脱的提交这一事实并不重要。提交仍然存在,合并将它们合并。
git push
,再次
您的 Git 现在可以发送他们的 Git 提交 M
,新的合并。如果他们使他们的 workbranch
标识提交 M
,他们现有的提交 G-H
仍然可以在 他们的 存储库中访问,因此他们对此感到满意。但是在这一点上,您复制了 git rebase
复制的所有提交(现在它们也会)。这根本不是你想要的。
注意:成功的 git push
将更新您的 origin/workbranch
以记住他们的 workbranch
现在记住提交 M
的事实,所以现在绘图看起来像这样:
G'-H'-M <-- workbranch (HEAD), origin/workbranch
/ /
...--F--I--J <- / -- master, origin/master, upstream/master
\ /
G-------H
(我们可以通过将 G-H
线移动到绘图的顶部来简化它,但我们不要那样做。)
要解决此问题,您必须执行以下操作:
- 强迫你的 Git 假装你根本没有进行合并提交
M
。
- 强制 Git 在
origin
设置 his workbranch
名称以记住提交 H'
而不是 M
.
此时最简单的 Git 命令集是 git reset --hard
和 git push --force
。让我们看看它们是如何工作的。
git reset --hard
我们首先让我们的Git忘记我们的提交M
:
git checkout workbranch # if needed - we're probably already there
这确保我们有:
G'-H'-M <-- workbranch (HEAD), origin/workbranch
/ /
...--F--I--J <- / -- master, origin/master, upstream/master
\ /
G-------H
现在在我们的存储库中。那么:
git reset --hard HEAD~1
HEAD~1
符号表示 向后移动一个 first-parent,从提交 M
到提交 H'
。这使得我们的名字 workbranch
指向提交 H'
。为了绘制它,让我们将提交 M
向下移动到底行:
G'-H' <-- workbranch (HEAD)
/ \
...--F--I--J <- \ -- master, origin/master, upstream/master
\ \
G-------H----M <-- origin/workbranch
既然我们的 workbranch
识别了提交 H'
,我们 运行:
git push --force origin workbranch
这有我们的 Git 调用他们的 Git,在 origin
,告诉它关于提交 H'
— 他们当然已经有了它,在这一点上—然后强制命令它们:设置你的分支名称 workbranch
指向提交 H'
!(这来自 --force
,并采用通常礼貌请求的地方。)
假设他们服从——那部分由他们决定,但如果你可以控制这个存储库,只要确保你给自己 force-push 权限——他们将移动 他们的 workbranch
指向提交 H'
,你的 Git 将相应地更新你的 origin/workbranch
:
G'-H' <-- workbranch (HEAD), origin/workbranch
/ \
...--F--I--J <- \ -- master, origin/master, upstream/master
\ \
G-------H----M [abandoned]
既然他们和你都没有 名称 来查找 提交 M
,你甚至不会看到它。一切都会像从未存在过一样:
G'-H' <-- workbranch (HEAD), origin/workbranch
/
...--F--I--J <-- master, origin/master, upstream/master
同样,这对于 rebase
来说是正常的
变基的问题是它通过复制提交到new-and-improved提交来工作。
Git 的问题通常是不愿放弃提交。它 想要 添加 提交,而不是删除它们以支持新的和改进的提交。
每个 Git 存储库都可以轻松地 添加新提交 。它不会那么容易忘记一个旧的。因此,要将此特定提交发送到 origin
,当 origin
记住您的旧提交 H
而不是您的 new-and-improved H'
时,您必须强制推送。您可以使用 --force-with-lease
,它添加了一种安全检查,他们的 workbranch
仍然记得 H
而不是其他提交。
如果 origin
Git 存储库还有其他用户,请记住他们也可能正在使用或添加 origin
的 workbranch
。您应该确保所有这些用户 期望 提交被删除和替换。否则其他用户会对这种行为感到惊讶。
避免 rebase 可以完全避免这种意外,但最终这完全取决于您和与您一起工作的任何人。如果你们都同意变基发生——一些提交可以消失,你不会把它们带回来我他们应该留下来[=446=]——然后你就可以这样工作了。
我今天早些时候问了这个问题,现在我尝试了更多的事情,我可能会更好地理解。
情况:
- 我有一个 F# 的分支
- 我有一个本地
master
和一个本地workbranch
,两者也在我的 fork 中。 - workbranch有PR
- Workbranch 更改了 7 个文件
- 一个 PR 被合并到 upstream master 中,对我自己的工作有用,所以我想 rebase 我的分支
- 我的 workbranch 落后 master 1 个提交。我想要最后一次提交,它有 26 个更改的文件
- 我喜欢看到我的变化,就像在 master 的领导下一样(因此:rebase)
我执行的命令:
git checkout master
git fetch upstream
git merge upstream/master
git push origin/master
git checkout workbranch // up-to-date with origin
git rebase master
git rebase --continue // after solving merge conflict
git pull . // not sure why there were changes to be pulled, was this where I went wrong?
git push .
这之后,我看到的结果是:
- 今天早些时候的 21 次提交
- 1 次来自 master 的提交,其中包含 26 个更改的文件
- 另外 8 次提交与之前的 21 次提交完全相同
- 此 rebase 的合并提交
- diff 显示 33 个文件已更改
在 github.com 的分支比较概述中,在我的分支中,我看到:
- 今天早些时候的 21 次提交
- 另外 8 次提交如上
- 变基的合并提交
- 此处的 diff 显示 7 个文件已更改
我希望看到的是我的原始提交,每个只有一次,没有重复,合并提交,并且应该没有来自master的提交。
我怀疑是pull-before-push的问题
这是正常的。请注意,这不是 理想的 ,但很正常——这是一些人完全避免使用 git rebase
的原因之一。
Long:为什么会这样
首先记住什么是 Git 提交是什么:
每次提交都会存储所有文件的快照,以及一些元数据:提交人的姓名和电子邮件地址、date-and-time 戳记等。
每个提交都有一个唯一的编号。这个数字不是一个简单的计数——它不是 1、2、3 等等——而是一个又大又丑的 random-looking(但根本不是随机的)哈希 ID。哈希 ID 是两个 Git 可以判断它们是否都有提交的方式,因为此哈希 ID 在 every Git 中的计算方式相同。如果他们的 Git 有提交而你没有,那么你的 Git 在其数据库中没有提交编号。如果您的 Git 有提交而他们没有,那么您的 Git 在其数据库中有提交编号(和提交),而他们没有。
此外,Git 存储库中的 历史记录 只是一组提交。 Git 通过在每个提交中存储提交的 parent 提交的提交编号(哈希 ID)来解决这个问题,或者对于合并提交,父提交(复数)。这些是在此提交之前的提交。
如果我们忽略合并提交,我们会变得简单,backwards-looking 提交链,我们可以这样绘制:
... <-F <-G <-H
这里的 H
代表链中 last 提交的实际哈希 ID。在 H
的元数据中,Git 存储了提交 G
的实际哈希 ID。所以通过读取 H
的内容,Git 可以找到 G
的提交号,这让 Git 读取包含 G
的提交号F
,依此类推。
A branch name in Git 简单地保存 last 提交的提交号——丑陋的大哈希 ID在链中。因此,如果您的分支 master
具有上述提交,我们可以这样绘制:
...--F--G--H <-- master
我们真的不需要 backwards-pointing 箭头 在 提交之间,只要我们记住它们总是指向后方,因为任何提交中的任何内容都不可能变了。这包括父提交的哈希 ID。
分支名称和其他名称,例如 remote-tracking 名称,但是,移动。所以我们会画出他们的箭头来提醒我们。
绘制您的设置
我们可以这样画出你的初始情况:
...--F <-- master, origin/master, upstream/master
\
G--H <-- workbranch, origin/workbranch
现在我们更新我们的 upstream/master
,其中有一些新的提交,它们具有自己唯一的哈希 ID:
git checkout master
git fetch upstream
这给了我们:
I--J <-- upstream/master
/
...--F <-- master (HEAD), origin/master
\
G--H <-- workbranch, origin/workbranch
git checkout
步骤确保我们的 当前 分支是 master
,即我们正在处理提交 F
。这就是为什么我们在这里将特殊名称 HEAD
附加到分支名称 master
。
接下来,我们 Git 移动 我们的名字 master
指向我们刚刚从 upstream
获得的最后一个新提交:
git merge upstream/master
产生:
I--J <-- master (HEAD), upstream/master
/
...--F <-- origin/master
\
G--H <-- workbranch
请注意 master
现在如何指向现有提交 J
。 origin
上的 Git 甚至还没有提交 I-J
,我们对其 master
的记忆,在我们的 origin/master
中,仍然指向提交 F
.
最后,我们运行:
git push origin master # note: not origin/master
我们 Git 在 origin
呼叫 Git。这就是为什么这是 origin master
而不是 origin/master
:我们想在 origin
调用 Git,并根据 our[=446= 发送提交] master
,这也是为什么最后一部分是master
而不是origin/master
的原因。因此,我们将提交 I-J
(我们通过 upstream
的 master
从 upstream
获得)发送到 origin
,并要求 origin
设置 他们的 master
指向提交 J
.
假设他们服从,这就是我们在这个过程结束时在本地拥有的:
I--J <-- master (HEAD), origin/master, upstream/master
/
...--F
\
G--H <-- workbranch, origin/workbranch
请注意,在所有这些过程中,没有 提交 发生变化。整个过程是关于从其他存储库(位于 upstream
的存储库)提交到特定的 Git 存储库(我们的,以及位于 origin
的存储库),并更新我们的分支名称(master
) 和 Git 中 origin
中的名称 master
(我们的 Git 在我们的 origin/master
中保留了记忆)。
(这一切都非常令人困惑:需要很长时间才能习惯所有重复。我发现将每个存储库视为不同的“人”会有所帮助:Upstream 先生了解提交 I-J
, 然后我们了解他们,然后我们告诉 Origin 先生。)
Rebase 假装更改提交
为了 git rebase
完成它的工作,它必须 假装 更改提交。这实际上是不可能的。相反,rebase 获取现有提交并使用它们进行 new 提交,这些提交略有不同,因此具有不同的提交编号。
让我们 re-draw commi 后没有 up-kink 的最终情况F
。我们可以随心所欲地绘制图表,只要我们可以从名称到提交,然后遵循内部 backwards-pointing 箭头。 git log --graph
命令绘制了一个图,其中较新的提交朝向图的 top,但对于 Whosebug,我更喜欢将较新的提交绘制在右侧。
...--F--I--J <-- master (HEAD), origin/master, upstream/master
\
G--H <-- workbranch, origin/workbranch
我们想要做的是假装我们从提交 J
进行提交 G
。当然,我们没有,但是git rebase
可以:
- 使用 Git 的 分离 HEAD 模式提取提交
J
到 work-tree; - 使用
git cherry-pick
复制提交G
到这里; - 再次使用
git cherry-pick
复制提交H
;最后 - 强制使用名称
workbranch
来标识 last-copied 提交。
变基操作在 git cherry-pick
的每个步骤中都可能遇到障碍,看起来您的操作曾经遇到过一次。
我们首先告诉 Git 提取提交 H
并在此处附加 HEAD
。这就是 git rebase
决定复制哪些提交的方式:它将查看 HEAD
。所以我们 运行:
git checkout workbranch
这给了我们:
...--F--I--J <-- master, origin/master, upstream/master
\
G--H <-- workbranch (HEAD), origin/workbranch
同样,提交没有改变,但我们现在正在处理从提交H
.
然后我们运行:
git rebase master
Git 现在列出 workbranch
上不在 master
上的提交的原始哈希 ID。请注意,master
包含提交 ...-F-I-J
,结束于 J
,而 workbranch
包含提交 ...-F-G-H
,结束于 H
。 F
s 和更早的提交被取消了,I-J
提交根本不在 workbranch
上,所以这里要复制的提交列表只是 G
和H
.
(在你的情况下,有两个以上的提交要复制,但结果应该很清楚。)
接下来,因为我们说过 git rebase master
,Git 对提交 J
:
...--F--I--J <-- HEAD, master, origin/master, upstream/master
\
G--H <-- workbranch, origin/workbranch
现在 Git 使用 git cherry-pick
(或或多或少等价的东西,取决于你的 Git 年份和你传递给 git rebase
的标志)来复制 更改 在提交 G
中所做的更改,现在 HEAD
所在的位置。如果一切顺利,Git 会自行进行新的提交。要记住它是 G
的副本,我们将其称为 G'
:
G' <-- HEAD
/
...--F--I--J <-- master, origin/master, upstream/master
\
G--H <-- workbranch, origin/workbranch
rebase 命令继续复制剩余的提交,给出:
G'-H' <-- HEAD
/
...--F--I--J <-- master, origin/master, upstream/master
\
G--H <-- workbranch, origin/workbranch
现在所有提交都已成功复制(或者您已修复它们并根据需要使用 git rebase --continue
),Git 将名称 workbranch
拉到指向 H'
提交,并且 re-attaches HEAD
:
G'-H' <-- workbranch (HEAD)
/
...--F--I--J <-- master, origin/master, upstream/master
\
G--H <-- origin/workbranch
它 似乎 两个现有的提交似乎以某种方式移动了,因为新的提交具有相同的作者和 time-stamp 以及日志消息等等。提交 numbers 有什么不同,但谁真的会 look 看那些又大又丑的哈希 ID?
我们的 Git 故意 忘记了 workbranch
曾经指向提交 H
。相反,我们的 workbranch
现在指向 new-and-improved 提交 H'
。但是请注意,我们的 Git 记得 origin
处的 Git 有 他们的 workbranch
记住现有的提交 H
.
git push
假设我们现在 Git 调用 他们的 Git,在 origin
,并发送提交 G'-H'
给他们:
git push origin workbranch
他们会将 G'
和 H'
放入他们自己的存储库中,至少是暂时的,然后考虑我们的要求让他们更改 他们的 名称workbranch
指向提交 H'
。但是现在,他们会说不。
当我们礼貌地要求他们将他们的 workbranch
从他们的(和我们的)H
移到我们的(现在也是他们的)H'
时,他们说 no 因为如果他们这样做,他们会忘记如何找到提交 H
。他们不知道 H'
是 new-and-improved 替代 H
。他们只知道,如果按照我们的要求去做,他们就会忘记 H
。他们将没有仍然可以找到的名字 H
.
所以,他们说不。
git pull
如果你现在 运行 git pull origin workbranch
,甚至 git pull
没有参数,你现在可以 Git 调用他们的 Git 并询问他们关于 他们的 workbranch
。他们会说:哦,当然,我的 workbranch
,它上面有这两个非常好的提交 G
和 H
,你喜欢吗? 如果您的 Git 已经扔掉了旧的 G-H
,它会占用这些副本。如果没有——你的 Git 肯定还有它们,因为你的 origin/workbranch
一直记得它们——你的 Git 说它已经有了它们,但无论如何谢谢,现在你的 Git 知道他们的 workbranch
指向提交 H
。因此,如果需要,您的 Git 会更新您的 origin/workbranch
(不需要,因为您的 origin/workbranch
已经还记得 H
):
G'-H' <-- workbranch (HEAD)
/
...--F--I--J <-- master, origin/master, upstream/master
\
G--H <-- origin/workbranch
和现在 你的 Git 运行任何git pull
的后半部分。
git pull
命令实际上由运行ning tw组成 Git 命令:
第一个命令总是
git fetch
。这就是让您的 Git 调用他们的 Git 并询问他们的workbranch
的步骤(也许还有他们的其他分支,具体取决于您 运行git fetch
).此步骤带来了他们拥有的任何提交,而您没有,而您的 Git 将需要这些提交。然后,您的 Git 会在必要时更新您的origin/*
名称。第二个命令默认为
git merge
。他们所说的任何提交上的合并 运行s 是 他们的 分支的最后一次提交。
所以在这里,你的 Git 运行s git merge
在提交 H
的哈希 ID 上——他们的 workbranch
,这是你的 origin/workbranch
.所以你的 Git 现在 合并 你的提交 H'
与共享提交 H
:
G'-H'-M <-- workbranch (HEAD)
/ /
...--F--I--J <- / -- master, origin/master, upstream/master
\ /
G-------H <-- origin/workbranch
您制作的那些副本,改进和丢弃旧的 G-H
,仍然存在。旧的 G-H
也仍然存在。新的合并提交 M
将 两个分支 合并在一起。一个分支包含您认为已经摆脱的提交这一事实并不重要。提交仍然存在,合并将它们合并。
git push
,再次
您的 Git 现在可以发送他们的 Git 提交 M
,新的合并。如果他们使他们的 workbranch
标识提交 M
,他们现有的提交 G-H
仍然可以在 他们的 存储库中访问,因此他们对此感到满意。但是在这一点上,您复制了 git rebase
复制的所有提交(现在它们也会)。这根本不是你想要的。
注意:成功的 git push
将更新您的 origin/workbranch
以记住他们的 workbranch
现在记住提交 M
的事实,所以现在绘图看起来像这样:
G'-H'-M <-- workbranch (HEAD), origin/workbranch
/ /
...--F--I--J <- / -- master, origin/master, upstream/master
\ /
G-------H
(我们可以通过将 G-H
线移动到绘图的顶部来简化它,但我们不要那样做。)
要解决此问题,您必须执行以下操作:
- 强迫你的 Git 假装你根本没有进行合并提交
M
。 - 强制 Git 在
origin
设置 hisworkbranch
名称以记住提交H'
而不是M
.
此时最简单的 Git 命令集是 git reset --hard
和 git push --force
。让我们看看它们是如何工作的。
git reset --hard
我们首先让我们的Git忘记我们的提交M
:
git checkout workbranch # if needed - we're probably already there
这确保我们有:
G'-H'-M <-- workbranch (HEAD), origin/workbranch
/ /
...--F--I--J <- / -- master, origin/master, upstream/master
\ /
G-------H
现在在我们的存储库中。那么:
git reset --hard HEAD~1
HEAD~1
符号表示 向后移动一个 first-parent,从提交 M
到提交 H'
。这使得我们的名字 workbranch
指向提交 H'
。为了绘制它,让我们将提交 M
向下移动到底行:
G'-H' <-- workbranch (HEAD)
/ \
...--F--I--J <- \ -- master, origin/master, upstream/master
\ \
G-------H----M <-- origin/workbranch
既然我们的 workbranch
识别了提交 H'
,我们 运行:
git push --force origin workbranch
这有我们的 Git 调用他们的 Git,在 origin
,告诉它关于提交 H'
— 他们当然已经有了它,在这一点上—然后强制命令它们:设置你的分支名称 workbranch
指向提交 H'
!(这来自 --force
,并采用通常礼貌请求的地方。)
假设他们服从——那部分由他们决定,但如果你可以控制这个存储库,只要确保你给自己 force-push 权限——他们将移动 他们的 workbranch
指向提交 H'
,你的 Git 将相应地更新你的 origin/workbranch
:
G'-H' <-- workbranch (HEAD), origin/workbranch
/ \
...--F--I--J <- \ -- master, origin/master, upstream/master
\ \
G-------H----M [abandoned]
既然他们和你都没有 名称 来查找 提交 M
,你甚至不会看到它。一切都会像从未存在过一样:
G'-H' <-- workbranch (HEAD), origin/workbranch
/
...--F--I--J <-- master, origin/master, upstream/master
同样,这对于 rebase
来说是正常的变基的问题是它通过复制提交到new-and-improved提交来工作。
Git 的问题通常是不愿放弃提交。它 想要 添加 提交,而不是删除它们以支持新的和改进的提交。
每个 Git 存储库都可以轻松地 添加新提交 。它不会那么容易忘记一个旧的。因此,要将此特定提交发送到 origin
,当 origin
记住您的旧提交 H
而不是您的 new-and-improved H'
时,您必须强制推送。您可以使用 --force-with-lease
,它添加了一种安全检查,他们的 workbranch
仍然记得 H
而不是其他提交。
如果 origin
Git 存储库还有其他用户,请记住他们也可能正在使用或添加 origin
的 workbranch
。您应该确保所有这些用户 期望 提交被删除和替换。否则其他用户会对这种行为感到惊讶。
避免 rebase 可以完全避免这种意外,但最终这完全取决于您和与您一起工作的任何人。如果你们都同意变基发生——一些提交可以消失,你不会把它们带回来我他们应该留下来[=446=]——然后你就可以这样工作了。