Git如何回滾一次錯誤的合併

原文發表在知乎專欄 前端雜貨鋪, 歡迎關注個人專欄,轉載請註明出處

今天不說前端,來聊聊git吧。
發現如今的小孩,玩框架一套一套的,等到玩點實質的工程化的東西就不行了。
git 這麼好的工具,培訓班怎麼能夠忽視他的重要性呢?前端

再來聊聊git的工做流程

不少人對Git到底是一個怎樣的系統,仍是隻知其一;不知其二。
在這裏強烈建議你們先理解git的核心思想和工做原理,有過subversion或者perforce使用經驗的人更是須要摒棄以前所見所學,從新接受這樣一個新思想。
咱們再也不這裏贅述其幾本原理,咱們來介紹一下其簡單工做流程。
Git以一個自有的思惟框架管理着三個不一樣的盒子Commit HistoryINDEXWorking Directorygit

  • Commit History 歷史記錄,存儲着全部提交的版本快照,並由當前分支引用的指針HEAD指向該分支最新一條提交。
  • INDEX 索引,也叫暫存區域。它是一個文件,保存着即將提交的文件列表快照。
  • Working Directory 工做目錄,是從git倉庫壓縮數據當前版本中解包出來的文件列表。因此你在本地磁盤看到的你項目源碼的文件列表,其實就是git開放給你的一個沙盒。在你將文件的修改天道到暫存區域並將快照記錄到歷史以前,你能夠隨意更改。

理解了這三者的含義後,咱們試着來理解一下git的工做流程。
一切的開始,混沌之間,咱們要幹一件大事,在terminal裏面敲打了幾下鍵盤github

git init

混沌初開,幻化三界:HEADINDEXWorking Directory。這就是世界最開始的樣子git倉庫彷彿就是掌管三界之神。而Working Directory就是他分配給你生產和工做的地方,你能夠在這裏肆意的創造。而爲了安全和管理的有序咱們須要把咱們的添加與修改的文件交給git倉庫。Git首先會將修改的文件標記起來放入暫存區、而後git找到暫存區域的文件內容將其永久性的存儲爲快照到git倉庫,此時HEAD的指針指向這個最新的快照。緩存

如圖,總結下三個步驟安全

  1. 在工做目錄中修改文件。
  2. 暫存文件,將文件的快照放入暫存區域。git add
  3. 提交更新,找到暫存區域的文件,將快照永久性存儲到 Git 倉庫目錄 git commit

git 的基本工做流程就是在不斷的重複這三個步驟,最終git倉庫目錄造成了一個快照堆棧,每產生一次新的版本,HEAD就會指向這個版本。框架

這裏咱們建立了下面這些文件:工具

├── README.md
├── v1.js
├── v2.js
└── v3.js

造成了下圖的提交歷史學習

3aa5dfb v3  (<- HEAD)
        |
5aab391 v2
        |
ff7b88e v1
        |
95d7816 init commit

下面咱們來看看怎麼利用checkout、reset、revert 來操做這個倉庫目錄測試

checkout 、reset 仍是 revert ?

checkout

版本控制系統背後的思想就是「安全」地儲存項目的拷貝,這樣你永遠不用擔憂何時不可復原地破壞了你的代碼庫。當你創建了項目歷史以後,git checkout 是一種便捷的方式,來將保存的快照「解包」到你的工做目錄上去。
git checkout 能夠檢出提交、也能夠檢出單個文件甚至還能夠檢出分支(此處省略)。spa

git checkout 5aab391

檢出v2,當前工做目錄和5aab391徹底一致,你能夠查看這個版本的文件編輯、運行、測試都不會被保存到git倉庫裏面。你能夠git checkout master 或者 git checkout -回到原來的工做狀態上來。

git checkout 5aab391 v1.js

以檢出v2版本對於v1.js的改動,只針對v1.js這個文件檢出到5aab391版本。因此 它會影響你當前的工做狀態,它會把當前狀態的v1.js文件內容覆蓋爲5aab391版本。因此除非你清楚你在作什麼,最好不要輕易的作這個操做。但這個操做對於捨棄我當前的全部改動頗有用:好比當前我在v1.js上面作了一些改動,但我又不想要這些改動了,而我又不想一個個去還原,那麼我能夠git checkout HEAD v1.js 或者 git checkout -- v1.js

reset 重置

git checkout 同樣, git reset 有不少用法。

git reset <file>

從暫存區移除特定文件,但不改變工做目錄。它會取消這個文件的緩存,而不覆蓋任何更改。

git reset

重置暫存區,匹配最近的一次提交,但工做目錄不變。它會取消全部文件的暫存,而不會覆蓋任何修改,給你了一個重設暫存快照的機會。

git reset --hard

加上--hard標記後會告訴git要重置緩存區和工做目錄的更改,就是說:先將你的暫存區清除掉,而後將你全部未暫存的更改都清除掉,因此在使用前肯定你想扔掉全部的本地工做。

git reset <commit>

將當前分支的指針HEAD移到 <commit>,將緩存區重設到這個提交,但不改變工做目錄。全部 <commit> 以後的更改會保留在工做目錄中,這容許你用更乾淨、原子性的快照從新提交項目歷史。

git reset --hard <commit>

將當前分支的指針HEAD移到 <commit>,將緩存區和工做目錄都重設到這個提交。它不只清除了未提交的更改,同時還清除了 <commit> 以後的全部提交。

能夠看出,git reset 經過取消緩存或者取消一系列提交的操做會摒棄一些你當前工做目錄上的更改,這樣的操做帶有必定的危險性。下面咱們開始介紹一種相對穩妥的方式 revert

revert 撤銷

git revert被用來撤銷一個已經提交的快照。但實現上和reset是徹底不一樣的。經過搞清楚如何撤銷這個提交引入的更改,而後在最後加上一個撤銷了更改的 新 提交,而不是從項目歷史中移除這個提交。

git revert <commit>

生成一個撤消了 <commit> 引入的修改的新提交,而後應用到當前分支。

例如:

81f734d commit after bug
        |
3a395af bug
        |
3aa5dfb v3  (<- HEAD)
        |
5aab391 v2
        |
ff7b88e v1
        |
95d7816 init commit

咱們在3a395af 引入了一個bug,咱們明確是因爲3a395af形成的bug的時候,以其咱們經過新的提交來fix這個bug,不如git revert , 讓他來幫你剔除這個bug。

git revert 3a395af

獲得結果

cfb71fc Revert "bug"
        |
81f734d commit after bug
        |
3a395af bug
        |
3aa5dfb v3  (<- HEAD)
        |
5aab391 v2
        |
ff7b88e v1
        |
95d7816 init commit

這個時候bug的改動被撤銷了,產生了一個新的commit,可是commit after bug沒有被清初。

因此相較於resetrevert不會改變項目歷史,對那些已經發布到共享倉庫的提交來講這是一個安全的操做。其次git revert 能夠將提交歷史中的任何一個提交撤銷、而reset會把歷史上某個提交及以後全部的提交都移除掉,這太野蠻了。

另外revert的設計,還有一個考量,那就是撤銷一個公共倉庫的提交。至於爲何不能用reset,大家能夠本身思考一下。
下面咱們就用一個麻煩事(回滾一個錯誤的合併),來說解這個操做。

合併操做

相對於常規的commit,當使用git merge <branch>合併兩個分支的時候,你會獲得一個新的merge commit.
當咱們git show <commit>的時候會出現相似信息:

commit 6dd0e2b9398ca8cd12bfd1faa1531d86dc41021a
Merge: d24d3b4 11a7112
Author: 前端雜貨鋪 
...............

Merge: d24d3b4 11a7112 這行代表了兩個分支在合併時,所處的parent的版本線索。

好比在上述項目中咱們開出了一個dev分支並作了一些操做,如今分支的樣子變成了這樣:

init -> v1 -> v2 -> v3  (master)
           \      
            d1 -> d2  (dev)

當咱們在dev開發的差很少了

#git:(dev)
git checkout master 
#git:(master)
git merge dev

這個時候造成了一個Merge Commit faulty merge

init -> v1 -> v2 -> v3 -- faulty merge  (master)
           \            /
            d1  -->  d2  (dev)

此時faulty merge有兩個parent 分別是v3 和 d2。

回滾錯誤的合併

這個merge以後還繼續在dev開發,另外一波人也在從別的分支往master合併代碼。變成這樣:

init -> v1 -> v2 -> v3 -- faulty merge -> v4 -> vc3 (master)
        \  \            /                     /
         \  d1  -->  d2  --> d3 --> d4  (dev)/
          \                                 / 
           c1  -->  c2 -------------------c3 (other)

這個時候你發現, 媽也上次那個merge 好像給共享分支master引入了一個bug。這個bug致使團隊其餘同窗跑不通測試,或者這是一個線上的bug,若是不及時修復老闆要罵街了。

這個時候第一想到的確定是回滾代碼,但怎麼回滾呢。用reset?不現實,由於太流氓不說,還會把別人的代碼也幹掉,因此只能用revert。而revert它最初被設計出來就是幹這個活的。

怎麼操做呢?首先想到的是上面所說的 git revert <commit> ,可是貌似不太行。

git revert faulty merge
error: Commit faulty merge is a merge but no -m option was given.
fatal: revert failed

這是由於試圖撤銷兩個分支的合併的時候Git不知道要保留哪個分支上的修改。因此咱們須要告訴git咱們保留那個分支m 或者mainline.

git revert -m 1 faulty merge

-m後面帶的參數值 能夠是1或者2,對應着parent的順序.上面列子:1表明v3,2表明d2
因此該操做會保留master分支的修改,而撤銷dev分支合併過來的修改。

提交歷史變爲

init -> v1 -> v2 -> v3 -- faulty merge -> v4 -> vc3 -> rev3 (master)
          \            /                     
           d1  -->  d2  --> d3 --> d4  (dev)

此處rev3是一個常規commit,其內容包含了以前在faulty merge撤銷掉的dev合併過來的commit的【反操做】的合集。

到這個時候還沒完,咱們要記住,由於咱們拋棄過以前dev合併過來的commit,下次dev再往master合併,以前拋棄過的實際上是不包含在裏面的。那怎麼辦呢?

恢復以前的回滾

很簡單咱們把以前master那個帶有【反操做】的commit給撤銷掉不就行了?

git checkout master
git revert rev3
git merge dev

此時提交歷史變成了

init -> v1 -> v2 -> v3 -- faulty merge -> v4 -> vc3 -> rev3 -> rev3` -> final merge (master)
          \            /                                               /
           d1  -->  d2  --> d3 --> d4  --------------------------------(dev)

總結

以上就是我想要講的關於git回滾代碼的一些操做,有不對的地方還望指正。另Git 是一門藝術,是一種很是精妙的設計,當你使用上手後,你會發現愈來愈多好玩的東西,併爲設計git的人默默點個贊。也但願在前端領域不管是初學仍是深鑿者,在追逐流行框架的時候,都不要忘了學習這些基礎的工具。

參考

相關文章
相關標籤/搜索