Git 如何優雅地回退程式碼
前言
從接觸程式設計就開始使用 Git 進行程式碼管理,先是自己玩 Github,又在工作中使用 Gitlab,雖然使用時間挺長,可是也只進行一些常用操作,如推拉程式碼、提交、合併等,更復雜的操作沒有使用過,看過的教程也逐漸淡忘了,有些對不起 Linus 大神。
出來混總是要還的,前些天就遇到了 Git 裡一種十分糟心的場景,併為之前沒有深入理解 Git 命令付出了一下午時間的代價。
先介紹一下這種場景,我們一個專案從 N 版本升到 A 版本時引入了另一專案的 jar 包,又陸續釋出了 B、C 版本,但在 C 版本後忽然發現了 A 版本引入的 jar 包有極大的效能問題,B、C 版本都是基於 A 版本釋出的,要修復 jar 包效能問題,等 jar 包再發版還得幾天,可此時線上又有緊急的 Bug 要修,於是就陷入了進退兩難的境地。
最後決定先將程式碼回退到 A 版本之前,再基於舊版本修復 Bug,也就開始了五個小時的受苦之路。
轉載隨意,文章會持續修訂,請註明來源地址:https://zhenbianshu.github.io 。
基礎試探
revert
首先肯定的是 revert,git revert commit_id
能產生一個 與 commit_id 完全相反的提交,即 commit_id 裡是新增, revert 提交裡就是刪除。
但是使用 git log 查看了提交記錄後,我就打消了這種想法,因為提交次數太多了,中途還有幾次從其他分支的 merge 操作。”利益於”我們不太乾淨的提交記錄,要完成從 C 版本到 N 版本的 revert,我需要倒序執行 revert 操作幾十次,如果其中順序錯了一次,最終結果可能就是不對的。
另外我們知道我們在進行程式碼 merge 時,也會把 merge 資訊產生一次新的提交,而 revert 這次 merge commit 時需要指定 m 引數,以指定 mainline
,這個 mainline 是主線,也是我們要保留程式碼的主分支,從 feature 分支往 develop 分支合併,或由 develop 分支合併到 master 的提交還好確定,但 feature 分支互相合並時,我哪知道哪個是主線啊。
所以 revert 的文案被廢棄了。
Reset
然後就考慮 reset
了, reset 也能使程式碼回到某次提交,但跟 revert 不同的是, reset 是將提交的 HEAD 指標指到某次提交,之後的提交記錄會消失,就像從沒有過這麼一次提交。
但由於我們都在 feature 分支開發,我在 feature 分支上將程式碼回退到某次提交後,將其合併到 develop 分支時卻被提示報錯。這是因為 feature 分支回退了提交後,在 git 的 workflow 裡,feature 分支是落後於 develop 分支的,而合併向 develop 分支,又需要和 develop 分支保持最新的同步,需要將 develop 分支的資料合併到 feature 分支上,而合併後,原來被 reset 的程式碼又回來了。
這個時候另一個可選項是在 master 分支上執行 reset,使用 --hard
選項完全拋棄這些舊程式碼,reset 後再強制推到遠端。
master> git reset --hard commit_id
master> git push --force origin master
但是還是有問題,首先,我們的 master 分支在 gitlab 裡是被保護的,不能使用 force push,畢竟風險挺大了,萬一有人 reset 到最開始的提交再強制 push 的話,雖然可以使用 reflog
恢復,但也是一番折騰。
另外,reset 畢竟太野蠻,我們還是想能保留提交歷史,以後排查問題也可以參考。
升級融合
rebase
只好用搜索引擎繼續搜尋,看到有人提出可以先使用 rebase
把多個提交合併成一個提交,再使用 revert 產生一次反提交,這種方法的思路非常清晰,把 revert 和 rebase 兩個命令搭配得很好,相當於使用 revert 回退的升級版。
先說一下 rebase,rebase 是”變基”的意思,這裡的”基”,在我理解是指[多次] commit 形成的 git workflow,使用 rebase,我們可以改變這些歷史提交,修改 commit 資訊,將多個 commit 進行組合。
介紹 rebase 的文件有很多,我們直接來說用它來進行程式碼回退的步驟。
- 首先,切出一個新分支 F,使用 git log 查詢一下
要回退到
的 commit 版本 N。 -
使用命令
git rebase -i N
, -i 指定互動模式後,會開啟 git rebase 編輯介面,形如:pick 6fa5869 commit1 pick 0b84ee7 commit2 pick 986c6c8 commit3 pick 91a0dcc commit4
- 這些 commit 自舊到新由上而下排列,我們只需要在 commit_id 前新增操作命令即可,在合併 commit 這個需求裡,我們可以選擇
pick(p)
最舊的 commit1,然後在後續的 commit_id 前新增squash(s)
命令,將這些 commits 都合併到最舊的 commit1 上。 - 儲存 rebase 結果後,再編輯 commit 資訊,使這次 rebase 失效,git 會將之前的這些 commit 都刪除,並將其更改合併為一個新的 commit5,如果出錯了,也可以使用
git rebase --abort/--continue/--edit-todo
對之前的編輯進行撤銷、繼續編輯。 - 這個時候,主分支上的提交記錄是
older, commit1, commit2, commit3, commit4
,而 F 分支上的提交記錄是older, commit5
,由於 F 分支的祖先節點是 older,明顯落後於主分支的 commit4,將 F 分支向主分支合併是不允許的,所以我們需要執行git merge master
將主分支向 F 分支合併,合併後 git 會發現 commit1 到 commit4 提交的內容和 F 分支上 commit5 的修改內容是完全相同的,會自動進行合併,內容不變,但多了一個 commit5。 - 再在 F 分支上對 commit5 進行一次 revert 反提交,就實現了把 commit1 到 commit4 的提交全部回退。
這種方法的取巧之處在於巧妙地利用了 rebase 操作歷史提交的功能和 git 識別修改相同自動合併的特性,操作雖然複雜,但歷史提交保留得還算完整。
rebase 這種修改歷史提交的功非常實用,能夠很好地解決我們遇到的一個小功能提交了好多次才好使,而把 git 歷史弄得亂七八糟的問題,只需要注意避免在多人同時開發的分支使用就行了。
遺憾的是,當天我並沒有理解到 rebase 的這種思想,又由於試了幾個方法都不行太過於慌亂,在 rebase 完成後,向主分支合併被拒之後對這些方式的可行性產生了懷疑,又加上有同事提出聽起來更可行的方式,就中斷了操作。
檔案操作
這種更可行的方式就是對檔案操作,然後讓 git 來識別變更,具體是:
- 從主分支上切出一個跟主分支完全相同的分支 F。
- 從檔案管理系統複製專案資料夾為 bak,在 bak 內使用
git checkout N
將程式碼切到想要的歷史提交,這時候 git 會將 bak 內的檔案恢復到 N 狀態。 - 在從檔案管理系統內,將 bak 資料夾下
除了 .git
資料夾下的所有內容複製貼上到原專案目錄下。git 會純從檔案級別識別到變更,然後更新工作區。 - 在原專案目錄下執行
add 和 commit
,完成反提交。
這種方式的巧妙之處在於利用 git 本身對檔案的識別,不牽涉到對 workflow 操作。
小結
最後終於靠著檔案操作方式成功完成了程式碼回退,事後想來真是一把心酸淚。
為了讓我的五個小時不白費,覆盤一下當時的場景,學習並總結一下四種程式碼回退的方式:
- revert 適合需要回退的歷史提交不多,且無合併衝突的情景。
- 如果你可以向 master 強推程式碼,且想讓 git log 裡不再出現被回退程式碼的痕跡,可以使用
git reset --hard + git push --force
的方式。 - 如果你有些 geek,追求用”正規而正統”的方式來回退程式碼,rebase + revert 滿足你的需求。
- 如果你不在乎是否優雅,想用最簡單,最直接的方式,檔案操作正合適。
git 真的是非常牛逼的程式碼管理工具,入手簡單,三五個命令組合起來就足夠完成工作需求,又對 geeker 們非常友好,你想要的騷操作它都支援,學無止境