多个开发人员如何使用 git rebase 在同一个分支上工作?

Gra*_*min 0 git git-rebase

我们遇到的问题是我们有我们的主development分支。我们有一张票,它依赖于多张票才能进入development分支。所以可以说 ticketA依赖于 ticketB并且C为了被合并到development.

我们一直在处理这个问题的方法是使一个A分叉的development再分支BC关闭的A。问题是我们想要将 development 的更新带入A, BC并且我们还想将更新从Binto 带入C,反之亦然。git rebase当有人在从 中进行更改后进行git push --forceArebase时,我们如何在不让每个人都删除其本地 A 分支的情况下使用它来执行此操作development

tor*_*rek 6

TL;DR 总结

历史就是提交。

Rebase 的意思是“复制提交,然后尝试忘记原件并使用副本”。这意味着“创造新的历史”。

Git 存储库得到分发(广泛复制)。通过变基创建的新历史仅影响一个存储库。这就是为什么您必须--force在推送期间使用,以强制另一个存储库接受“历史重写”。但是,这只会影响一个其他的副本,而不是所有的其他副本。

您可以使用一些技巧来减轻疼痛,但疼痛本身并没有真正消失。由您来选择您喜欢哪种疼痛。主要技巧是使用git rebase:要么git pull完全避免,要么将其配置为git rebase作为第二个 Git 命令运行。但这仅适用于一个级别。

另一种方法是不使用--force. 这要简单得多,但确实会给您留下错综复杂的历史。那仍然可能更可取。

描述

在 Git 中,在很大程度上,分支名称并不重要。它们确实在这里和那里被使用,有多种用途,但它们非常流畅和无常——提交非常不同,提交是永久性的,永远不会改变(但可以复制!)。因此,正如您所说,“创建一个A分支development,然后再分支BC关闭A”对您没有好处:Git 不在乎名称,因为它们一直在变化;它只关心提交,而不关心。

你从这个开始:

...--e--f--g   <-- development
Run Code Online (Sandbox Code Playgroud)

(由于您对分支使用大写的单字母名称,因此我在此处为每个提交切换为小写的单字母名称)。然后再添加几个名称:

...--e--f--g   <-- development, A, B, C
Run Code Online (Sandbox Code Playgroud)

现在只要有人——任何人——拥有这个存储库结构,即在他们的存储库中有这四个分支名称,就会:

git checkout development
Run Code Online (Sandbox Code Playgroud)

并进行一些新的提交,这就是他们存储库中发生的事情:

...--e--f--g   <-- A, B, C
            \
             h--i  <-- development (HEAD)
Run Code Online (Sandbox Code Playgroud)

新提交进入,每个新提交像往常一样记录其前一个父提交。代表当前分支的分支名称(该用户的 Git 已记录在该用户的 HEAD 文件中)表示此存储库中的当前分支是名为 的分支development,因此此 Git 不断重新设置为新提交的标签,是development。因此development ,此存储库中的标签已升级为指向两个新提交中的最新提交。

其他标签没有移动。


现在,实际上,每个用户都有自己的存储库。用户之间必须相互协调,这也是通过分支名称发生的——但现在至少涉及两个Git,所以有两组或更多组分支名称。一切都必须通过这个镜头来看待:我有我的分支,你有你的分支,当我和你说话时,我称你为我的“远程”,我重新命名你的分支,这样它们就不会干扰我的。

每个开发人员都可以直接与其他每个开发人员交谈。例如,如果您的组有标准的前三个人,分别名为 Alice、Bob 和 Carol,Alice 可以有两个遥控器,分别命名为 Bob 和 Carol,Bob 可以有两个遥控器,分别命名为 Alice 和 Carol,Carol 可以有两个遥控器,嗯,明显的名字。这显然扩展性很差:在 30 名开发人员的团队中,每个人都有 29 个遥控器。

所以,你可能有一个集中式服务器,甚至可能在像 GitHub 这样的高档托管网站上。这让每个人都只有一个遥控器,他们总是称之为origin,因为 Git 会自动使用该名称,除非您告诉它使用其他名称。

这就是名称 likeorigin/masterorigin/development来自的地方,也是我们origingit fetch origin和时使用名称的原因git push origin <branch>。这只是一个普通的遥控器:每个人都可以分享他或她的承诺的地方。

这里的棘手部分是这个originGit 存储库是一个Git 存储库。因此,它具有提交,这是永久性的,但有一样可怕,尴尬的哈希ID名称9b00c5a...badbeef...等等,没有人能记得; 所以它也有分支名称,比如development.

分支名称的主要功能是记住这些提交哈希 ID 之一。这就是为什么我以我绘制它们的方式在上面绘制图形的原因:分支名称“指向”其相应分支的提示提交。

请注意,一次提交可以并且经常是同时在多个分支上。如果您添加一个指向提交的新分支名称i( 的当前提示development),突然所有以前在development诸如e--f--g--h--i)上的提交现在也都在这个新分支上。删除这个新的分支名称,并且刚好在两个分支上的commitshi,现在只在一个分支上。

分支名称有一个次要功能:通过让 Git找到一个特定的提交,它可以保护该提交不被 Grim Collector、Git 的“垃圾收集器”或“gc”回收。然后该提交会保护其所有历史记录——导致分支提示的所有先前提交。由于 Git 通常通过在提示处增加新提交来移动分支名称,因此新提交(受分支名称保护)保护旧提示,保护更旧的提示,一直回到有史以来的第一次提交。

请注意,Git 是反向工作的:我们总是从提示开始,然后通过查找每个提交的父级来向后遍历历史记录——提交。每个提交都“指向”其父提交该提交序列就是历史。这就是git log向我们展示的;和gitk或图形查看器,或使用--graphwith git log,向我们展示从提交到其父项的联系。

具有两个直接子级的父级在图中形成一个分支。为了让这个分支继续存在,两个分支现在都需要一个名字。让我们git checkout B(进入B分支)并进行新的提交以实现这一目标:

             j   <-- B (HEAD)
            /
...--e--f--g   <-- A, C
            \
             h--i  <-- development
Run Code Online (Sandbox Code Playgroud)

这里的父母是g,两个孩子是hj。当前分支名称,B已移动:它现在指向j

如果我们现在检查分支C并进行一两次新的提交,则图片变得更难绘制,因为我们需要将A指向提交g而 ASCII 艺术实际上没有那么多空间:

             j   <-- B
            /_------ A
...--e--f--g--k--l  <-- C (HEAD)
            \
             h--i  <-- development
Run Code Online (Sandbox Code Playgroud)

不过,我们再次假设所有这些都发生在一个存储库中——在一种不存在其他存储库的真空中。它也变得太难画了,所以让我们回到只有分支名称Bdevelopments,假设我们已经,不知何故,在存储库origin和一个人的存储库中都进入了这种状态——爱丽丝的,说——所以我们现在在 Alice 和origin存储库中都有这个:

...--e--f--g--j  <-- B
            \
             h--i  <-- development
Run Code Online (Sandbox Code Playgroud)

现在,如果 Bobgit fetch从运行origin,并假设 Bob 还没有自己的分支(他必须至少有一个,我们只是没有绘制它),这就是 Bob 得到的:

...--e--f--g--j  <-- origin/B
            \
             h--i  <-- origin/development
Run Code Online (Sandbox Code Playgroud)

也就是说,对于 Bob 来说,Bob 的 Git 会记住 Boborigin上次运行时的情况git fetch origin(注意:让我们暂时避免git pull,但请注意,它所做的第一件事是 run git fetch origin)。它通过重命名它在 上找到的分支来实现origin,以便它们不再是分支。

如果Bob现在运行git checkout development,鲍勃的Git的认定,鲍勃不具有一个development呢。它搜索 Bob 的存储库并找到origin/development,它肯定看起来很像development,因此 Bob 的 Git创建一个新的 development,指向指向的相同提交origin/development。现在鲍勃有这个:

...--e--f--g--j  <-- origin/B
            \
             h--i  <-- development (HEAD), origin/development
Run Code Online (Sandbox Code Playgroud)

如果 Bob 跑了,git checkout master他仍然会有名字origin/B(指向j)、development(指向i)和origin/development(也指向i);但他会检查其他一些提交(也许d我们还没有绘制?),并将他的 HEAD 附加到master.

您可能想知道为什么 Alice 没有这些origin/名字。事实是,她确实如此。她origin/B指着jorigin/development指着i。请注意,Alice已经有了她自己的名字Bdevelopment名字。Bob(还)没有自己的B,只有他自己的development

origin不过,上面的存储库可能没有任何origin/名称。它只有自己的分支。这是因为它没有用户使用它;不存在的用户不会运行git fetch origin。)


现在让我们回到 Alice,假设 Alice 运行 agit rebase来变基,development以便它在 Alice 之后B

$ git checkout development
$ git rebase B
Run Code Online (Sandbox Code Playgroud)

什么git rebase复制一些提交。

它选择复制哪些提交的方式是查看当前分支,然后查看其参数。

当前分支是development并且参数是B。这是 Alice 现在所拥有的(这与 Bob 所拥有的非常相似):

...--e--f--g--j  <-- B, origin/B
            \
             h--i  <-- development (HEAD), origin/development
Run Code Online (Sandbox Code Playgroud)

git rebase要复制的提交是在当前分支( development)上的提交,除了在 branch 上的任何提交B。好吧,提交...--e--f-g--j--i正在进行development并且...--e--f--g--j正在进行B。所以 Gitje--f--g--h--i集合中减去——这很简单,没有j,所以什么也没发生。然后 Gitg从集合中减去,并且...--e--f,离开h--i.

这些是要复制的提交。

它们被复制后将去的地方是任何 commitB指向的地方,即,它们将在 commit 之后去j

现在 Git 制作副本(使用 Git 所谓的“分离的 HEAD”模式):

                h'-i'   <-- HEAD
               /
...--e--f--g--j  <-- B, origin/B
            \
             h--i  <-- development, origin/development
Run Code Online (Sandbox Code Playgroud)

这些新副本具有不同的哈希 ID。他们是不同的提交!它们是原始文件的副本(有一些更改)h--i,因此我们h'-i'在这里称它们为。原件仍在那里,因为仍然有名字将它们固定在原处。

的最后一步git rebase是强行移动原始分支名称,development指向最尖端的复制提交,并重新附加您的 HEAD:

                h'-i'   <-- development (HEAD)
               /
...--e--f--g--j  <-- B, origin/B
            \
             h--i  <-- origin/development
Run Code Online (Sandbox Code Playgroud)

仍然保留的名称h--iorigin/development。Alice的混帐现在翻译development成“承诺i'”(无论其新的哈希ID为),所以她的历史改写development与结束i'时,返回h',然后再次进入jgf,等,在Git的通常向后时尚。

强制推送

如果爱丽丝现在尝试git push origin development,她会被拒绝。这是因为 Git over atorigin仍然有这个:

...--e--f--g--j  <-- B
            \
             h--i  <-- development
Run Code Online (Sandbox Code Playgroud)

Alice 的 Git 发送提交h'i',即她origin没有的提交,以及一个提议:

                h'-i'   <-- proposal: move development here
               /
...--e--f--g--j  <-- B
            \
             h--i  <-- development
Run Code Online (Sandbox Code Playgroud)

Origin 的 Git 看着提案说:呃,不,这不是“快进”。拒绝!

记得早些时候我们看到 Git 分支通过在其末端增加新提交来增长。如果 Alice 正在推送添加到 的新提交h--i,那就没问题了:这是一个“快进”操作(一次添加多个提交,而不是一次添加一个)。但是她正在推动这些新的提交,然后提议完全development退出h--i,这不行:这不是“快进”。

但是 Alice 可以用--force它来使它成为一个命令,而不是一个提议;然后,只要 origin 的 Git 不反对太多,origin 的 Git 就会服从并将其存储库更改为如下所示:

                h'-i'   <-- development
               /
...--e--f--g--j  <-- B
Run Code Online (Sandbox Code Playgroud)

这是除了爱丽丝(和她origin自己)之外的所有人痛苦的开始。

请注意,提交h--i刚刚从origin. (如果服务器 atorigin正在使用 Git 的reflogs,则那里的 reflogs 将保留它们一段时间。默认情况下,服务器不会保留 reflogs,但是。)

现在 Bob 感受到了上游 rebase 的痛苦

在这一点上,当 Bob 运行时——git fetch origin记住,如果你(或 Bob)正在使用git pull,你实际上是在先运行——Bobgit fetch origin得到更新的origin/development,因为这些origin/whatever名称只是盲目地跟随远程存储库上发生的任何事情。这就是 Bob 想要的:Bob 的分支没有命名origin/whatever;这些origin/whatevers 是远程跟踪分支(始终与 Bob 的所有分支分开)。

所以,现在 Bob 有了这个(让我们假设 Bob 又回来了,development所以它也是他的 HEAD):

                h'-i'   <-- origin/development
               /
...--e--f--g--j  <-- origin/B
            \
             h--i  <-- development (HEAD)
Run Code Online (Sandbox Code Playgroud)

由 Bob 来(以某种方式)找出我们所说的上游变基origin/development以前指向提交i,现在指向提交i'。鲍勃没有写提交h--i自己,他只有他们自己development,因为那是他的Git将他development回来时,他跑了git checkout development创造他的development

但对于 Bob 的 Git 来说,看起来确实像 Bob 自己写的那些提交,因为origin/development没有它们。

在这种特殊情况下(Bob 没有自己的工作),Bob 只需运行git reset --hard origin/development即可让他的 Git 移动development到 match origin/development。但是如果 Bob确实在 上做了一个新的提交development呢?让我们画出这种情况:

                h'-i'   <-- origin/development
               /
...--e--f--g--j  <-- origin/B
            \
             h--i--k  <-- development (HEAD)
Run Code Online (Sandbox Code Playgroud)

Bob 现在要做的就是做他自己的事情git rebase。他需要复制他的承诺k来之后i'

使用 fork-point 进行自动上游 rebase 处理

Git 有一个特性,它使用 reflogs(上面没有说明)来尝试找出哪些提交实际上是 Bob 的。它通常有效:Bob 的 Git 可以找出kBob 的提交是什么,并且提交h--i是从较早的origin/development值遗留下来的。

因此,鲍勃可以运行:

git rebase --fork-point origin/development
Run Code Online (Sandbox Code Playgroud)

当鲍勃在 (鲍勃的) 上时development。该--fork-point选项使 Git 尝试解决所有这些问题。

请注意,这--fork-point是某些变基的默认设置,而不是其他变基的默认设置。这个想法是让所有这些工作无缝和自动地进行。实际上,接缝处处可见。

住的地方--fork-point就会自动使用是git rebase不带任何参数,或者git rebasegit pull运行,如果你使git pull运行git rebase

默认情况下,改为git pull运行git merge……并将原始文件与新文件合并,从而恢复 Alice 试图通过重写历史记录删除的那些提交!因此,要么根本不使用(我的偏好),要么将其设置为第二步。(它的第一步总是。)h--ih'--i'git pullgit rebase git fetch

不幸的是,所有这一切仅development在您origin/development通过将developmentorigin/development设置为上游而自动重新建立在您的基础上时才有效。如果您运行一个显式git rebase origin/development则会关闭--fork-point选项,您也必须指定--fork-point。这一切都非常令人困惑……这可能就是为什么有些人更喜欢git pull,这对您隐藏了所有这些。问题是,隐藏起来有点太原始了:它并不总是有效,你需要知道发生了什么,以及为什么,这样你才能修复它。

当自动的东西不起作用时,你必须手动变基

如果没有任何自动化可以完成这项工作,每个用户(Bob、Carol、Dmitry 等)都可以执行手册git rebase, 或git rebase -i. 交互式 rebase 允许用户删除不应复制的提交,h--i例如上面示例中的设置。这不是最有趣的练习。它确实给你留下了干净的历史;但这种干净的历史是否值得痛苦由你来决定。