远程Git rebase为何邪恶:详细原因

4
我来自中心化版本控制的背景,正在尝试在Git中确定我们的工作流程(新公司,年轻的代码库)。我找不到一个简单而详细的答案,关于远程分支上的rebase到底是做什么的。我知道它会重写历史记录,并且通常应该仅限于本地分支。

我目前正在尝试验证的工作流程涉及远程协作分支,每个开发人员“拥有”一个分支,以共享代码。 (对于每个项目和功能请求都有2个开发人员和最多3个开发人员,似乎过于繁琐,而且超过了获得的好处。)

然后我看到了this answer,尝试了一下,它实现了我想要的效果——开发人员经常提交并推送到自己的协作分支,当他知道哪些内容可以发布到暂存区时,他可以远程rebase(合并和重新组织)再合并到develop分支。

接下来就是原始问题了——如果远程分支是为了协作而存在,那么其他人迟早会拉取它。如果不允许“访客开发者”提交到该协作分支,则分支所有者进行rebase会发生什么?


1
在 Git 中理解这一点的关键是,在 Git 中,分支名称几乎不相关。重要的是原始提交哈希 ID。Rebase 通过将现有提交复制到新的提交来工作。如果复制中有任何一个位改变了,新的提交就会有一个新的、不同的 ID。分支结构由提交 ID 形成。除此之外,一切都只是这两个基本事实的结果(加上 fetch 和 push 使用名称来传输 ID 的想法)。 - torek
另一个理解的关键是,“远程分支”的概念本质上只是一种礼貌的虚构。你的Git存储库是你的。当你使用git fetch时,你从别人那里收集一些名称/ID对,并且你的 Git 重命名这些名称,并将ID作为“远程分支”添加/更新到你的存储库中。一旦fetch完成,这就没有什么remote了:它只是你的Git记住“上次我和他们交谈时,他们说master1234567...”。 - torek
现在把这两个结合起来:你与某个远程进行交流,它说“branch = 1234567...”。然后他们(拥有该远程的人)进行了变基。现在你再与他们交流,他们说“branch = abcdef0...”。现在由你自己去想办法弄清楚发生了什么。如果每个人事先都同意“变基会发生”,那么你可能可以弄清楚发生了什么。如果没有——如果你期望“永远不会发生变基”——你会感到相当困惑。 - torek
@torek这是帮助我理解Git分支的关键。你能把它转换成答案吗? - mlhDev
我来试试看... - torek
3个回答

23

这并不是真正的“邪恶”,而是实现和期望的问题。

我们从一堆事实开始:

  • 每个 Git 哈希表示某个唯一的对象。针对我们在这里需要考虑的只有提交对象。每个哈希是将加密哈希函数(对于 Git 具体来说,是 SHA-1)应用于对象的内容的结果。对于一个提交,其内容包括源树的 ID;作者、提交者的姓名、电子邮件地址和时间/日期戳;提交消息;以及最重要的,父提交的 ID。

  • 即使只更改内容中的单个位,也会得到一个新的、非常不同的哈希 ID。哈希函数的加密属性,它们用于验证每个提交(或其他对象),还意味着没有办法让一些不同的对象具有相同的哈希 ID。Git 也依靠此功能在仓库之间传输对象。

  • 变基通过将提交复制到新提交来工作(必须)。即使除此之外没有任何变化——通常,与新副本相关联的源代码与原始源代码有所不同——变基的整个重点是重新定位一些提交链。例如,我们可能从以下内容开始:

  • ...--o--*--o--o--o   <-- develop
             \
              o--o       <-- feature
    

    分支feature在提交*时与分支develop分离,但现在我们想要featuredevelop的最新提交处开始,所以我们将其变基。结果是:

    ...--o--*--o--o--o        <-- develop
             \        \
              \        @--@   <-- feature
               \
                o--o          abandoned [used to be feature, now left-overs]
    
  • 两个@是原始两个提交的副本。

  • 分支名称(如develop)只是指向(单个)提交的指针。我们认为“分支”的东西,如两个提交@--@,是通过从每个提交向其父级倒退形成的。

  • 分支总是期望增加新的提交。很正常发现developmaster新增了一些提交,因此该名称现在指向一个提交,或者是指向许多提交中的最后一个提交,而这些提交都指回了该名称所指的位置。

  • 每当您让您的Git与其他Git及其另一个存储库同步(无论到什么程度),您的Git和他们的Git都会交换ID -- 具体取决于传输方向以及您要求您的Git使用的任何分支名称

  • 远程跟踪分支实际上是与您的存储库相关联的实体。您的远程跟踪分支origin/master实际上是您的Git记住“在我们上次通话时,origin的Git说master是什么”。

  • 现在,我们将这七个项目作为示例,看看git fetch如何工作。例如,您可以运行git fetch origin。此时,您的Git调用origin上的Git,并询问其有关其分支的信息。它们会说类似于master = 1234567branch = 89abcde的内容(虽然哈希值确实都是40个字符长,而不是这些7个字符长)。

    如果您的Git已经拥有这些提交对象,则几乎完成了!否则,它会请求他们的Git发送这些提交对象,以及您的Git需要理解它们的任何其他对象。附加的对象是与这些提交一起的任何文件,以及那些提交使用但您还没有的任何父提交,以及父提交的父提交,依此类推,直到找到您拥有的某些提交对象。这将为您提供所有新历史记录所需的所有提交和文件。1

    一旦您的Git安全地存储了所有对象,它将使用新的ID更新远程跟踪分支。他们的Git刚刚告诉您,他们的master1234567,因此现在您的origin/master设置为1234567。对于他们的branch也是一样:它变成了您的origin/branch,而您的Git保存了哈希89abcde

    如果现在您运行git checkout branch,您的Git将使用origin/branch创建一个新的本地标签,指向89abcde。让我们来画一画:

    ...--o--*--o--1   <-- master, origin/master
             \
              o--8    <-- branch, origin/branch
    

    为了使事情变得更有趣,让我们在分支branch上进行自己的新提交。假设它被编号为aaaaaaa...

    ...--o--*--o--1    <-- master, origin/master
             \
              o--8     <-- origin/branch
                  \
                   A   <-- branch
    

    那么有趣的问题是,如果它们(你获取的Git)重新设置基础,则会发生什么。假设他们将 branch 重新设置到 master 上。这会复制一些提交记录。现在你运行 git fetch,你的 Git 看到他们说 branch = fedcba9。你的 Git 检查是否有此对象;如果没有,则获取它(和其文件)及其父代(和该提交的文件),直到我们达到某个共同点 - 实际上将会是提交记录 1234567

    现在,拥有了这个:

    ...--o--*--o--1        <-- master, origin/master
             \     \
              \     o--F   <-- origin/branch
               \
                o--8--A    <-- branch
    

    我这里用F来表示提交fedcba9,也就是现在origin/branch指向的那个提交。

    如果你后来看到这个提交历史时没有意识到上游开发者已经变基了他们的branch(你的origin/branch),你可能会认为写了所有三个提交o--8--A,因为它们在你的branch而不再在origin/branch上。但它们不在origin/branch上的原因是上游开发者已经放弃了它们,取而代之的是新的提交。这有点难以确定那些新的提交实际上是副本,并且你也应该放弃这些提交。


    1如果分支按着“正常”的、“预期”的方式增长,你的Git和他们的Git很容易找出你的Git需要从他们那里获取哪些提交:你的origin/master告诉你上次看到他们的master在哪里,现在他们的master指向更长的提交链的末端。你需要的提交恰好是在他们master上,且出现在你的origin/master末端之后。

    如果分支以不太典型的方式洗牌,则有些困难。在最一般的情况下,他们只需按哈希ID枚举所有对象,直到你的Git告诉他们他们已经到达一个你已经拥有的提交为止。具体细节还受浅克隆的影响。


    这并非不可能

    这并非不可能,自Git 2.0版本以来,现在内置了工具可以让Git帮你找出来。(具体来说是通过git merge-base --fork-point调用,由git rebase --fork-point使用,它使用你的origin/branch的引用日志(reflog)来确定o--8链曾经在origin/branch上。这仅适用于那些引用日志条目被保留的时间段,但这默认至少为30天,让您有一个月的时间去跟进。这是你的时间线中的30天:从你运行git fetch开始算起,而不是上游变基发生多久之前。)

    这实际上归结为,如果你和上游开发者事先同意某个特定的分支集合被变基了,那么你可以安排在他们每次进行变基时在你的代码库中做出必要的调整。但是,在更典型的开发过程中,你不会期望他们进行变基,如果他们没有这样做——即他们从未“放弃”你获取到的一个已经发布的提交——那么就没有什么需要从中恢复的东西。


专业地解释,感谢您的正确表述。 - Alphas Supremum

2
重新设置(或重写历史记录)已发布(远程)分支的主要问题在于重新集成基于它们的工作变得困难。因此,如果这些远程分支仅用于审核,而没有进行任何提交,甚至是合并提交,通常不会有太多问题。否则,合并和解决冲突可能很快成为主要麻烦。

0

对你的问题的简单回答是……

只要没有其他用户在远程公共“协作”分支上进行更改(另一个开发人员没有新提交),那么那些单个开发人员正在变基,那么拉取这些分支到他们本地存储库的访客不应该有任何问题,并且看到相同的干净的变基代码。不应该出现额外的合并提交。

我建议您告诉您的开发人员始终先拉取主存储库的最新更新,然后再将代码变基并推送到远程存储库。当他们在变基之前获取最新的主分支更改时,他们不会构建更多的合并或在推送变基分支时出现错误。

当两个或更多的开发人员在同一个分支上进行新提交时,再加上另一个开发人员未添加这些更改就推送了一个变基时,变基就会出现问题。当这种情况发生时,拉取代码库和远程分支的所有开发人员将具有不同提交“头”引出的重复分支,接着是尝试连接其工作分支的每个相同副本的恶心合并提交,然后是合并以将它们一起解决。变基分支将不得不与他们的本地分支合并。它可以工作,并且没有冲突,但Git的历史记录相当混乱,因为它使每个分支的提交翻倍(仍然存在于主分支上的经过变基和未经变基的版本),并使管理曾经是单一分支历史记录的操作变得非常复杂。

但是您描述的只读访客查看分支的情况是可以的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接