天天看點

Git復原的常用手法

傳統VCS的復原操作

對于版本控制系統VCS來說,復原這個操作應該是個很普通也是很重要的需求。

如果你是傳統VCS,比如SVN或者P4來說,revert是個最直覺,也是最直接的手段,當然前提是你的修改還沒有被送出到遠端的中央倉庫。

如果你已經ci了你的code到了遠端中央倉庫,那revert恐怕也無能為力,隻能借助其他指令workaroud這個問題,比如:你用SVN的話,就得來個逆向merge操作,把所有的修改都merge回去。

但這樣做也有一些弊端:

這次merge會作為一次全新的commit記錄記錄下來,也就是說它不能真正從你的曆史記錄裡面抹掉你那次不想要的修改。通常情況下其實也沒啥大不了的,除非你個人潔癖就是不想看到以前的那次commit記錄或者你真的幹了啥不想讓别人知道的事情。

Git時代的復原操作

但當發展到git時代,這種復原操作的複雜度,已經随着git模型本身的特點,變得不那麼簡單了。

熟悉git的人都知道,為了分布式的需求,git将每一個網絡節點作為了一個完整的VCS,也就是每個單台的host在沒有網絡的前提下,都是一個不受任何影響可以滿足除了和其他節點同步(比如:git pull/push這類)之外的幾乎所有操作。

為了達到這種效果,git不僅在本地有一個完整的local repository,而且将原本簡單的working tree(或者叫working directory)也切成了兩塊區域——working tree和index(也叫stage)。

這樣,光從本地修改的角度來看,你的修改就可能存在三塊區域中,working tree、index或者commit之後的曆史對象區域。下面我們一個一個各個區域一般都怎麼復原。

working tree内的復原

這個屬于最簡單一種情形,本質上說也是和傳統VCS中revert直接對應的一種場景,隻是這裡不叫revert了,而是git checkout,這種情形很簡單,這裡就不做截圖展示了。列出依稀常用的指令形式如下:

  • git checkout file1 (復原單個檔案)
  • git checkout file1 file2 ... fileN (一次復原多個檔案,中間用空格隔開即可)
  • git checkout . (直接復原目前目錄一下的所有working tree内的修改,會遞歸掃描目前目錄下的所有子目錄)

index内的復原

這部分復原也不複雜,因為這部分的復原,隻要你勤快點使用git status指令,指令的輸出上都會給你提示你需要幹啥。隻是這個過程一般被分為了兩步:

  1. 将index區域中修改過的檔案移除index,也就是恢複到working tree中。這部用git reset來解決。
  2. 一旦檔案重新回到working tree中,復原操作就是上面提到的git checkout喽。

這個看個截圖直覺點:

我working tree下的原始檔案資訊如下

Git復原的常用手法

 我修改了a.txt和my_dir/b.txt,并将将他們加入了index區域,目前運作git status得到如下輸出

Git復原的常用手法

 這裡再執行git reset . 将目前目錄及子目錄内的所有修改移出index區域,再次運作git status指令

Git復原的常用手法

 到這一步之後,就用上面提到git checkout就可以解決問題了。

commit之後的復原

這種情形是git本地復原裡面最複雜,也是最容易讓人迷糊的了,因為針對不同的情況,方法比較多,是以不是很好記。

  • 修改最後一次commit的記錄:很多時候先要復原僅僅是因為自己對最後一次的commit的漏掉(注意,這裡說的漏掉不僅僅是你少送出了檔案的修改,也包括你多送出了一下你不想要送出的東西)了一些東西,想要復原這次commit之後再重新commit。如果是這樣的話,沒有必要真的非要先復原再重新commit。隻要在在自己已經滿意了自己所有的修改之後,直接執行git commit --amend,就可以開啟上次送出的“補救”送出模式,然後把你對上次所有漏掉的東西加上去就好了。下面看個例子:我進行了一次錯誤的送出,修改的内容如下:
    Git復原的常用手法
     目前commit 記錄如下:
    Git復原的常用手法
     現在我想補救這次commit,相當于取消這次新加入的檔案b.txt、取消對a.txt第三行的修改,然後加入我真正想要的修改:在my_dir下增加一個c.txt,并且修改a.txt的第三行為另外一句話。
    Git復原的常用手法
    Git復原的常用手法
     再次通過git log檢視commit記錄
    Git復原的常用手法
     請注意比較最新的一次commit的修改,其實已經被修改為另一個SHA1的值了。這裡請注意,從某種意義上說(實際上這種替換在reflog中很容易追蹤到痕迹,隻是在所有的commit逆向引用鍊條中,我們已經找不到之前的那個fad4...),這種操作已經做到了無痕修改最後一次送出。這和SVN的逆向merge是本質不同的。
  • 復原中間的某次送出(當然也包括最後一次):比如我想要復原上圖中倒數第二次送出,就是HEAD^那次,我們先通過git show HEAD^看看那次送出都幹了啥?
    Git復原的常用手法
     然後再通過git revert HEAD^ 來復原這次操作,然後我們得到了下面的提示:
    Git復原的常用手法
     杯具,沖突了。。。其實,隻要你熟悉任何一種VCS工具,想想這個場景,其實也是挺正常的。那就git status看看哪些個檔案沖突了吧。
    Git復原的常用手法
     其實你隻要仔細看看上面的說明資訊,應該已經知道該怎麼解決這個沖突了。明顯,a.txt是沖突發生的檔案:
    Git復原的常用手法
     打開這個檔案,可以看到标準的沖突辨別檔案。這裡正是之前我們采用補救式送出方式修改的那句話。至于沖突怎麼解決很容易,看你究竟想要啥了,自己去編輯,去掉沖突範圍辨別符号,儲存檔案即可。然後按照git正常的流程再次送出,編輯送出的資訊即可。再次送出之後的log資訊如下:
    Git復原的常用手法
     上面我們基本上示範了一個标準的revert場景(包括了沖突解決),從這個過程可以看出,git revert和SVN的逆向merge幾乎如出一轍,就是将你需要復原的那次commit所做的所有操作,反向操作一次,然後重新做一一個單獨的commit對象進行送出。這個過程是否發生沖突,就取決于你的修改了。請注意,這個過程你雖然復原了你不想要的修改内容,但是你沒法抹掉那次commit在history中的資訊,請注意上圖的第三行,他依舊堅挺的躺在那裡。這個也是git revert的特點。當然,git revert實際上也提供-n(--no-commit)參數,用來表示僅将revert的修改展現在目前的working tree,不自動進行送出。但是如果你真的想復原那些修改的話,再次commit這個環節是逃不掉的。
  • 復原最後的N次送出(永遠從commit的history中抹掉這些記錄):這種場景就輪到git reset登場了。git reset的幫助文檔寫的非常清楚,在復原commit的場景中,他的作用就是将目前的HEAD reset到你指定的那個分支。但這個過程中最值得注意的就是你使用的參數,最常用的主要是--soft(個人推薦使用這個,他不會修改你目前index或者working tree中所做的任何修改)/--mixed(你在reset時不加任何參數時的預設行為,會默默把你在index中的修改給滅了!)/--hard(這個是我絕的最危險的參數,會把你index和working tree中的所有修改毀滅的毛都不剩,使用之前請三思,這确實是你要的行為!)這三種。因為我推薦使用--soft參數,下面主要示範復原到3f412...那次的記錄(git reset --soft HEAD~2):
    Git復原的常用手法
     從上面可以看出來,你的index區域忽然多了很多未送出的修改,這些就是復原回來的記錄,要怎麼處理他們,就看你的了。這時我再來看看log的記錄資訊:
    Git復原的常用手法
     最新的送出已經變成我們希望的那次了。其實從git reset的解釋中,我們就可以看出,git reset是一個“斬斷”式的復原操作,因為你把目前的HEAD指針直接移動到了你需要復原到的那次記錄。而git本身的commit鍊條是逆向回溯的,是以你在送出曆史裡面再也找不到目前HEAD指向的commit之後的記錄了。(不過如果你是git文藝青年的話,你當然知道,想找到那些表面上找不到的commit,通過reflog也是易如反掌)。

好了,到這裡,常用的git復原操作和場景都介紹完了。希望對不熟悉git的TX能有所幫助。