在 squash 合并到 master 之后将提交保留在功能分支中

Keep commits in feature branch after squash merge to master

目标:在 feature-branch 开发了一个新功能后,我想 "merge" 通过一次提交将其 mastermaster 的提交历史中。但是,我仍然希望能够访问每个更改行的原始提交消息,即使在删除 feature-branch 之后也是如此。

基本原理:这类似于使用 Subversion 将分支合并到 trunk 的默认行为。优点是 trunk/master 的历史保持精简,即它只包含一个单一的高级提交消息,如 Develop feature x。但是,如果我不确定为什么代码的特定部分更改为现在的样子,在 Subversion 中我可以使用 svn blame --use-merge-history 进行更深入的挖掘并查看原始提交消息。

可能的解决方案:据我了解,git master 中的单个提交可以使用 git merge --squash 策略实现.然而,这似乎 not 实际上创建了一个 merge-commit 但只是一个不保留完整历史的常规提交 feature-branch.事实上,一旦我之后删除 feature-branch,它的提交最终将被垃圾收集,因为它的提交对象现在基本上无法访问。

因此我的问题最后是:如何在将特性分支压缩合并到 master 并删除它之后保留特性分支中的提交,并且没有任何额外要求(比如为每个删除的分支创建标签)?

不要那样做。进行常规合并。

如果您想要查看 作为单个实体的要素,请使用git log --first-parent。这会将您的 git log 引导至 避免 探索 b运行ch.

一侧

让我们简要地看一下提交图是什么样的。提交图是每个提交的绘图,显示它如何连接回其 parent 提交。常规 (non-merge) 提交和合并提交之间的区别在于,常规提交仅连接回 一个 之前的提交,而合并连接回 两个(或更多,但您不会进行此类合并,因此无需尝试在此处绘制它们)。

记住,每个提交都有一个唯一的哈希 ID — 一大串丑陋的数字和字母,表示 那个 提交,世界上每个 Git 都同意的是为 that commit 保留——但是这些 hash ID 对人类没有意义,所以我们可以将它们画成小圆圈 o,或者用大写字母代替它们。还请记住,您可以按照自己喜欢的方式绘制图形:重要的是 提交 和它们的连接箭头,它们始终指向后方(从后来的提交到较早的提交)。

然后,一个简单的提交字符串可能如下所示:

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

不知何故,您找到了现有提交 H 的实际哈希 ID。你用它来让你的 Git 找出提交,包括它的作者姓名和日志消息之类的东西,以供查看。提交 H 本身包含早期提交 G 的实际哈希 ID。因此,您的 Git 可以找出提交 G 并向您显示作者姓名和日志消息。该提交包含早期提交的哈希 ID F。这个过程一直持续到 Git 到达 第一次 提交,它不会指向更早的任何东西,因为它不能,或者直到你厌倦了 git log 输出并停止寻找。

您是如何找到哈希 ID H 的?好吧,如果有一个 later 提交,你——或者你的 Git——从那个后来的提交中得到了 H。但是如果 H 是 b运行ch master 中的 last 提交,你从 H 中得到了 H 的哈希 ID =203=]姓名master。当你添加一个新提交 master 时,你的 Git 在新提交 I 中记录 H 的散列,然后将新提交的哈希 ID 写入 name master。因此根据定义,b运行ch 名称始终包含 latest 提交的哈希 ID。 Git 从那里开始并向后工作。

现在让我们看一组更复杂的b运行ches。我们不会再费心将连接箭头绘制为 arrows,因为一旦它们在某些提交中,它们就会一直被冻结 read-only。 (All every 提交的部分像这样永远冻结。) names 随着时间的推移而移动,虽然,所以让我们画出这些箭头:

...--F--G--H   <-- master
            \
             I--J   <-- feature

namemaster选择commitH;名称 feature 选择提交 J。如果出于某种原因我们返回 master 并添加更多提交,我们将得到:

...--F--G--H--K--L   <-- master
            \
             I--J   <-- feature

如果愿意,我们可以这样画,目前,我是这样画的:

             K--L   <-- master
            /
...--F--G--H
            \
             I--J   <-- feature

如果我们现在 git checkout master; git merge feature 我们将得到一个真正的合并提交:

             K--L
            /    \
...--F--G--H      M   <-- master (HEAD)
            \    /
             I--J   <-- feature

所附的 HEAD 提醒我们,master 是我们现在检查过的 b运行ch,以备不时之需。这包括我们 运行 git log 没有说明 git log 应该首先查看哪个提交。 Git 将使用 HEAD 查找当前提交,现在是 M。当我们 运行 git commit 进行新提交时也很重要:new 提交的 parent 将是 current commit,并且 Git 将更新 current b运行ch name——HEAD 附加到的那个——以记住哈希 ID新提交的。 这就是为什么 M 第一个 parent 是 L 以及为什么 master 现在是提交 M。合并提交的特点是它有两个 parents。第一个是L,第二个是J.

如果您现在 运行 git log,Git 将首先从提交 M 开始,向您显示合并的日志消息。然后它会查看 both 提交 LJ 并尝试同时向您展示。它实际上不能,所以它选择一个首先显示。它选择哪一个取决于你给 git log 的排序选项。默认是首先显示具有最新提交者时间戳的那个。

如果你说 --first-parent,但是,git log 根本不会 查看提交 J。它只会查看 M 的第一个 parent,即 L。它将显示提交 L,然后后退一步以提交 K 并显示,然后后退一步以提交 H,并显示,依此类推。

(请注意,我们现在可以安全地删除名字 feature.)

Fast-forward 合并不是合并

我插入提交的原因 K-L 是为了使绘制图形更容易和更对称。更现实地说,如果你在 b运行ches 上开发特性,然后将它们合并到 master 上,你只需要:

...--F--G--H   <-- master (HEAD)
            \
             I--J   <-- feature

当你去合并的时候feature。 运行 git merge feature,你的 Git 会注意到上次提交 Hmerge base 仍然提交 H,但是这一次,提交 H 也是 lastmaster 中的提交。这意味着 Git 可以跳过合并的实际工作。

Git 将这种 not-a-merge 操作称为 fast-forward 合并 。为避免这种情况,您必须使用 git merge --no-ff 一次(或使用 GitHub 的 "merge" 按钮,该按钮始终执行非 fast-forward,真正的合并)。

强制与 --no-ff

进行真正的合并

如果我们进行 --no-ff 合并,Git 将进行真正的合并。它会将提交 H 的快照与提交 H 的快照进行比较,并将 HJ 进行比较,因为真正的合并必须如此;然后它将合并这些更改并进行合并提交(这次我将其称为 K )。这给了我们这张图:

...--F--G--H------K   <-- master (HEAD)
            \    /
             I--J   <-- feature

当我们这里运行 git log时,Git会访问commit K并显示出来,然后同时访问HJ .默认情况下,排序顺序将使其接下来打印 J,然后是 I,然后是 H。所以我们将看到所有功能提交。

但是如果我们将 --first-parent 添加到我们的 git log,Git 将访问提交 K。然后它将遵循 first parent 链接返回提交 H,并显示它。然后它将返回提交 G,并显示,依此类推。

如果愿意,我们现在可以删除名称 feature,但如果愿意,我们也可以在 feature 上继续开发:

...--F--G--H------K   <-- master
            \    /
             I--J   <-- feature (HEAD)

此处 HEAD 的新位置暗示我们 运行 git checkout feature。现在新提交扩展 feature:

...--o--o--o------o   <-- master
            \    /
             o--o--o--o--o   <-- feature (HEAD)

如果我们现在 git checkout mastergit merge feature,即使不强制合并,我们也会得到真正的合并。 (不过,将 --no-ff 添加到合并命令并没有什么坏处。)它看起来像这样:

...--o--o--o------o--------o   <-- master (HEAD)
            \    /        /
             o--o--o--o--o   <-- feature

使用 git log --first-parent、Git 将显示 master 上的最后一次提交,然后是 master 上的上一次合并,依此类推:我们永远看不到完成的工作在 feature.

都在那里,如果我们想要它很容易找到:只是运行 git log 没有 --first-parent。当该功能真正完成并且最后一次合并就位时,您可以安全地删除 name feature。同时,您可以随时创建新功能,从图中任意位置的任何提交开始,处理它们,并最终合并它们。例如,假设您需要快速修复 master:

...--o--o--o------o--------o--o   <-- master (HEAD)
            \    /        /
             o--o--o--o--o   <-- feature

现在做一个辅助 feature2:

...--o--o--o------o--------o--o   <-- master, feature2 (HEAD)
            \    /        /
             o--o--o--o--o   <-- feature

并在 feature2 开始提交:

                                o--o   <-- feature2 (HEAD)
                               /
...--o--o--o------o--------o--o   <-- master
            \    /        /
             o--o--o--o--o   <-- feature

同时继续 feature 的工作:

                                o--o   <-- feature2
                               /
...--o--o--o------o--------o--o   <-- master
            \    /        /
             o--o--o--o--o--o--o--o   <-- feature (HEAD)

准备就绪后,您可以合并 feature2,这同样需要 --no-ff:

                                X--Y   <-- feature2
                               /    \
...--o--o--o------o--------o--W------Z   <-- master (HEAD)
            \    /        /
             o--o--o--o--o--o--o--o   <-- feature

(注意 Zfirst parent 是 W,不是 Y;注意我们是运行没有字母,这就是为什么 Git 不使用简单的短数字或字母作为提交 ID!)。

也许 feature2 即将完成:

                                o--o   <-- feature2
                               /    \
...--o--o--o------o--------o--o------o----o   <-- master (HEAD)
            \    /        /              /
             o--o--o--o--o--o--o--o--o--o

--first-parent 标志继续完成仅跟随 master-to-previously-on-master 链接的工作,就在图的中间,没有偷看侧面图