【消失的代碼】Git 合并分支導(dǎo)致代碼消失
1. 問題背景
A 頁面的代碼莫名其妙消失了,而且不清楚是什么時(shí)候被刪的。


發(fā)現(xiàn)這個(gè)問題之后,心里除了一句“草泥馬”以外,也萌生了很多疑惑。比如說,團(tuán)隊(duì)在代碼上線前,是有 CR 流程的,為什么這個(gè)代碼消失的 commit 會(huì)逃過這么多高工的法眼?
我們希望能找回代碼,并查出是哪次 commit 涉及到的,進(jìn)而找出操作過程,以防后續(xù)再有人出現(xiàn)類似操作。
2. 處理方式
2.1 通過 git log 查找出修改過指定文件的 commit
目前文件已經(jīng)被刪除了,但是根據(jù)項(xiàng)目的代碼結(jié)構(gòu),可以推測出原本是存在 A/index.js 這個(gè)文件的。
嘗試檢測一下在所有歷史記錄中,對該文件的處理,用到的命令如下:
git log --stat --full-history --simplify-merges -- A/index.js

上述命令將會(huì)展示涉及到該文件更改的 commit,從輸出結(jié)果我們可以看到,在 fix:1 這個(gè) commit 中,刪了 200 行代碼,而之后就再?zèng)]有 commit 處理過該文件了,所以可以推測文件就是在這個(gè) commit 中被刪除了。
然后通過 git checkout 6df716248794c3c54873f73002b8bd0854ac0805,去到刪除操作前最后修改過該文件的的 commit,即可拿到被刪除前的代碼了。
2.2. 解釋一下命令及每個(gè)參數(shù)的作用
2.2.1. git log 查看對指定文件修改過的commit
git log -- A/index.js
只使用上述命令去查找文件歷史,會(huì)存在一個(gè)問題:如果文件目前不存在,則什么記錄都沒有。

既然如此,我們先把代碼恢復(fù),再看看會(huì)展示什么:

上圖可以看到,只有恢復(fù)之后的那次 commit 的記錄。刪除代碼、以及刪除代碼前對該文件的所有 commit 都不會(huì)展示出來。這又是為什么呢?
這是因?yàn)?git log 的一個(gè)默認(rèn)策略:

也就是默認(rèn)模式下,git log 會(huì)簡化文件歷史,如果一些分支合起來看之后的結(jié)果是相同的,就不會(huì)展示這些分支。
因?yàn)橹皩@個(gè) index.js 文件從新建到刪除,中間的所有 commit 合起來看是相互抵消的(因?yàn)槲募詈蟊粍h除了,相當(dāng)于沒有新建過),所以單單輸入 git log 指令,什么也看不到。即使代碼被恢復(fù)后再輸入 git log 指令,也只會(huì)展示恢復(fù)代碼的那次 commit。
2.2.2. --stat 生成差異統(tǒng)計(jì)
git log 默認(rèn)情況下不會(huì)生成文件差異:

加了 --stat 參數(shù),即可生成文件差異的統(tǒng)計(jì),執(zhí)行以下命令:
git log --stat -- A/index.js

對比沒加 –stat 參數(shù)的結(jié)果,可以看到多輸出了文件的變更記錄,具體到變更了多少文件、多少行代碼。
2.2.3. --full-history
由 2.2.1 的介紹可知,git log 的默認(rèn)模式是會(huì)簡化文件歷史的。為此,我們需要加上 --full-history 這個(gè)參數(shù),去掉這個(gè)簡化的功能。

執(zhí)行以下命令:
git log --full-history -- A/index.js

對比 2.2.1,可以看到加了 --full-history 參數(shù)的輸出結(jié)果沒有進(jìn)行簡化,所有處理過該代碼的 commit 都展示出來了。
2.2.4. --simplify-merges
--simplify-merges 可以增強(qiáng) --full-history 的能力,因?yàn)?--full-history 會(huì)把一些無用的合并 commit 也輸出出來(可以看 2.2.3 中的 commit 信息,有一些是 Merge branch xxx),增加 --simplify-merges 參數(shù)可以去除這些無用的 commit 信息。

執(zhí)行以下命令:
git log --full-history --simplify-merges -- A/index.js

對比 2.2.3 中的輸出結(jié)果,可以看到已經(jīng)沒有 Merge branch xxx 的 commit 了,這里展示的每個(gè) commit 都是實(shí)實(shí)在在對指定文件進(jìn)行了修改的。
再加上 --stat 參數(shù)輸出文件的差異信息,最終可以得出我們前文使用到的查詢指令:
git log --stat --full-history --simplify-merges -- <path>
3. 分析原因
3.1 為什么代碼被刪除了,CR 時(shí)卻沒有發(fā)現(xiàn),仍能合到主干?
從上面的分析可以知道,代碼是在 fix:1 這個(gè) commit 中被刪除的。而在工蜂(公司內(nèi)類似 gitlab 的代碼管理平臺)中,根本就沒有記錄顯示代碼被刪除。

我們使用 git show 命令來看下該 commit 的更改內(nèi)容:

結(jié)果發(fā)現(xiàn)沒有顯示任何文件更改。
這就是 CR 時(shí)沒有發(fā)現(xiàn)問題的原因了,因?yàn)閯h除代碼的記錄根本就沒有出現(xiàn)在工蜂上,所以沒人知道這些代碼被刪除了。
3.2 為什么工蜂和 git show 無法展示該 commit 的記錄呢?
3.2.1 工蜂的結(jié)論

到底是不是因?yàn)檫@個(gè)原因呢?實(shí)踐出真知,我們用一個(gè)例子去試一下:
在一個(gè)項(xiàng)目內(nèi),模擬兩個(gè)分支在同時(shí)進(jìn)行開發(fā),在分支 A 新增了文件 new2.js,且修改 const.js。
新建 new2.js 如下:

修改 const.js 如下:

然后分支 B 再修改了 const.js:

分支 B 在 push 的時(shí)候,則需要處理一下沖突文件了。
此時(shí)我們關(guān)注到暫存區(qū)里的 new2.js:

如果在此時(shí)把 new2.js 從暫存區(qū)里剔除,沖突選擇 Current Change,再提交代碼,就能成功復(fù)現(xiàn)工蜂不展示代碼被刪的問題了。

如果去 VSCode 上看,還是可以看到代碼被刪除的:

3.2.2 分析一下

合并后,項(xiàng)目的主干路徑變?yōu)榱思t色的三個(gè)點(diǎn),相當(dāng)于 A 分支的兩個(gè)修改都被 B 分支的 merge 操作覆蓋掉了(新文件剔除出暫存區(qū)、沖突選擇分支B部分)。最終 fix:fix1 節(jié)點(diǎn)相對于分支 B 的最新節(jié)點(diǎn)沒有變化,故工蜂中 fix:fix1 節(jié)點(diǎn)顯示沒有文件變化。在分支 A 里新增的 new2.js 文件,相對于合并后的主干代碼來說,就像從來沒有出現(xiàn)過一樣,所以在合并分支的節(jié)點(diǎn)中就不會(huì)有它被刪除的記錄。
回到丟失代碼的項(xiàng)目里,打開 VSCode 的 git 管理模塊查看該 commit:

能夠看到是修改了很多文件的,其中就有刪除 A 頁面代碼的記錄,和我們例子的表現(xiàn)一致。
所以可以證明工蜂說的沒錯(cuò),應(yīng)該是當(dāng)時(shí)操作者在合并代碼時(shí),不知因?yàn)槭裁丛?,?A 頁面代碼剔除出了暫存區(qū),最終導(dǎo)致 A 頁面的代碼像消失了一樣。
4. 預(yù)防措施
目前發(fā)現(xiàn)代碼被刪除是被動(dòng)的,也就是需要去找這些代碼時(shí),才能發(fā)現(xiàn)代碼不見了,這也是代碼被刪了 8 個(gè)月才被發(fā)現(xiàn)的原因之一。
所以我們希望能夠化被動(dòng)為主動(dòng),通過程序去幫助開發(fā)者提前發(fā)現(xiàn)這些問題,而不是在需要用到這些代碼的時(shí)候,才發(fā)現(xiàn)代碼已經(jīng)沒了,時(shí)間久了再排查、恢復(fù)都比較困難。
因此可以考慮實(shí)現(xiàn)一個(gè) 主干檢查程序,將手動(dòng)的處理方式改為使用代碼邏輯去實(shí)現(xiàn),然后每隔一段時(shí)間觸發(fā)一次,檢查有無類似的情況發(fā)生,能夠做到出現(xiàn)類似情況發(fā)生后及時(shí)通知到開發(fā)者。
最后
歡迎加我微信,拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...
歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專業(yè)的技術(shù)人...

