Git 多个合并分支 - 如何在合并到主分支时避免多次提交

dbN*_*ice 0 git repository git-merge git-rebase

我的 Git 存储库如下所示:

我创建了 2 个分支 - Branch_1 和 Branch_2。现在我终于准备好将此 Branch_2 合并到 Master 分支中。但是当我合并时,它显示了 Branch_1 和 Branch_2 的所有提交,因为中间有多个合并。任何人都可以建议如何在这种情况下继续进行一次提交,然后将我的代码合并到主分支吗?

git log --oneline --graph --color --all --decorate

* 36dbb26 (origin/Branch_2) changed abc
* 1a7bf25 changed T
* 110095a changed Z
*   1087d5d Merge remote-tracking branch 'origin/Branch_1' into Branch_2
|\
| * 8c9d02a (origin/Branch_1) sleep added between each processing to discover partitions
| * ca401cb changed S
| * 20a4edd changed R
* 3f472ef install package
*   1087d5d Merge remote-tracking branch 'origin/Branch_1' into Branch_2
|\
| * 8c9d02a (origin/Branch_1) adding y
| * ca401cb changed g 
| * 97c326d changed f 
* | fd543bf changed c
* | 7b24330 (HEAD -> master, origin/master, origin/HEAD) fix D
* | 53aecb4 adding x
|/
* 49d3bda changed e
| * 213ea18 (origin/Feature_branch) changed d
| * 0b3b675 changed c
|/
* df6ac90 Adding c 
* 96699ff Adding b 
* 99f165f Adding a 
Run Code Online (Sandbox Code Playgroud)

我想要如下所示的最终结果:(来自 fd543bf 的所有提交合并为 1 个提交)

 * 36dbb26 (HEAD -> master, origin/Branch_2) changed R-All consolidated
 * 7b24330 (origin/master) Fix D
Run Code Online (Sandbox Code Playgroud)

tor*_*rek 6

长话短说

\n

你可能只是想要git log --first-parent。

\n

长的

\n
\n

...但是当我合并时,它显示了 Branch_1 和 Branch_2 的所有提交,因为中间有多个合并。

\n
\n

不,这不是原因。您看到所有这些提交的原因是因为您实际上拥有所有这些提交。

\n

这里要理解的是,归根结底,Git 就是提交。提交是 Git 中的存储单位。1 提交是您拥有的,也是您想要的。如果你不想要这些提交,那么你想要的一定是其他一些提交。提交是你得到的一切,所以你最好想要提交。(如果你想要别的东西,不要使用 Git。但是许多其他版本控制系统也是面向提交的,所以你可能会发现无论如何你仍然会得到提交,所以你最好坚持使用 Git,除非......好吧,请阅读下一段。)

\n

Git 中分支名称的存在有一个主要原因:查找提交。这就是 Git 与其他版本控制系统的不同之处。在许多版本控制系统中,分支是提交的容器,您可以通过检查分支来检查提交:分支中包含的提交集就是您将看到的提交集(如果您是这样要求的)。但这不是 Git 中分支名称的工作方式。

\n

在 Git 中,提交可以是\xe2\x80\x94,并且通常是\xe2\x80\x94,甚至可能是同时在许多甚至所有分支上的\xe2\x80\x94 。那是因为 Git 的分支名称不是容器。他们不持有承诺。他们只是让你找到提交。每个名字都会找到一个提交。是提交本身找到了其余的提交。

\n

每个 Git 提交都由两部分组成,我们稍后将对此进行描述。每次提交都是通过其唯一的哈希 ID找到。每个提交都有这些哈希 ID 之一;该哈希 ID 可以说是提交的“真实名称”。如果没有哈希 ID,Git 实际上无法找到提交。2 因此,分支名称包含一个哈希 ID,根据定义,该 ID是该分支中包含的最后一次提交。该提交又包含一组哈希 ID\xe2\x80\x94,通常只是早期提交的一个\xe2\x80\x94 ,这些也是分支的一部分。

\n

当我们有一个分支名称(例如main或feature),它包含一些哈希 ID 时,我们说该分支名称指向该分支的最后一次提交(或tip) :

\n
            <-H   <--feature\n
Run Code Online (Sandbox Code Playgroud)\n

但是这里的commit H\xe2\x80\x94H代表真正的哈希ID,无论它是什么\xe2\x80\x94 都有一些早期提交的哈希ID G。所以我们说这H指向G:

\n
        <-G <-H   <--feature\n
Run Code Online (Sandbox Code Playgroud)\n

但提交G也向后指向更早的提交:

\n
... <-F <-G <-H   <-- feature\n
Run Code Online (Sandbox Code Playgroud)\n

依此类推,一直回到第一次提交。这实际上不能向后指向较早的提交,所以它就是不向后指向,这就是 Git 停止向后工作的地方。

\n

因此,这就是分支上提交的含义:我们从分支名称开始,它会自动确定该分支上的最后一次提交,然后向后工作。但如果是这样的话……好吧,假设我们有这样的东西,其中 commitI指向H,并且 commitK也指向H:

\n
          I--J   <-- br1\n         /\n...--G--H\n         \\\n          K--L   <-- br2\n
Run Code Online (Sandbox Code Playgroud)\n

哪个分支持有提交H?

\n

Git 的答案是提交H现在同时在两个分支上。 所有早期的提交也是如此。此外,即使H是第三个分支上的最后一次提交:

\n
          I--J   <-- br1\n         /\n...--G--H   <-- main\n         \\\n          K--L   <-- br2\n
Run Code Online (Sandbox Code Playgroud)\n

现在仍然如此。现在所有三个分支H都已提交。

\n

因此,在 Git 中,包含某些提交的分支集是动态且流动的。重要的不是分支名称,而是提交之间的连接。分支名称很有用,但只是为了帮助您入门。其他一切都与提交有关。

\n
\n

1由于提交由较小的部分组成,因此可以在较低级别上工作。但这大致类似于将分子(如盐)分解成原子\xe2\x80\x94钠金属和氯\xe2\x80\x94,甚至是质子、中子和电子等亚原子粒子。一旦你像这样把它们分解了,它们就不再有用了,无论如何都不像盐那样有用。你不能用金属钠或氯来调味食物,尤其不能用中子。

\n

2有一些维护命令\xe2\x80\x94 git fsck,git gc特别是\xe2\x80\x94,它们只是查看存储库中的每个提交,并找出哪些提交连接到哪些其他提交,依此类推。这非常慢,因此这不是您在日常操作中使用 Git 的方式。在像 Linux 内核这样的更大的存储库中,或git checkout有时git log会花费几秒钟,但git fsck或git gc可能需要几分钟。其中一些取决于计算机及其文件系统等的速度,但对比非常明显:通过哈希 ID 查找提交速度很快,但以任何其他方式查找它通常非常慢。

\n
\n

提交的两部分是快照和元数据

\n

我们上面提到每个提交都有两个部分。这些都是:

\n
    \n
  • 主要数据,快照。在这里,Git 会始终保存3自您或其他人提交时每个文件的名称和内容的只读快照。这允许您\xe2\x80\x94或其他任何人\xe2\x80\x94取回截至该快照的所有这些文件。

    \n
  • \n
  • 元数据。在这里,Git 保存提交者的姓名和电子邮件地址。Git 会保存提交时的日期和时间戳。(Git 实际上每次提交都有两个名称、地址和时间字段,尽管大多数人通常只看一个。)Git 允许您添加一条描述\xe2\x80\x94a日志消息\xe2\x80\x94explaining如果你愿意的话,你为什么做出这个承诺。而且,对于 Git 本身来说很关键,这也是 Git 存储那些早期提交的哈希 ID 的地方。Git 保留了此类哈希 ID 的列表。大多数提交只有一个条目,它告诉 Git 该提交的父级是什么。

    \n
  • \n
\n

它是元数据中的父项,让 Git 向您显示一个提交\xe2\x80\x94,它是一个快照,而不是一组更改\xe2\x80\x94作为一组更改。如果我们连续两次提交:

\n
... <-F <-G ...\n
Run Code Online (Sandbox Code Playgroud)\n

我们从 F(父级)和G(子级)中取出快照并进行比较,相同的部分没有改变,不同的部分......好吧,比较这些会告诉你什么改变了。这就是 Git 显示的内容:更改。但为了获得这些更改,Git 需要两次提交才能获得两个快照。

\n
\n

3虽然任何提交的任何部分都不能更改,但并非所有提交都必须永远持续,因此说“永远”是夸大其词。给定提交的哈希 ID,如果 Git 可以找到该提交,则该提交就是该提交。这不是任何其他提交。它一定是您上次查看时具有该哈希 ID 的提交。换句话说,提交仍然存在,因此它没有改变,并且它的文件仍然和当时一样。

\n

但是,您可以让 Git删除提交。这并不容易:Git 的构建目的是在保留现有提交的同时添加新的提交,并且您使用的大多数日常命令都是以这种方式工作的。但是,您可以通过一些努力,使一些提交变得很难找到。一旦你这样做了,并且让它们在足够长的时间内无法找到(除非通过维护命令),Git 最终将确定它们一定是不需要的垃圾,并将它们真正扔掉。维护命令git gc尤其执行此操作。一旦发生这种情况,如果您将哈希 ID 保存在其他地方\xe2\x80\x94,例如将其写在白板上\xe2\x80\x94 并正确输入,Git 会说我没有任何带有该 ID 的内容。

\n

因为 Git 是为了添加提交而构建的,当两个 Git 连接并具有 Git 性别时,接收的 Git 通常非常愿意将所有发送的 Git 的新提交添加到自己身上,新的提交就像病毒一样传播。因此,仅仅因为您添加了一次提交,然后又撤回了一次,并不意味着它没有到达其他 Git。稍后可能会回复您:

\n
    \n
  • 不要害怕进行临时提交,但请记住,如果您让其他 Git 与您的 Git 对话,他们可能会复制您的临时提交,并稍后将它们呈现给您\xe2\x80\x94,所以要小心哪些存储库你让你的存储库与 Git 进行交互,或者小心让敏感数据进入你的临时提交,或者两者兼而有之。

    \n
  • \n
  • 另请注意,当您使用 时git push,您可以选择 Git 发送哪些提交到其他 Git,因此对您来说git push更安全\xe2\x80\x94 您选择发送哪些提交(包括临时提交)\xe2\x80\x94 比您选择发送 \xe2\x80\x94 更安全允许世界各地的所有用户读取您的存储库(从而读取您的所有临时提交)。

    \n
  • \n
\n

当然,接收 Git 时必须非常小心。这就是为什么像 GitHub 这样的托管网站提供访问控制(这不是直接内置于 Git 本身的东西,而是一个附加组件)。

\n
\n

合并是与多个父级的提交

\n

当我们有不同的工作时,例如:

\n
          I--J   <-- br1\n         /\n...--G--H\n         \\\n          K--L   <-- br2\n
Run Code Online (Sandbox Code Playgroud)\n

我们可能想将这两种不同的工作结合起来。这样,我们可以获得一个提交,添加某人添加的功能br1 和某人添加的功能br2。这就是git merge目的。

\n

现在,git merge作为命令,并不总是进行合并提交。我们需要仔细区分动词形式to merge,意思是组合工作,以及名词或形容词形式,a merge或merge commit,意思是通过工作组合产生的提交:

\n
    \n
  • 动词形式“合并”是git merge通常(或至少经常)所做的事情。

    \n
  • \n
  • 名词形式merge或其等效形容词merge commit是 Git 在完成合并工作后通常(或至少经常)所做的事情。

    \n
  • \n
\n

所以你可以看到它们密切相关,但不是同一件事。一是过程;另一个是结果。

\n

我们不会详细介绍该过程如何工作,但是当合并的结果是合并提交时,该合并提交就像任何其他提交一样,除了它不是只有一个父项,而是有两个或多个。(大多数合并提交恰好有两个父项;我将在后面的部分中讨论这个或更多部分。)请记住,所有提交都有其两个部分:快照和父项列表。合并提交的特殊之处在于它的列表有两个或多个父项。

\n

现在,任何新提交的第一个父级就是您开始的提交。你跑:

\n
git checkout br1\n
Run Code Online (Sandbox Code Playgroud)\n

然后你做一些事情来进行新的提交,最终,你运行git commit. Git 通过以下方式构建一个新的提交,并使用新的且唯一的哈希 ID:

\n
    \n
  • 保存每个文件当前形式的快照;4
  • \n
  • 收集元数据:您的姓名、电子邮件地址、当前日期和时间、日志消息等;
  • \n
  • 使用当前提交的哈希 ID作为新提交的父级,将这些全部写出;最后
  • \n
  • 将新提交的哈希 ID 写入当前分支名称。
  • \n
\n

这可能就是你获得 commit 的方式J,例如:你运行了git checkout br1,它提取了 commit I。然后您使用 进行了新的提交git commit。新提交的父级是 commit I,因此J指向回I,现在名称 br1选择了 commitJ而不是选择 commit I。

\n

然而,当您用于git merge进行新提交时,5 Git 不会写出单父提交并推进分支名称。这次,Git 写出多父提交。新提交的父级列表中的第一个父级与平常相同,但至少有一个额外的父级进入列表。

\n

在本例中,附加父级是您运行时选择的提交git merge:

\n
git checkout br1\ngit merge br2\n
Run Code Online (Sandbox Code Playgroud)\n

这会导致 Git 使用提交L作为其他提交。因此,在合并两个分支上的工作并生成适当的快照之后,Git 现在会进行新的合并提交 M,如下所示:

\n
          I--J\n         /    \\\xe2\x82\x81\n...--G--H      M   <-- br1 (HEAD)\n         \\    /\xc2\xb2\n          K--L   <-- br2\n
Run Code Online (Sandbox Code Playgroud)\n

这里(HEAD)表示我们正在“on”branch br1,因此新的提交M是branch的新提示br1。CommitM有两个父级,而不是通常的父级:第一个父级是 commit J,分支br1刚才指向的地方。第二个父级是 commit L。分支名称br2没有改变,所以它仍然指向 commit L。

\n

因为M指向L以及指向J,提交K-L现在都在分支上br1。 这就是为什么你git log向他们展示:他们存在并且在树枝上。Git通过转到 commit 来找到M它们,然后向后返回到提交J 和 L,从这两个返回到提交I 和 ,然后K从这两个返回到提交H。(当然,Git 必须小心地访问提交H一次,即使现在有两种方法可以到达那里。但这对于 Git 来说很容易做到。)

\n
\n

4快照是根据 Git索引中的文件副本创建的,而不是根据您可以查看和使用的文件创建的。这就是为什么 Git 让你git add如此频繁地运行。

\n

5如果合并出现合并冲突,则合并过程会中途停止并让您修复冲突。最终的git commitorgit merge --continue将完成合并并进行合并提交。为了实现这一点,在中间停止之前,在冲突的合并状态中间git merge写出这个特殊的。该命令检查此状态并完成合并,而不是进行普通的单父提交。git commit

\n
\n

章鱼合并

\n

既然您在某种程度上抱怨必须进行多次合并提交才能合并多个分支,那么是时候提及 Git 的octopus merge 了。假设我们有一个“主线分支”和两个或多个从中产生的功能,可能来自单个起点提交,也可能来自多个起点:

\n
       o--o--o   <-- feature1\n      /\n...--o--o--o   <-- main (HEAD)\n         \\\n          o--o   <-- feature2\n
Run Code Online (Sandbox Code Playgroud)\n

我们可以一次合并两个功能分支:

\n
       o--o--o   <-- feature1\n      /       \\\n...--o--o---o--M   <-- main (HEAD)\n         \\\n          o--o   <-- feature2\n
Run Code Online (Sandbox Code Playgroud)\n

进而:

\n
       o--o--o   <-- feature1\n      /       \\\n...--o--o---o--M--N   <-- main (HEAD)\n         \\       /\n          o-----o   <-- feature2\n
Run Code Online (Sandbox Code Playgroud)\n

这个方法没有什么问题。效果很好。 这里的主线分支main现在有两个双父合并提交M和N. 的第一个父级N是M; 的第一个父级M是主线上直接位于其左侧的提交。的第二个父级N显示如何feature2合并,而的第二个父级M显示如何feature1合并。

\n

Git在某些情况下提供了\xe2\x80\x94的能力,因为在进行这种合并时,没有好的方法来进行合并冲突解决,所以章鱼合并必须是无冲突的\xe2\x80\x94才能使用单个合并承诺得到这个结果:

\n
       o--o--o   <-- feature1\n      /       \\\n...--o--o--o---M   <-- main (HEAD)\n         \\    /\n          o--o   <-- feature2\n
Run Code Online (Sandbox Code Playgroud)\n

这里的提交M有三个父母,而不是只有两个。第一个父级像往常一样位于其左侧的正后方。feature1第二个和第三个父级是来自和的剩余两个分支提示提交feature2。

\n

我们通过运行得到这个:

\n
git checkout main\ngit merge feature1 feature2\n
Run Code Online (Sandbox Code Playgroud)\n

我们命名两个提交的事实git merge利用了-s octopus 合并策略,它尝试合并所有这些提交(使用章鱼样式合并基本算法),并且只有在没有冲突的情况下才进行合并。这意味着您可以使用两个常规的双亲合并进行一些合并,而无法使用三亲章鱼进行这些合并;但有些人喜欢章鱼合并,因为它们一次性将所有功能捆绑在一起,并表明不存在冲突(嗯,可能)。6

\n

请注意,章鱼合并仍然会导致将所有提交放在合并到的分支上(在本例中main)。当您运行时, Git 简单地跟踪合并的所有git log父级,以便您看到属于该分支的所有提交。

\n
\n

6由于 Git 是一组工具,而不是一个完整的解决方案,因此可以构建一个根本不实际使用的章鱼合并git merge,或者经过两次常规合并。但不要这样做。我们甚至不会考虑你如何做到这一点。

\n
\n

查看较少的提交

\n

git log一次一个地遍历提交,从提交向后移动到其父级。每当遇到合并提交时,它都可以选择向后移动到哪个提交。但它并不坚持向您显示每个提交,甚至不坚持以这种方式移动到每个可到达的提交。它只是默认显示每个提交。

\n

您可以限制您看到的提交,也可以限制首先访问的git log提交。如果您限制访问的提交集,您就会自动限制看到的提交,因此这是非常强大的。我们不会在这里查看所有血淋淋的细节,而只会查看一个非常有用且重要的选项:。--first-parent

\n

当我们使用 时--first-parent,我们是在告诉 Git:每当你到达一个合并提交时,假装这个合并提交只有一个父级,即它的第一个父级。 换句话说,完全忽略合并的提交,甚至不走这些路径。7 如果我们有:

\n
          I--J\n         /    \\\xe2\x82\x81\n...--G--H      M--N--O--P   <-- main (HEAD)\n         \\    /\xc2\xb2\n          K--L\n
Run Code Online (Sandbox Code Playgroud)\n

在点 处发生一些合并M,并且我们运行git log,我们将看到提交P、O、N、M、J、L、K、I、H、 等(其中 和 之间的提交按M某种H顺序发生)。8 但是如果我们运行:

\n
git log --first-parent\n
Run Code Online (Sandbox Code Playgroud)\n

步行将假装提交M只有一个父级 ,J并且我们将按顺序访问提交P、O、N、M、J、I、H、 等等。我们甚至从不查看提交K-L,因此我们永远不会看到它们。

\n
\n

7请注意,就像岔路口稍后会重新汇合一样,如果您反转路径\xe2\x80\x94从原始目的地沿着路回到原始起点\xe2\x80\x94现在的连接是什么一个叉子,以前的叉子现在变成了连接。因此,由于 Git 是向后工作的,合并实际上是事物分支的地方,而分支点是事物聚集在一起的地方。这真的取决于你如何看待它。

\n

8当合并git log在图形遍历中提供分叉时,提交的实际顺序来自您提供的排序选项。默认排序是首先显示最高提交日期。如果在进行所有提交时所有计算机时钟都是准确的,则这会按正确的顺序显示提交,但有时一台计算机的时钟关闭,并且提交可能会奇怪地混合在一起。在困难的情况下,考虑使用它git log --graph来帮助查看实际的提交图结构。

\n
\n

其他选项

\n

正如我在这个答案的顶部提到的,如果您不想要这些提交,那么您必须需要一些其他提交。当我说这些提交时,我所说的都是一般性的\xe2\x80\x94Git 存储提交,所以这就是你得到的\xe2\x80\x94 但也是具体的。如果您不想合并提交,请首先不要进行合并提交。(正如他们所说,“不开始任何事情,就不会成为任何事情”。)

\n

现在,这有一些巨大的缺点。如果您不进行合并提交,则无法保留您所做的实际原始工作。不过你确实有这样的选择。例如,当您运行时,git merge您可以使用。git merge --squash这告诉 Git 经历合并过程,但在最后进行普通的、非合并的单父提交。--no-commit(它也会无缘无故地打开。9)

\n

如果您确实使用此方法,请记住删除在合并操作之前找到提交的分支名称,因为这些提交现在对于执行它们的(单个)挤压合并来说是多余的。如果您允许这些提交稍后重新出现,它们可能会造成麻烦。这在很多方面与让临时或不正确的提交逃逸到其他 Git 存储库的病毒效应相同:Git 的构建是为了添加提交,而不是丢弃它们。但是,通过进行挤压合并(不会留下合并痕迹),您会在将来为自己设置一个陷阱,除非那些现在不需要的提交真的永远消失了。

\n

如果您有多个合并要做,并且每个合并都有一些冲突需要解决,您可以将它们作为正常(非挤压)合并或挤压合并来执行。结果将是多个提交:多个合并提交或多个普通单父提交。在执行上述任一操作后,您可以使用git reset --soft来使新的合并或不合并提交难以找到,然后使用 plaingit commit来创建一个新的、单一的、普通的提交,该提交与最终合并具有相同的快照。与 一样git merge --squash,您现在通常应该将合并的分支视为“死亡”,并且您应该摆脱这些提交并假装它们从未存在过,并希望它们永远不会回来困扰您。

\n

这并不是一件错误的事情,但它需要理解你在做什么。仅当您了解后果时才这样做。

\n
\n

9几乎可以肯定,隐含的内容-n只是原始 shell 脚本实现的剩余部分,在 Git 的行为中一直小心地保留着。这很烦人,因为如果你想要这种行为,你可以使用git merge -n --squash. 但现在这是多余的。

\n