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

Git multiple merge branches- how to avoid multiple commit while merging to master branch

我的 Git 存储库如下所示:

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

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 

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

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

TL;DR

您可能只想要 git log --first-parent

... But when I did merge it showed all the commits for Branch_1 & Branch_2 because of multiple merge in between.

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

这里要理解的是,最终,Git 都是关于提交的。 Commit是Git.1中的存储单位 Commit是你有的,你想要的。如果你不想要 这些 提交,你想要的必须是一些 other 提交。提交就是您所得到的,所以您 更好 想要提交。 (如果你想要别的东西,不要使用 Git。但许多其他版本控制系统也是 commit-oriented,所以你可能会发现你仍然会得到提交,所以你不妨坚持Git,除非……嗯,读下一段。)

B运行ch 名称,在 Git 中存在一个主要原因:find 提交。这是 Git 与其他版本控制系统不同的地方。在许多版本控制系统中,b运行ch 是提交的容器,您可以通过检查 b运行ches 来检查提交:b运行ch 中包含的提交集合是如果这是你的要求,你会看到一组提交。但这不是 b运行ch 名称在 Git.

中的工作方式

在 Git 中,一个提交可以——而且经常是——在很多,甚至 all,b运行ches 同时。那是因为 Git 的 b运行ch 名称不是容器。他们不 hold 提交。他们只是让你找到提交。每个名称找到 一个 提交。 提交本身 找到提交的 rest

每个 Git 提交都由两部分组成,稍后我们将对此进行描述。每个提交都通过其唯一的哈希 ID 找到。每个提交都有这些哈希 ID 之一;该哈希 ID 可以说是提交的“真名”。没有哈希 ID,Git 根本找不到提交。2 所以一个 b运行ch 名称包含一个哈希 ID,根据定义, last 包含在 b运行ch 中的提交。该提交又持有一组哈希 ID——通常只是一个——较早的提交,它们是也是 b运行ch[=490= 的一部分].

当我们有一个 b运行ch 名称,如 mainfeature,它包含一些哈希 ID,我们说 b运行ch 名称 指向最后一个,或提示,提交b运行ch:

            <-H   <--feature

但是提交 H——这里的 H 代表真正的哈希 ID,不管它是什么——具有某个早期提交 G 的哈希 ID。所以我们说 H 指向 G:

        <-G <-H   <--feature

但是提交 G 也向后指向 still-earlier 提交:

... <-F <-G <-H   <-- feature

等等,一直回到有史以来的第一次提交。从字面上看,这个 不能 向后指向较早的提交,所以它不会,这就是 Git 停止向后工作的地方。

所以,这就是提交 在 b运行ch 上的意思: 我们从 b运行ch 名称开始,它自动确定那个 b运行ch 上的 last 提交,然后向后工作。但如果是这样的话……好吧,假设我们有这样的事情,其中​​提交 I 指向回 H,并且提交 K 也指向 H

          I--J   <-- br1
         /
...--G--H
         \
          K--L   <-- br2

哪个 b运行ch 持有提交 H?

Git 的回答是提交 H 现在在 both b运行ches 同时time. 所有较早的提交也是如此。此外,即使 H 是某些 third b运行ch:

上的 last 提交
          I--J   <-- br1
         /
...--G--H   <-- main
         \
          K--L   <-- br2

依旧如此。提交 H 现在在 所有三个 b运行ches.

因此,在 Git 中,包含一些提交 组 b运行 是动态和流动的。重要的不是 b运行ch 名称,而是从提交到提交的连接。 b运行ch 名称很有用,但只是为了让您入门。其他一切都是关于提交


1因为提交是由更小的部分组成的,所以可以在较低的级别上工作。但这大致类似于将盐等分子分解成原子——金属钠和氯——甚至是质子、中子和电子等亚原子粒子。一旦你像这样分解它们,它们就不再 有用 了,不像盐那样有用。您不能用金属钠或氯调味食物,尤其不能用中子调味。

2有一些维护命令——特别是git fsckgit gc——简单地查看每个 在存储库中提交并找出哪些连接到其他连接等等。这是非常slw,所以这不是你在day-to-day操作中使用Git的方式。在像 Linux 内核这样更大的存储库中,git checkoutgit log 有时会花费几秒钟,但 git fsckgit gc 可能需要很多时间分钟。其中一些取决于您的计算机及其文件系统等的速度,但对比非常明显:通过哈希 ID 查找提交是 fast,但通过任何其他方式查找它通常非常慢。


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

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

  • 主要数据,一张快照。在这里,Git 永久保存3 每个文件的名称和内容的 read-only 快照,截至您或任何人提交时。这允许您——或其他任何人——取回该快照的所有这些文件。

  • 元数据。这里,Git 保存提交人的姓名和电子邮件地址。 Git 为 他们提交时保存 date-and-time-stamp。 (Git 实际上每个提交有两个 name-and-address-and-time 字段,在这里,尽管大多数人通常只看一个。)Git 允许您添加描述 - 日志消息—解释 为什么 你做出这个提交,如果你愿意的话。而且,Git 本身的键,这也是 Git 存储那些 earlier-commit 哈希 ID 的地方。 Git 保留此类哈希 ID 的列表。大多数提交只有一个条目,它告诉 Git 提交的 parent 是什么。

元数据中的 parent 让 Git 向您显示提交——这是一个快照,而不是一组更改——作为一组变化。如果我们连续两次提交:

... <-F <-G ...

然后我们从F(parent)和G(child)中取出快照和比较它们,相同的不改变,不相同的...好吧,比较它们会告诉你什么改变了。这就是 Git 显示的内容: 更改 。但是要获得这些更改,Git 需要 两次 提交,以获得两个快照。


3虽然任何提交的任何部分都不能更改,但并非所有提交都必须永远持续,所以说for all time 是言过其实了。给定提交的哈希 ID,如果 Git 可以找到该提交,则该提交就是 那个提交。这不是任何其他提交。它必须是您上次查看时具有该哈希 ID 的提交。换句话说,提交仍然存在,所以它没有改变,它的文件仍然是原来的样子。

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

因为 Git 是为 add 提交而构建的,当两个 Git 连接并拥有 Git-sex 时,接收 Git 通常非常愿意添加 all 发送 Git 的新提交给自己,新提交像病毒一样传播。因此,仅仅因为您添加了但随后撤回了提交,并不意味着它没有传给其他人 Git。稍后可能会回复您:

  • 不要害怕临时提交,但是记住,如果你让其他Git与你的[=691交谈=],他们可能会 复制 你的临时提交,并稍后将它们呈现给你——所以要么小心你让你的仓库有哪些仓库 Git-sex,要么小心关于让敏感数据进入您的临时提交,或两者兼而有之。

  • 还请注意,当您使用 git push 时, 选择您的 Git 发送给其他 Git,所以 git push 对你来说 更安全 ——你可以选择发送哪些提交,包括临时提交——而不是让所有用户都可以读取你的存储库(并且因此阅读你所有的临时提交)。

接收Gits,当然要小心了。这就是为什么像 GitHub 这样的托管网站提供访问控制(这不是直接内置到 Git 本身,而是 add-on)。


合并是一个以上的提交[=756=

当我们有分歧的工作时,例如:

          I--J   <-- br1
         /
...--G--H
         \
          K--L   <-- br2

我们可能想要合并这两个不同的工作线。这样,我们就可以获得一个提交,其中添加了某人在 br1 中添加的功能和 某人在 br2 中添加的功能。这就是 git merge 的意思。

现在,git merge,作为一个命令,并不总是进行 合并提交。我们需要仔细区分动词形式 to merge,意思是 to combine work,以及名词或形容词形式 合并合并提交,意思是由于执行work-combining:

而产生的提交
  • 合并的动词形式是git merge通常(或至少经常)使用的形式。

  • 名词形式 a merge 或其形容词等价物 a merge commit 是什么 Git 通常(或至少经常)在完成 合并 工作后生成。

所以你可以看出这些是密切相关的,但不是一回事。一个是过程;另一个是结果。

我们不会详细介绍该过程的工作原理,但是当合并的结果是 合并提交 时,合并提交就像任何其他提交一样,除了没有 单个 parent,它有 两个或更多 。 (大多数合并提交恰好有两个 parent;我将在后面的部分中介绍 或更多 部分。)请记住,所有提交都有两个部分:快照,和 list-of-parents。 合并提交的特别之处在于它的列表有两个或更多parents

现在,任何新提交的 第一个 parent 就是您开始的提交。你运行:

git checkout br1

然后你做一些事情来进行新的提交,最终,你 运行 git commit。 Git 构建一个 new 提交,具有新的唯一哈希 ID,作者:

  • 保存每个文件的当前形式的快照;4
  • 收集元数据:您的姓名、电子邮件地址、当前 date-and-time、您的日志消息等;
  • 将这些全部写出来,使用当前提交的哈希ID作为新提交的parent;最后
  • 将新提交的哈希 ID 写入 当前 b运行ch 名称

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

然而,当您使用 git merge 进行新提交时,5 Git 不会写出 single-parent 提交并推进 b运行ch 名称。这次,Git 写出 multi-parent 提交。新提交的 parent 列表中的 first parent 与往常相同,但至少有一个额外的 parent 进入列表。

额外的 parent,在这种情况下,是您在 运行 git merge:

时选择的提交
git checkout br1
git merge br2

这导致 Git 使用提交 L 作为另一个提交。所以,在合并两个 b运行ches 的工作并得到一个合适的快照之后,Git 现在使新的 merge commit M 像这个:

          I--J
         /    \₁
...--G--H      M   <-- br1 (HEAD)
         \    /²
          K--L   <-- br2

这里的(HEAD)表示我们“在”b运行chbr1,所以新提交M就是b[=的新提示782=]ch br1。 Commit Mtwo parents 而不是通常的:第一个 parent 是 commit J,其中 b运行ch br1 刚才点过。 第二个 parent 是提交 L。 b运行ch name br2没有变,所以还是指向commit L.

因为 M 指向 L 以及 J,提交 K-L 现在在 b运行ch br1. 这就是您的 git log 显示它们的原因:它们存在并且在 b运行ch 上。 Git 通过提交 M 找到 它们,然后返回到 both 提交 J andL,从这两个,到两个提交IandK,从这两个,提交 H。 (当然,Git 必须小心访问提交 H 一次,即使现在有两种方法可以到达那里。但这对 Git 来说很容易做到。)


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

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


章鱼合并

由于您在某种程度上抱怨必须进行多次合并提交才能合并多个 b运行ch,因此是时候提及 Git 的 章鱼合并。假设我们有一个“主线 b运行ch”和两个或多个 spring 来自它的特性,可能来自单个起点提交,也可能来自多个起点:

       o--o--o   <-- feature1
      /
...--o--o--o   <-- main (HEAD)
         \
          o--o   <-- feature2

我们可以一次合并两个特征 b运行ches:

       o--o--o   <-- feature1
      /       \
...--o--o---o--M   <-- main (HEAD)
         \
          o--o   <-- feature2

然后:

       o--o--o   <-- feature1
      /       \
...--o--o---o--M--N   <-- main (HEAD)
         \       /
          o-----o   <-- feature2

这个方法没有问题。它工作正常。 主线 b运行ch,main 现在有两个 two-parent 合并提交 MNN的第一个parent是MM 的第一个 parent 是直接在主线上左侧的提交。 Nsecond parent 显示了 feature2 是如何合并的,second parent M 显示了 feature1 是如何合并的。

Git 提供了能力——在某些情况下,因为在进行这种合并时,没有很好的方法来解决合并冲突,所以章鱼合并必须是 conflict-free——使用单个合并提交以获得此结果:

       o--o--o   <-- feature1
      /       \
...--o--o--o---M   <-- main (HEAD)
         \    /
          o--o   <-- feature2

Commit M 这里有 三个 parent 而不是只有两个。第一个 parent 像往常一样在它的正后方左侧。第二个和第三个 parent 是来自 feature1feature2.

的剩余两个 branch-tip 提交

我们通过 运行ning 得到这个:

git checkout main
git merge feature1 feature2

我们命名两个提交的事实使得 git merge 使用 -s octopus 合并策略 ,它试图合并所有这些提交(使用章鱼样式merge base algorithm)并且只有在没有冲突的情况下才进行合并。这意味着有些合并你可以用两个常规two-parent合并你不能用three-parent章鱼做;但有些人喜欢章鱼合并,因为它们一次将所有特征结合在一起,表明没有冲突(嗯,可能)。6

请注意,章鱼合并 still 导致将 all 提交放在 merged-into b运行 ch(在本例中为 main)。 Git 简单地跟随 all parents 的合并,当你 运行 git log 时,你会看到 所有属于 b运行ch.

的提交

6因为Git是一套工具,而不是一个完整的解决方案,所以可以构建一个实际上不使用[=57的章鱼合并=] ,或者经历了两次常规合并。但是不要那样做。我们甚至不会看如何可以做到这一点。


查看更少的提交

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

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

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

          I--J
         /    \₁
...--G--H      M--N--O--P   <-- main (HEAD)
         \    /²
          K--L

在点 M 处发生了一些合并,我们 运行 git log,我们将看到提交 PONMJLKIH 等(MH 按某种顺序发生)。8 但是如果我们 运行:

git log --first-parent

walk 会假装提交 M 只有一个 parent、J,我们将访问提交 PONMJIH,依此类推。我们甚至从未 提交 K-L,所以我们从未看到它们。


7请注意,就像岔路口以后会重新汇合一样,如果你改变方向——沿着这条路从你原来的目的地回到你原来的站要点——join现在是fork,以前的fork现在是join。因此,由于 Git 向后工作,merges 实际上是 b运行ch 和 b运行ch points 是事情走到一起的地方。这真的完全取决于你如何看待它。

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


其他选项

正如我在这个答案的顶部提到的,如果你不想要 这些 提交,你必须想要 一些其他 提交.当我说 这些提交 时,我说的都是一般性的——Git 存储提交,所以这就是你得到的全部——但也是具体的。如果您不想 merge 提交,请不要​​首先 make 合并提交。 (“不开始 none,不会 none”,正如他们所说。)

现在,这有一些巨大的缺点。如果您不进行 merge 提交,您将无法保留您所做的实际原始工作。不过你确实有这个选择。例如,当您 运行 git merge 时,您可以使用 git merge --squash。这告诉 Git 通过合并 过程 ,但是要进行普通的 non-merge 单个 parent 提交 最后。 (它也无缘无故地打开 --no-commit9

如果您确实使用此方法,请记住删除在合并操作之前找到提交的b运行ch名称 因为这些提交现在与执行它们的(单个)squash-merge 是多余的。如果您允许这些提交稍后重新出现在视图中,它们很可能会造成麻烦。在许多方面,这与让临时或不正确的提交逃逸到其他一些 Git 存储库的那种病毒效应是相同的问题:Git 被构建为 add提交,而不是丢弃它们。但是通过做一个不留合并痕迹的squash-merge,你在以后给自己设了一个陷阱,除非那些now-unwanted提交真的永远消失了。

如果您有多个合并要执行,并且每个合并都有一些冲突需要解决,您可以像正常 (non-squash) 合并或挤压合并那样执行它们。结果将是多次提交:多次合并提交,或多次普通 single-parent 提交。您可以在执行其中任一操作后,然后使用 git reset --soft 使新的 merge-or-not-merge 提交 难以找到 ,然后使用普通的 git commit制作一个新的、单一的、普通的提交,它与最终合并具有相同的 snapshot。与 git merge --squash 一样,您现在通常应该认为合并的 b运行ches“已死”,您应该摆脱这些提交并假装它们从未存在过,希望它们永远不会回来困扰您。

这样做并没有错,但需要了解自己在做什么。明白后果才去做。


9隐含的 -n 几乎可以肯定只是原始 shell 脚本实现的遗留物,在 [=691= 中一直小心保存]的行为。这很烦人,因为如果您想要 这种行为,您可以 使用git merge -n --squash。不过现在这是多余的。

在一次提交中压缩所有内容:调用 git reset --soft 然后调用 git commit :

# from Branch_2 :
git reset --soft master
git commit