git 在分支之间挑选和合并时的意外行为

Unexpected behaviour when git cherry-pick and merge between branches

令我惊讶的是,在 git 中精心挑选之后所做的更改在合并时变得过时了。这是一个完整的例子。

以下一切照常。

  1. 创建一个代码库
  2. 添加带有测试“rotums kanoner och krut”的文件
  3. 签出一个新分支,并添加文本行“mutors kanoner och krut”
  4. 检查 master 并使用“mutors kanoner och krut”挑选提交
Mac:git user1$ mkdir myrepo; cd myrepo; git init
Initialized empty Git repository in /Users/user1/tmp/git/myrepo/.git/

Mac:myrepo user1$ echo "rotums kanoner och krut" > rotum.txt

Mac:myrepo user1$ git add rotum.txt 

Mac:myrepo user1$ git commit -m "Added file"
[master (root-commit) 1044abb] Added file
 1 file changed, 1 insertion(+)
 create mode 100644 rotum.txt

Mac:myrepo user1$ git checkout -b mybranch
Switched to a new branch 'mybranch'

Mac:myrepo user1$ echo "mutors kanoner och krut" >> rotum.txt  

Mac:myrepo user1$ git commit -am "Added mutor"
[mybranch 19afeba] Added mutor
 1 file changed, 1 insertion(+)

Mac:myrepo user1$ git checkout master
Switched to branch 'master'

Mac:myrepo user1$ git cherry-pick 19af
[master cce2ca5] Added mutor
 Date: Wed May 19 16:12:04 2021 +0200
 1 file changed, 1 insertion(+)

Mac:myrepo user1$ cat rotum.txt  
rotums kanoner och krut
mutors kanoner och krut

现在是意外行为发生的时候。

  1. 我删除了添加和精心挑选的行(我通过覆盖文件来完成此操作,非常规方法,但在这种情况下很有用)。
  2. 然后我将我的分支合并到master。我希望 f63dc50 中所做的更改(删除一行)会保留下来,但它神秘地消失了。 “mutors kanoner och krut”这一行又回来了。
Mac:myrepo user1$ echo "rotums kanoner och krut" > rotum.txt  

Mac:myrepo user1$ cat rotum.txt  
rotums kanoner och krut

Mac:myrepo user1$ git commit -am "Removed mutor"
[master f63dc50] Removed mutor
 1 file changed, 1 deletion(-)

Mac:myrepo user1$ git merge mybranch
Merge made by the 'recursive' strategy.
 rotum.txt | 1 +
 1 file changed, 1 insertion(+)

Mac:myrepo user1$ cat rotum.txt
rotums kanoner och krut
mutors kanoner och krut

这是预期的行为还是错误?

这里的关键是 git 没有记录提交是从一个分支到另一个分支的挑选的事实;它只是根据您指定的提交创建一个新提交(这同样适用于“git rebase”)。

就 git 而言,您有这些提交:

  • 1044abb 创建文件
  • 19afeba 添加行
  • cce2ca5 添加行
  • f63dc50 删除行

请注意,我没有将这些提交描述为“在”一个分支或另一个分支上,因为严格来说这在 git 中没有任何意义;一个分支指向一个提交,其他提交可以通过“父”指针访问。

在合并两个分支的时候:

  • 1044abb、cce2ca5 和 f63dc50 可从“master”访问
  • 1044abb 和 19afeba 可从“mybranch”访问
  • master中的文件只有一行
  • “mybranch”中的文件有两行

合并时,git 根据两个分支可访问的最新提交确定“合并基础”;在这种情况下,即 1044abb。然后它会查看该提交与正在合并的两个分支之间的差异:

  • 在 1044abb 和 f63dc50(“master”)之间文件未更改
  • 在 1044abb 和 19afeba(“mybranch”)之间,文件添加了一行

然后它将这两个更改结合起来,并应用它们来生成文件的新版本。由于合并的一方想加线,而另一方什么也不想做,所以解决办法是加线。

结果是:

  • cce2ca5、f63dc50 和 19afeba 都可以从“master”访问
  • 19afeba 可从“mybranch”访问
  • 你已经master checked了,里面又出现了这条线

或者更简洁地说:您已经添加了该行,删除了它,然后又添加了它。

配图:

   Initial commit: create file
   |    Add a line in file (cherry-pick b)
   |    |    Remove line, return file to its state in a.
   v    v    v
   a----c----d  <- master
    \
     b <- mybranch
     ^
     Add a line in file

当您将 mybranch 合并到 master 时:

  • git 寻找关闭的共同祖先(又名合并基础,在图中提交 a.),
  • 它查看 master(提交 d)并发现,与 a. 相比,文件保持不变
  • 它查看 mybranch(提交 b)并发现,与 a. 相比,应该添加一行

因此合并成功且没有冲突,并且“引入”来自 mybranch 的更改。


您遇到这种情况是因为:

  1. git merge 不检查 mastermybranch ,
  2. 历史中的中间提交
  3. git cherry-pick 创建新的、不相关的提交,提交图中没有任何内容表明“这些更改已经包含”,
  4. 碰巧可以组合更改而不会发生冲突(你的例子很简单,但“没有冲突”在实际情况中很可能会发生)。

提供进一步的观点:

  • 如果你使用 git rebase :
git checkout mybranch
git rebase master

git mergegit rebase 确实 比较提交列表,如果重新设置的提交引入与目标中的另一个提交完全相同的更改分支,该提交被删除。

在您的示例中:b 不会被重新应用,因为它引入了与已经在 master.

上的 c 完全相同的更改
  • 如果您合并了提交 b 而不是 cherry-picking :
       merge 'mybranch'
       v
   a---c----d  <- master
    \ /
     b <- mybranch

然后 git merge mybranch 会说 already merged,并且不会重新应用提交 b