导语:程序员的血腥复仇——论如何偷偷修改代码而不被别人发现... 背景介绍

上周笔者在工作中发现git仓库出现了一个奇怪的问题,master分支中某文件的一次commit丢失掉了,但diff中没有任何记录,这让笔者一度怀疑是git或者code平台自己出了问题。

在code平台一条条比对后发现变动发生在feature分支merge master分支之后。

原本SHA为8950d的edit.vue 文件最近一次修改是在一周前。

在merge之后该文件回滚到了两周前。

通过查询该文件的commit记录,可以看到最近的一次SHA为49c1a的commit确实丢掉了。

先明确前提,这是在一次merge中丢失的,而非经历了rebase或者reset操作,并没有对历史记录进行修改。

这里回顾下整个过程中的git 操作流,先从master checkout一个feature分支,在该分支提交了几次commit,merge master 到 feature,然后在master再次merge feature。

应该说这里虽然有不规范之处(没有提交merge request而是本地直接在master上merge然后push),但整体还算常规操作,即使是在merge中发生了冲突,不小心操作失误,按道理也不会没有diff记录。

merge的parent-1和parent-2

google一下找到了一篇相似的文章https://blog.laisky.com/p/git-merge/

该文章是在master分支上git pull,由于pull 的默认行为是 pull —merge,所以其实也是在merge中丢失的。

原文作者给出了一个比较清晰的解释:

众所周知,merge 是将两个 branch 合并为一个,所以每一个 merge commit 拥有两个 parents。当我们在 gitlab 或者 source tree 查看一个提交的具体修改时,其实就是将本次提交和其 parent 做 diff。而由于 merge commit 有两个 parent,并会将其排序为 1 和 2,当你试图查看一个 merge commit 的修改时,其实显示的是相对于 parent-1 的 diff。这样的一个问题是,如果 remote 不幸成为了 parent-2,那么你就可以通过巧妙的构造 parent-1 来实现一次“隐身”的代码修改。

我们提取原文核心,重点在于merge时的diff记录是相对于当前分支,假如当前分支是两周前的版本,而外来分支是一周前的版本,当merge时放弃掉一周前的版本,对原分支来说这次merge之后与之前并未发生改变,所以diff中自然也没有记录。

merge request 的不同之处

这个解释似乎也说的过去,不过在合并到master分支之前必然要本地merge一下master才可以快速合并,这个操作是逃避不了的,如果在本地merge时错误解决冲突会被隐藏下来,这岂不是git一个很大的缺陷吗?那code平台的merge request后的code review还有意义吗?

笔者自己搭建了一个测试仓库发现如果提交merge request,在code review的diff界面是看得到这次修改的,在提交之后也能在history中看到diff。难道gitlab(code平台应该是基于gitlab开发的)平台自己的diff算法更高级,所以才能发现这次错误?

笔者到这里产生了一个猜测,在本地操作的时候git 的diff算法有缺陷,它简单地把每一次commit的diff patch在一起,而code平台是老老实实做了两个文件夹的diff。

git diff的差异

在google之后,果然发现了不同(其实并不然…)!

在几个stackoverflow的问答和github的issue中笔者发现 github平台的pull request(虽然gitlab是merge request,实际上差不多)是使用了git diff的三点操作,而直接diff是两点操作,区别如下:

笔者一度以为突破口就在这里,但是仔细分析了git log —graph之后发现在merge request之前本地feature分支就已经merge了一次master,在这个情形下git diff的两点操作和三点操作根本没什么不同。

链接: What are the differences between double-dot “..” and triple-dot “…” in Git diff commit ranges? - Stack Overflow

https://github.community/t5/How-to-use-Git-and-GitHub/GitHub-pull-requests-showing-invalid-diff-for-already-merged/td-p/3000

merge的原理和fast-forward

Git merge采取三路合并策略,三路分别是基准分支(分叉的节点)、mine、theirs。

如果mine和theirs相对基准都发生了改变 那git 就报冲突,然后让你人工决断。否则,git将取相对于base变化的那个为最终结果。

一次普通的merge会新建一个commit节点(7号节点)。

而如果在feature分支从master checkout之后,master并未出现新的commit,就会出现三种策略。

[图片]

默认git merge会采取第三种策略,直接将master指针移到feature的头上即可,这里不会出现一个message为“merge xxx into xxx”的commit。

回到问题发生的场景上,在feature分支上执行git merge master的时候发生了一次普通的合并,生成一个“merge xxx into xxx”的commit,由于上文说到的原因,这个commit节点没有记录diff。当checkout回master再从master merge feature分支的时候,满足了fast-forward的条件,所以没有再次进行diff操作,没有对上次失误进行再次检查。

而code平台merge request默认的操作是—no-ff(这里补充一下,github是有squash选项的,但是code平台不支持),所以会强制再次进行一次diff,这时候上次merge中隐藏的错误得到了一个再次暴露出来的机会,在code review中就可以发现了。

解决方案

这个问题出现的根本原因有两个:

  • 浅层原因:merge时错误处理了冲突
  • 深层原因:没有走code平台merge request,没有禁止master分支直接pull

笔者回顾这个问题时想到,假如别有用心的人利用这种机制上的漏洞,在merge中故意修改代码,这些修改将不会出现在git的任何一次commit diff中,除非对master分支上一个挨一个commit排查。

甚至于在merge时采取squash或者rebase等方法,把这次commit 与其他commit混淆起来,是否就可以彻底把自己隐匿起来呢?

为了避免重现此次错误,强烈建议提高master分支敏感性,设置为protected分支禁止直接操作,所有对master分支的merge统一走merge request!

额外提一句,还应该避免在公用开发机上设置code平台 ssh 密钥,防止被盗用身份提交commit。

是否真的发生过利用这种方案恶意报复公司的案例呢?笔者也是很好奇。

文章来源于腾讯云开发者社区,点击查看原文