多个开发人员如何使用 git rebase 在同一个分支上工作?
How can multiple developers work on the same branch using git rebase?
我们 运行 遇到的问题是我们有主要的 development
分支。我们有一张票,它依赖于多张票才能进入 development
分支。所以假设 ticket A
依赖于 ticket B
和 C
以便合并到 development
。
我们处理这个问题的方法是从 development
分支出 A
,然后从 A
分支出 B
和 C
。问题是我们想将开发更新更新到 A
、B
和 C
,我们还想将更新从 B
更新到 C
和反之亦然。当有人在从 development
进行更改后对 A
进行 git push --force
变基时,我们如何使用 git rebase
来做到这一点而不让每个人都删除他们的本地 A 分支?
TL;DR 总结
历史是次提交。
Rebase 表示 "copy commits, then try to forget the originals and use the copies instead"。这意味着 "make up a new history".
Git 存储库得到 分发 (广泛复制)。通过变基创建的新历史只会影响一个存储库。这就是为什么您必须在推送期间使用 --force
,以强制另一个存储库接受 "history rewrite"。但这只会影响 一个 个其他副本,而不影响 所有 个其他副本。
您可以使用一些技巧来减轻疼痛,但疼痛本身并没有真正消失。您可以选择喜欢哪种疼痛。主要技巧是使用 git rebase
:要么完全避免使用 git pull
,要么将其配置为 运行 git rebase
作为第二个 Git 命令。但这仅适用于一个级别。
替代方法是不使用 --force
。这要简单得多,但确实会给您留下错综复杂的历史记录。那可能还是更可取。
描述
在Git中,在很大程度上,b运行ch names并不重要。它们确实在这里和那里被使用,出于多种目的,但它们非常流动和无常——非常 不像 提交,后者是永久的并且永远不会改变(但 可以复制!)。因此,正如您所说,"make an A
branch off development
and then branch B
and C
off A
":Git 不关心 names,这对您没有好处,因为它们不断变化;它只关心提交,而不会。
你从这个开始:
...--e--f--g <-- development
(因为你为 b运行ches 使用了大写 one-letter 名称,我在这里为每个提交切换为小写 one-letter 名称)。然后再添加几个名称:
...--e--f--g <-- development, A, B, C
现在一旦某人——任何人——拥有这个存储库结构,也就是说,在他们的存储库中确实有这四个 b运行ch 名称,就会:
git checkout development
并进行了一些新的提交,这就是 他们的 存储库中发生的事情:
...--e--f--g <-- A, B, C
\
h--i <-- development (HEAD)
新提交进入,每个新提交都像往常一样记录其先前的 parent。表示 current b运行ch 的 b运行ch 名称(该用户的 Git 已记录在该用户的 HEAD 文件中)表示current b运行ch在这个仓库中是名为development
的那个,所以这个Git保持re-setting的标签是新的做出的承诺是 development
。因此,此存储库中的标签 development
已提前指向两个新提交中的最新提交。
其他标签没有移动。
现在,实际上,每个用户都有自己的 存储库。用户必须相互协调,这也通过 b运行ch 名称发生——但现在至少有 两个 Git 参与,所以有 两组或更多组b运行ch名称。一切都必须通过这个镜头来看待:我有 我的 b运行ches,你有 your b运行ches,当我和你说话时,我称你为我的 "remote" 并且我将 你的 重命名为 b运行ches 这样他们就不会干扰我的
每个开发人员都可以直接与其他所有开发人员交谈。例如,如果您的组有 the standard first three people named Alice, Bob, and Carol,Alice 可能有两个遥控器,分别名为 Bob 和 Carol,Bob 可能有两个遥控器,分别名为 Alice 和 Carol,而 Carol 可能有两个遥控器,名字很明显。这显然很难扩展:有 30 名开发人员,每个人都有 29 个遥控器。
因此,您可能拥有一台集中式服务器,甚至可能位于像 GitHub 这样的高级托管站点上。这让每个人都只有 一个 遥控器,他们总是称之为 origin
,因为 Git 会自动使用该名称,除非你告诉它使用其他名称。
这就是 origin/master
和 origin/development
等名称的来源,也是我们在 git fetch origin
和 git push origin <branch>
时使用名称 origin
的原因。那只是一个普通的遥控器:每个人都可以分享他或她的提交的地方。
这里棘手的部分是这个 origin
Git 存储库是一个 Git 存储库。所以它有提交,这些提交是永久性的,但有可怕的、笨拙的 hash-ID 名称,如 9b00c5a...
和 badbeef...
等等,没有人能记住;所以它也有 b运行ch names,比如 development
.
A b运行ch 名称的main 功能是记住这些提交哈希ID 之一。这就是为什么我按照我绘制图表的方式绘制图表:b运行ch names "point to" the tip commit of their corresponding b运行国际象棋
请注意,一个提交可以而且经常是同时在多个 b运行 上提交。如果你添加一个新的 b运行ch 名称指向提交 i
(development
的当前提示),突然所有曾经在 development
上的提交如 e--f--g--h--i
) 现在也在这个新的 b运行ch 上。删除这个新的 b运行ch 名称,并提交 h
和i
,正好在两个 b运行ches 上,现在只在一个 b运行ch 上。
A b运行ch 名称具有次要功能:通过让 Git 找到 一个特定的提交,它 保护 承诺不会被 Grim Collector,Git 的 "garbage collector" 或 "gc" 收割。然后,该提交会保护其所有历史记录——导致 b运行ch 提示的所有先前提交。由于 Git 通常通过在尖端增加新提交来移动 b运行ch 名称,新提交(受 b运行ch 名称保护)保护旧尖端,从而保护 even-older 提示,一直回到有史以来的第一次提交。
请注意 Git 向后工作:我们总是从提示开始并通过历史记录向后工作 - 提交 - 通过找到每个提交的 parent。每个提交 "points back" 到它的 parent,并且提交序列 是 历史。这就是 git log
向我们展示的; gitk
或图形查看器,或将 --graph
与 git log
一起使用,向我们展示了从提交到他们的 parent 的联系。
A parent 和两个立即数 children 在图中形成一个 b运行ch。为了让这个 b运行ch 继续存在,两个分支现在都需要一个名字。让我们 git checkout B
(进入 B
b运行ch)并进行新的提交以实现此目的:
j <-- B (HEAD)
/
...--e--f--g <-- A, C
\
h--i <-- development
这里的parent是g
,两个children是h
和j
。当前的 b运行ch name, B
, 已经移动:它现在指向 j
.
如果我们现在检查 b运行ch C
并进行一两次新的提交,图片将变得更难绘制,因为我们需要离开 A
指向提交g
而 ASCII 艺术并没有那么大的空间:
j <-- B
/_------ A
...--e--f--g--k--l <-- C (HEAD)
\
h--i <-- development
不过,我们再次假设所有这些都发生在 一个 存储库中——在一种没有 其他 存储库的真空中存在。它也变得太难画了,所以让我们回到只有 b运行ches name B
和 development
s,让我们假设我们已经以某种方式进入了这种状态在 origin
的存储库和一个人的存储库(比如说 Alice 的)上,所以我们现在在 Alice 和 origin
存储库中都有:
...--e--f--g--j <-- B
\
h--i <-- development
现在如果 Bob 运行s git fetch
来自 origin
,并假设 Bob 还没有自己的 b运行(他必须至少有一个,我们只是没有绘制它),这就是 Bob 得到的结果:
...--e--f--g--j <-- origin/B
\
h--i <-- origin/development
也就是说,Bob 的 Git 记得 Bob 上次 origin
中的内容 Bob 运行 git fetch origin
(注意:让我们避免 git pull
现在,但请注意它做的第一件事是 运行 git fetch origin
)。它通过 重命名 它在 origin
上找到的 b运行ches 来做到这一点,这样它们就不再是 b运行ches 了。
如果 Bob 现在 运行s git checkout development
,Bob 的 Git 发现 Bob have a development
yet .它搜索 Bob 的存储库并找到 origin/development
,它确实看起来很像 development
,所以 Bob 的 Git 创建 一个 new development
,指向与 origin/development
相同的提交。现在鲍勃有这个:
...--e--f--g--j <-- origin/B
\
h--i <-- development (HEAD), origin/development
如果 Bob 运行s git checkout master
他仍然会有名字 origin/B
(指向 j
),development
(指向 i
) 和 origin/development
(也指向 i
);但他会检查其他一些提交(也许 d
我们还没有画进去?),并将他的 HEAD 附加到 master
.
您可能想知道为什么爱丽丝没有这些 origin/
名字。事实上,她 确实 。她有 origin/B
指向 j
,origin/development
指向 i
。请注意,爱丽丝已经 也 有她自己的 B
和 development
名字。 Bob(还)没有他自己的 B
,只有他自己的 development
。
(虽然 origin
上的存储库可能没有任何 origin/
名称。它只有自己的 b运行ches。这是因为它没有有用户使用它;non-existent 用户没有 运行 git fetch origin
。)
现在让我们回到爱丽丝,假设爱丽丝 运行 是一个 git rebase
变基 development
以便它出现在爱丽丝的 B
之后:
$ git checkout development
$ git rebase B
git rebase
所做的是复制一些提交。
它选择提交复制的方式是查看当前b运行ch,然后查看其参数。
当前b运行ch为development
,自变量为B
。这是 Alice 现在拥有的(与 Bob 非常相似):
...--e--f--g--j <-- B, origin/B
\
h--i <-- development (HEAD), origin/development
git rebase
要复制的提交是 当前 b运行ch (development
), 除了 在 b运行ch B
上的任何提交。嗯,提交 ...--e--f-g--j--i
在 development
上,...--e--f--g--j
在 B
上。所以 Git 从 e--f--g--h--i
集合中减去 j
——这很简单,没有 j
,所以什么也不会发生。然后Git从集合中减去g
,还有...--e--f
,留下h--i
.
这些是要复制的提交。
地方他们会去的地方,在被复制之后,是提交 B
指向的任何内容,即它们将在提交 j
.
之后进行
现在 Git 制作副本(使用 Git 调用的 "detached HEAD" 模式):
h'-i' <-- HEAD
/
...--e--f--g--j <-- B, origin/B
\
h--i <-- development, origin/development
这些新副本具有不同的哈希 ID。它们是不同的提交!它们是原始h--i
的副本(有一些变化),因此我们在这里称它们为h'-i'
。原件还在那里,因为仍然有名字将它们固定到位。
git rebase
的最后一步是强制移动原来的b运行ch名称,development
指向tip-most复制的commit,并且re-attach 你的头:
h'-i' <-- development (HEAD)
/
...--e--f--g--j <-- B, origin/B
\
h--i <-- origin/development
还保留着h--i
的名字是origin/development
。 Alice 的 Git 现在 t运行 将 development
变成 "commit i'
"(不管它的新哈希 ID 是什么),所以她重写的 development
的历史以 [=138 结束=],返回到 h'
,然后返回到 j
、g
、f
,等等,以 Git 通常的倒退方式。
强制推送
如果爱丽丝现在尝试git push origin development
,她会得到拒绝。这是因为 origin
上的 Git 仍然有这个:
...--e--f--g--j <-- B
\
h--i <-- development
Alice 的 Git 发送提交 h'
和 i'
,即她拥有 origin
没有的提交,以及一个提案:
h'-i' <-- proposal: move development here
/
...--e--f--g--j <-- B
\
h--i <-- development
Origin 的 Git 看着提案说:呃,不,那不是 "fast forward"。拒绝!
记得之前我们看到 Git b运行ches 通过在它们的末端增加新的提交来增长。如果 Alice 将 添加到 的新提交推送到 h--i
,那没问题:这是一个 "fast forward" 操作(一次添加多个提交,而不是一次添加一个一次)。但是她正在推动这些新的提交,然后提议将 development
完全从 h--i
中抽离出来,那是 而不是 好的:那不是 "fast forward"。
但是 Alice 可以使用 --force
使其成为命令,而不是提议;然后,只要 origin 的 Git 没有 object 太多,origin 的 Git 服从并将其存储库更改为如下所示:
h'-i' <-- development
/
...--e--f--g--j <-- B
这是除了爱丽丝(和 origin
自己)之外的所有人痛苦的开始。
请注意,提交 h--i
刚刚从 origin
的存储库中消失。 (如果 origin
的服务器正在使用 Git 的 reflogs,那里的 reflogs 将保留它们一段时间。默认情况下,服务器 不过,不要 保留引用日志。)
现在 Bob 感受到了上游 rebase 的痛苦
此时,当 Bob 运行s git fetch origin
——记住,如果你(或 Bob)正在使用 git pull
,你真的 运行ning git fetch origin
首先——Bob 得到更新后的 origin/development
,因为这些 origin/whatever
名称只是盲目地跟随远程存储库上发生的任何事情。这就是 Bob 想要的:Bob 的b运行chees 没有命名origin/whatever
;那些 origin/whatever
是 remote-tracking b运行ches(总是与 Bob 的所有 b运行ches 分开)。
所以,现在 Bob 有了这个(让我们假设 Bob 已经回到 development
所以它也是他的 HEAD):
h'-i' <-- origin/development
/
...--e--f--g--j <-- origin/B
\
h--i <-- development (HEAD)
由 Bob(以某种方式)弄清楚存在我们所说的 上游 rebase:origin/development
用于指向提交 i
现在指向提交 i'
。 Bob 自己没有写提交 h--i
,他只在他的 development
上写了它们,因为那是他的 Git 设置他的 development
的地方,当他 运行 git checkout development
那 创造了 他的 development
.
但在 Bob 的 Git 看来,这些提交确实像是 Bob 自己编写的,因为 origin/development
没有它们。
在这种特殊情况下(Bob 没有自己的工作),Bob 可以 运行 git reset --hard origin/development
让他的 Git 移动他的 development
以匹配 origin/development
。但是,如果 Bob did 在 development
上进行了新的提交怎么办?让我们画出这个情况:
h'-i' <-- origin/development
/
...--e--f--g--j <-- origin/B
\
h--i--k <-- development (HEAD)
鲍勃现在要做的就是做他自己的那种git rebase
。他需要 复制 他的提交 k
以在 i'
.
之后
使用 fork-point
自动处理上游 rebase
Git 有一个功能,它使用引用日志(上面未说明)到 try 来找出哪些提交实际上是 Bob 的。它通常很有效:Bob 的 Git 可以找出 Bob 的提交 k
,并且提交 h--i
是较早的 origin/development
值遗留下来的。
因此,Bob 可以 运行:
git rebase --fork-point origin/development
当鲍勃在(鲍勃的)development
。 --fork-point
选项使 Git 尝试解决所有这些问题。
请注意,--fork-point
是某些 rebase 的 默认值 ,而其他则不是。这个想法是让所有这些工作无缝自动进行。实际上,接缝到处都是。
自动使用--fork-point
的地方是没有参数的git rebase
,或者git pull
运行s [=351=的git rebase
]if 你让 git pull
运行 git rebase
.
默认情况下,git pull
运行s git merge
...并且 合并 原始的 h--i
和新的 h'--i'
,带回 Alice 试图通过她的历史重写删除的那些提交!因此,要么根本不使用 git pull
(我的偏好),要么将其设置为使用 git rebase
作为第二步。 (它的第一步是总是git fetch
。)
不幸的是,所有这一切只有当你自动将你的development
变基到你的origin/development
时才有效,因为你的development
将origin/development
设置为它的[=351] =]上游。如果您 运行 显式 git rebase origin/development
,则 关闭 --fork-point
选项,并且您也必须指定 --fork-point
。这一切都非常令人困惑......这可能就是为什么有些人更喜欢 git pull
,它对你隐藏了所有这些。问题是,它太原始了,无法隐藏:它并不总是有效,你需要知道发生了什么,为什么,这样你就可以修复它。
当自动的东西不起作用时,你必须手动变基
如果 none 的自动化将完成这项工作,则每个用户(Bob、Carol、Dmitry 等)都可以手动 git rebase
或 git rebase -i
。交互式变基允许用户删除不应复制的提交,例如上例中设置的 h--i
。这不是最有趣的练习。它确实给您留下了干净的历史;但这种干净的历史是否值得你来决定。
我们 运行 遇到的问题是我们有主要的 development
分支。我们有一张票,它依赖于多张票才能进入 development
分支。所以假设 ticket A
依赖于 ticket B
和 C
以便合并到 development
。
我们处理这个问题的方法是从 development
分支出 A
,然后从 A
分支出 B
和 C
。问题是我们想将开发更新更新到 A
、B
和 C
,我们还想将更新从 B
更新到 C
和反之亦然。当有人在从 development
进行更改后对 A
进行 git push --force
变基时,我们如何使用 git rebase
来做到这一点而不让每个人都删除他们的本地 A 分支?
TL;DR 总结
历史是次提交。
Rebase 表示 "copy commits, then try to forget the originals and use the copies instead"。这意味着 "make up a new history".
Git 存储库得到 分发 (广泛复制)。通过变基创建的新历史只会影响一个存储库。这就是为什么您必须在推送期间使用 --force
,以强制另一个存储库接受 "history rewrite"。但这只会影响 一个 个其他副本,而不影响 所有 个其他副本。
您可以使用一些技巧来减轻疼痛,但疼痛本身并没有真正消失。您可以选择喜欢哪种疼痛。主要技巧是使用 git rebase
:要么完全避免使用 git pull
,要么将其配置为 运行 git rebase
作为第二个 Git 命令。但这仅适用于一个级别。
替代方法是不使用 --force
。这要简单得多,但确实会给您留下错综复杂的历史记录。那可能还是更可取。
描述
在Git中,在很大程度上,b运行ch names并不重要。它们确实在这里和那里被使用,出于多种目的,但它们非常流动和无常——非常 不像 提交,后者是永久的并且永远不会改变(但 可以复制!)。因此,正如您所说,"make an A
branch off development
and then branch B
and C
off A
":Git 不关心 names,这对您没有好处,因为它们不断变化;它只关心提交,而不会。
你从这个开始:
...--e--f--g <-- development
(因为你为 b运行ches 使用了大写 one-letter 名称,我在这里为每个提交切换为小写 one-letter 名称)。然后再添加几个名称:
...--e--f--g <-- development, A, B, C
现在一旦某人——任何人——拥有这个存储库结构,也就是说,在他们的存储库中确实有这四个 b运行ch 名称,就会:
git checkout development
并进行了一些新的提交,这就是 他们的 存储库中发生的事情:
...--e--f--g <-- A, B, C
\
h--i <-- development (HEAD)
新提交进入,每个新提交都像往常一样记录其先前的 parent。表示 current b运行ch 的 b运行ch 名称(该用户的 Git 已记录在该用户的 HEAD 文件中)表示current b运行ch在这个仓库中是名为development
的那个,所以这个Git保持re-setting的标签是新的做出的承诺是 development
。因此,此存储库中的标签 development
已提前指向两个新提交中的最新提交。
其他标签没有移动。
现在,实际上,每个用户都有自己的 存储库。用户必须相互协调,这也通过 b运行ch 名称发生——但现在至少有 两个 Git 参与,所以有 两组或更多组b运行ch名称。一切都必须通过这个镜头来看待:我有 我的 b运行ches,你有 your b运行ches,当我和你说话时,我称你为我的 "remote" 并且我将 你的 重命名为 b运行ches 这样他们就不会干扰我的
每个开发人员都可以直接与其他所有开发人员交谈。例如,如果您的组有 the standard first three people named Alice, Bob, and Carol,Alice 可能有两个遥控器,分别名为 Bob 和 Carol,Bob 可能有两个遥控器,分别名为 Alice 和 Carol,而 Carol 可能有两个遥控器,名字很明显。这显然很难扩展:有 30 名开发人员,每个人都有 29 个遥控器。
因此,您可能拥有一台集中式服务器,甚至可能位于像 GitHub 这样的高级托管站点上。这让每个人都只有 一个 遥控器,他们总是称之为 origin
,因为 Git 会自动使用该名称,除非你告诉它使用其他名称。
这就是 origin/master
和 origin/development
等名称的来源,也是我们在 git fetch origin
和 git push origin <branch>
时使用名称 origin
的原因。那只是一个普通的遥控器:每个人都可以分享他或她的提交的地方。
这里棘手的部分是这个 origin
Git 存储库是一个 Git 存储库。所以它有提交,这些提交是永久性的,但有可怕的、笨拙的 hash-ID 名称,如 9b00c5a...
和 badbeef...
等等,没有人能记住;所以它也有 b运行ch names,比如 development
.
A b运行ch 名称的main 功能是记住这些提交哈希ID 之一。这就是为什么我按照我绘制图表的方式绘制图表:b运行ch names "point to" the tip commit of their corresponding b运行国际象棋
请注意,一个提交可以而且经常是同时在多个 b运行 上提交。如果你添加一个新的 b运行ch 名称指向提交 i
(development
的当前提示),突然所有曾经在 development
上的提交如 e--f--g--h--i
) 现在也在这个新的 b运行ch 上。删除这个新的 b运行ch 名称,并提交 h
和i
,正好在两个 b运行ches 上,现在只在一个 b运行ch 上。
A b运行ch 名称具有次要功能:通过让 Git 找到 一个特定的提交,它 保护 承诺不会被 Grim Collector,Git 的 "garbage collector" 或 "gc" 收割。然后,该提交会保护其所有历史记录——导致 b运行ch 提示的所有先前提交。由于 Git 通常通过在尖端增加新提交来移动 b运行ch 名称,新提交(受 b运行ch 名称保护)保护旧尖端,从而保护 even-older 提示,一直回到有史以来的第一次提交。
请注意 Git 向后工作:我们总是从提示开始并通过历史记录向后工作 - 提交 - 通过找到每个提交的 parent。每个提交 "points back" 到它的 parent,并且提交序列 是 历史。这就是 git log
向我们展示的; gitk
或图形查看器,或将 --graph
与 git log
一起使用,向我们展示了从提交到他们的 parent 的联系。
A parent 和两个立即数 children 在图中形成一个 b运行ch。为了让这个 b运行ch 继续存在,两个分支现在都需要一个名字。让我们 git checkout B
(进入 B
b运行ch)并进行新的提交以实现此目的:
j <-- B (HEAD)
/
...--e--f--g <-- A, C
\
h--i <-- development
这里的parent是g
,两个children是h
和j
。当前的 b运行ch name, B
, 已经移动:它现在指向 j
.
如果我们现在检查 b运行ch C
并进行一两次新的提交,图片将变得更难绘制,因为我们需要离开 A
指向提交g
而 ASCII 艺术并没有那么大的空间:
j <-- B
/_------ A
...--e--f--g--k--l <-- C (HEAD)
\
h--i <-- development
不过,我们再次假设所有这些都发生在 一个 存储库中——在一种没有 其他 存储库的真空中存在。它也变得太难画了,所以让我们回到只有 b运行ches name B
和 development
s,让我们假设我们已经以某种方式进入了这种状态在 origin
的存储库和一个人的存储库(比如说 Alice 的)上,所以我们现在在 Alice 和 origin
存储库中都有:
...--e--f--g--j <-- B
\
h--i <-- development
现在如果 Bob 运行s git fetch
来自 origin
,并假设 Bob 还没有自己的 b运行(他必须至少有一个,我们只是没有绘制它),这就是 Bob 得到的结果:
...--e--f--g--j <-- origin/B
\
h--i <-- origin/development
也就是说,Bob 的 Git 记得 Bob 上次 origin
中的内容 Bob 运行 git fetch origin
(注意:让我们避免 git pull
现在,但请注意它做的第一件事是 运行 git fetch origin
)。它通过 重命名 它在 origin
上找到的 b运行ches 来做到这一点,这样它们就不再是 b运行ches 了。
如果 Bob 现在 运行s git checkout development
,Bob 的 Git 发现 Bob have a development
yet .它搜索 Bob 的存储库并找到 origin/development
,它确实看起来很像 development
,所以 Bob 的 Git 创建 一个 new development
,指向与 origin/development
相同的提交。现在鲍勃有这个:
...--e--f--g--j <-- origin/B
\
h--i <-- development (HEAD), origin/development
如果 Bob 运行s git checkout master
他仍然会有名字 origin/B
(指向 j
),development
(指向 i
) 和 origin/development
(也指向 i
);但他会检查其他一些提交(也许 d
我们还没有画进去?),并将他的 HEAD 附加到 master
.
您可能想知道为什么爱丽丝没有这些 origin/
名字。事实上,她 确实 。她有 origin/B
指向 j
,origin/development
指向 i
。请注意,爱丽丝已经 也 有她自己的 B
和 development
名字。 Bob(还)没有他自己的 B
,只有他自己的 development
。
(虽然 origin
上的存储库可能没有任何 origin/
名称。它只有自己的 b运行ches。这是因为它没有有用户使用它;non-existent 用户没有 运行 git fetch origin
。)
现在让我们回到爱丽丝,假设爱丽丝 运行 是一个 git rebase
变基 development
以便它出现在爱丽丝的 B
之后:
$ git checkout development
$ git rebase B
git rebase
所做的是复制一些提交。
它选择提交复制的方式是查看当前b运行ch,然后查看其参数。
当前b运行ch为development
,自变量为B
。这是 Alice 现在拥有的(与 Bob 非常相似):
...--e--f--g--j <-- B, origin/B
\
h--i <-- development (HEAD), origin/development
git rebase
要复制的提交是 当前 b运行ch (development
), 除了 在 b运行ch B
上的任何提交。嗯,提交 ...--e--f-g--j--i
在 development
上,...--e--f--g--j
在 B
上。所以 Git 从 e--f--g--h--i
集合中减去 j
——这很简单,没有 j
,所以什么也不会发生。然后Git从集合中减去g
,还有...--e--f
,留下h--i
.
这些是要复制的提交。
地方他们会去的地方,在被复制之后,是提交 B
指向的任何内容,即它们将在提交 j
.
现在 Git 制作副本(使用 Git 调用的 "detached HEAD" 模式):
h'-i' <-- HEAD
/
...--e--f--g--j <-- B, origin/B
\
h--i <-- development, origin/development
这些新副本具有不同的哈希 ID。它们是不同的提交!它们是原始h--i
的副本(有一些变化),因此我们在这里称它们为h'-i'
。原件还在那里,因为仍然有名字将它们固定到位。
git rebase
的最后一步是强制移动原来的b运行ch名称,development
指向tip-most复制的commit,并且re-attach 你的头:
h'-i' <-- development (HEAD)
/
...--e--f--g--j <-- B, origin/B
\
h--i <-- origin/development
还保留着h--i
的名字是origin/development
。 Alice 的 Git 现在 t运行 将 development
变成 "commit i'
"(不管它的新哈希 ID 是什么),所以她重写的 development
的历史以 [=138 结束=],返回到 h'
,然后返回到 j
、g
、f
,等等,以 Git 通常的倒退方式。
强制推送
如果爱丽丝现在尝试git push origin development
,她会得到拒绝。这是因为 origin
上的 Git 仍然有这个:
...--e--f--g--j <-- B
\
h--i <-- development
Alice 的 Git 发送提交 h'
和 i'
,即她拥有 origin
没有的提交,以及一个提案:
h'-i' <-- proposal: move development here
/
...--e--f--g--j <-- B
\
h--i <-- development
Origin 的 Git 看着提案说:呃,不,那不是 "fast forward"。拒绝!
记得之前我们看到 Git b运行ches 通过在它们的末端增加新的提交来增长。如果 Alice 将 添加到 的新提交推送到 h--i
,那没问题:这是一个 "fast forward" 操作(一次添加多个提交,而不是一次添加一个一次)。但是她正在推动这些新的提交,然后提议将 development
完全从 h--i
中抽离出来,那是 而不是 好的:那不是 "fast forward"。
但是 Alice 可以使用 --force
使其成为命令,而不是提议;然后,只要 origin 的 Git 没有 object 太多,origin 的 Git 服从并将其存储库更改为如下所示:
h'-i' <-- development
/
...--e--f--g--j <-- B
这是除了爱丽丝(和 origin
自己)之外的所有人痛苦的开始。
请注意,提交 h--i
刚刚从 origin
的存储库中消失。 (如果 origin
的服务器正在使用 Git 的 reflogs,那里的 reflogs 将保留它们一段时间。默认情况下,服务器 不过,不要 保留引用日志。)
现在 Bob 感受到了上游 rebase 的痛苦
此时,当 Bob 运行s git fetch origin
——记住,如果你(或 Bob)正在使用 git pull
,你真的 运行ning git fetch origin
首先——Bob 得到更新后的 origin/development
,因为这些 origin/whatever
名称只是盲目地跟随远程存储库上发生的任何事情。这就是 Bob 想要的:Bob 的b运行chees 没有命名origin/whatever
;那些 origin/whatever
是 remote-tracking b运行ches(总是与 Bob 的所有 b运行ches 分开)。
所以,现在 Bob 有了这个(让我们假设 Bob 已经回到 development
所以它也是他的 HEAD):
h'-i' <-- origin/development
/
...--e--f--g--j <-- origin/B
\
h--i <-- development (HEAD)
由 Bob(以某种方式)弄清楚存在我们所说的 上游 rebase:origin/development
用于指向提交 i
现在指向提交 i'
。 Bob 自己没有写提交 h--i
,他只在他的 development
上写了它们,因为那是他的 Git 设置他的 development
的地方,当他 运行 git checkout development
那 创造了 他的 development
.
但在 Bob 的 Git 看来,这些提交确实像是 Bob 自己编写的,因为 origin/development
没有它们。
在这种特殊情况下(Bob 没有自己的工作),Bob 可以 运行 git reset --hard origin/development
让他的 Git 移动他的 development
以匹配 origin/development
。但是,如果 Bob did 在 development
上进行了新的提交怎么办?让我们画出这个情况:
h'-i' <-- origin/development
/
...--e--f--g--j <-- origin/B
\
h--i--k <-- development (HEAD)
鲍勃现在要做的就是做他自己的那种git rebase
。他需要 复制 他的提交 k
以在 i'
.
使用 fork-point
自动处理上游 rebaseGit 有一个功能,它使用引用日志(上面未说明)到 try 来找出哪些提交实际上是 Bob 的。它通常很有效:Bob 的 Git 可以找出 Bob 的提交 k
,并且提交 h--i
是较早的 origin/development
值遗留下来的。
因此,Bob 可以 运行:
git rebase --fork-point origin/development
当鲍勃在(鲍勃的)development
。 --fork-point
选项使 Git 尝试解决所有这些问题。
请注意,--fork-point
是某些 rebase 的 默认值 ,而其他则不是。这个想法是让所有这些工作无缝自动进行。实际上,接缝到处都是。
自动使用--fork-point
的地方是没有参数的git rebase
,或者git pull
运行s [=351=的git rebase
]if 你让 git pull
运行 git rebase
.
默认情况下,git pull
运行s git merge
...并且 合并 原始的 h--i
和新的 h'--i'
,带回 Alice 试图通过她的历史重写删除的那些提交!因此,要么根本不使用 git pull
(我的偏好),要么将其设置为使用 git rebase
作为第二步。 (它的第一步是总是git fetch
。)
不幸的是,所有这一切只有当你自动将你的development
变基到你的origin/development
时才有效,因为你的development
将origin/development
设置为它的[=351] =]上游。如果您 运行 显式 git rebase origin/development
,则 关闭 --fork-point
选项,并且您也必须指定 --fork-point
。这一切都非常令人困惑......这可能就是为什么有些人更喜欢 git pull
,它对你隐藏了所有这些。问题是,它太原始了,无法隐藏:它并不总是有效,你需要知道发生了什么,为什么,这样你就可以修复它。
当自动的东西不起作用时,你必须手动变基
如果 none 的自动化将完成这项工作,则每个用户(Bob、Carol、Dmitry 等)都可以手动 git rebase
或 git rebase -i
。交互式变基允许用户删除不应复制的提交,例如上例中设置的 h--i
。这不是最有趣的练习。它确实给您留下了干净的历史;但这种干净的历史是否值得你来决定。