本文做者: marklai(賴澤浩)- 高級軟件工程師,十年軟件配置管理經驗,現服務於 CSIG 雲與智慧產業事業羣質量部
Git 是一個靈活和強大的版本管理工具,正確使用可以有效促進團隊協做,防止版本丟失。然而實踐中,有些開發人員會或有意或無心地誤用部分 Git 的功能,給團隊帶來困擾,甚至形成損失。不恰當的代碼回滾操做是其中的主要問題之一。git
本文主要分享針對不一樣場景的代碼回滾操做,以及如何搶救誤刪的內容。github
咱們先經過一個項目團隊真實出現過的典型案例,來看看不恰當的代碼回滾可能帶來的問題。數據庫
(1)小紅、小黃、小藍共同工做在同一條分支上。緩存
(2)小紅利用reset
回滾了一些內容,發現 push 失敗,最後用 push -f
操做成功。 更甚者,push -f
提示目標是保護分支(例如master
)而沒法推送成功,因而小紅取消了分支保護,從而使得push -f
成功。編輯器
(3)小黃小藍進行常規 git pull,遇到了一大堆衝突,而且 commit 歷史都亂了!工具
(4)過一段時間,須要查看某次發佈的源代碼,卻發現沒法找到準確的代碼!原來它恰好被小紅以前reset
掉了。ui
在盤點常見的代碼回滾場景以前,有必要認識一下 Git 的四個工做區域。spa
日常咱們 clone 一個代碼庫以後,本地看起來就是一個包含全部項目文件的目錄。其實從邏輯上能夠分爲四個工做區域:3d
.git
,這就是 Git 本地倉庫的數據庫。工做區中的項目文件實際上就是從這裏簽出(checkout)而獲得的,修改後的內容最終提交後記錄到本地倉庫中。
一個基本的 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 -- 文件名
完全丟棄修改。
即已經提交到本地代碼庫了,不過尚未 push 到遠端。這時候可用 git reset
命令,命令格式爲:
git reset <要回滾到的 commit>
或者 git reset --hard <要回滾到的 commit>
需注意的是,提供的是 要回滾到的 commit,該 commit 以後的提交記錄會被丟棄。
示例:
git reset
默認會將被丟棄的記錄所改動的文件保留在工做區中,以便從新編輯和再提交。加上 --hard
選項則不保留這部份內容,需謹慎使用。
有時 commit 以後發現剛纔沒改全,想再次修改後仍記錄在一個 commit 裏。利用 "git reset" 可達到這個目的,不過,Git 還提供了更簡便的方法來修改最近一次 commit。
命令格式以下:
git commit --amend [ -m <commit說明> ]
若是命令中不加-m <commit說明>
部分,則 Git 拉起編輯器來輸入日誌說明。示例:
請注意,"git commit --amend" 只可用於修改本地未 push 的 commit,不要改動已 push 的 commit!
注意!此時不能用 "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 個父節點,按 一、2 數字排序,對於要回滾「分支合入主幹的 commit」,經常使用"-m 1",即用主幹記錄做爲主線。 回滾合併 commit 是一個較爲複雜的話題,做爲通常性建議,應避免回滾合併 commit。對該話題感興趣的可進一步瞭解: https://github.com/git/git/blob/master/Documentation/howto/revert-a-faulty-merge.txt
本節再來說一個示例,以便你們更好地理解git reset
和git revert
的差別。
分支初始狀態以下:
若是執行 git reset B
工做區會指向 B
,其後的提交(C、D)被丟棄。
此時若是作一次新提交生成 C1
,C1
跟 C、D 沒有關聯。
git revert B
回滾了B
提交的內容後生成一個新 commit E
,原有的歷史不會被修改。
雖然說 Git 是一款強大的版本管理工具,通常來講,提交到代碼庫的內容不用擔憂丟失,然而某些特殊狀況下仍免不了要作搶救找回,例如不恰當的 reset、錯刪分支等。這就是 git reflog
派上用場的時候了。
"git reflog"是恢復本地歷史的強力工具,幾乎能夠恢復全部本地記錄,例如被 reset 丟棄掉的 commit、被刪掉的分支等,稱得上代碼找回的「最後一根救命稻草」。
然而須要注意,並不是真正全部記錄"git reflog"都可以恢復,有些狀況仍然無能爲力:
非本地操做的記錄 "git reflog"能管理的是本地工做區操做記錄,非本地(如其餘人或在其餘機器上)的記錄它就無從知曉了。
未 commit 的內容 例如只在工做區或暫存區被回滾的內容(git checkout -- 文件 或 git reset HEAD 文件)。
過久遠的內容 "git reflog"保留的記錄有必定時間限制(默認 90 天),超時的會被自動清理。另外若是主動執行清理命令也會提早清理掉。
一個典型場景是執行 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 文件仍是須要的,因而將該文件版本單獨恢復到工做區中。
場景:用"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",你確定哪裏作錯了!