Git 代碼回滾與找回的藝術

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

導語

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

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

一個典型案例

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

(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掉了。ui

認識 Git 的四個工做區域

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

日常咱們 clone 一個代碼庫以後,本地看起來就是一個包含全部項目文件的目錄。其實從邏輯上能夠分爲四個工做區域:3d

  • 工做區
    也稱工做目錄、工做副本,簡單來講就是 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 個父節點,按 一、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",你確定哪裏作錯了!

相關文章
相關標籤/搜索