Git 代碼回滾與找回的藝術


本文作者: marklai(賴澤浩)- 高級軟件工程師,十年軟件配置管理經驗,現服務於 CSIG 雲與智慧產業事業群質量部。

導語

Git 是一個靈活和強大的版本管理工具,正確使用能夠有效促進團隊協作,防止版本丟失。然而實踐中,有些開發人員會或有意或無意地誤用部分 Git 的功能,給團隊帶來困擾,甚至造成損失。不恰當的代碼回滾操作是其中的主要問題之一。

本文主要分享針對不同場景的代碼回滾操作,以及如何搶救誤刪的內容。

一個典型案例

我們先通過一個項目團隊真實出現過的典型案例,來看看不恰當的代碼回滾可能帶來的問題。

(1)小紅、小黃、小藍共同工作在同一條分支上。

exam-1-1.png

(2)小紅利用reset回滾了一些內容,發現 push 失敗,最后用 push -f 操作成功。
更甚者,push -f提示目標是保護分支(例如master)而無法推送成功,於是小紅取消了分支保護,從而使得push -f成功。

exam-1-2.png

(3)小黃小藍進行常規 git pull,遇到了一大堆沖突,並且 commit 歷史都亂了!

exam-1-3.png

(4)過一段時間,需要查看某次發布的源代碼,卻發現無法找到准確的代碼!原來它剛好被小紅之前reset掉了。

認識 Git 的四個工作區域

在盤點常見的代碼回滾場景之前,有必要認識一下 Git 的四個工作區域。

平常我們 clone 一個代碼庫之后,本地看起來就是一個包含所有項目文件的目錄。其實從邏輯上可以分為四個工作區域:

  • 工作區
    也稱工作目錄、工作副本,簡單來說就是 clone 后我們看到的包含項目文件的目錄。我們日常開發操作也是在工作區中進行的。

  • 本地倉庫(.git)
    在工作區中有個隱藏目錄.git,這就是 Git 本地倉庫的數據庫。工作區中的項目文件實際上就是從這里簽出(checkout)而得到的,修改后的內容最終提交后記錄到本地倉庫中。
    Tips:不要手動修改 .git 目錄的內容

  • 暫存區
    也稱緩存區,邏輯上處於工作區和本地倉庫之間,主要作用是標記修改內容,暫存區里的內容默認將在下一次提交時記錄到本地倉庫中。

  • 遠端倉庫
    團隊協作往往需要指定遠端倉庫(一般是一個,也可以有多個),團隊成員通過跟遠端倉庫交互來實現團隊協作。

git-working-area.png

一個基本的 Git 工作流程如下:

  1. 工作區中修改文件
  2. 暫存文件,將文件存放在暫存區
  3. 將改動從暫存區提交到本地倉庫
  4. 本地倉庫推送到遠端倉庫

常見的代碼回滾場景

回滾場景:僅在工作區修改時

當文件在工作區修改,還沒有提交到暫存區和本地倉庫時,可以用 git checkout -- 文件名 來回滾這部分修改。

不過需要特別留意的是這些改動沒有提交到 Git 倉庫,Git 無法追蹤其歷史,一旦回滾就直接丟棄了。

示例:
git status 查看,還沒提交到暫存區的修改出現在 “Changes not staged for commit:” 部分。

change-in-workspace.png

執行以下命令回滾工作區的修改:

git checkout -- build.sh

回滾場景:已添加到暫存區時

即執行過 git add 添加到暫存區,但還沒 commit,這時可以用 git reset HEAD 文件名 回滾。 通過git status可以看到相關提示:

change-in-staging.png

執行以下命令回滾暫存區的修改:

git reset HEAD build.sh

回滾后工作區會保留該文件的改動,可重新編輯再提交,或者 git checkout -- 文件名 徹底丟棄修改。

回滾場景:已 commit,但還沒有 push 時

即已經提交到本地代碼庫了,不過還沒有 push 到遠端。這時候可用 git reset 命令,命令格式為:

git reset <要回滾到的 commit> 或者 git reset --hard <要回滾到的 commit>

需注意的是,提供的是 要回滾到的 commit,該 commit 之后的提交記錄會被丟棄。

示例:

change-in-local-repo.png

git reset 默認會將被丟棄的記錄所改動的文件保留在工作區中,以便重新編輯和再提交。加上 --hard 選項則不保留這部分內容,需謹慎使用。

回滾場景:修改本地最近一次 commit

有時 commit 之后發現剛才沒改全,想再次修改后仍記錄在一個 commit 里。利用 "git reset" 可達到這個目的,不過,Git 還提供了更簡便的方法來修改最近一次 commit。

命令格式如下:

git commit --amend [ -m <commit說明> ]

如果命令中不加-m <commit說明> 部分,則 Git 拉起編輯器來輸入日志說明。示例:

amend-latest-commit.png

請注意,"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-reset-and-revert.png

示例:

revert-demo.png

過程中如果遇到問題(如處理沖突時搞亂了),可用 "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 resetgit revert的差異。

分支初始狀態如下:

reset-revert-0.png

  • 如果執行 git reset B
    工作區會指向 B,其后的提交(C、D)被丟棄。

reset-revert-1-1.png

此時如果做一次新提交生成 C1C1跟 C、D 沒有關聯。

reset-revert-1-2.png

  • 如果執行 git revert B
    回滾了B提交的內容后生成一個新 commit E,原有的歷史不會被修改。

reset-revert-2-1.png

找回已刪除的內容

雖說 Git 是一款強大的版本管理工具,一般來說,提交到代碼庫的內容不用擔心丟失,然而某些特殊情況下仍免不了要做搶救找回,例如不恰當的 reset、錯刪分支等。這就是 git reflog派上用場的時候了。

"git reflog"是恢復本地歷史的強力工具,幾乎可以恢復所有本地記錄,例如被 reset 丟棄掉的 commit、被刪掉的分支等,稱得上代碼找回的“最后一根救命稻草”。

然而需要注意,並非真正所有記錄"git reflog"都能夠恢復,有些情況仍然無能為力:

  1. 非本地操作的記錄
    "git reflog"能管理的是本地工作區操作記錄,非本地(如其他人或在其他機器上)的記錄它就無從知曉了。

  2. 未 commit 的內容
    例如只在工作區或暫存區被回滾的內容(git checkout -- 文件 或 git reset HEAD 文件)。

  3. 太久遠的內容
    "git reflog"保留的記錄有一定時間限制(默認 90 天),超時的會被自動清理。另外如果主動執行清理命令也會提前清理掉。

Reflog - 恢復到特定 commit

一個典型場景是執行 reset 進行回滾,之后發現回滾錯了,要恢復到另一個 commit 的狀態。

reflog-exam-1-1.png

我們通過git reflog查看 commit 操作歷史,找到目標 commit,再通過 reset 恢復到目標 commit。

reflog-exam-1-2.png

通過這個示例我們還可以看到清晰、有意義的 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-exam-2-1.png

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 重建分支。

示例:

reflog-exam-3-1.png

找回合流后刪除的分支

作為 Git 優秀實踐之一,開發分支合流之后即可刪掉,以保持代碼庫整潔,只保留活躍的分支。
一些同學合流后仍保留着分支,主要出於“分支以后可能還用得到”的想法。其實大可不必,已合入主干的內容不必擔心丟失,隨時可以找回,包括從特定 commit 重建開發分支。並且,實際需要用到舊開發分支的情況真的很少,一般來說,即使功能有 bug,也是基於主干拉出新分支來修復和驗證。

假如要重建已合流分支,可通過主干歷史找到分支合並記錄,進而找到分支節點,基於該 commit 新建分支,例如:

git branch dev/feature-abc 1f85427

recover-merged-branch.png

關於代碼回滾的一些建議

以下是關於特定命令的使用建議:

此外,總體來講,回滾要謹慎,不要過於依賴回滾功能,避免使用"git push -f"。正如某哲人所說:如果用到"git push -f",你肯定哪里做錯了!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM