繁体   English   中英

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

[英]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 的所有提交,因为它们之间有多次合并。 谁能建议在这种情况下如何在将我的代码合并到主分支之前进行一次提交?

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;博士

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

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

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

这里要理解的是,最后,Git都是关于 commits的。 提交是 Git 中的存储单元。 1提交是您所拥有的,也是您想要的。 如果您不想要这些提交,那么您想要的必须是其他一些提交。 提交就是你得到的全部,所以你最好想要提交。 (如果你想要别的东西,不要使用 Git。但是许多其他版本控制系统也是面向提交的,所以你可能会发现你仍然得到提交,所以你最好坚持使用 Git,除非......好吧,阅读下一段。)

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

在 Git 中,提交可以——而且经常是——同时出现在许多甚至所有分支 那是因为 Git 的分支名称不是容器。 他们不持有提交。 他们只是让你找到提交。 每个名称都会找到一个提交。 提交本身找到提交的rest

每个 Git 提交由两部分组成,我们将在后面介绍。 每个提交都通过其唯一的 hash ID找到 每个提交都有这些 hash ID 之一; hash ID 是提交的“真实姓名”。 如果没有 hash ID,Git 根本找不到提交。 2因此,分支名称包含一个 hash ID,根据定义,它是该分支中包含的最后一次提交。 该提交又包含一组 hash ID(通常只是一个),这些 ID 也是早期提交的一部分,它们也是分支的一部分

当我们有一个分支名称,如mainfeature ,它包含一些 hash ID,我们说分支名称指向分支的最后一个或tip提交:

            <-H   <--feature

但是这里的提交H - H代表真正的 hash ID,不管它是什么——具有一些早期提交G的 hash ID。 所以我们说H指向G

        <-G <-H   <--feature

但是提交G还指向更早的提交:

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

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

所以,这就是提交在分支上的含义:我们从分支名称开始,它会自动确定该分支上的最后一次提交,然后向后工作。 但如果是这样的话......好吧,假设我们有这样的事情,其中提交I指向H ,并且提交K也指向H

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

哪个分支持有提交H

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

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

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

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


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

2有一些维护命令——特别是git fsckgit gc只需查看存储库中的每个提交,并找出哪些提交连接到其他提交等等。 这很慢,所以这不是您在日常操作中使用 Git 的方式。 In a bigger repository like the Linux kernel, a git checkout or git log will take up to a few seconds sometimes, but a git fsck or git gc could take many minutes. 其中一些取决于您的计算机及其文件系统的速度等,但对比非常明显:通过 hash ID 查找提交很快,但以任何其他方式查找它通常非常慢。


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

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

  • 主要数据,快照 在这里,Git 一直保存3每个文件名称和内容的只读快照,该快照是您或任何人进行提交时的时间。 这使您(或其他任何人)可以取回该快照的所有这些文件。

  • 元数据 在这里,Git 保存了提交人的姓名和 email 地址。 Git 保存了他们提交的日期和时间戳。 Git实际上每次提交都有两个名称和地址和时间字段,尽管大多数人通常只看一个。)喜欢。 而且,Git 本身的关键,这也是 Git 存储那些早先提交的 hash ID 的地方。 Git 保留了此类 hash ID 的列表。 大多数提交只有一个条目,它告诉 Git 提交的级是什么。

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

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

我们F (父)和G (子)中取出快照并比较它们,相同的部分没有改变,而不同的部分......嗯,比较它们会告诉你发生了什么变化. 这就是 Git 显示的内容:更改 但要获得这些更改,Git 需要两次提交,以获得两个快照。


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

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

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

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

  • Note, too, that when you use git push , you choose which commits your Git sends to some other Git, so git push is safer for you —you choose which commits, including temporary ones, you send—than if you allow all users everywhere读取您的存储库(并因此读取所有临时提交)。

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


合并是与多个父级的提交

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

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

我们可能希望两条不同的工作线结合起来。 这样,我们可以得到一个提交,添加某人在br1中添加的特性某人在br2中添加的特性。 这就是git merge的目的。

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

  • 动词形式,即 merge ,是git merge通常(或至少经常)所做的。

  • 名词形式, a merge ,或其对应的形容词, a merge commit ,是 Git 通常(或至少经常)在完成合并工作后所做的。

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

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

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

git checkout br1

然后你做一些事情来做一个新的提交,最终你运行git commit Git 使用新的唯一 hash ID 构建的提交,通过:

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

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

但是,当您使用git merge进行新提交时, 5 Git 不会写出父提交并推进分支名称。 这一次,Git 写出一个父提交。 新提交的父级列表中的第一个父级与往常一样,但至少有一个额外的父级进入列表。

在这种情况下,附加父级是您在运行git merge时选择的提交:

git checkout br1
git merge br2

这会导致 Git 使用提交L作为另一个提交。 因此,在合并两个分支上的工作并提出适当的快照后,Git 现在像这样进行新的合并提交M

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

这里的(HEAD)表示我们“在”分支br1 ,因此新提交M是分支br1的新提示。 提交M两个父节点,而不是通常的父节点:第一个父节点是提交J ,其中分支br1曾经指向刚才。 第二个父母是提交L 分支名称br2没有改变,所以它仍然指向提交L

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


4快照是由 Git索引中的文件副本制作的,而不是您可以查看和使用的文件。 这就是为什么 Git 让你经常运行git add

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


章鱼合并

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

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

我们可以一次合并两个特征分支:

       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

这种方法没有任何问题。 它工作正常。 主线分支,这里的main ,现在有两个双父合并提交MN N的第一个父节点是M M的第一个父级是直接在其左侧的提交,在主线上。 N第二个父节点显示了feature2是如何合并的, M第二个父节点显示了feature1是如何合并的。

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

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

这里的提交M三个父母,而不是只有两个。 像往常一样,第一个父母就在它的左边。 第二个和第三个父母是来自feature1feature2的剩余两个分支提示提交。

我们通过运行得到这个:

git checkout main
git merge feature1 feature2

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

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


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


查看更少的提交

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

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

当我们使用--first-parent时,我们是在告诉 Git:每当你到达一个合并提交时,假设这个合并提交只有一个父级,即它的第一个父级。 换句话说,完全忽略合并的提交,甚至不要沿着这些路径走。 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只有一个父节点J ,我们将按顺序访问提交PONMJIH等等。 我们甚至从不查看提交KL ,因此我们从未见过它们。


7请注意,就像在稍后重新连接的道路上的分叉一样,如果您反转您的路径 - 从原来的目的地沿着道路回到原来的起点 - 什么连接现在是一个分叉,而什么是一个分叉现在是加入。 因此,由于 Git 向后工作,合并实际上是事物分支的地方,而分支点是事物聚集的地方。 这真的取决于你如何看待它。

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


其他选项

正如我在此答案顶部提到的那样,如果您不想要这些提交,则必须要进行其他一些提交。 当我说这些提交时,我既是泛泛地说——Git 存储提交,这就是你得到的全部——但也是具体的。 如果您不想要合并提交,请不要首先进行合并提交。 (正如他们所说,“不要开始,就不会没有”。)

现在,这有一些巨大的缺点 如果您不进行合并提交,则无法保留您所做的实际原始工作。 不过你确实有这个选择。 例如,当您运行git merge时,您可以使用git merge --squash 这通过合并过程告诉 Git 到 go ,但要在最后进行普通的非合并单亲提交 (它也打开--no-commit ,没有充分的理由。 9

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

如果您有多个合并要做,并且每个合并都会有一些冲突需要解决,您可以将它们作为正常(非壁球)合并或壁球合并进行。 结果将是多个提交:多个合并提交,或多个普通的单父提交。 您可以在完成其中任何一项之后,然后使用git reset --soft使新的合并或不合并提交难以找到,然后使用普通的git commit进行新的单一普通提交与最终合并相同的快照 git merge --squash ,您现在通常应该认为合并的分支“已死”,您应该摆脱这些提交并假装它们从未存在并希望它们永远不会回来困扰您。

这不是一件错误的事情,但它需要了解你在做什么。 只有在您了解后果的情况下才这样做。


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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM