压缩默认的合并?
我们使用单独的分支进行非平凡的错误修复和功能。 通过执行频繁的git checkout <x>; git merge master
分支与master保持同步git checkout <x>; git merge master
git checkout <x>; git merge master
。
我注意到合并时,git使用多个不相关的消息来污染日志文件。 例如,不是一个“Merge <X>到Master”或“Merge Master into <X>”,git会添加所有提交消息。 它在Master上是一个治理问题(处理沙过程),因为在开发过程中可能存在于分支中的错误并不是并且在Master分支中不存在。
更糟糕的是,分支和主人之间的行为是不同的。 将主人合并到分支时,会生成类似于“将主人合并到<X>”中的日志条目。 但是,将分支合并到Master中时,没有“Merge <X>成为Master”。 根据日志,它好像开发分支从未存在,合并也从未发生过。
我知道我必须做一些特殊的事情才能让git像预期的那样行事; 即如何使用git merge --squash? (它的经典git modus operandi:采取简单的操作并使其变得困难)。
我的问题是,如何让我--squash
在合并过程中的默认操作?
(注意:我是通过跟随最近一个关于你的问题的链接来到这里的,我不确定你还有多少关心这件事,但不妨回答一下。)
你不能让Git默认为所有分支做一个“合并”,但是你可以在默认情况下为某些分支做一个压缩“合并”。 既然你特别想让这种事情发生,只为了master
,那可能就是你想要的。
让我们快速回顾一下git merge
真正含义,因为在通常的Git中,Git使所有事情复杂化。 和这个:
我们使用单独的分支进行非平凡的错误修复和功能。 通过执行频繁的git checkout <x>; git merge master
分支与master保持同步git checkout <x>; git merge master
git checkout <x>; git merge master
。
与许多人认为Git中“正确”的工作流程相反。 我对Git的工作流是否可以被称为“正确”有一些疑问:-),但有些比其他人更成功,这绝对是其中一个更成功的。 (我认为它可以很好地工作,正如下面的扩展讨论中所指出的那样)。
1,我尽量保持简短。 :-)随意浏览,尽管这里有一堆重要的材料。 如果TL; DR,就直接跳到最后。
提交图
如你所知,但其他人可能不会在Git中由提交图来控制。 Every1提交有一些父提交,或者在合并提交的情况下,有两个或多个父提交。 做一个新的提交,我们得到一些分支:
$ git checkout funkybranch
并在工作树上做一些工作, git add
一些文件,最后git commit
将结果git commit
给分支funkybranch
:
... work work work ...
$ git commit -m 'do a thing'
当前提交是funkybranch
名称指向的(单一,单一)提交。 Git通过读取HEAD
发现: HEAD
通常包含分支的名称,并且分支包含提交的原始SHA-1哈希ID。
为了进行新的提交,Git从我们所在的分支中读取当前提交的ID,将索引/登台区域保存到存储库中,2将新提交的当前提交的ID作为新提交的父项写入, -last-将新提交的ID写入分支信息文件。
这是分支如何增长:从一次提交开始,我们创建一个新的分支,然后移动分支名称以指向新的提交。 当我们将它作为线性链时,我们会得到一个很好的线性历史记录:
... <- C <- D <- E <-- funkybranch
提交E
(实际上可能是e35d9f...
或其他)是当前提交。 它指向D
因为当我们做E
时, D
是当前的提交; D
指向C
因为当时C
是当前的; 等等。
当我们用例如git checkout -b
创建新的分支时,我们所做的只是告诉Git创建一个新名称,指向一些现有的提交。 通常这只是当前的提交。 所以如果我们在funkybranch
和funkybranch
点提交E
,我们运行:
git checkout newbranch
那么我们得到这个:
... <- C <- D <- E <-- funkybranch, newbranch
也就是说,两个名字都指向提交E
Git知道我们现在是在newbranch
因为HEAD
说newbranch
。 我也喜欢在这种绘图中加入:
... <- C <- D <- E <-- funkybranch, HEAD -> newbranch
我也喜欢以更紧凑的方式绘制我的图形。 我们知道提交总是指向他们的父母“及时向后”,因为在我们提交D
之前,不可能做出新的提交E
所以这些箭头总是指向左边,我们可以画一两条破折号:
...--C--D--E <-- funkybranch, HEAD -> newbranch
(然后如果我们不需要知道哪个提交是哪个,我们可以为每个提供一个圆形的o
节点,但现在我会在这里使用单个大写字母)。
如果我们现在做一个新的提交 - 提交F
- newbranch
会导致新newbranch
进步(因为正如我们从HEAD
看到的那样,我们正在新newbranch
)。 所以让我们来画一下:
...--C--D--E <-- funkybranch
F <-- HEAD -> newbranch
现在让我们再次git checkout funkybranch
,并在那里做一些工作并提交它,使新的提交G
:
...--C--D--E--G <-- HEAD -> funkybranch
F <-- newbranch
(并且HEAD
现在指向funkybranch
)。 现在我们有一些可以合并的东西。
1Well,除root提交之外的每个提交。 在大多数Git仓库中,只有一个根提交,这是第一次提交。 显然,它不能有一个父提交,因为每个新提交的父对象是我们进行新提交时提交的最新对象。 完全没有提交,当我们进行第一次提交时,目前还没有提交。 所以它成为一个根提交,然后所有后来的提交都是它的子孙,孙子,等等。
2大部分“保存”工作实际上都发生在每个git add
。 索引/分段区域包含散列ID,而不是实际的文件内容:当您运行git add
时,文件内容被保存起来。 这是因为Git的图不仅仅是提交对象,还有存储库中的每个对象。 这是Git与Mercurial相比如此之快的一部分(它将文件保存在提交时间而不是增加时间)。 幸运的是,与提交图本身不同,这是用户不需要知道或关心的事情。
Git合并
像以前一样,我们必须在某个分支上.1我们在funkybranch
,所以我们都很好去:
$ git merge newbranch
在这一点上,大多数人似乎认为魔术会发生。 但它根本不是魔术。 Git现在找到我们当前的提交和我们指定的合并基础,然后运行两个git diff
命令。
合并基础简单地说就是两个分支上的“共同”第一次提交 - 两个分支上的第一次提交。 我们在funkybranch
,它指向G
我们给Git分支名称newbranch
,它指向提交F
所以我们合并了提交G
和F
,并且Git遵循它们的两个父指针,直到它到达两个分支上的提交节点。 在这种情况下,这是提交E
:commit E
是合并基础。
现在Git运行这两个git diff
命令。 我们将合并基础与当前的提交进行比较: git diff <id-of-E> <id-of-G>
。 第二个差异比较合并基础与其他提交: git diff <id-of-E> <id-of-F>
。
最后,Git试图结合这两组更改,将结果写入我们当前的工作树。 如果这些更改看起来独立,Git将采取这两种方式。 如果它们似乎发生碰撞,Git会以“合并冲突”结束并让我们清理它。 如果它们看起来是相同的变化,Git只需要一次更改。
所有这些“看起来”的东西都是在纯粹的文本基础上完成的。 Git不了解代码。 它看起来像“删除一行读取++x;
”和“添加一行读取y *= 2;
这些看起来不同,只要它们看起来在不同的区域中,它会删除一行,一个添加到合并库中的文件中,将结果放入工作树中。
最后,假设一切顺利,并且合并不会因为冲突而停止,那么Git将提交新的提交。 新的提交是一个合并提交,这意味着它有两个父母。 第一个父 - 订单很重要 - 就是当前提交,就像正常的非合并提交一样。 第二个父母是另一个提交。 一旦将提交安全写入存储库,Git将像往常一样将新提交的ID写入分支名称中。 所以,假设合并工作,我们得到这个:
...--C--D--E--G--H <-- HEAD -> funkybranch
/
F <-- newbranch
注意newbranch
没有移动:它仍然指向提交F
HEAD
也没有改变:它仍然包含名字funkybranch
。 只有funkybranch
已经发生了变化:它现在指向新的合并提交H
,并且H
指向G
,也指向F
1Git对此有点分裂。 如果我们git checkout
一个原始的SHA-1或其他不是分支名称的东西,它会进入一个它称为“分离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:有向无环图。
当Git走图时,就像它为许多操作所做的那样,它通常会沿着所有路径返回。 由于合并有两个父母,因此走图的git log
显示父提交。 因此这被认为是一个特征:
例如,不是一个单一的“融入主”或“融入主”,git将添加所有提交消息。
Git正在跟踪,因此记录了原始提交序列 - 提交H
, G
, E
, D
等 - 以及合并提交序列F
, E
, D
等等。 当然,它只显示每次提交一次; 并且默认情况下,它通过日期标记对这些提交进行排序,如果每个提交的日期重叠,那么它们混合在一起。
如果你不想看到通过合并的“另一面”提交的提交,Git有办法做到这一点:-- --first-parent
告诉每个Git命令走过graph1只跟随第一个父代每次合并。 另一方仍然存在于图中,它仍然影响着Git如何计算合并基础等东西,但git log --first-parent
不会显示它。
1这是很多Git命令。 他们使用,或者在git log
本身的情况下,是git rev-list
变体,它是Git的通用图形漫步程序。 这段代码是推动,获取,平分,记录,指责,重组以及其他许多方面的核心。 它的文档有一组令人眼花缭乱的选项。 要知道作为临时用户的关键是 - --first-parent
(刚才在这里讨论); --no-walk
(抑制图形走完全); --ancestry-path
(简化源代码树相关工作的历史); --simplify-by-decoration
简化了--simplify-by-decoration
(简化了git log
输出的历史git log
); --branches
,-- --remotes
和--tags
(通过分支,远程或标签名称选择图形走路的起点); --merges
和--no-merges
(包括或排除合并提交); --since
和--until
(限制按日期范围提交); 和基本..
和...
(两点和三点)图形子集操作。
合并的好处
合并到位意味着分支上的开发可以在该分支上继续进行,而后面的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
在这里,除了在根提交A
之后在master
完成两个早期提交之外,所有开发都发生在侧分支feature1
和feature2
。 提交C
, D
, E
, F
和G
都是合并的(在这种情况下,严格来说就是把它们合并到master
),在功能准备就绪的时候把这个功能部件放到master
。
请注意,当我们对master
进行提交C
时,我们做了:
$ git checkout master; git merge feature1
其中A
作为合并基础, B
和J
作为两个提示合并。 当我们做D
:
$ git checkout master; git merge feature2
我们有A
作为合并基础, C
和H
作为两个提示提交。 到目前为止,这没什么特别的。 但是,当我们做出E
,我们有这么多至今(最终o
S,甚至是I
,对feature2
可能或不可能已全部到位,他们有没有影响):
o--o--o--o--H--o--o <-- feature2
/
A--o--B---C-----D <-- master
/
o--o--J--o--o--K <-- feature1
master
和feature1
的合并基础是两个分支上的第一个提交,它们是提交J
,这是我们合并来制作C
提交。 因此,要做到这一点归并,Git的比较J
VS D
我们从带来-The代码feature2
-和J
VS K
:在新的代码(只有新的代码) feature1
。 如果一切顺利,或者一旦我们解决合并冲突,这使得提交E
,我们现在有:
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
当我们再次合并feature2
时。 这一次的合并基础是犯下H
:移动从直背feature2
不久命中H
和移动从E
到D
,然后高达H
从master
也命中H
。 因此,现在Git比较H
和E
,这是我们从feature1
, H
和I
是我们添加到feature2
的新东西,并将它们合并。
合并的缺点
树有一些非常好的图论性质,比如保证一个简单的合并基。 任意DAG可能会失去这些属性。 特别是,合并两种方式 - 将master
合并到branch
,并将branch
合并到master
branch
中 - “纵横交错”中的结果,这可以为您提供多个合并基础。
合并也使图( git log
)非常难以遵循。 使用--first-parent
或--simplify-by-decoration
帮助,特别是如果您练习良好的合并,但这些图形自然会变得混乱。
壁球合并
壁球融合避免了这些问题,但通过付出相当沉重的代价来实现:它们不是合并。 (很快,我们将看到如何处理这个问题。)
当你运行git merge --squash
,Git在寻找合并基础方面经历了与之前相同的运动,并且做出了两个不同之处:merge-base vs current-commit,merge-base vs other-commit。 然后它将这些更改按照与正常提交完全相同的方式进行组合。 但是,它会做出一个普通的提交.1新提交只有一个来自当前分支的父代。
让我们看看与feature1
和feature2
具有相同序列的feature2
:
o--o--o <-- feature2
/
A--o--B <-- master
o--o--J <-- feature1
我们做git checkout master; git merge --squash feature1
git checkout master; git merge --squash feature1
做出新的提交C
Git比较A
和B
,看看我们对master
做了什么, A
和J
看看他们(我们)在feature1
上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
与C
以及A
与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 checkout master; git merge --squash feature1
。
上次,Git比较了J
-vs- D
和J
-vs- K
,因为提交J
是我们的合并基础。
这一次,提交A
是(仍然)我们的合并基础。 Git比较A
对D
和A
对K
如果我们上次在C
上解决了冲突,那么我们可能不得不再次解决它们。 这很糟糕 - 但我们还没有失去。
1Ordinary,而不是合并。 因此,压缩合并根本不是合并:这是“让我完成工作”提交,但它不是合并提交。 我们还需要一个真正的合并提交; 我们将在下一节讨论这一点。
Git实际上在这里停止并迫使你运行git commit
来实现壁球提交。 为什么? 谁知道,这是Git。 :-)
壁球合并可以工作
为了解决上述问题,我们只需要重新合并(使用非压扁的“真正合并”)从master
返回到feature
分支。 也就是说,我们不是简单地将哪个特征分支合并到master
分支中,而是继续处理特征分支,我们这样做:
o--o--o--o--H--*-o--o <-- feature2
/ /
A--o--B---C----D <-- master
o--o--J---*--o--o--K <-- feature1
这些标记为*
新提交是从主设备(non-squash)合并到feature1
和feature2
。 我们制作了壁球合并C
来获取从A
到J
所做的更改。 所以我们然后做一个真正的合并到feature1
,最好使用直接来自master
1的树(它也有o--B--
任何好东西)。 (我们还在feature2
上做了*
,就像做一般准备一样,在master
上创建D
后引入从A
到H
所有内容。就像feature1
上的*
一样,我们可能只需要源自master
的源代码树。)
现在我们已经准备好从feature1
引入更多的工作,我们可以再做一次(压扁)合并。 master
和feature1
的合并基础是提交C
,两个提示是D
和K
,这正是我们想要的。 Git的合并代码会得出一个相当接近的结果; 我们修复任何冲突,测试,修复任何破坏,并提交; 然后我们再按照以前的做法将另一个“准备工作”从master
合并到feature1
中。
这个工作流程比“融入主”更复杂一些,但应该给出好的结果。
1Git并没有使这一切变得微不足道:我们希望合并一个-s theirs
策略,Git根本就没有这个策略。 有一种简单的方法可以使用“管道”命令获得所需的效果,但是我会将这个答案留给已经疯狂的答案。
所以,如果这一切都有效,那机制怎么样?
请注意,我们想要的是merge --squash
合并到主,但常规(非南瓜)合并时从主合并。 换一种说法:
$ git checkout master && git merge foo
应该使用--squash
,但是:
$ git checkout foo && git merge master
不应该使用--squash
。 (从上一节的脚注中复制树可能很好,但应该是不必要的:合并结果应该基本上始终是树的master
树。)
当git merge
运行时,它会查看当前分支(因为它总是必须的)。 如果该分支有一个名称,如果我们不处于“分离的HEAD”模式,Git会查看您的配置,以查看存储在branch.branch.mergeOptions
下的值。 这里的任何字符串值都被扫描,就好像它是git merge
命令的一部分。
因此:
$ git config branch.master.mergeOptions "--squash"
(引号不是技术上需要,你可以添加--global
后git config
,前branch.master.mergeOptions
)设置你的当前仓库做壁球合并到master
。 (使用--global
,它--global
其设置为所有存储库的个人默认值,但在特定存储库中设置的任何branch.master.mergeOptions
将覆盖这些全局值。)
上一篇: squash default on a merge?
下一篇: Git : Merge multiple commits from one branch into another