【问题标题】:How to make --squash default on a merge?如何使 --squash 在合并时默认?
【发布时间】:2016-05-20 17:54:26
【问题描述】:

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

我注意到在合并时,git 用多个不相关的消息污染了日志文件。例如,git 将添加所有提交消息,而不是单个“Merge into Master”或“Merge Master into ”。它是 Master 上的治理(处理和过程)问题,因为在开发过程中分支中可能存在的错误是 not 并且 不是 曾经出现在 Master 分支中。

更糟糕的是,分支和主控之间的行为是不同的。将 master 合并到分支时,会生成类似“Merge Master into ”的日志条目。但是,在将分支合并到 Master 时,没有“Merge into Master”。根据日志,就好像开发分支从未存在过,并且从未发生过合并。

我了解到我必须做一些特别的事情才能让 git 表现得像预期的那样;即How to use git merge --squash?(它的经典git 操作方式:把简单的事情变得困难)。

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

【问题讨论】:

  • 请原谅我的无知 Bryce...git config --global merge.squash true 只需要这些吗?博文没有讨论它。
  • 对不起,我不知道。这看起来像是您遇到的问题的剪切和粘贴解决方案,因此我为您提供了链接。我没有尝试在我的机器上复制它。无论如何,如果你打算在 git 中做任何你不确定的事情,你应该备份你的数据。
  • 关于“没有合并就消失的开发分支”的问题,请使用git merge --no-ff 始终创建合并提交,即使它可以通过快进合并来解决。
  • @knittl - 我们stopped using Git development branches。所有这些问题现在都解决了。我使用 问题无法再重现的原因投了第一票。

标签: git merge


【解决方案1】:

为了让 Git 总是为 master 分支做一个壁球“合并”:

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

说明

默认情况下,您不能让 Git 对 所有 分支执行 squash “合并”,但 可以 使其默认为 执行 squash “merge” em>一些分支。由于您对仅针对 master 实现这一点特别感兴趣,因此这可能正是您想要的。

让我们快速1回顾一下git merge 的真正作用,因为按照通常的 Git 方式,Git 使一切变得复杂。而且,这个:

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

与许多人认为的 Git 中“正确”的工作流程相反。我对是否可以将任何 Git 工作流程称为“正确”存在一些疑问 :-) ,但有些比其他工作流程更成功,这绝对是最成功的工作流程之一的反面。 (我确实认为它可以很好地工作,如下面的扩展讨论中所述。)


1嗯,我试图保持简短。 :-) 尽管这里有很多重要的材料,但请随意略读。如果 TL;DR,直接跳到最后。

提交图

如您所知,但其他人可能不会,在 Git 中,提交图可以控制很多。每个1 提交都有一些父提交,或者在合并提交的情况下,有两个或更多父提交。为了进行新的提交,我们进入某个分支:

$ git checkout funkybranch

在工作树中做一些工作,git add 一些文件,最后git commit 将结果分支到funkybranch

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

当前 提交是名称funkybranch 指向的(一个,单个)提交。 Git 通过读取HEAD 发现这一点:HEAD 通常包含分支的名称,而分支包含提交的原始 SHA-1 哈希 ID。

为了进行新的提交,Git 从我们所在的分支读取当前提交的 ID,将 index/staging-area 保存到存储库中,2 使用当前提交的 ID 作为新提交的父提交,并且 - 最后 - 将 new 提交的 ID 写入分支信息文件。

这是一个分支的增长方式:从一个提交开始,我们创建一个新的,然后移动分支名称以指向新的提交。当我们将其作为线性链执行时,我们会得到一个很好的线性历史:

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

Commit E(实际上可能是 e35d9f... 或其他)是当前提交。它指向D,因为D当我们进行E时的当前提交; D 指向 C,因为 C 在那个时候是最新的;等等。

当我们使用例如git checkout -b 创建新分支 时,我们所做的只是告诉Git 创建一个新的名称,指向一些现有的提交。通常这只是当前的提交。所以如果我们在funkybranchfunkybranch 指向提交E 并且我们运行:

git checkout newbranch

然后我们得到这个:

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

也就是说,两个名称都指向提交E。 Git 知道我们现在在newbranch,因为HEADnewbranch。我也喜欢将它包含在这种绘图中:

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

我还喜欢以更紧凑的方式绘制图表。我们知道提交总是指向他们的父母“时间倒退”,因为在我们提交 D 之前不可能进行新的提交 E。所以这些箭头总是指向左边,我们可以画一两个破折号:

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

(如果我们不需要知道哪个提交是哪个,我们可以为每个提交一个圆形的o 节点,但现在我将在这里坚持单个大写字母)。

如果我们现在进行新的提交——提交F——它会导致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 存储库中,只有一个根提交,即第一次提交。显然它不能有父提交,因为每个新提交的父提交都是我们进行新提交时当前的任何提交。根本没有提交,当我们进行第一次提交时,还没有当前提交。所以它变成了一个根提交,然后所有later提交都是它的孩子、孙子、曾孙等等。

2大部分“保存”工作实际上发生在每个git add。 index/staging-area 包含哈希 ID,而不是实际的文件内容:当您运行 git add 时,文件内容已被保存。这是因为 Git 的图表不仅包含提交对象,还包含存储库中的 每个 对象。这是 Git 比 Mercurial 快的部分原因(它在提交时保存文件而不是添加时间)。幸运的是,与提交图本身不同,这是用户不需要知道或关心的事情。

Git 合并

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

$ git merge newbranch

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

合并基础只是2 两个分支上“共同”的第一个提交——两个 分支上的第一个提交。我们在funkybranch,它指向G。我们为 Git 提供了分支 name newbranch,它指向提交 F。所以我们正在合并提交GF,Git 会跟随它们的父指针,直到它到达两个分支上的提交节点。在这种情况下,commit E:commit E 是合并基础。

现在 Git 运行这两个 git diff 命令。将合并基础与当前提交进行比较:git diff &lt;id-of-E&gt; &lt;id-of-G&gt;。第二个差异将合并基础与另一个提交进行比较:git diff &lt;id-of-E&gt; &lt;id-of-F&gt;

最后,Git 尝试组合这两组更改,将结果写入我们当前的工作树。如果更改看起来独立,Git 会同时采用它们。如果它们似乎发生冲突,Git 会以“合并冲突”停止并让我们清理它。如果它们看起来是相同的更改,Git 只会复制一份更改。

所有这些“似乎”的东西都是在纯文本的基础上完成的。 Git 不懂代码。它只会看到诸如“删除一行阅读++x;”和“添加一行阅读y *= 2;”之类的内容。它们看起来不同,所以只要它们看起来在不同的区域,它就会一个删除一个添加,到合并库中的文件,将结果放入工作树中。

最后,假设一切顺利并且合并不会因冲突而停止,Git 会进行新的提交。新提交是一个合并提交,这意味着它有两个3 父级。 first 父级(顺序很重要)是 当前提交,就像常规的非合并提交一样。 second 父级是另一个提交。一旦提交安全地写入存储库,Git 就会像往常一样将新提交的 ID 写入分支名称。所以,假设合并有效,我们得到:

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

注意newbranch 没有移动:它仍然指向提交FHEAD 也没有改变:它仍然包含名称 funkybranch。只有funkybranch 发生了变化:它现在指向新的合并提交HH 又指向G,也指向F


1Git 对此有点精神分裂。如果我们git checkout 原始 SHA-1 或任何其他不是分支 name 的东西,它就会进入一种称为“分离 HEAD”的状态。在内部,这是通过将 SHA-1 哈希直接推入 HEAD 文件来实现的,这样 HEAD 会给出提交 ID,而不是分支的名称。但是 Git 做其他所有事情的方式使它工作起来,就好像我们在一个特殊的分支上,它的名字只是一个空字符串。它是(单个)匿名分支——或者,等效地,它是名为 HEAD 的分支。所以从某种意义上说,我们总是在一个分支上:即使 Git 说我们不在任何分支上,Git 也说我们在一个特殊的匿名分支上。

这会引起很多混乱,如果不允许这样做可能会更明智,但 Git 在 git rebase 内部使用它,所以它实际上非常重要。如果变基出错,这个细节就会泄露出去,你最终不得不知道“分离的 HEAD”是什么意思。

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

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

Merge 将图从树变为 DAG

合并——真正的合并:有两个或更多父级的提交——有一堆重要的——关键的,甚至——副作用。主要的一个是合并的存在导致提交图数据结构从 (其中分支只是分叉并自行增长)变为 DAG:有向无环图。

当 Git 遍历图形时,就像它对许多操作所做的那样,它通常会沿着 所有 路径返回。由于合并有两个父级,因此遍历图表的git log 显示了两个父级提交。因此,这被认为是一项功能:

例如,git 将添加所有提交消息,而不是单个“Merge into Master”或“Merge Master into”。

Git 遵循并因此记录原始提交序列——提交HGED 等等——合并的提交序列FED 等等。当然,它只显示一次提交;默认情况下,它按日期标记这些提交进行排序,如果每个分支都有许多日期重叠的提交,则将两个分支混合在一起。

如果您不想看到通过合并的“另一边”进入的提交,Git 有办法做到这一点:--first-parent 告诉每一个运行的 Git 命令图1 仅跟随每个合并的 first 父级。另一边仍然在图中,它仍然会影响 Git 如何计算诸如合并基数之类的东西,但 git log --first-parent 不会显示它。


1这是相当多的 Git 命令。他们使用,或者在 git log 本身的情况下,git rev-list 的变体,这是 Git 的通用图形步行程序。这段代码是 push、fetch、bisect、log、blame、rebase 和许多其他代码的核心。它的文档有一系列令人眼花缭乱的选项。作为普通用户需要了解的关键是--first-parent(刚刚在此处讨论); --no-walk(完全禁止图形行走); --ancestry-path(简化源树相关工作的历史); --simplify-by-decoration(简化了 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 上完成的两个早期提交之外,所有开发都发生在分支feature1feature2 上。提交CDEFG 都是合并(在这种情况下,严格合并到master),当它准备好时将功能工作带入master

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

$ git checkout master; git merge feature1

它发现 A 作为合并基础,BJ 作为两个提示提交合并。当我们制作D:

$ git checkout master; git merge feature2

我们有 A 作为合并基础,CH 作为两个提示提交。到目前为止,这并没有什么特别之处。但是当我们制作E 时,到目前为止我们已经有了这么多(最后的os,甚至I,在feature2 上可能已经或可能没有到位——它们没有效果):

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

masterfeature1 的合并基础是两个 分支上的第一个提交,即提交J,这是我们合并以生成C 的提交.因此,为了进行这种合并,Git 比较 JD(我们从 feature2 引入的代码)以及 JKnew 代码(并且只有新的代码)在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,然后从master 向上移动到H 也会命中H .所以现在Git比较HE,这是我们从feature1引入的,HI,这是我们添加到feature2东西,并仅合并那些。

合并的缺点

树有一些非常好的图论属性,例如保证一个简单的合并基础。任意 DAG 可能会失去这些属性。特别是,以两种方式进行合并 - 将 master 合并到 branch branch 合并到 master - 导致“交叉合并”,可以为您提供多个合并基础。 p>

合并也使图表 (git log) 很难跟踪。使用 --first-parent--simplify-by-decoration 会有所帮助,尤其是在您练习良好的合并时,但这些图表自然会变得混乱。

壁球合并

Squash 合并避免了这些问题,但为此付出了相当大的代价:它们根本不是合并。 (很快,我们将看到如何处理这个问题。)

当您运行git merge --squash 时,Git 会执行与之前相同的动作来查找合并基础,并进行两个差异:合并基础与当前提交,以及合并基础与其他提交。然后它以与常规提交完全相同的方式组合更改。但随后它会进行普通提交。1新提交只有一个父级,取自当前分支。

让我们看看feature1feature2 对同一序列的作用:

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

我们通过git checkout master; git merge --squash feature1 进行新的提交C。 Git 比较 AB 以查看我们在 master 上所做的事情,以及 AJ 以查看他们(我们)在 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 比较 AC,以及 AH,与上次相同。我们现在得到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-DJ-vs-K,因为提交 J 是我们的合并基础。

这一次,commit A (仍然)是我们的合并基础。 Git 比较 AD,以及 AK。如果上次我们在C 解决了冲突,我们可能不得不再次解决它们。这很糟糕——但我们还没有迷路。


1普通,相对于合并。因此,squash 合并根本不是合并:它是“让我完成工作”提交,但它不是 merge 提交。我们需要一个真正的合并提交另外;我们将在下一节中讨论。

Git 实际上停在这里并强制你运行git commit 来进行壁球提交。为什么?谁知道呢,是 Git。 :-)

Squash 合并可以工作

要解决上述问题,我们只需重新合并(使用-squash“真正合并”)从master 回到feature分支机构。也就是说,我们不是简单地从任何功能分支合并到master,然后继续在功能分支上工作,而是这样做:

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

这些标记为* 的新提交是(非壁球)合并来自 master, feature1feature2。我们进行了 squash 合并 C 以获取从 AJ 所做的更改。所以我们然后真正合并到feature1,最好直接使用来自master1 的树(它也包含o--B-- 中的任何好东西)。 (我们还在feature2 上制作了*,就像一般准备一样,在制作master 上的D 以引入从AH 的所有内容。就像* 上的* 我们可能只是想要直接来自master 的源代码树。)

现在我们已经准备好从feature1 引入更多工作,我们可以再做一次(壁球)合并。 masterfeature1的merge-base是commit C,两个tip分别是DK,这正是我们想要的。 Git 的合并代码会得出一个相当接近的结果;我们修复任何冲突、测试、修复任何破损并提交;然后我们再做一次“准备工作”,像以前一样将 from master 返回 into feature1

这个工作流程比“合并到主”的工作流程要复杂一些,但是应该给出好的结果。


1Git 并没有让这一切变得微不足道:我们希望与 -s theirs 策略合并,而 Git 根本没有。 一种使用“管道”命令获得所需效果的简单方法,但我将把它排除在这个答案之外,这已经很长了。

那么,如果一切正常,那么机制呢?

请注意,当合并 master 时,我们想要的是merge --squash,但是当合并 from master 时,我们需要常规(非壁球)合并。换句话说:

$ git checkout master && git merge foo

应该使用--squash,但是:

$ git checkout foo && git merge master

不应该使用--squash。 (从上一节的脚注中复制的树可能很好,但应该是不必要的:合并结果基本上应该始终是直接从master 出来的树。)

git merge 运行时,它会查看当前分支(始终必须如此)。如果那个分支有一个名字——如果我们没有处于“分离的 HEAD”模式——Git 然后会查看你的配置,寻找存储在branch.<em>branch</em>.mergeOptions 下的值。此处的任何字符串值都会被扫描,就好像它是 git merge 命令的一部分一样。

因此:

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

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

【讨论】:

  • td,dr: $ git config branch.master.mergeOptions "--squash"
  • @Joanvo:是的,我试图用“跳到最后”位来说明这一点......
  • 我继续在顶部添加了命令,帖子的其余部分标记为“解释”
【解决方案2】:

我最终像这样创建了一个 bash shell 命令别名

alias gmg='git merge --squash'

此外,我也不希望自动进行更改,因此最终创建了以下 bash 函数

gitmerge() {
        git merge --squash "$1"
        git restore --staged .
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-05-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-23
    • 1970-01-01
    • 2019-09-21
    相关资源
    最近更新 更多