本文作者: marklai(賴澤浩)- 高級軟件工程師,十年軟件配置管理經驗,現服務於 CSIG 雲與智慧產業事業群質量部。
導語
Git 是一個靈活和強大的版本管理工具,正確使用能夠有效促進團隊協作,防止版本丟失。然而實踐中,有些開發人員會或有意或無意地誤用部分 Git 的功能,給團隊帶來困擾,甚至造成損失。不恰當的代碼回滾操作是其中的主要問題之一。
本文主要分享針對不同場景的代碼回滾操作,以及如何搶救誤刪的內容。
一個典型案例
我們先通過一個項目團隊真實出現過的典型案例,來看看不恰當的代碼回滾可能帶來的問題。
(1)小紅、小黃、小藍共同工作在同一條分支上。
(2)小紅利用reset
回滾了一些內容,發現 push 失敗,最后用 push -f
操作成功。
更甚者,push -f
提示目標是保護分支(例如master
)而無法推送成功,於是小紅取消了分支保護,從而使得push -f
成功。
(3)小黃小藍進行常規 git pull,遇到了一大堆沖突,並且 commit 歷史都亂了!
(4)過一段時間,需要查看某次發布的源代碼,卻發現無法找到准確的代碼!原來它剛好被小紅之前reset
掉了。
認識 Git 的四個工作區域
在盤點常見的代碼回滾場景之前,有必要認識一下 Git 的四個工作區域。
平常我們 clone 一個代碼庫之后,本地看起來就是一個包含所有項目文件的目錄。其實從邏輯上可以分為四個工作區域:
-
工作區
也稱工作目錄、工作副本,簡單來說就是 clone 后我們看到的包含項目文件的目錄。我們日常開發操作也是在工作區中進行的。 -
本地倉庫(.git)
在工作區中有個隱藏目錄.git
,這就是 Git 本地倉庫的數據庫。工作區中的項目文件實際上就是從這里簽出(checkout)而得到的,修改后的內容最終提交后記錄到本地倉庫中。
Tips:不要手動修改 .git 目錄的內容 -
暫存區
也稱緩存區,邏輯上處於工作區和本地倉庫之間,主要作用是標記修改內容,暫存區里的內容默認將在下一次提交時記錄到本地倉庫中。 -
遠端倉庫
團隊協作往往需要指定遠端倉庫(一般是一個,也可以有多個),團隊成員通過跟遠端倉庫交互來實現團隊協作。
一個基本的 Git 工作流程如下:
- 在
工作區
中修改文件 - 暫存文件,將文件存放在
暫存區
- 將改動從
暫存區
提交到本地倉庫
- 從
本地倉庫
推送到遠端倉庫
常見的代碼回滾場景
回滾場景:僅在工作區修改時
當文件在工作區修改,還沒有提交到暫存區和本地倉庫時,可以用 git checkout -- 文件名
來回滾這部分修改。
不過需要特別留意的是這些改動沒有提交到 Git 倉庫,Git 無法追蹤其歷史,一旦回滾就直接丟棄了。
示例:
用 git status
查看,還沒提交到暫存區的修改出現在 “Changes not staged for commit:” 部分。
執行以下命令回滾工作區的修改:
git checkout -- build.sh
回滾場景:已添加到暫存區時
即執行過 git add
添加到暫存區,但還沒 commit,這時可以用 git reset HEAD 文件名
回滾。 通過git status
可以看到相關提示:
執行以下命令回滾暫存區的修改:
git reset HEAD build.sh
回滾后工作區會保留該文件的改動,可重新編輯再提交,或者 git checkout -- 文件名
徹底丟棄修改。
回滾場景:已 commit,但還沒有 push 時
即已經提交到本地代碼庫了,不過還沒有 push 到遠端。這時候可用 git reset
命令,命令格式為:
git reset <要回滾到的 commit>
或者 git reset --hard <要回滾到的 commit>
需注意的是,提供的是 要回滾到的 commit,該 commit 之后的提交記錄會被丟棄。
示例:
git reset
默認會將被丟棄的記錄所改動的文件保留在工作區中,以便重新編輯和再提交。加上 --hard
選項則不保留這部分內容,需謹慎使用。
回滾場景:修改本地最近一次 commit
有時 commit 之后發現剛才沒改全,想再次修改后仍記錄在一個 commit 里。利用 "git reset" 可達到這個目的,不過,Git 還提供了更簡便的方法來修改最近一次 commit。
命令格式如下:
git commit --amend [ -m <commit說明> ]
如果命令中不加-m <commit說明>
部分,則 Git 拉起編輯器來輸入日志說明。示例:
請注意,"git commit --amend" 只可用於修改本地未 push 的 commit,不要改動已 push 的 commit!
回滾場景:已 push 到遠端時
注意!此時不能用 "git reset",需要用 "git revert"!
注意!此時不能用 "git reset",需要用 "git revert"!
注意!此時不能用 "git reset",需要用 "git revert"!
重要事情說三遍!之所以這樣強調,是因為 "git reset" 會抹掉歷史,用在已經 push 的記錄上會帶來各種問題;而 "git revert" 用於回滾某次提交的內容,並生成新的提交,不會抹掉歷史。
示例:
過程中如果遇到問題(如處理沖突時搞亂了),可用 "git revert --abort" 取消本次回滾行為。
如果要回滾的是一個合並 commit,revert 時要加上"-m <父節點序號>",指定回滾后以哪個父節點的記錄作為主線。合並的 commit 一般有 2 個父節點,按 1、2 數字排序,對於要回滾“分支合入主干的 commit”,常用"-m 1",即用主干記錄作為主線。
回滾合並 commit 是一個較為復雜的話題,作為一般性建議,應避免回滾合並 commit。對該話題感興趣的可進一步了解:https://github.com/git/git/blob/master/Documentation/howto/revert-a-faulty-merge.txt
Reset 與 revert 對比
本節再來講一個示例,以便大家更好地理解git reset
和git revert
的差異。
分支初始狀態如下:
- 如果執行
git reset B
工作區會指向B
,其后的提交(C、D)被丟棄。
此時如果做一次新提交生成 C1
,C1
跟 C、D 沒有關聯。
- 如果執行
git revert B
回滾了B
提交的內容后生成一個新 commitE
,原有的歷史不會被修改。
找回已刪除的內容
雖說 Git 是一款強大的版本管理工具,一般來說,提交到代碼庫的內容不用擔心丟失,然而某些特殊情況下仍免不了要做搶救找回,例如不恰當的 reset、錯刪分支等。這就是 git reflog
派上用場的時候了。
"git reflog"是恢復本地歷史的強力工具,幾乎可以恢復所有本地記錄,例如被 reset 丟棄掉的 commit、被刪掉的分支等,稱得上代碼找回的“最后一根救命稻草”。
然而需要注意,並非真正所有記錄"git reflog"都能夠恢復,有些情況仍然無能為力:
-
非本地操作的記錄
"git reflog"能管理的是本地工作區操作記錄,非本地(如其他人或在其他機器上)的記錄它就無從知曉了。 -
未 commit 的內容
例如只在工作區或暫存區被回滾的內容(git checkout -- 文件 或 git reset HEAD 文件)。 -
太久遠的內容
"git reflog"保留的記錄有一定時間限制(默認 90 天),超時的會被自動清理。另外如果主動執行清理命令也會提前清理掉。
Reflog - 恢復到特定 commit
一個典型場景是執行 reset 進行回滾,之后發現回滾錯了,要恢復到另一個 commit 的狀態。
我們通過git reflog
查看 commit 操作歷史,找到目標 commit,再通過 reset 恢復到目標 commit。
通過這個示例我們還可以看到清晰、有意義的 commit log 非常有幫助。假如 commit 日志都是"update"、"fix"這類無明確意義的說明,那么即使有"git reflog"這樣的工具,想找回目標內容也是一件艱苦的事。
Reflog - 恢復特定 commit 中的某個文件
場景:執行 reset 進行回滾,之后發現丟棄的 commit 中部分文件是需要的。
解決方法:通過 reflog 找到目標 commit,再通過以下命令恢復目標 commit 中的特定文件。
git checkout <目標 commit> -- <文件>
示例:
Reset 回滾到 commit 468213d 之后,發現原先最新狀態中(即 commit d57f339)的 build.sh 文件還是需要的,於是將該文件版本單獨恢復到工作區中。
Reflog - 找回本地誤刪除的分支
場景:用"git branch -D"刪除本地分支,后發現刪錯了,上面還有未合並內容!
解決方法:通過 reflog 找到分支被刪前的 commit,基於目標 commit 重建分支。
git branch <分支名> <目標commit>
Reflog 記錄中,"to <分支名>"(如 moving from master to dev/pilot-001) 到切換到其他分支(如 moving from dev/pilot-001 to master)之間的 commit 記錄就是分支上的改動,從中選擇需要的 commit 重建分支。
示例:
找回合流后刪除的分支
作為 Git 優秀實踐之一,開發分支合流之后即可刪掉,以保持代碼庫整潔,只保留活躍的分支。
一些同學合流后仍保留着分支,主要出於“分支以后可能還用得到”的想法。其實大可不必,已合入主干的內容不必擔心丟失,隨時可以找回,包括從特定 commit 重建開發分支。並且,實際需要用到舊開發分支的情況真的很少,一般來說,即使功能有 bug,也是基於主干拉出新分支來修復和驗證。
假如要重建已合流分支,可通過主干歷史找到分支合並記錄,進而找到分支節點,基於該 commit 新建分支,例如:
git branch dev/feature-abc 1f85427
關於代碼回滾的一些建議
以下是關於特定命令的使用建議:
此外,總體來講,回滾要謹慎,不要過於依賴回滾功能,避免使用"git push -f"。正如某哲人所說:如果用到"git push -f",你肯定哪里做錯了!