【git重案组】如何逃避git blame的追踪?
上周筆者在工作中發現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-2google一下找到了一篇相似的文章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
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。
是否真的發生過利用這種方案惡意報復公司的案例呢?筆者也是很好奇。
總結
以上是生活随笔為你收集整理的【git重案组】如何逃避git blame的追踪?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 校园技术工坊丨云开发校园执行官招募开启!
- 下一篇: Atlas元数据存储模型分析