如何使合并时使用默认值? [英] How to make --squash default on a merge?

查看:106
本文介绍了如何使合并时使用默认值?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我们使用独立的分支进行非平凡的错误修复和功能。通过执行频繁的 git checkout< x>将分支保持与主控同步; git merge master



我注意到合并时,git会用多个不相关的消息来污染日志文件。例如,git不是将一个Merge< X>转换为Master或Merge Master into< X>,而是将添加所有提交消息。它在Master上是一个治理问题(处理沙过程),因为在开发过程中分支中可能存在的bug是 not ,并且 不是 在Master分支中存在。

更糟糕的是,行为在分支和主人之间是不同的。将主人合并到分支时,会生成类似于将主人合并到< X>的日志条目。但是,在将分支合并到Master中时,不存在合并到主的< X> ;.根据日志,它好像开发分支从来没有存在过,合并也从未发生。



我学会了我必须做一些特殊的事情才能让git按照预期行事;即如何使用git merge --squash?(它的经典git modus operandi ):将一些简单的东西)

我的问题是,如何在合并期间使 - squash 为默认操作

解决方案

(注意:我是通过关注最近的一个关于你的问题的链接来到这里的。你可能还在乎这个,但是不妨回答一下。)



默认情况下,你不能让Git为所有的 / em>分支,但是您可以 使其默认为某些分支进行压缩合并。由于您特别有兴趣只为 master 做这件事,这可能正是您想要的。



让我们做一个快速的 1 回顾 真正的作用,因为在通常的Git中,Git使所有事情复杂化。而且,这:


我们使用单独的分支来进行不重要的错误修复和功能。通过执行频繁的 git checkout< x>,分支与主人保持同步。 git merge master


与许多人认为是正确的工作流程相反在Git中。我对Git的工作流是否可以被称为正确有一些疑问:-),但有些比其他人更成功,这绝对是其中一个更成功的。 (我认为它可以很好地工作,正如在下面的扩展讨论中指出的那样)。




1 sup>好吧,我试着把它缩短。 :-)随意浏览,虽然这里有一堆重要的材料。如果TL; DR,直接跳到最后。



提交图



众所周知,但其他人可能不会在Git中由提交图来控制。每个 1 提交都有一些父提交,或者在合并提交的情况下,有两个或多个父代。为了做一个新的提交,我们得到了一些分支:

  $ git checkout funkybranch 

并在工作树中执行一些工作, git add 一些文件,最后 git commit 结果分支 funkybranch

  ...工作工作... 
$ git commit -m'做一件事'



current 提交是名称 funkybranch 指向的(单一)提交。通过阅读 HEAD HEAD ,Git通常会包含分支的名称,并且该分支包含原始SHA- 1提交的哈希ID



为了进行新的提交,Git从我们所在的分支中读取当前提交的ID,保存索引/区域写入存储库, 2 用新当前提交的ID作为新提交的父进程写新提交,最后将 new 提交的ID写入分支信息文件。

这是分支如何增长的:从一次提交开始,我们创建一个新分支,然后移动分支名称以指向新的提交。当我们把它作为一个线性链时,我们得到了一个很好的线性历史:

  ...<  -  C<  - D < -  E < -  funkybranch 

提交 E (实际上可能是 e35d9f ... 或其他)是当前的提交。它指向 D ,因为 D 当前是我们在电子; D 指向 C ,因为 C 点;等等。



当我们使用 git checkout -b ,我们所做的只是告诉Git创建一个新的名称,指向一些现有的提交。通常这只是当前的提交。因此,如果我们在 funkybranch funkybranch 点提交 E 和我们运行:

  git checkout newbranch 

然后我们得到这个:

  ...<  -  C<  - D < -  E < -  funkybranch,newbranch 

即两个名称都指向commit 电子。因为 HEAD 表示 newbranch ,所以Git知道我们在 newbranch C>。我也想在这种绘画中加入:

  ...<  -  C < -  D < - E  -  funky branch,HEAD  - > newbranch 

我也喜欢以更紧凑的方式绘制我的图形。我们知道提交总是指向他们的父母及时向后,因为在提交 D 之前,不可能做出新的提交 E / code>。所以这些箭头总是指向左方,我们可以画出一两个破折号:

  ...-- C- D- -E < -  funkybranch,HEAD  - > newbranch 

(然后如果我们不需要知道哪个是哪个,我们可以绘制每一个节点都有一个 o 节点,但现在我会坚持使用单个大写字母)。



如果我们现在做一个新的提交 - 提交 F -it导致 newbranch 前进(因为我们可以从 HEAD ,我们在 newbranch )。所以让我们来画一下:

  ...-- C  -  D  -  E < -  funkybranch 
\
F< - HEAD - > newbranch

现在再来看看 git checkout funkybranch 并在那里做一些工作并提交它,使得新提交 G

  ...  -  C  -  D  -  E  -  G < -  HEAD  - > funkybranch 
\
F < - newbranch

(和 HEAD 现在指向 funkybranch )。现在我们有一些我们可以合并的东西。




1 那么除了 root提交。在大多数Git仓库中,只有一个根提交,这是第一次提交。显然,它不能有一个父提交,因为每个新提交的父对象是我们进行新提交时提交的最新对象。完全没有提交,当我们进行第一次提交时,目前还没有提交。所以它成为一个根提交,然后所有的后来提交都是它的子代,孙代,曾祖孙等。



大多数保存工作实际上发生在每个 git add 。索引/分段区域包含哈希ID,而不是实际的文件内容:当您运行 git add 时,文件内容将被保存。这是因为Git的图不仅仅是提交对象,还有存储库中的每个对象。这是Git与Mercurial相比速度如此之快的一部分(它在提交时将文件保存起来而不是增加时间)。幸运的是,与提交图本身不同,用户不需要知道或关心的内容。


$ b

Git合并



像以前一样,我们必须在某个分支上。 1 我们在 funkybranch 上,所以我们都很好:

  $ git merge newbranch 

在这一点上,大多数人似乎认为魔术会发生。但它根本不是魔术。 Git现在在我们当前的提交和我们命名的那个之间找到了 merge base ,然后运行两个 git diff 命令。
$ b

合并基础只是 2 两个分支上的第一个共同提交 - 第一个提交在两个分支上。我们在 funkybranch ,它指向 G 。我们给了Git分支 name newbranch ,它指向提交 F 。所以我们合并了提交 G F ,并且Git遵循它们的父指针,直到达到提交节点这是在两个分支。在这种情况下,这是提交 E :提交 E 是合并基础。



现在Git运行这两个 git diff 命令。我们将合并基础与当前提交进行比较: git diff< id-of-E> < ID-的-g取代; 。第二个差异比较合并基础和另一个提交: git diff< id-of-E> < id-of-f>



最后,Git试图结合这两组更改,将结果写入我们当前的工作-树。如果更改似乎独立,那么Git将采用它们两个。如果他们似乎发生冲突,Git将以合并冲突结束并让我们清理它。如果它们看起来是相同的,那么Git只需要一次更改。



所有这些看起来的东西都是在一个纯粹的文本基础。 Git不了解代码。它只是看到诸如删除一行阅读 ++ x; 和添加一行阅读 y * = 2;

最后,假设一切顺利,并且合并不会因为冲突而停止,Git会提交一个新的提交,新的提交是一个合并提交,它意味着它有两个 3 父母,第一个父亲 - 顺序很重要 - 就是当前提交,就像正常的非合并提交一样。第二个父母是另一个提交,一旦提交被安全地写入到存储库,Git像往常一样将新提交的ID写入分支名称中。因此,假设合并工作,我们得到这个:

  ...  -  C  -  D  -  E  -  G  -  H  -   -  HEAD  - > ; funkybranch 
\ /
F < - newbranch

请注意, newbranch 尚未移动:它仍指向提交 F HEAD 并没有改变:它仍然包含名称 funkybranch 。只有 funkybranch 已经改变:它现在指向新的合并提交 H ,并且 H 指向 G ,并且也指向 F






1 Git对此有点分裂。如果我们 git checkout 一个原始SHA-1或其他不是分支 name 的东西,它会进入一个状态,它会调用detached HEAD 。在内部,这通过将SHA-1散列直接放入 HEAD 文件中,以使 HEAD 给出提交ID ,而不是分支的名称。但是,Git做其他事情的方式使得它的工作方式就好像我们在一个特殊的分支上,它的名字就是空字符串。它是(单个)匿名分支 - 或者等价地,它是名为 HEAD 的分支。所以从某种意义上说,我们总是在一个分支上:即使Git说我们不在任何分支上,Git也会说我们在特殊的匿名分支上。



这引起了很多混淆,如果它不被允许,它可能更明智,但Git在 git rebase ,所以它实际上非常重要。如果rebase出问题了,这个细节就会泄漏出去,结果你必须知道HEAD是什么意思,而且是。

2 我故意忽略这里的一个硬性案例,这发生在有多个可能的合并基础提交时。 Mercurial和Git在这里使用不同的解决方案:Mercurial在随机选择一个(似乎是),而Git为您提供选项。这些情况很少发生,理想情况下,即使发生这种情况,Mercurial的简单方法仍然有效。

3 两个或更多,真的: Git支持章鱼合并的概念。但是没有必要去那里。 : - )

合并将图形从树中更改为DAG



合并 - 真合并:提交两个或两个以上的父母 - 有一些重要的,偶然的副作用。主要的一点是,合并的存在会导致提交图数据结构从更改,其中分支只是自行分离并增长到DAG中:一个有向无环图。 / p>

当Git对图进行操作时,就像操作这么多操作一样,它通常会返回全部路径。由于合并有两个父母,所以 git log 会显示父提交。因此,这被认为是一个功能:


例如,而不是一个单一的融入主或合并主入,git会添加所有提交消息。

Git跟踪并记录原始提交序列 - 提交 H G E D F E 等等 - >, D ,依此类推。当然,它只显示每次提交一次;并且默认情况下,它们通过它们的日期戳对这些提交进行排序,如果每个提交的日期重叠都有很多提交,那么它们混合在一起。



如果你不想看到通过合并的另一端进行的提交,Git有办法做到这一点: - first-parent 会告诉每个Git命令走过图 1 以仅跟随每个合并的父第一个。另一方仍然存在于图表中,它仍然影响Git如何计算合并基数,但 git log --first-parent 不会显示






1 这是相当多的Git命令。它们使用或 git log 本身, git rev-list ,这是Git的通用图形漫步程序。这段代码是推动,获取,平分,记录,指责,重组以及其他许多方面的核心。它的文档有一组令人眼花缭乱的选项。要知道作为一个临时用户的关键是 - first-parent (这里只讨论); - 不散步(抑制图形完全走路); - 祖先路径(简化了源码树相关工作的历史记录); - 简化装饰(简化 git log 输出)的历史记录; - 分行 - 远程和 - 标记(通过分支,远程或标签名选择图形走路的起点); - 合并 - 不合并(包含或排除合并提交); - 自 - 直到(限制按日期范围提交);和基本 .. ... (二点和三点)图形子集操作。



合并的好处



合并到位意味着分支上的开发可以继续分支和后面的 git merge 找到一个更新的 - 因此更简单的合并基础。考虑一下这个图,其中只有几个提交有单字母名称:

  o  -  o  -  o  -  o- -H  -  o  -  o  -  I<  -  feature2 
/ \\
A - o - B - C ----- D - E- ---- F - G< - master
\\ / / /
o - o - J - o - o - K - o - o - L < - feature1

这里,除了两个早于 master 在root提交 A 之后,所有的开发都发生在分支 feature1 特征2 。提交 C D E F G 全部合并(在这种情况下,严格来说, master ),当它准备就绪时,将特性工作带入 master 中。

请注意,当我们提交 C master 上,我们做了:

  $ git checkout master; git merge feature1 

找到 A 作为合并基础和 B J 作为两个提示合并提交。当我们做出 D

  $ git checkout master; git merge feature2 

我们有 A 作为合并基础和 C H 作为两个提示提交。到目前为止,这没什么特别的。但是当我们做出 E 的时候,我们到目前为止已经有这么多了(最终的 o s,甚至<$ c $对于 feature2 可能会或可能不存在 - 它们不起作用):

  o  -  o  -  o  -  o  -  H  -  o  -  o < -  feature2 
/ \
A - o - B --- C ----- D < - 主
\ /
o - o - J - o - o - K < ; - feature1

master feature1 分支上的第一个提交,它是提交 J ,这是我们合并的 C 。所以为了做到这一点,Git比较了 J vs D - 我们从 feature2 - 和 J vs K 代码(只有新代码)在 feature1 上。如果一切顺利,或者一旦我们解决了合并冲突,这会使提交 E ,现在我们有:

<$ p $ < - c $ c> o - o - o - o - H - o - o - I < - feature2
/ \
A - o - B --- C ----- D -E < - 主
\ / $
o - o - J - o - o - K- -o -o < - feature1

当我们去合并 feature2 再次。这次合并基础是commit H :直接从 feature2 后退 H ,并从 E 移动到 D ,然后达到 H从 master > H 。因此,现在Git比较了 H E ,这是我们从 feature1 code>和 H vs I ,这是 new 添加到 feature2 ,并合并这些。



合并的缺点



树具有一些非常好的图论性质,例如保证一个简单的合并基础。任意DAG可能会失去这些属性。特别是,合并两种方式 - 将 master 合并到分支 合并分支转换为 master -results incriss cross merges,可以给你多个合并基础。



合并还使图( git log )非常难以遵循。使用 - first-parent - simplified-by-decoration 有帮助,特别是如果您练习良好的合并,但这些图自然变得混乱。



壁球合并



壁球合并避免了问题,但是通过支付一个相当沉重的代价:它们根本就不合并。 (很快,我们将看到如何处理这个问题。)



运行 git merge --squash ,Git在寻找合并基础方面经历了与之前相同的运动,并且做出了两个差异:merge-base vs current-commit,merge-base vs other-commit。然后它以与正常提交完全相同的方式结合所做的更改。但是,它会提交一个普通的提交。 1 新提交只有一个父进程,取自当前分支。



让我们看看在执行相同序列时使用 feature1 feature2

  o  -  o  -  o < -  feature2 
/
A - o - B < - master
\
o - o - J < - feature1

我们做 git checkout master; git merge --squash feature1 来创建新的提交 C 。 Git比较 A vs B 来看看我们对 master 和 A vs J 来查看他们(我们)在 feature1 。 Git结合了这些变化,我们获得提交 C ,但只有一个父代:

  o  -  o  -  o < -  feature2 
/
A - o - B - C < - master
\
o - o - J < - feature1

现在我们将 D 作为来自 feature2 的壁球:

  o  -  o  -  o  -  o  -  H<  -  feature2 
/
A - o - B - C< - master
\
o - o - J - o - o < - feature1

Git比较 A vs C A vs H ,与上次相同。我们现在得到 D 。到目前为止,它几乎是一样的,除了分支没有重新加入的地方。但现在是时候让 E

  o  -  o -  o  -  o  -  H  -  o  -  o<  -  feature2 
/
A - o - B - C - - D < - master

o - o - J - o - o - K < - feature1

我们运行 git checkout master; git merge --squash feature1 和以前一样。



上次,Git比较 J -vs - D J -vs - K ,作为提交 J 是我们的合并基础。



这一次,提交 A (仍然)是我们的合并基础。 Git比较 A vs D A vs ķ。如果我们上次在 C 上解决了冲突,则可能需要再次解决它们。这很糟糕 - 但我们还没有流失。






这些新的提交,标记为 * ,是从 c> feature1 特征2 。我们进行了压缩合并 C ,以从 A J 。因此,我们然后真正地合并到 feature1 中,最好使用直接从 master 1 (它也有 o - B - 的好东西)。 (我们还在 feature2 上制作了 * ,就像做了 D master 上引入 A H 。就像 * feature1 上一样,我们可能只希望源代码树从<$ c $ )



现在我们已经准备好从 feature1 ,我们可以做另一个(压缩)合并。 master feature1 的合并基础是commit C ,这两个提示是 D K ,这正是我们想要的。 Git的合并代码会得出一个相当接近的结果;我们修复任何冲突,测试,修复任何破坏,并提交;然后我们从 master 返回 feature1 进行另一个准备工作merge code>和以前一样。



这个工作流程比merge into master更复杂一点,但应该给出了很好的结果。






1 Git并没有使这个完全不重要:我们想要合并有一个 -s他们的策略,Git根本就没有这个策略。 是一种使用管道命令获得理想效果的简单方法,但我会将这个答案留在这个已经疯狂的答案中。



所以,如果这一切都有效,那机制怎么样?



注意我们要的是 merge --squash 时将合并到master中,但是当从 合并时定期(非squash)合并。换句话说:

  $ git checkout master&& git merge foo 

应该使用 - squash ,但是:

  $ git checkout foo&& git merge master 

should not use - 壁球。 (从上一节的脚注中复制树可能很好,但应该是不必要的:合并结果基本上总是直接出自 master 之外的树) p>

git merge 运行时,它会查看当前分支(因为它总是必须的)。如果该分支有一个名称 - 如果我们不处于分离HEAD模式 - Git然后查看您的配置,存储在分支下的值。 branch .mergeOptions 。这里的任何字符串值都被扫描,就像它是 git merge 命令的一部分一样。

因此:

  $ git config branch.master.mergeOptions--squash

(这些引号在技术上并不是必需的,您可以在 git config之后添加 - global ,在 branch.master.mergeOptions 之前)设置你的当前版本库以进行压缩合并到 master 。 (使用 - global ,它将此设置为所有存储库的个人默认值。任何 branch.master.mergeOptions set但是在一个特定的仓库中将覆盖这些全局的仓库。)


We use separate branches for non-trivial bug fixes and features. The branchhes are kept in-sync with master by performing frequent git checkout <x>; git merge master.

I noticed when merging, git pollutes the log files with multiple, non-relevant messages. For example, rather than a single "Merge <X> into Master" or "Merge Master into <X>", git will add all the commit messages. Its a problem with governance (processes sand procedures) on Master because the bugs that may have been present in a branch during development are not and were not ever present in the Master branch.

Worse, the behaviors are different between branches and master. When merging master into branches, there is a log entry generated similar to "Merge Master into <X>". However, when merging a branch into Master, there is no "Merge <X> into Master". According to the logs, its as if the development branch never existed and the merge never occurred.

I learned I had to do something special to make git behave as expected; namely How to use git merge --squash? (Its classic git modus operandi: take something simple and make it difficult).

My question is, how do I make --squash the default action during a merge?

解决方案

(Note: I got here by following the link from a more recent question of yours. I'm not sure how much you still care about this, but might as well answer it.)

You can't make Git do a squash "merge" by default for all branches, but you can make it do a squash "merge" by default for some branches. Since you are particularly interested in making this happen only for master, that may be just what you want.

Let's do a quick1 review of what git merge really does since, in the usual Git fashion, Git complicates everything. And, this:

We use separate branches for non-trivial bug fixes and features. The branches are kept in-sync with master by performing frequent git checkout <x>; git merge master.

is reversed from what many people believe to be the "correct" work-flow in Git. I have some doubts as to whether any Git work-flow can be called "correct" :-) , but some are more successful than others, and this is definitely the reverse of one of the more successful ones. (I do think it can work well, as noted in the extended discussion below.)


1Well, I tried to keep it short. :-) Feel free to skim, although there's a bunch of important material here. If TL;DR, just jump straight to the end.

The commit graph

As you know, but others may not, in Git, much is controlled by the commit graph. Every1 commit has some parent commit, or in the case of a merge commit, two or more parents. To make a new commit, we get on some branch:

$ git checkout funkybranch

and do some work in the work-tree, git add some files, and finally git commit the result to branch funkybranch:

... work work work ...
$ git commit -m 'do a thing'

The current commit is the (one, single) commit to which the name funkybranch points. Git finds this by reading HEAD: HEAD normally contains the name of the branch, and the branch contains the raw SHA-1 hash ID of the commit.

To make the new commit, Git reads the ID of the current commit from the branch we're on, saves the index/staging-area into the repository,2 writes the new commit with the current commit's ID as the new commit's parent, and—last—writes the new commit's ID to the branch information file.

This is how a branch grows: from one commit, we make a new one, and then move the branch name to point to the new commit. When we do this as a linear chain, we get a nice linear history:

... <- C <- D <- E   <-- funkybranch

Commit E (which might actually be e35d9f... or whatever) is the current commit. It points back to D because D was the current commit when we made E; D points back to C because C was current at that point; and so on.

When we make new branches with, e.g., git checkout -b, all we are doing is telling Git to make a new name, pointing to some existing commit. Usually this is just the current commit. So if we are on funkybranch and funkybranch points to commit E and we run:

git checkout newbranch

then we get this:

... <- C <- D <- E   <-- funkybranch, newbranch

That is, both names point to commit E. Git knows that we're on newbranch now because HEAD says newbranch. I like to include that in this kind of drawing too:

... <- C <- D <- E   <-- funkybranch, HEAD -> newbranch

I also like to draw my graphs in a bit more compact fashion. We know that commits always point "backwards in time" to their parents, because it's impossible to make new commit E before we've made commit D. So these arrows always point leftward and we can just draw one or two dashes:

...--C--D--E   <-- funkybranch, HEAD -> newbranch

(and then if we don't need to know which commit is which, we can just draw a round o node for each one, but for now I will stick to single uppercase letters here).

If we make a new commit now—commit F—it causes newbranch to advance (because, as we can see from HEAD, we're on newbranch). So let's draw that:

...--C--D--E      <-- funkybranch
            \
             F    <-- HEAD -> newbranch

Now let's git checkout funkybranch again, and do some work there and commit it, making new commit G:

...--C--D--E--G   <-- HEAD -> funkybranch
            \
             F    <-- newbranch

(and HEAD is now pointing to funkybranch). Now we have something we can merge.


1Well, every commit except for root commits. In most Git repositories there is just one root commit, which is the very first commit. Obviously it cannot have a parent commit, since the parent of each new commit is whatever commit was current when we made the new commit. With no commits at all, there is no current commit yet when we make the first commit. So it becomes a root commit, and then all later commits are its children, grandchildren, great-grand-children, and so on.

2Most of the "save" work actually happens at each git add. The index/staging-area contains hash IDs, rather than actual file contents: the file contents were saved away when you ran git add. This is because Git's graph is not just of commit objects, but of every object in the repository. This is part of what makes Git so fast as compared to, e.g., Mercurial (which saves the files away at commit time rather than add time). Fortunately this, unlike the commit graph itself, is something users need not know or care about.

Git merge

As before, we have to be on some branch.1 We're on funkybranch, so we are all good to go:

$ git merge newbranch

At this point, most people seem to think that Magic Happens. It's not magic at all though. Git now finds the merge base between our current commit and the one we named, and then runs two git diff commands.

The merge base is simply2 the first commit "in common" on the two branches—the first commit that is on both branches. We are on funkybranch, which points to G. We gave Git the branch name newbranch, which points to commit F. So we're merging commits G and F, and Git follows both of their parent pointers until it reaches a commit node that is on both branches. In this case, that's commit E: commit E is the merge base.

Now Git runs those two git diff commands. One compares the merge base against the current commit: git diff <id-of-E> <id-of-G>. The second diff compares the merge base against the other commit: git diff <id-of-E> <id-of-F>.

Finally, Git attempts to combine the two sets of changes, writing the result to our current work-tree. If the changes seem independent, Git takes both of them. If they seem to collide, Git stops with a "merge conflict" and makes us clean it up. If they seem to be the same changes, Git takes just one copy of the changes.

All of this "seems" stuff is done on a purely textual basis. Git has no understanding of code. It just sees things like "delete a line reading ++x;" and "add a line reading y *= 2;. Those look different, so as long as they seem to be in different areas, it does the one delete and the one add, to the files in the merge-base, putting the result in the work-tree.

Last, assuming all goes well and the merge does not stop with a conflict, Git makes a new commit. The new commit is a merge commit, which means it has two3 parents. The first parent—the order matters—is the current commit, just as with regular, non-merge commits. The second parent is the other commit. Once the commit is safely written to the repository, Git writes the new commit's ID into the branch name, as usual. So, assuming the merge works, we get this:

...--C--D--E--G--H  <-- HEAD -> funkybranch
            \   /
              F     <-- newbranch

Note that newbranch has not moved: it still points to commit F. HEAD has not changed either: it still contains the name funkybranch. Only funkybranch has changed: it now points to the new merge commit H, and H points back to G, and also to F.


1Git is a bit schizoid about this. If we git checkout a raw SHA-1, or anything else that is not a branch name, it goes into a state it calls "detached HEAD". Internally, this works by shoving the SHA-1 hash directly into the HEAD file, so that HEAD gives the commit ID, rather than the name of the branch. But the way Git does everything else makes it work as though we're on a special branch whose name is just the empty string. It's the (single) anonymous branch—or, equivalently, it's the branch named HEAD. So in one sense, we're always on a branch: even if Git says that we're not on any branch, Git also says that we're on the special anonymous branch.

This causes a lot of confusion, and it might be more sensible if it weren't allowed, but Git uses it internally during git rebase, so it's actually pretty important. If the rebase goes wrong, this detail leaks out, and you wind up having to know what "detached HEAD" means, and is.

2I am deliberately ignoring a hard case here, which occurs when there are multiple possible merge base commits. Mercurial and Git use different solutions here: Mercurial picks one at (what seems to be) random, while Git gives you options. These cases are rare though, and ideally, even when they do occur, Mercurial's simpler method works anyway.

3Two or more, really: Git supports the concept of an octopus merge. But there's no need to go there. :-)

Merge changes the graph from a tree to a DAG

Merges—true merges: commits with two or more parents—have a bunch of important—critical, even—side effects. The main one is that the presence of a merge causes the commit graph data structure to change from a tree, where branches simply fork off and grow on their own, into a DAG: a Directed Acyclic Graph.

When Git walks the graph, as it does for so many operations, it usually follows all paths back. Since a merge has two parents, git log, which walks the graph, shows both parent commits. Hence this is considered a Feature:

For example, rather than a single "Merge into Master" or "Merge Master into ", git will add all the commit messages.

Git is following, and hence logging, both the original commit sequence—commits H, G, E, D, and so on—and the merged-in commit sequence F, E, D, and so on. Of course, it shows each commit only once; and by default, it sorts these commits by their date-stamps, intermingling the two branches if each one has many commits with dates that overlap.

If you don't want to see the commits that came in via the "other side" of a merge, Git has a way to do that: --first-parent tells every Git command that walks the graph1 to follow only the first parent of each merge. The other side is still there in the graph, and it still affects how Git computes things like the merge base, but git log --first-parent won't show it.


1This is quite a lot of Git commands. They use, or in the case of git log itself, are, variants of git rev-list, which is Git's general purpose graph-walk program. This code is central to push, fetch, bisect, log, blame, rebase, and numerous others. Its documentation has a dizzying array of options. The key ones to know as a casual user are --first-parent (just discussed here); --no-walk (suppresses graph walking entirely); --ancestry-path (simplifies history for source tree related work); --simplify-by-decoration (simplifies history for git log output); --branches, --remotes, and --tags (selects starting points for graph walking by branch, remote, or tag name); --merges and --no-merges (include or exclude merge commits); --since and --until (limit commits by date ranges); and the basic .. and ... (two and three dot) graph subsetting operations.

Benefits of merges

Having the merge in place means that development on a branch can continue on that branch, and a later git merge finds a newer—and hence less complicated—merge base. Consider this graph, where only a few commits have single-letter names:

  o--o--o--o--H--o--o--I        <-- feature2
 /             \        \
A--o--B---C-----D--E-----F--G   <-- master
 \       /        /        /
  o--o--J--o--o--K--o--o--L     <-- feature1

Here, except for two early commits done on master after the root commit A, all development has taken place on side branches feature1 and feature2. Commits C, D, E, F, and G are all merges (in this case, strictly into master), bringing the feature-work into master when it was ready.

Note that when we made commit C on master, we did:

$ git checkout master; git merge feature1

which found A as the merge base and B and J as the two tip commits to merge. When we made D:

$ git checkout master; git merge feature2

we had A as the merge base and C and H as the two tip commits. So far, this is nothing special. But when we made E, we had this much so far (the final os, and even I, on feature2 may or may not have been in place—they have no effect):

  o--o--o--o--H--o--o           <-- feature2
 /             \
A--o--B---C-----D               <-- master
 \       /
  o--o--J--o--o--K              <-- feature1

The merge base of master and feature1 is the first commit that is on both branches, which is commit J, which is the one we merged in to make C. So to do this merge, Git compares J vs D—the code we brought in from feature2—and J vs K: the new code (and only the new code) on feature1. If all goes well, or once we fix merge conflicts, this makes commit E and we now have:

  o--o--o--o--H--o--o--I        <-- feature2
 /             \
A--o--B---C-----D--E            <-- master
 \       /        /
  o--o--J--o--o--K--o--o        <-- feature1

when we go to merge feature2 again. This time the merge base is commit H: moving straight back from feature2 soon hits H, and moving from E to D and then up to H from master also hits H. So now Git compares H vs E, which is what we brought in from feature1, and H vs I, which is the new stuff we added to feature2, and merges just those.

Drawbacks of merges

Trees have some very nice graph-theoretic properties, such as a guarantee of a single simple merge-base. Arbitrary DAGs may lose these properties. In particular, doing merges both ways—merging master into branch and merging branch into master—results in "criss cross merges" that can give you multiple merge bases.

Merges also make the graph (git log) very hard to follow. Using --first-parent or --simplify-by-decoration helps, especially if you practice good merging, but these graphs just naturally get messy.

Squash merges

Squash merges avoid the problems, but do so by paying a fairly heavy price: they are not merges at all. (Soon, we'll see how to deal with this.)

When you run git merge --squash, Git goes through the same motions as before in terms of finding a merge base, and making two diffs: merge-base vs current-commit, and merge-base vs other-commit. It then combines the changes in exactly the same way as for a regular commit. But then it makes an ordinary commit.1 The new commit has just a single parent, taken from the current branch.

Let's see that in action for the same sequence with feature1 and feature2:

  o--o--o                       <-- feature2
 /
A--o--B                         <-- master
 \ 
  o--o--J                       <-- feature1

We do git checkout master; git merge --squash feature1 to make new commit C. Git compares A vs B to see what we did on master, and A vs J to see what they (we) did on feature1. Git combines those changes and we get commit C, but with only one parent:

  o--o--o                       <-- feature2
 /
A--o--B---C                     <-- master
 \
  o--o--J                       <-- feature1

Now we'll make D as a squash from feature2:

  o--o--o--o--H                 <-- feature2
 /
A--o--B---C                     <-- master
 \
  o--o--J--o--o                 <-- feature1

Git compares A vs C, and A vs H, same as last time. We now get D. So far it's much the same, except that there are no points where the branches rejoin. But now it is time to make E:

  o--o--o--o--H--o--o           <-- feature2
 /
A--o--B---C-----D               <-- master
 \
  o--o--J--o--o--K              <-- feature1

We run git checkout master; git merge --squash feature1 as before.

Last time, Git compared J-vs-D and J-vs-K, as commit J was our merge base.

This time, commit A is (still) our merge base. Git compares A vs D, and A vs K. If there were conflicts we solved at C last time, we probably have to solve them again. This is bad—but we're not lost yet.


1Ordinary, as opposed to merge. As such, a squash merge is not a merge at all: it's a "get me the work done" commit, but it's not a merge commit. We need a real merge commit in addition; we will get to this in the next section.

Git actually stops here and forces you to run git commit to make the squash commit. Why? Who knows, it's Git. :-)

Squash merges can work

To solve the above, we just need to re-merge (with a non-squash "real merge") from master back to the feature branches. That is, instead of simply merging from whichever feature branch into master, and then continuing to work on the feature branch, we do this:

  o--o--o--o--H--*-o--o        <-- feature2
 /              /
A--o--B---C----D               <-- master
 \         \
  o--o--J---*--o--o--K         <-- feature1

These new commits, marked *, are (non-squash) merges from master, into feature1 and feature2. We made squash merge C to pick up changes made from A to J. So we then make a real merge into feature1, preferably using the tree straight from master1 (which has whatever goodies were in o--B-- as well). (We also made the * on feature2, just as general preparation, after making D on master to bring in everything from A to H. Like the * on feature1 we probably just want the source tree straight from master.)

Now that we're ready to bring in more work from feature1, we can just do another (squash) merge. The merge-base of master and feature1 is commit C, and the two tips are D and K, which is just what we want. Git's merge code will come up with a reasonably close result; we fix up any conflicts, test, fix any breakage, and commit; and then we do another "prep work" merge from master back into feature1 as before.

This work-flow is a bit more complicated than the "merge into master" one, but should give good results.


1Git does not make this totally trivial: we want a merge with a -s theirs strategy, which Git simply doesn't have. There is an easy way to get the desired effect using "plumbing" commands, but I'll leave that out of this answer, which is already crazy-long.

So, if that all works, how about the mechanics?

Note that what we want is merge --squash when merging into master, but regular (non-squash) merge when merging from master. In other words:

$ git checkout master && git merge foo

should use --squash, but:

$ git checkout foo && git merge master

should not use --squash. (The tree copying from the footnote in the previous section might be nice, but should be unnecessary: the merge result should basically always be the tree straight out of master.)

When git merge runs, it looks at the current branch (as it always must). If that branch has a name—if we're not in "detached HEAD" mode—Git then looks at your configuration, for a value stored under branch.branch.mergeOptions. Any string value here is scanned as if it were part of the git merge command.

Hence:

$ git config branch.master.mergeOptions "--squash"

(the quotes are not technically required, and you can add --global after git config, before branch.master.mergeOptions) sets up your current repository to do squash-merges into master. (With --global, it sets this as your personal default for all repositories. Any branch.master.mergeOptions set in a particular repository will override these global ones, though.)

这篇关于如何使合并时使用默认值?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

1 普通的,相反的合并。因此,压缩合并根本不是合并:它是让我完成工作提交,但它不是合并提交。除了外,我们还需要真正的合并提交;我们将在下一节讨论这个问题。



Git实际上在这里停止并强制你运行 git commit 到使南瓜承诺。为什么?谁知道,这是Git。 : - )



壁球合并 can 工作



为了解决上述问题,我们只需要从 master 重新合并 -squashreal merge code>功能分支。也就是说,不是简单地从任何一个功能分支合并到 master 中,然后继续在功能分支上工作,我们这样做:

  o  -  o  -  o  -  o  -  H  -  *  -  o  -  o < -  feature2 
/ /
A - o - B - C - D - - - -
\ $
o - o - J - * - o-- o - K < - feature1
查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆