前言
前情提要:Git應用詳解第三講:本地分支的重要操作
git
作為一款版本控制工具,其最核心的功能就是版本回退,沒有之一。熟悉git
版本回退的操作能夠讓你真真正正地放開手腳去開發,不用小心翼翼,怕一不小心刪除了不該刪除的文件。本節除了介紹版本回退的內容之外,還會介紹stash
的使用。
一、版本回退
在git
中永遠有后悔葯可吃,總是可以回到版本庫的某一個時刻,這就叫做版本回退;
如上圖所示:當前master
分支指針指向D
,通過版本回退可以使master
指向C
、B
或A
。進行版本回退的命令大體上有三種:reset
、revert
和checkout
。下面就來一一講解:
Ⅰ.git reset
1.參數
reset
命令可以添加很多參數,常用的有--mixed
、--soft
和--hard
三種。下圖為一次完整提交的四個階段:
三個參數大體上的區別為:
--mixed
:為默認值,等同於git reset
。作用為:將文件回退到工作區,此時會保留工作區中的文件,但會丟棄暫存區中的文件;--soft
:作用為:將文件回退到暫存區,此時會保留工作區和暫存區中的文件;--hard
:作用為:將文件回退到修改前,此時會丟棄工作區和暫存區中的文件;
下面就來詳細地講解它們的使用方法:
首先在master
分支進行四次提交,每次提交在test.txt
中添加一行文本信息:
--mixed
該參數為默認值,作用為:將文件回退到工作區中:如下圖所示,將test.txt
文件回退一次提交:
可以看到第四次提交對test.txt
的修改操作被回退到了工作區當中,並且保留了工作區中第四次提交對test.txt
所做的修改,所以工作區中的test.txt
文件內容與回退前一致。
--soft
該參數的作用為:將文件回退到暫存區中:如下圖所示,將test.txt
文件回退一次提交:
可以看到第四次提交對test.txt
的修改操作被回退到了暫存區當中,並且保留了工作區和暫存區中第四次提交對test.txt
所做的修改,所以,工作區中的文件內容與回退前一致。
--hard
該參數的作用為:將文件回退到修改前:如下圖所示,將test.txt
文件回退一次提交:
可以看到test.txt
直接回到了進行第四次提交前,此時刪除了工作區和暫存區中第四次提交對test.txt
所做的修改。所以,工作區變得干凈了,test.txt
文件內容回退到剛完成第三次提交時。
2.寫法
為了方便演示reset
的各種使用方法,下面的指令都采用--hard
參數。
git reset --hard HEAD^
該命令的作用為回退一次提交:
回退后的狀態為:
可以看到,該方法會同時改變了HEAD
和master
指針的指向;
git reset --hard HEAD^^
該命令的作用為回退兩次提交:
回退后的狀態為:
同樣,使用--hard
參數回退,工作區是干凈的;可以看到,該方法也會同時改變HEAD
和master
指針的指向;
git reset --hard HEAD~n
該命令的作用為回退n
次提交:
可以看到使用了--hard
參數,回退結果符合預期,並且該方法也會同步修改HEAD
和分支master
指針的指向。
注意:該方式只能向前回退,不能向后回退。
上述命令中的
HEAD
可以更換為分支名,比如master
:git reset --hard master~n
該命令表示將
master
分支回退n
次提交。由於HEAD
始終指向當前分支,所以使用分支名和使用HEAD
效果是一樣的。
git reset --hard commit_id
該指令的作用為回退到指定的commit id
的提交版本;由於commit id
是不會重復的,一般只需要寫前幾(6
)位就可以識別出來。通過commit id
的回退方式既可以向前回退,也可以向后回退。如下所示,從1st commit
往后回退到4th commit
,其中4th commit
的commit id = bdb373...
。
為了熟悉該指令,我們分兩種方式進行回退:使用--hard
參數與使用默認參數。
-
使用
--hard
參數從圖中可以看出:通過第四次提交的
commit_id: bdb373
順利地從第一次提交向后回退到了第四次提交,並且工作區干凈。該方法也同時修改了HEAD
和分支master
的指向,具體過程為: -
使用默認參數
可以看到切換回了
4th commit
,但是工作區的test.txt
文件並沒有變化;這是因為,在4th -> 1st
的過程中,需要在工作區中刪除test.txt
文件中的2nd line、3rd line、4th line
。通過默認參數--mixed
,將4th commit
對文件的修改回退到了工作區當中,如下圖所示:這個過程丟棄了暫存區中對文件的刪除操作,但是保留了工作區中對文件的刪除操作。所以,工作區中的
test.txt
文件仍然處於刪除了三行內容的狀態。此時只需要將修改操作從階段
1
移動到修改前的階段0
,即可將文件恢復到修改前的狀態,並清空工作區。可以采用git restore test.txt
實現:
Ⅱ.git revert
revert
是回滾,重做的意思。不同於reset
直接通過改變分支指向來進行版本回退,並且不產生新的提交;revert
是通過額外創建一次提交,來取消分支上指定的某次提交的方式,來實現版本回退的。如下圖所示,假如想要重做提交B
,重做前與重做后的狀態為:
所謂重做提交B
,指的是在新建的提交B'
中取消提交B
中所做的一切操作。也就是說revert
的思想為:通過創建一個新提交來取消不要的提交。所以,提交數會增加。
1.參數
git
同樣為revert
提供了許多參數,常用的有以下三種。為了演示它們的作用,首先需要設置對應的測試環境:在dev
分支上進行四次提交,每次提交都為test.txt
添加一行內容:
-e
-e
參數是--edit
的縮寫,為revert
指令的默認參數,即git revert -e
等同於git revert
。該參數的作用為在重做過程中,新建一次提交的同時編輯提交信息。比如通過以下命令重做上述的dev2
提交:
git revert f4a95
執行該指令后會創建一次新的提交來取消提交dev2
所做的一切操作,並且會進入vim
編輯器,編輯新提交的提交注釋:
如下圖所示,提交dev2
為文件test.txt
添加的dev2
文本被取消了,並且dev
分支上多了一次提交:
--no-edit
該參數的作用為不編輯由於revert
重做,所新增提交的注釋信息。如下圖所示,通過:
git revert --no-edit f4a95b
重做提交dev2
的過程中,並不會進入vim
編輯器編輯新增提交的注釋信息,而是采用默認的注釋信息:Revert "dev2"
:
-n
-n
參數是--no-commit
的簡寫形式,作用為對revert
重做某次提交時所產生的修改,不進行提交,也就是不會新增一次提交;
如下圖所示,這是revert
指令通過新建提交B'
來取消提交B
的過程,分為0~4
個階段。不添加-n
參數時,revert
指令會產生一次額外提交B'
,此時處於下圖中的第3
階段。而使用-n
參數時,雖然revert
指令也會通過新建提交B'
來重做提交B
。但是,此時還處於生成提交B'
的過程,還沒有完全生成提交B'
,也就是處於下圖中的第2
階段。
這種做法的好處是,允許我們干涉revert
重做過程,手動進行提交。如下圖所示,通過:
git revert -n f4a95
重做提交dev2
的過程中,手動暫停了重做過程。雖然提交dev2
對test.txt
所做的修改已被撤銷,但是這一重做操作還未進行提交:
這樣我們既可以修改重做過程中不滿意的地方,也可以隨意添加注釋。修改完后,通過手動提交的方式,完成重做(REVERTING
)操作:
2.寫法
revert
指令也有多種寫法,下面介紹主要的幾種。為了方便演示,下列指令都采用默認參數-e
手動編輯每次新增提交的注釋信息。
git revert commit_id
這是最常用的寫法,通過commit_id
精准地選擇想要重做的提交。分兩種情況:
-
情況一:重做最新一次提交,不會發生沖突。
例如:通過以下指令,重做
dev
分支上最新的一次提交dev2
:git revert f4a95b
首先進入
vim
編輯器編輯新增提交的注釋信息:隨后完成重做操作,如下圖所示;可見提交
dev2
給test.txt
添加的dev2
內容被刪除了,並且多出一次提交,說明重做成功: -
情況二:重做非最新一次提交,會發生沖突。
例如:通過以下指令,重做
dev
分支上的第三次提交dev1
:git revert dbde45
會出現合並沖突:
使用
git mergetool
指令,通過vim
編輯器的工具vimdiff
顯示沖突文件test.txt
:回車進入
vim
編輯器界面,解決沖突:解決沖突之后,手動進行一次提交,完成
revert
過程: -
為什么會出現沖突?
通過上面的例子不難看出,
revert
操作生成的新提交其實是通過兩次提交合並而成的。如下圖所示:- 首先,將被重做的提交
dev1
的前一次提交2nd
復制一份,即圖中的2nd'
; - 然后,將它與當前分支的最新提交
dev2
進行合並,由此生成revert
操作新增的提交;
知道了
revert
操作新增的提交的由來后,就不難解釋為什么會出現合並沖突了,如下圖所示:合並的兩次提交中,文件
test.txt
的內容不一樣。git
不知道以哪個版本為准,自然會導致自動合並失敗,需要手動合並。 - 首先,將被重做的提交
git revert HEAD
該指令的作用為重做所在分支的最新一次提交,並且不會發生沖突:
git revert HEAD^
該指令的作用為重做所在分支的倒數第二次提交,會發生沖突,需要手動合並,完成重做操作:
git revert HEAD^^
該指令的作用為重做所在分支的倒數第三次提交,會發生沖突,需要手動合並,完成重做操作:
git revert HEAD~n
該指令的作用為重做所在分支的倒數第n+1
次提交,會發生沖突,需要手動合並,完成重做操作。過程與上述一致,這里就不再贅述了。
總結:常用
git revert commit_id
這種方式。
3.撤銷revert
操作
思路很簡單,再次通過revert
操作取消上一次的revert
操作(即所謂"負負得正")。
操作前,dev
分支上的提交記錄和test.txt
文件內容如下:
通過:git revert --no-edit f4a95
重做提交dev2
(--no-edit
表示不修改新增提交的注釋):
重做后,多了一次提交,並且test.txt
文件中刪除了dev2
這一行內容。此時,可以通過:
git revert --no-edit 582d127
重做上一次重做操作,以此達到取消上一次重做操作的目的:
如上圖所示,雖然多出了一次提交,但是test.txt
文件中被刪除的dev2
內容被恢復了,這樣就撤銷了revert
操作。
Ⅲ.git checkout
1.git checkout commit_id
使用checkout
可以進行版本回退,如直接使用:
git checkout cb214
回退到提交3rd
,此時會出現如下提示:
注意到,切換后HEAD
指向的不再是master
分支,而是cb214...
即第三次提交,查看歷史提交記錄:
可看到只有3
次提交,什么意思呢?如下圖所示:
通過git checkout
讓HEAD
指針指向了第3
次提交,可以將它想象為一個新的分支。但是卻沒有實際創建分支,即此時head
指向的由提交1~3
組成的commit
對象鏈條處於游離狀態;
接着,在HEAD
還指向游離的提交節點3
的基礎上對文件做出新的修改:
- 此時如果我們切換回
master
分支,會出現下列錯誤
提示顯示:如果沒有保存就從游離的提交上切換到master
分支,這一修改就會被checkout
命令覆蓋。我們可以在切換前進行一次提交操作:
此時的狀態為:
- 在游離的
Commit
對象鏈中進行了一次提交之后,再次通過:git checkout master
切換到master
分支:
提示大意為:如果沒有任何分支指向剛才在游離的Commit
對象鏈中進行的提交,那么該提交就會被忽略。此時的狀態如下圖所示:
如果想要創建一個分支保存(指向)這條游離的Commit
對象鏈,現在就是很好的時機。根據上述提示的命令:
git branch mycommit c4d5cc3
創建指向commit_id
為c4d5cc3
的提交(即上述的提交節點5
)的分支mycommit
:
由此游離的commit
對象鏈得以被新分支所指向,並得到了保存,此時的狀態如下圖所示:
總結:
通過
checkout
進行版本回退會造成游離的提交對象鏈,需要額外創建一個分支進行保存;因此,使用
checkout
進行版本回退的思路為,先切換到想要回退的提交版本,再刪除進行版本回退的分支dev
。最后,創建一個新的dev
分支指向游離的提交對象鏈,完成分支dev
的版本回退,簡稱"偷天換日";只要有分支指向,提交就不會被丟棄。
Ⅳ.revert
與reset
的選擇
由於checkout
會造成游離的提交對象鏈,所以,一般不使用checkout
而是使用reset
和revert
進行版本回退:
-
revert
通過創建一個新提交的方式來撤銷某次操作,該操作之前和之后的提交記錄都會被保留,並且會將該撤銷操作作為最新的提交; -
reset
是通過改變HEAD
和分支指針指向的方式,進行版本回退,該操作之后的提交記錄不會被保留,並且不會創建新的提交;
在個人開發上,建議使用reset
;但是在團隊開發中建議使用revert
,特別是公共的分支(比如master
),這樣能夠完整保留提交歷史,方便回溯。
Ⅴ.回退方法匯總
版本回退主要有三大方式:reset
、revert
和checkout
,各方式的比較如下:
方法 | 效果 | 向前回退 | 向后回退 | 同步修改HEAD 與分支指向 |
---|---|---|---|---|
git reset --hard HEAD^ |
往前回退1 次提交 |
能 | 否 | 是 |
git reset --hard HEAD^^ |
往前回退2 次提交 |
能 | 否 | 是 |
git reset --hard HEAD~n |
往前回退n 次提交 |
能 | 否 | 是 |
git reset --hard <commit_id> |
回退到指定commit id 的提交 |
能 | 能 | 是 |
git revert HEAD |
重做最新一次提交 | 能 | 否 | 是 |
git revert HEAD^ |
重做倒數第二次提交 | 能 | 否 | 是 |
git revert HEAD^^ |
重做倒數第三次提交 | 能 | 否 | 是 |
git revert HEAD~n |
重做倒數第n+1 次提交 |
能 | 否 | 是 |
git revert commit_id |
重做指定commit_id 的提交 |
能 | 能 | 是 |
git checkout commit_id |
回退到指定commit id 的提交 |
能 | 能 | 否 |
從上表可知,只有下列三種方式可以自由地向前向后回退:
git reset --hard commit_id
git revert commit_id
git checkout commit_id
但是,使用checkout
進行回退會出現游離的提交,需要創建一個新分支進行保存,所以不常用。
二、git stash
1.git stash
的作用
git stash
指令的作用為:對沒有提交到版本庫的,位於工作區或暫存區中游離的修改進行保存,在需要時可進行恢復。具體應用場景如下:
在master
分支進行兩次提交:1st
和2nd
,隨后創建並切換到dev
分支。在dev
分支上進行一次提交(dev1
),此時兩分支的狀態為:
隨后在dev
分支上給文件test.txt
添加一行dev2
,但是不提交到暫存區,直接切換到master
分支,會出現如下錯誤:
圖中顯示的錯誤大意為:在dev
分支上的修改會被checkout
操作覆蓋。下面我們來看看,將dev
分支上的這一修改操作添加到暫存區后,再切換分支,是否還會出現同樣的問題:
可見還是會出現該錯誤,這初步驗證了位於工作區和暫存區中的修改都會被checkout
操作覆蓋的結論。原因如下圖所示:
雖然在dev
分支上修改了文件,但是沒有將這一修改操作進行提交。這樣就不會產生提交節點,就如上圖所示,修改dev2
是游離的,在切換分支的時候會被丟棄。
這種情況在日常開發中很常見,當在develop
分支上開發新功能的時候,master
分支出現緊急情況需要切換回去進行修復。但是,當前分支的新功能還沒開發完全,貿然切換分支,原來開發的內容就會因被覆蓋而丟失,怎么辦呢?
有人可能會說進行一次commit
不就可以了嗎?確實可以。但是,這樣不符合提交的代碼就是正確代碼的原則。更好的解決方法為使用git stash
,如下圖所示:
可見git stash
可以將當前dev
分支上,位於在工作區或暫存區中的修改,在未提交的情況下進行了保存;並且將分支回退到修改前的狀態,保存過后,就可以很順暢地切換回master
分支了。
圖中的
WIP
(working in progress
)表示的是正在進行的工作;
當我們在master
分支上完成了工作,再次切換回dev
分支時,查看test.txt
文件:
發現切換分支前所做的修改dev2
消失了,這是為什么呢?
-
其實,上面通過
git stash
將dev
分支上工作區或暫存區中的修改,提交到了stash
區域進行保存,並將dev
分支回退到修改前的狀態。如下圖所示: -
切換到
master
分支時test
分支上的修改依舊會被覆蓋。所以,再次回到dev
分支時需要從stash
區域中恢復切換分支前保存的修改;
怎樣恢復通過git stash
保存到stash
中的修改呢?可以通過:
git stash list
查看該分支上被stash
保存的修改:
繼續給test.txt
文件添加內容:dev3
,並通過以下指令保存修改的同時添加注釋:
git stash save '注釋'
- 首先,通過上述命令可以修改
stash
中存儲修改的備注信息; - 其次,雖然在
test
分支上進行了兩次修改,但是使用git stash
保存修改后,文件test.txt
並沒有實際被修改;
2.恢復stash
存儲的修改
方法有很多,主要有以下三種:
git stash pop
如圖所示,通過上述命令將stash
中存儲的最新一次修改恢復了。相信你已經發現了,stash
與棧非常類似:先保存的修改,排在最后,序號最大;后保存的修改,排在最前,序號最小;
恢復了最新一次修改后,再次查看stash
:
可以看到存儲的修改只剩下一條了,由此可推斷出git stash pop
作用為:
- 第一:恢復
stash
中存儲的最新一次修改; - 第二:將該修改從
stash
中刪除;
git stash apply
如上圖所示,使用該指令時發生了合並沖突。這是因為,stash
中保存的每一次修改代表的都是一個版本。
-
如上圖所示,在
test
分支上,進行第一次修改后,通過git stash
將該修改作為修改0
保存到stash
中,此時分支中的文件並沒有發生改變; -
進行第二次修改后,通過
git stash
將修改作為修改1
保存到stash
中,分支中的文件依舊沒有發生改變;此時的stash
中相當於保存着同一分支上兩個修改后的版本; -
此時通過
git stash pop
取出修改0
,與test
分支進行合並;再通過git stash pop
取出修改1
,再次與test
分支進行合並,兩個版本合並自然會產生沖突。
手動解決沖突后,要進行一次提交才算完成了手動合並;隨后查看stash
:
修改0
仍然存在,說明git stash apply
的作用為取出stash
中最新(前面)的修改並與分支進行合並。但是,stash
中存儲的該修改並不會被刪除;
git stash apply stash@{n}
這是最常用的方法,作用為從stash
中恢復特定的修改,並且不刪除stash
中的該修改。
將test.txt
的兩次修改通過git stash
存儲到stash
中,如下圖所示:
通過git stash apply stash@{1}
恢復stash
中存儲的修改1
:
如上圖所示,成功地恢復了stash
中的修改1
,並且stash
中的修改1
並沒有被刪除;
總結:
git stash pop
:恢復並刪除stash
中存儲的最新修改;git stash apply
:恢復但不刪除stash
中存儲的最新修改;git stash apply stash@{0}
:恢復但不刪除stash
中存儲的特定提交;
以上就是這一節的全部內容了,相信看到這里的你已經能夠熟練地使用
Git
進行版本回退了。下一節將會介紹大名鼎鼎的Github
與Git
的圖形化操作界面。期待與你再次相見!