在 squash 合并到 master 之后将提交保留在功能分支中
Keep commits in feature branch after squash merge to master
目标:在 feature-branch
开发了一个新功能后,我想 "merge" 通过一次提交将其 master
在 master
的提交历史中。但是,我仍然希望能够访问每个更改行的原始提交消息,即使在删除 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 提交 L
和 J
并尝试同时向您展示。它实际上不能,所以它选择一个首先显示。它选择哪一个取决于你给 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 会注意到上次提交 H
的 merge base 仍然提交 H
,但是这一次,提交 H
是 也是 last 在 master
中的提交。这意味着 Git 可以跳过合并的实际工作。
Git 将这种 not-a-merge 操作称为 fast-forward 合并 。为避免这种情况,您必须使用 git merge --no-ff
一次(或使用 GitHub 的 "merge" 按钮,该按钮始终执行非 fast-forward,真正的合并)。
强制与 --no-ff
进行真正的合并
如果我们进行 --no-ff
合并,Git 将进行真正的合并。它会将提交 H
的快照与提交 H
的快照进行比较,并将 H
与 J
进行比较,因为真正的合并必须如此;然后它将合并这些更改并进行合并提交(这次我将其称为 K
)。这给了我们这张图:
...--F--G--H------K <-- master (HEAD)
\ /
I--J <-- feature
当我们这里运行 git log
时,Git会访问commit K
并显示出来,然后同时访问H
和J
.默认情况下,排序顺序将使其接下来打印 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 master
和 git 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
(注意 Z
的 first 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
链接的工作,就在图的中间,没有偷看侧面图
目标:在 feature-branch
开发了一个新功能后,我想 "merge" 通过一次提交将其 master
在 master
的提交历史中。但是,我仍然希望能够访问每个更改行的原始提交消息,即使在删除 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 提交 L
和 J
并尝试同时向您展示。它实际上不能,所以它选择一个首先显示。它选择哪一个取决于你给 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 会注意到上次提交 H
的 merge base 仍然提交 H
,但是这一次,提交 H
是 也是 last 在 master
中的提交。这意味着 Git 可以跳过合并的实际工作。
Git 将这种 not-a-merge 操作称为 fast-forward 合并 。为避免这种情况,您必须使用 git merge --no-ff
一次(或使用 GitHub 的 "merge" 按钮,该按钮始终执行非 fast-forward,真正的合并)。
强制与 --no-ff
进行真正的合并
如果我们进行 --no-ff
合并,Git 将进行真正的合并。它会将提交 H
的快照与提交 H
的快照进行比较,并将 H
与 J
进行比较,因为真正的合并必须如此;然后它将合并这些更改并进行合并提交(这次我将其称为 K
)。这给了我们这张图:
...--F--G--H------K <-- master (HEAD)
\ /
I--J <-- feature
当我们这里运行 git log
时,Git会访问commit K
并显示出来,然后同时访问H
和J
.默认情况下,排序顺序将使其接下来打印 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 master
和 git 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
(注意 Z
的 first 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
链接的工作,就在图的中间,没有偷看侧面图