是什么导致git重写提交而不是重用现有提交?

时间:2016-09-22 19:54:25

标签: git git-merge git-rebase

我已实施管理配置文件的工作流程,如下所示:

  • production在功能上等同于实时生产服务器上的内容,因为服务器在验证本地没有任何更改后,会定期执行production签出。
  • pre-production在功能上等同于实时预生产服务器上的内容(请参阅production)。
  • development实际上与实时开发服务器上的内容相同(请参阅production)。
  • master是准备合并到production的一系列事物。如果没有排队,则指向与production相同的提交。
  • 每次向master提交时,rebase -p master --no-ffpre-production都会发出development
  • 每次向production提交时,rebase -p production --no-ff都会发出master。标记unity被强制更新到此提交点。对production rebases master的提交以及master的变更迫使pre-production和`development to rebase。
  • 新的feature/*分支始终是从最新的unity点创建的(也可能来自生产,但这主要是为了减少可能意外跟踪生产分支的用户的混淆。)< / LI>

我们已经在生产中使用这个工作流程几周了,并解决了大部分问题。我注意到的一个奇怪之处是,预生产的一些合并修改了合并的提交,而其他合并则没有。

例如:

unity   merge feature/foo to pre-production
|       |
A------>C
 \     /
  \-->B   feature/foo

            unity (merge feature/bar to master, merge master to production)
            |   merge feature/foo to pre-production
            |   |
A---------->D-->E
 \-->B     /   /
  \-------/-->B'
   \---->C

            unity (merge feature/bar to master, merge master to production)
            |   merge feature/foo to pre-production
            |   |   merge feature/baz to pre-production
            |   |   |
A---------->E-->F-->G
 \-->B     /   /   /
  \-------/-->B'  /
   \---->C       /
    \---------->D

            merge feature/bar to master, merge master to production
            |   unity (merge feature/qux to master, merge master to production)
            |   |   merge feature/foo to pre-production
            |   |   |   merge feature/baz to pre-production
            |   |   |   |
A---------->E-->F-->G-->I
 \-->B     /   /   /   /
  \-------/---/-->B'  /
   \---->C   /       /
    \-------/------>D
     \-----H

如果我查看预生产历史,这大概就是我在简单规模上看到的(一些分支可能有多个提交,一些可能有一个或两个)。我也遗漏了主人,因为它通常与生产完全相同,包括任何&#34; master =&gt; production&#34;提交。

我不明白为什么B&#39; (feature / foo的副本,但未附加到feature / foo分支)存在,具有修改的提交日期,而D(feature / baz,实际上和与预生成合并)可以按原样存在,通过多个rebase程序。

如果有一种方法可以在rebase期间强制使用feature / baz功能,那么这将是首选,尽管它不是真正的问题,因为一旦分支被转移到生产或被放弃/整个问题就消失了删除。我最感兴趣的是试图理解&#34;为什么&#34;在git如何处理这个问题,以及是否有办法强制一条路径超过另一条路径。

1 个答案:

答案 0 :(得分:1)

(我担心这很长,并不是一个直接的答案 - 它是我在其他事情之间写的长篇博文后期答案中的另一个。)

进入&#34;为什么&#34;有点棘手。首先,让我们看看&#34;什么&#34;。

重写?重用?两者都没有!

在一个重要的基础意义上,Git从不重写提交,但在另一个意义上,可以通过重写来重用提交(但实际上不会重写它们)。这个概念乍一看是非常古怪的,需要解释。最后,它与Git 无法必须重新使用提交的时间(及其原因)联系在一起。

Git可能会将(部分或全部)提交复制到新提交中 - 这是filter-branchrebase原则上所做的事情 - 或者它可以保留提交并构建其他提交通过创建使用该提交的ID作为其父ID的新提交(或者在合并的情况下使用多个父ID之一)来提交。例如,后者是正常的git commitgit merge所做的。

在任何情况下,这里至关重要的是提交的ID,或者实际上任何Git的对象, 提交(或对象),在重要的和基本的意义。 ID是根据提交的完整内容构造的加密哈希,并且具有完全相同内容的任何提交具有相同的ID,并且任何相同的ID具有完全相同的内容。如果你提出的哈希值与之前的内容相同,那么Git根本不会让你存储新的内容:它会坚持对象已经存在,当你要求内容时通过生成的ID,您将获得内容,而不是新内容。

这确实意味着Git在同样的基本意义上是有限的:只有2个 160 对象可以存在于任何Git存储库中, 1 和一旦你存储了所有这些,没有新的对象可以进入。幸运的是,这个数字是如此巨大,以至于可以合理安全地假设你不仅永远不会填满它,但事实上,你永远不会找到两个散列到相同数字的不同内容。

这在实践中意味着Git的对象存储只是一次附加:在这个级别,你给Git一些内容(和一个类型)并要求它将对象写入存储库,使用git hash-object -w。 Git计算哈希值,Git然后存储对象并告诉你哈希值,或者什么都不做并打印哈希值。然后使用此哈希检索内容以仔细检查实际上是否存储了您的内容(而不是由于哈希冲突而重新使用的其他内容),或者只是假设您的内容已存储,或者是已经在场了。

后一种情况在存储文件时很常见,因为每次提交都会存储每个文件。如果第一次提交有10个文件,第二次提交有相同的10个文件但只有一个被更改,则第二次提交重新使用9个文件。 (事实上​​,除非你再次明确git add所有十个文件,Git甚至可以优化掉#9;假装存储9个重复使用的文件&#34;步骤。但如果你做了git add全部10个,只有一个已经改变,然后10个blob-object写中的9个简单地计算了一些现有对象的散列,并重新使用了该对象。)

1 这假设Git永远致力于SHA-1,它产生160位的哈希摘要。 Git的某些部分使切换变得困难,而其他部分则使其变得容易。 Mercurial有一个类似的问题,只是它的内部格式允许直接切换到256位散列。如果有人想要更大的东西(参见https://en.wikipedia.org/wiki/Secure_Hash_Algorithm并注意有512位哈希值),Mercurial也会遇到一些困难。

提交中的内容是什么?

理解这一点的第二个关键是查看实际提交的实际内容。这是来自Git的Git存储库中的一个:

$ git cat-file -p HEAD~2 | sed 's/@/ /'
tree fba3eb43b1cdde5c0201287b16b295fee295b495
parent 930b67ebd7450a72248111582c1955cd6f174519
parent 5cb5fe4ae0f9329843c9b028b45df9c6b987c851
author Junio C Hamano <gitster pobox.com> 1473719678 -0700
committer Junio C Hamano <gitster pobox.com> 1473719678 -0700

Merge branch 'sb/transport-report-missing-submodule-on-stderr'

Message cleanup.

* sb/transport-report-missing-submodule-on-stderr:
  transport: report missing submodule pushes consistently on stderr

我在这里选择了一个合并,因此它有两个父母,而不是更典型的单亲。这里的重要事项是:

  • tree:每次提交总会有一个;这是提交的顶级树的哈希ID。 (然后你可以git cat-file -p该树对象找到它的子树和文件。)

  • parent:每个父ID有一个parent行。这些提供了父提交的ID。

  • authorcommitter:每行有一行,分为三部分,分别列出了人名和电子邮件地址以及时间戳。

然后是空白行,然后是提交消息的主题和正文。 Git通常不会在空行后解释部分,也不对其施加任何约束;前面的部分有一个规范的格式,虽然某些版本的Git也不那么挑剔。 2

这意味着提交的哈希ID由树,父ID,作者和提交者名称/电子邮件/时间值以及消息确定。如果你从一个提交对象复制这些值,一点一点地没有任何改变,然后让Git散列并写出结果值,你将得到相同的对象ID,存储相同的提交数据。它实际上是相同的提交:就像blob对象从一个提交重用到另一个提交一样,只要它们是逐位相同的,提交与先前的提交一点一点地相同重新使用。

但是,如果改变单个位,则SHA-1的性质意味着最终的散列非常不同。而且,如果您进行 new 提交,甚至重新使用树,父ID,作者姓名,作者电子邮件,提交者名称和提交者电子邮件, new 提交将通常有一个新的,不同的时间戳,因为现在的时间与一秒钟前的时间不一样。 (这些时间戳字符串计算秒数,基本上是Unix time_t值。)

因此,通常是提交与其他每次提交都有不同的ID。要使新提交真正匹配现有提交,您必须保持所有位相同,包括时间戳。您可以执行此操作 - git filter-branch命令是故意执行的。但请注意,这也意味着 ID必须匹配,逐位匹配。这意味着新提交将重用任何现有父级。在我们继续git rebase时,请记住这一点。

2 我们已经看到filter-branch会在标题部分意外修改Unicode,或导致提交正文中非换行终止的最终行成为换行符的情况,从而以我们没想到的方式改变提交的哈希值。然后,此更改通过父ID行将更改传播到每个后代提交。但原则上至少,git filter-branch尝试不触及此,并将任何更改留给您自己的过滤器,以便通过逐位保留提交来保留提交ID。

Rebase复制提交,但通常会更改

rebase的工作方式 - 与filter-branch的工作方式几乎相同 - 是提取一些现有的提交,让你做一些更改,然后从中进行新的提交结果。大多数情况下,至少有两个同时发生的变化:

  • 您从另一个树(与第一次提交的树枝相关联的树,或者#34;在执行第一次提交时提交到#34;提交)开始。对于这棵树,你可以从你正在复制的提交中提取更改:Git通过将该提交与其父级进行区分,然后将diff的结果应用于树,以便为您提交的提交执行此操作从

  • 并且,您从另一个开始。新副本的新父级是新副本的提交。

如果最终的tree对象不同,或者parent行不同,或者两者都不同,则生成的提交会有一个新的不同哈希。

现在,rebase并不总是真的必须复制提交。假设我们有以下内容:

...--B--C--D            <-- main
            \
             E--F--G    <-- topic

如果你git checkout topic; git rebase main,Git通过列出可从topic(此处显示的每个提交)到达的提交来找到要复制的提交,然后减去每个可从main到达的提交(提交结束于B--C--D)。它计算复制的目标是提交D,即main的提示。因此,必须在E之后立即复制D - 即,将D作为其父级,然后将F复制到E之后,并且在G之后F来到E。但D已将-f作为其父级,因此它可以执行此操作&#34; copy&#34;什么都不做。

只要可以,就会编写rebase代码,除非使用--no-ff--allow-empty。在这种情况下,它继续使用复制技术。 (请参阅https://www.kernel.org/pub/software/scm/git/docs/howto/revert-a-faulty-merge.html了解执行此操作的时间和原因。)因为这些是副本,所以它们使用新的(当前)时间并获得新的时间戳。

这里有一个潜在的缺陷:因为时间戳有一秒的粒度,如果这个rebase发生得足够快 - 如果从脚本运行很多rebase可能会发生 - 它可能最终产生一点一点相同的提交。如果发生此,则新提交确实 旧提交。

Rapid-fire提交

使用git checkout -b feat1 main git commit --allow-empty -m 'create branch for feature' git checkout -b feat2 main git commit --allow-empty -m 'create branch for feature' 时,同样的事情会影响脚本编写的分支。假设您有一个执行此操作的脚本:

main

这里的想法是从 E <-- feat1 / ...--D <-- main \ F <-- feat2 创建两个新分支,每个分支都有自己的(空)提交:

E

现在,您可以在某些外部数据库中记录提交Ftree 45ee45ee... parent dddddd... author A U Thor <auth@thor> 123456789 -0700 committer A U Thor <auth@thor> 123456789 -0700 create branch for feature 的ID,以用于跟踪两个功能分支上完成工作的后续用途。但是,如果使用相同的作者和提交者名称以及电子邮件进行的两个新提交是在相同的第二个中进行的,那么这两个提交都会读取:

...--D     <-- main
      \
       E   <-- feat1, feat2

这两个提交是逐位相同的,因此具有相同的内部提交ID。我们得到的不是上面绘制的图表,而是这一个:

{{1}}

(修复很简单:给他们不同的提交消息,和/或在提交之间等待一秒。这个特殊问题可能看起来不太可能,但我发生在我身上!幸运的是它只是为了测试。)