轉載自http://hittyt.iteye.com/blog/1961386
傳統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命令,命令的輸出上都會給你提示你需要干啥。只是這個過程一般被分為了兩步:
- 將index區域中修改過的文件移除index,也就是恢復到working tree中。這部用git reset來解決。
- 一旦文件重新回到working tree中,回滾操作就是上面提到的git checkout嘍。
這個看個截圖直觀點:
我working tree下的原始文件信息如下
我修改了a.txt和my_dir/b.txt,並將將他們加入了index區域,當前運行git status得到如下輸出
這里再執行git reset . 將當前目錄及子目錄內的所有修改移出index區域,再次運行git status命令
到這一步之后,就用上面提到git checkout就可以解決問題了。
commit之后的回滾
這種情形是git本地回滾里面最復雜,也是最容易讓人迷糊的了,因為針對不同的情況,方法比較多,所以不是很好記。
- 修改最后一次commit的記錄:很多時候先要回滾僅僅是因為自己對最后一次的commit的漏掉(注意,這里說的漏掉不僅僅是你少提交了文件的修改,也包括你多提交了一下你不想要提交的東西)了一些東西,想要回滾這次commit之后再重新commit。如果是這樣的話,沒有必要真的非要先回滾再重新commit。只要在在自己已經滿意了自己所有的修改之后,直接執行git commit --amend,就可以開啟上次提交的“補救”提交模式,然后把你對上次所有漏掉的東西加上去就好了。下面看個例子:我進行了一次錯誤的提交,修改的內容如下:
目前commit 記錄如下:
現在我想補救這次commit,相當於取消這次新加入的文件b.txt、取消對a.txt第三行的修改,然后加入我真正想要的修改:在my_dir下增加一個c.txt,並且修改a.txt的第三行為另外一句話。
再次通過git log查看commit記錄
請注意比較最新的一次commit的修改,其實已經被修改為另一個SHA1的值了。這里請注意,從某種意義上說(實際上這種替換在reflog中很容易追蹤到痕跡,只是在所有的commit逆向引用鏈條中,我們已經找不到之前的那個fad4...),這種操作已經做到了無痕修改最后一次提交。這和SVN的逆向merge是本質不同的。 - 回滾中間的某次提交(當然也包括最后一次):比如我想要回滾上圖中倒數第二次提交,就是HEAD^那次,我們先通過git show HEAD^看看那次提交都干了啥?
然后再通過git revert HEAD^ 來回滾這次操作,然后我們得到了下面的提示:
杯具,沖突了。。。其實,只要你熟悉任何一種VCS工具,想想這個場景,其實也是挺正常的。那就git status看看哪些個文件沖突了吧。
其實你只要仔細看看上面的說明信息,應該已經知道該怎么解決這個沖突了。明顯,a.txt是沖突發生的文件:
打開這個文件,可以看到標准的沖突標識文件。這里正是之前我們采用補救式提交方式修改的那句話。至於沖突怎么解決很容易,看你究竟想要啥了,自己去編輯,去掉沖突范圍標識符號,保存文件即可。然后按照git正常的流程再次提交,編輯提交的信息即可。再次提交之后的log信息如下:
上面我們基本上演示了一個標准的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):
從上面可以看出來,你的index區域忽然多了很多未提交的修改,這些就是回滾回來的記錄,要怎么處理他們,就看你的了。這時我再來看看log的記錄信息:
最新的提交已經變成我們希望的那次了。其實從git reset的解釋中,我們就可以看出,git reset是一個“斬斷”式的回滾操作,因為你把當前的HEAD指針直接移動到了你需要回滾到的那次記錄。而git本身的commit鏈條是逆向回溯的,所以你在提交歷史里面再也找不到當前HEAD指向的commit之后的記錄了。(不過如果你是git文藝青年的話,你當然知道,想找到那些表面上找不到的commit,通過reflog也是易如反掌)。
好了,到這里,常用的git回滾操作和場景都介紹完了。希望對不熟悉git的TX能有所幫助。