Git必備技之隨意撤銷

Git作為一個版本管理系統(VCS)最基本的功能就是保證所有操作的追蹤和回退。但是,很多初學者會被各種各樣的命令,各種引數給弄蒙了,加上命令列提示基本都是英文,所以就跟瞎子一樣,生怕一不小心把這弄丟了,一不小心把庫弄壞了,pull不了,也push不了,不知道怎麼辦了。實際上git中任何操作,包括破壞性、刪除性的操作都可以可以回退。本文中蟲蟲就帶大家瞭解Git中各種撤消回退大法,等你熟悉了這些方法,可以讓你放開手一心只搞程式碼,Git只是幫你插上翅膀,讓你在程式碼庫中自由的翱翔。
分支策略(工作流)
Git是一個去中心化的分散式版本控制系統,這意味著程式碼倉的版本控制不是統一的,各個客戶端都是一個完整的倉庫,然後互相通過Git伺服器基於commits進行變更的交換。為了避免這種多來源的變更導致混亂,開發人員必須遵循各自開發組工作流程(如果沒有的話,要制定一個),該流程取決於團隊內部工作流程:如何撤消、如何更改某些變化?如何提交PR/MR?如何稽核和合並分支?。

有三個典型的協作流程:Git flow,Github flow和 GitLab flow。利用可以在開發相同功能和無縫協作的情況下解決開發人員衝突和開發平衡。
Git flow
由Vincent Driessen提出,主要是建立兩個長期分支Master和Devlop開發分支,同時又臨時性的功能分支、補丁分支和預釋出分支。版本衍變流程圖如下:

GitHub flow
Github flow 是Github提出的相比較Git flow簡配版,是 Github網站使用的工作流程。

該流程簡練,主要為配合CI/CD(持續整合和釋出),實現快速迭代。
Gitlab flow
Gitlab flow結合了上面兩種流的優點,是gitlab產品使用的方法。Gitlab flow以master分支為基礎,只有master接受的commit才可以合併得到其他分支

在該流程下,版本釋出也是基於master分支來推進。

撤消本地的變化
在你沒有執行push變化推送到遠端庫(Git伺服器)之前,所有變化只會影響你自己,不會影響其他人。所以你可以隨意處置這些變化,就算全部丟了,也可以通過clone重新複製一份,你所丟失的只不過是本地一些變化,包括未push的歷史記錄(已經commit未push),本地索引區staging(已經add未commit)和工作區的變化(未add),以及我們在之前的文章《讓你提高工作效率的Git的技巧 》中提到的四種檔案狀態:已暫存 (Staged),已修改(Modified),未修改(Unmodified)以及未跟蹤(Untracked)。

對於不同的階段的變化,撤銷的方法也各不相同:未分級的本地更改(在您提交之前)
未commit的工作區檔案變化
對已經更改但還沒有新增到暫存區的變化時,Git本身提供一種解決方案來撤銷對某個檔案的更改。假如我們我們編輯了一個檔案vim <file>,沒有新增的暫存區,檔案應為unstaged檔案(如果檔案已建立,未跟蹤Untracked)。可以通過git statu命令檢視檔案狀態:

在該狀態下,你有三種方法選擇撤銷這個更改:
放棄所有本地更改,但儲存它們以便以後重複使用git stash 。
撤銷本地變化(永久丟棄)git checkout -- <file> 。
永久丟棄對所有檔案的所有本地更改 git reset --hard 。
快速儲存本地更改(git stash)
你在開發正當中,接到一個緊急bug修復任務。由於你功能沒完全寫,你沒法馬上就commit,但你需要換到另一個分支,以完成緊急的修復。這時候你就需要祭出神器git stash來儲存你時下的工作,切換到需要修復的分支,修復,commit,push。再用git stash pop回到你的工作狀態,繼續寫你的程式碼。
git stash 還支援如下其他操作:
git stash save xxx :可以在儲存時新增備註資訊(類似於commit資訊),這這樣對於多個stash管理和識別將更加方便,設定備註資訊後,可以在list中顯示這個資訊。

git stash list :列出所有以前暫儲過的工作狀態(支援多次的git stash暫存)。
git stash pop :使用者回到上一個儲存的工作狀態並將其從stash儲存列表中刪除(類似於陣列的pop的操作)。

git stash apply xxx : 回到指定的一個stach儲存列表的工作狀態,但將不會從stashed列表中刪除。

commit之前分階段的本地更改
假設已經添加了一些檔案到暫存區,但是你不想在該該次commit中包含他們。但是又不想撤銷對這些檔案的修改,只是想將他們從暫存區移除。當然如果你想撤銷這些變化的話,和上一部分提到那樣可使用git reset --hard或者git stash。讓我們回到我們示例倉庫:
首先,用git status 看看目前的狀態:

我要要從暫存去移除一些檔案,比如我們移除four.txt:
git reset HEAD four.txt
結果,被移除的檔案處於未跟蹤狀態:

如果不新增檔案,則會從暫存去移除所有的檔案,但是檔案修改都儲存,都位於工作區。
git rest

可以使用git stash 儲存所有工作區檔案變化,以及新增到暫存區的檔案狀態,這是工作區會回到上一次commit狀態的檔案狀態。但是可以之後隨時返回到該工作狀態。
如果要丟棄所有檔案變化,和暫存區的狀態,則使用git reset --hard。結果和上一個方法類似。但是沒有stash的暫存項,也無法在會到該工作狀態。
commit的本地變更
commit提交後,版本控制系統就會正式記錄該變化,以git物件的形式儲存修改,進入歷史存檔,可以push到遠端倉,並和其他人做版本交換。在未push之前,該的修改是仍未公開(無法與其他開發者交換)。所以,做撤消操作不會影響別人,我們可以隨意操作。一旦程式碼push到遠端倉,我們的做撤銷操作就要格外注意了,儘量不要影響,不然整改團隊都不能pull,push,別人會拿刀砍你,釀出血案的。

不修改歷史(revert)
在實際的使用中,有可能有些預先的commit可能最終不是預期要push到遠端倉的,或者是一個有bug的commit。這是我們可以可以簡單地用git revert commit-id撤銷這個commit。此命令會反轉該commit中的增加的git物件,並刪除commit,它不會修改git歷史記錄。
假設有以下順序提交的A,B,C,D,E提交:A-B-C-D-E,現在我們想要撤銷有問題程式碼的到B的commit。至於如何判別B是否bug問題,蟲蟲之前的文章中有提到過就是使用git bisect,這兒就不在詳述,可以關注蟲蟲,瀏覽以前的文章。git bisect A..E
bisect會我們通過二分法reset到A到E之間的一個commit,我們做測試,然後判斷程式碼是否正常,根據這個狀態來迭代知道找到問題的commit B。
撤銷B的commit引入的狀態,我們使用:
git revert B-commit-id
如果僅僅是撤銷部分檔案或者目錄,但是儲存在暫存區,則用:
git checkout B-commit-id <file>
如果要撤銷B的commit狀態,並且暫存區移除則使用rest
git reset B-commit-i <file>
還有一個方法,我們也可以選用,那就是建立一個新分支,從有問題的地方開一個bug分支。比如A-B-C-D歷史記錄,現在發現C和D有問題。這是除了我們重置到B commit,並強推 F(這會導致與其他開發人員衝突)。這是新的歷史揭露為A-B-F,大家都必須強制reset -f 才跟上你的push。另一個更可取得方法是,不改變當前的歷史,從B開始新建立一個新的分支,並在該分支commit F。
git checkout B-commit-i
git checkout -b new-path-of-feature
git commit -a

修改commit歷史(rebase)
還有一個常用的修改的命令就死git rebase。他也提供-i選項實現互動式的操作。在-i模式下,可以開啟一個編輯器你在該編輯器中使用一些指令來操作commit歷史:
reword commit資訊還,編輯最近一次commit的訊息。(可以用命令列git commit --amend)
edit 編輯提交內容(提交引入的修改)和訊息
squash 將多個提交合併為一個提交,並且提供自定義或整合的commit訊息
drop 刪除commit
我們來舉個例項。假想現在我們的倉庫歷史為all-hello-new,要刪除hello。
rebase當前提交範圍:
git rebase -i fbaf080184ed
命令開啟你預設的編輯器,你編輯其中只需寫下指令drop New,但你保留所有其他pick提交的預設內容。

儲存並退出編輯會自動執行rebase。結果:
如果你想修改commit hello中引入的東西,類似的方法:
git rebase -i fbaf08018
命令開啟編輯器,您可以在提交前編寫edit new,但保留所有其他pick提交的預設內容。

儲存並退出編輯器執行,進行編輯和提交更改:
git commit -a
反悔你的撤消
有時你撤銷了一些修改,但是又發現這些修改還是有用的,又想反悔。通過命令git reflog我們可以追回所有已經分離的(git log不顯示)commit-id。
要檢視儲存庫歷史記錄並跟蹤舊提交,可以使用以下命令:
git reflog show

輸出顯示儲存庫的歷史記錄。第一列為commit-id,其他列HEAD旁邊的數字表示之前commit了多少次,可以當做下表來做引用該次的commit,在git命令(commit,rebase,merge,...)中當做引數代替commit-id使用,最後一列該記錄的描述。
撤銷遠端倉中的變更
不修改commit歷史撤消遠端變更
這操作和修改本地提交的本地不修改歷史撤銷更改大致相同。它是撤消任何遠端庫上或者公共分支的commit的首選方法。對這種需求,最好的的方法是使用分支,分支使能夠在新開發中引入現有修改(通過合併)和還可以提供了明確的commit時間序列和開發結構。

要撤銷某些commit-id中引入的更改,我們可以通過revert簡單地建立的commit中恢復commit-id(置換新增和刪除),比如上圖的需求刪除B,我們可以通過revert
git revert B-commit-id
或建立一個新分支:
git checkout B-commit-id
git checkout -b new-path-of-feature
修改歷史記錄的撤消遠端更改

當你想隱藏遠端倉中有些敏感新的資訊時候,則必須要用該方法(一般不要用,不然結果見第三部的圖)。比如倉庫中包含了token,密碼,SSH私鑰等。這樣做的會讓你失去了真正的commit 歷史程序。還要注意的是,即便是修改了歷史記錄,commit被分離(detach了),依然可以通過commit-id訪問(git沒有執行自動清理分離commit之前)。 還有就是別人如果還沒有同步該修改,他客戶端裡的資訊也是完全的。
修改歷史記錄
確定好要修改的內容之後(歷史記錄的範圍或範圍舊提交),使用git rebase -i commit-id。然後,此命令將顯示所有提交當前版本選擇commit-id並允許修改,壓縮,刪除提交。
git rebase -i commit1-id..commit3-id
然後根據我們第三部分提到指令做修改。
修改後,通過git push -f強制推送到遠端庫生效( 慎用慎用慎用! )。
篩選要刪除的敏感資訊檔案(git filter-branch)
Git還允許從過去的提交中刪除敏感資訊。我們可以使用行git filter-branch,它允許我們對rebase歷史記錄做過濾。這個命令也是通過rebase修改歷史記錄,比如要刪除某些歷史記錄歷史檔案一共使用:
git filter-branch --tree-filter 'rm filename' HEAD
注意git filter-branch命令在大型庫上可會很慢。還有一些相對較快的,讓我們篩選特定檔案的第三方工具,比如 BFG Repo-cleaner 。
