git合并丢失代码问题分析与解决(错误操作导致)

问题描述

我们在主干dev和branch1分支上进行并行开发。当要把branch1功能的代码合并到dev上时,发现dev上开发的部分功能代码找不到了。

那么,是在branch1上,作了删除提交导致的吗?然而,查提交日志,并没有发现删代码的提交记录。

难道一个分支有一个功能,另一个分支没这个功能,git合并时就有可能把这块功能代码丢掉?跟功能添加时间顺序有关系?

为了解决这个问题和相关的疑问,我们需要先了解下git合并的过程。

git-merge过程

稍微了解点git基础的应该都知道,合并是用的git merge命令。它只有两种,一种是快速合并(fast-forward),还有一种是三方合并(thirdparty merge)。

如上图所示,当两个分支有直系关系时,使用快速合并,git不产生新的commit结点,只是把head进行更新,如dev指向C4

三方合并稍显复杂点,它会产生一个新的commit结点,并把head指向它。它会先去找这两个要合并分支的最近公有结点,如图中,C3C5的最近公有父结点为C1。然后,git对 C1C3C5三个结点进行三方合并产生新结点C6。这里的三方合并,具体来说,就是把 C5相较于C1的 diff差异应用到 C3上,最后产生C6 这个commit结点。

现在回答上面的疑问,三方合并其实只看三个点的内容,和中间结点无任何关系,更别提跟时间有关系了。在一个分支上删除代码,如果合并时没有冲突的话,合并后是会直接删除的。

所以,我们找到了问题的初步方向了。dev上的代码合并后没了,一定是branch1分支有问题!!!

注:知道了git-merge的流程后,我们还可以知道,只要我们把这次合并代码丢失问题解决了,后续从branch1分支拉出去的分支代码再合并到dev时,都不用再解决这个代码丢失问题了。因为,合并后的提交结点和branch1分支拉出去分支的后续提交结点的父结点,已经变成branch1的当前结点了。如,C6的后续提交和C5的后续提交结点,公有结点都变成C5了。

问题起因及检测

为了描述问题方便,我把场景简化,搞了个demo,大家可以去下面地址clone:

# git clone https://git.coding.net/myswift/git-merge.git

提交记录用sourcetree看,是这样的(你可能已经发现问题了):

dev合并branch1时,dev上,dev func 1部分的提交丢失。

首先,让我们找最近公共结点吧。如果两个分支并行太久的话,可能不好直接找出来。我们可以使用git merge-base:

# git merge-base 98d19a4 0acedcb
9447776f5ee8c53536c947a1e13bfdead13f002b 

我们发现最近的公共结点是9447776。然而,这个公共结点,并不是我们设想的。我们设想的最近公共结点应该是两个分支刚开始并行的那个结点(如图中c3275e2)。进一步发现,9447776的下一个结点有个Merge,而且是把dev合并到branch1!!!

这就是问题的根源了,dev主干开发的一般是下个版本的功能,一般是把分支的代码合到主干上,把主干的代码逆向合并到分支上肯定是有问题的!!!

回到开头的问题,我们看Merge结点变更记录,并没有发现有删除代码的地方啊?原因是,你看到的合并结点的修改记录,是针对一边的。回到介绍三方合并的那个图,把branch1合并到dev产生结点C6,那么C6的提交记录中显示的修改,是C6针对C3结点的。在我们的示例中,合并结点74a8d10的提交变更,显示的是74a8d10对branch1中c26c5e3的变更,而branch1中本来就没有dev中的代码,所以合并后变更根本不会显示删除。

如果,你去比较合并结点和另一边的变更,你就可以发现问题:

# git diff 9447776 74a8d10
diff --git a/test.c b/test.c
index 150de8d..d19a020 100644
--- a/test.c
+++ b/test.c
@@ -7,8 +7,8 @@ void base_func() {
        printf("this is a crash %d\n", *p);
 }

-void dev_func_1() {
-       printf("dev func 1\n");
+void branch_func_1(){
+       printf("branch func1\n");
 }

你可以明显看到,在合并时,把dev中的dev_func_1函数删除掉了。

总结问题的原因是,在正式合并前,进行了逆向的合并,并在合并中悄悄把主干代码删除掉了。一般如果查看提交记录中,没有看到删除记录,那么很有可能是之前的Merge中把代码删除了。可以使用 merge-basegit diff 工具来进行定位,也可以用来检测是否有问题。

注:很多人可能认为只要管好自己的分支就行了,然后把别的分支合过来,并在合并时或合并后随意删除另一分支的代码。这样当以后再和该分支合并时,就会有问题。好的做法,应该是只把另一个分支上你需要的提交用cherry-pick移过来,而不是直接合并别人的分支,再删除你不需要的代码。如,只把dev上的fec5b84优化cherry-pick复制到branch1上即可。

解决思路

既然我们发现了问题的原因,并知道怎么去规避、检测。那么,如果已经发生了问题,怎么去解决呢?这个可能是大家更关心的。

其实我们最终的目标是,把branch1和dev进行合并,产生一个合并节点,并且这个合并结点的代码是正确的。

注:有些人可能不太明白为什么一定要产生一个git合并记录节点。通过各种手段,只要保证dev上代码正确不就行了?结论是不行,因为如果没有git合并记录的话,从branch1拉出来的所有分支再想合并到dev时,还是要解决下这个代码丢失的问题(没想明白,可以再看下前面git-merge过程部分),而且如果把branch1分支悬着不合并,也影响分支查看。

确保合并后代码正确

奔着这个目标,我们首先来确保代码的正确。

1. dev重置到合并前

既然最后合并branch1到dev会导致dev丢代码,我们首先把dev重置到合并前。

# git checkout dev
# git reset --hard HEAD~1

2. 创建tmp分支,绕过错误的合并74a8d10

我们知道branch1是有问题的,因为进行了合并dev的操作。所以,基于branch1创建一个临时分支tmp。

# git checkout branch1
# git checkout -b tmp

把tmp的提交记录重塑,使tmp分支回到branch1上的,合并dev到branch1那个错误的合并之前的结点,示例中 74a8d10之前的那个c26c5e3结点,并提交一个新记录,这样tmp内容与branch1一样,而完全跟那个74a8d10结点没关系了。

# git checkout tmp
# git reset c26c5e3
# git add .
# git commit -m "内容与branch1一致"

注:reset和reset --hard的区别,可以参考文末资料1。

3. 合并tmp到dev

# git checkout dev
# git merge tmp

这里dev和tmp合并时,它们的最近公共结点就不是之前错误的9447776了,而是我们设想的、dev和branch1最初分开的,c3275e2结点。

解决冲突,并add进暂存区后,我们代码就是正确的了(先不急着提交)。

产生合并commit对象

上面代码正确了,如果我们直接commit的话,这个合并结点,就变成dev和tmp的合并了,而我们要的是dev和branch1的合并。所以,我们要产生一个dev和branch1合并的结点,并且内容是当前dev和tmp合并后的代码。显然,git merge不能满足我们的需求,我们需要更底层的git命令,就是git merge过程中,调用的底层命令。

需要按序要用到 write-tree -> commit-tree -> update-ref,这三条底层命令。这部分命令,可以查看参考资料2。

1. write-tree产生tree对象

# git add .
# git write-tree
853c36012082314f9463f3819d0a24da49dc5bb1

我们产生了SHA-1值为 853c360的tree对象。

2. commit-tree产生commit对象

# git commit-tree 853c360 -p 98d19a4 -p 0acedcb -m "Merge branch 'branch1' into dev"
675baf3973508ee03306cc5a36fe489d694e107f

我们把tree对象 853c360进行了提交,并设置它的两个父结点为dev和branch1,产生了commit对象675baf3。我们可以看下这个结点的情况:

# git cat-file 675baf3 -p

tree 853c36012082314f9463f3819d0a24da49dc5bb1
parent 98d19a4a5913f18a2c0e9821e114df9995b23d82
parent 0acedcb89e4d25a0256fcbe7fba0bbc13de9d92e
author Vincent <xxx> 1498497182 +0800
committer Vincent <xxx> 1498497182 +0800

Merge branch 'branch1' into dev

3. 更新head

使用如下命令,更新dev指向这个新的commit对象, 675baf3

# git update-ref refs/heads/dev 675baf3

最终合并结果如下:

可以验证,branch1合并到dev了,而且内容是正确的(即不会少dev fun 1部分的代码)。

这个解决问题的示例代码,也上传到coding了,两份示例代码,之前的结点都是一致的。

# git clone https://git.coding.net/myswift/git-merge2.git

注:知道了git merge这些底层命令,你可以更加灵活地解决git问题,你可以结点随意合并,head随便指,是不是很开心,哈哈。

更粗暴的方法

如果你觉得底层命令不好理解。你可以:

  • 先整个目录拷备下工程(包含.git目录),比如拷贝到bak目录

  • 在工程中直接合并branch1到dev上,不解决冲突,不提交

  • 在bak目录,按照上面确保代码正确的方法,在bak目录合并出正确的代码。

  • 把bak目录中,除了.git目录外的东东,全部拷贝覆盖到原来工程目录中

  • 在原来工程目录中,提交

这样比较好理解,缺点是工程如果大的话,拷来拷去花费时间比较长,而且不够优雅。

其他解决思路

上面描述的思路,我认为是最行之有效的。也试了其他思路,比如:

  • 查看git merge的参数,发现并没有可以自由设置base节点的方法,只有设置发现base节点的策略,而且这些策略发现的base节点都是那个错误的合并。

  • undo merge。参考资料3。然而,感觉revert merge的能力有限,加-m1参数、和-m2参数,均无法满足要求。

  • rebase branch1。错误发生在branch1,那么重建branch1呢?把所有branch1上合并后的提交都重新提交呢?结果发现branch1上有太多合并冲突,rebase时,要把这个合并的冲突重新解决,很麻烦。

这些思路,大家也可以继续研究下,感觉不能解决问题,也可能是我了解得有问题。当然,你有其他思路,也希望你交流下。

迷思

本文中,是因为错误地把dev合并到branch1上,导致了后面合并的问题。但是,我们真实遇到的场景,虽然看起来是一样的,也可以用文中的方法解决,但是也有细微不同,而且不知道如何出现这个问题。

真实的场景下,也会出现一个dev合并到branch1的Merge提交,但是显示的信息是 "Revert xxx",据提交人员讲,这个确实是做的Revert操作,不知如何变成Merge结点了。用的sourcetree,提交人员也没法说清怎么必现这个问题。

如果,你知道怎么操作能出现这个问题,希望你告诉我。。。

总结

文中描述了一种可能导致git合并代码丢失的错误操作,并讲解了如何规避、检测、解决这种错误。并粗略介绍了,git merge流程,git merge底层过程。

说简单点,问题是因为悄悄在合并中把代码删除了。解决思路是,悄悄在后面的合并中把代码加回来。

参考

1. git-recipes

2. Git 内部原理-commit对象

3. Reverting a Merge

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • 1.git的安装 1.1 在Windows上安装Git msysgit是Windows版的Git,从https:/...
    落魂灬阅读 12,638评论 4 54
  • 谁曾见过我们, 是你们还是我。 记忆的谜影看见自己, 是熟悉还是似曾相识。 记忆的你是你还是心的语! 现实的你看清...
    狮子jack阅读 181评论 0 0
  • 这次,只有历史遗迹。 原先我是这么想的。很美好。 看多了南方的山水建筑,就想去看看中西部的世界。特别是前两个月的兰...
    Erinyes_阅读 526评论 1 0
  • 我的世界和别人不同,别人能看到一个我,而我能看到,只有自己能看到的——黑骑士。 小学时,我被冤枉偷了东西,我哭着辩...
    PM聆音者阅读 283评论 2 4
  • 文/小昭11 看完《人生在于心安》,这本星云大师的著作之后受益匪浅,对于生命有了更加深刻的理解和思考,在这里与大...
    小昭11阅读 1,765评论 9 4