概述
這次主要來講講Git
的反悔操作,自己平時(shí)在寫代碼的過程中經(jīng)常會(huì)出現(xiàn)想要棄用所有的改動(dòng)或回滾到上一次commit
的情況。Git
上的反悔操作有reset
、rebase
、revert
等,每個(gè)操作各有區(qū)別和對(duì)應(yīng)的使用場(chǎng)景,這里做下總結(jié)。
Git
的反悔操作有兩大類:
- 撤銷改動(dòng) ( Undoing Change )
- 重寫歷史 ( Rewriting History )
文章大部分翻譯于 Undoing Changes 和 Rewriting history,并結(jié)合了自己的一些理解和補(bǔ)充。
撤銷改動(dòng)(Undoing Change)
git checkout
git checkout
有三個(gè)不同的功能:切換分支、回滾至某個(gè)commit、回滾一個(gè)文件至某個(gè)commit。切換分支是git checkout
最常見的功能,不做介紹,這里主要介紹下它在撤銷文件改動(dòng)上的應(yīng)用。
回滾至某個(gè)commit
git chekcout <commit>
上面的命令是回滾到工作目錄中指定的 commit 上,這是一個(gè) 只讀 操作,不會(huì)影響到當(dāng)前工作區(qū)的狀態(tài),它在你查看舊版本的文件時(shí)不會(huì)損壞你的代碼倉庫。通常,HEAD
指向master分支或其他本地分支,當(dāng)使用git checkout
回滾到以前的 commit 時(shí),HEAD
就不再指向某個(gè)分支了,而是直接指向一個(gè)commit,這時(shí)就叫做detached HEAD
狀態(tài)。
切換到detached HEAD
狀態(tài)時(shí),會(huì)有一個(gè)警告。
這個(gè)警告是告訴你,你現(xiàn)在做的所有事情與你開發(fā)項(xiàng)目的其余工作區(qū)是分離的,即所有的改動(dòng)與本地倉庫的任一分支都無關(guān),不會(huì)影響到其他的分支的狀態(tài)。如果你準(zhǔn)備在detached HEAD
狀態(tài)下開發(fā)新的feature,那將會(huì)沒有分支允許你回退這里,當(dāng)你不可避免地切換到其他分支時(shí),將沒有任何辦法引用到這個(gè)feature。你可以把detached HEAD
狀態(tài)看作是正在一個(gè)未命名的分支上。
HEAD 和 detached HEAD 的區(qū)別可以參考 How can I reconcile detached HEAD with master/origin?
將英文翻譯為中文經(jīng)常會(huì)詞不達(dá)意,很難把握,建議還是看英文原文:)。
示例
假設(shè)你正在進(jìn)行一次瘋狂的重構(gòu),但現(xiàn)在你不確定是否要繼續(xù)下去。這時(shí)你想要看一下開始這次重構(gòu)之前項(xiàng)目原來的樣子,首先你需要找到你想要查看的版本的ID。
git log --oneline
假設(shè)你的項(xiàng)目歷史看起來像下面這樣:
b7119f2 Continue doing crazy things
872fa7e Try something crazy
a1e8fb5 Make some important changes to hello.py
435b61d Create hello.py
9773e52 Initial import
你可以使用git checkout
查看Make some important changes to hello.py
這次commit,如下:
git checkout a1e8fb5
這讓你的工作區(qū)切換到了a1e8fb5
comimit的狀態(tài)。你可以查看文件、編譯項(xiàng)目、運(yùn)行測(cè)試用例,甚至編輯文件,完全不用擔(dān)心丟失項(xiàng)目“當(dāng)前”的狀態(tài),你在這里做的所有修改都不會(huì)被保存到項(xiàng)目中。當(dāng)你想要繼續(xù)那次瘋狂的重構(gòu)時(shí),你需要回到項(xiàng)目的“當(dāng)前”狀態(tài)。
git checkout master
回滾一個(gè)文件至某個(gè)commit
git checkout <commit> <file>
回滾一個(gè)文件到以前的一個(gè)版本,這個(gè)操作會(huì) 影響 當(dāng)前工作區(qū)的狀態(tài)。
你可以在一個(gè)新的快照中重新提交這個(gè)舊版本,當(dāng)然也包含其他任何文件。實(shí)際上,checkout
的這個(gè)用法和revert
類似,只不過是僅針對(duì)一個(gè)文件。
示例
如果你只對(duì)單個(gè)文件感興趣,你可以使用 git checkout
獲取到該文件的舊版本。比如,如果你只想要看看 某次commit下的hello.py
文件,可以使用下面的命令:
git checkout a1e8fb5 hello.py
記住,不像切換commit,這會(huì)影響當(dāng)前項(xiàng)目的狀態(tài)。這個(gè)舊版本的文件的狀態(tài)會(huì)變?yōu)?Change to be committed
,給你一個(gè)機(jī)會(huì)將該文件恢復(fù)到先前的版本。
如果你決定不需要保留這個(gè)舊版本了,你可以切換到最近的版本,如下:
git checkout HEAD hello.py
git revert
git revert
可以撤銷一個(gè)已提交的快照(snapshot),但它解決的是如何撤銷已提交的被引入的改動(dòng),并生成內(nèi)容來追加一個(gè)新的提交,而不是從項(xiàng)目的歷史中移除這個(gè)提交,這避免了丟失歷史記錄,這對(duì)于項(xiàng)目的每一次修改的歷史記錄的完整性來說非常重要,并這是服務(wù)于可靠的多人協(xié)作開發(fā)的。
git revert <commit>
這句命令會(huì)撤銷這次<commit>所有被引入的改動(dòng),生成一個(gè)新的commit,并應(yīng)用在當(dāng)前分支上。
當(dāng)你想從你的項(xiàng)目歷史中移除一個(gè)完整的commit時(shí),就應(yīng)該使用git revert
。比如,你正在追蹤一個(gè)Bug并發(fā)現(xiàn)它是在一次單一的commit中被引入的,你可以手動(dòng)進(jìn)行修改,刪除有Bug的代碼來修復(fù)它,然后提交一個(gè)新的快照,但這樣很麻煩,效率也很低,你更應(yīng)該做的是,使用git revert
自動(dòng)完成,撤銷這次commit所有被引入的改動(dòng)。
Reverting vs. Resetting
很重要的一點(diǎn),revert
是對(duì)一次單一的commit的撤銷,并不是真正意義上的回滾。它不是通過移除項(xiàng)目中一次commit后面的所有提交來“回滾”之前的狀態(tài),實(shí)際上那樣的操作在Git
上被叫做reset
,而不是revert
。
比起reset
,revert
有兩個(gè)重要的好處:
revert
不會(huì)改變項(xiàng)目的歷史。如果那些commits已經(jīng)推到了共享的代碼倉庫,它會(huì)是一個(gè)“安全”的操作。為什么改變共享代碼倉庫的歷史是危險(xiǎn)的,請(qǐng)看后面的git reset
的介紹。revert
可以作用于歷史中 任意 的單一的commit節(jié)點(diǎn),然而reset
只能做到從當(dāng)前 最新 的commit開始回滾。比如說,如果你想要只撤銷一次舊的指定的commit,使用git reset
,你則必須移除該commit和該commit之后出現(xiàn)的所有commits,然后再把那些隨后的commit重新提交。毫無疑問,這種撤銷的方式一點(diǎn)都不優(yōu)雅。
示例1
下面的例子是git revert
的一個(gè)簡(jiǎn)單示例,提交了一個(gè)快照,然后立即使用revert
撤銷了它。
# Edit some tracked files
# Commit a snapshot
git commit -m "Make some changes that will be undone"
# Revert the commit we just created
git revert HEAD
注意:在
revert
后,第4次commit仍然被保留在項(xiàng)目歷史中,git revert
新增了一個(gè)新的commit來撤銷它的改動(dòng),而不是刪除它。結(jié)果就是,第3次和第5次commit的代碼是完全一樣的,第4次commit依然保留在歷史中,以防我們想要重新回滾到這里。
示例2
假設(shè)你發(fā)現(xiàn)在某次commit中引入了一個(gè)bug,你想使用 git revert
來回滾。查看歷史:
git log --oneline
項(xiàng)目歷史如下:
417e4a9 commit 4
427d76b commit 3
1642475 introduced a bug
71d3ef7 commit 1
bf4f6f6 git initial
使用 revert
回滾到 1642475
git revert 1642475
但你會(huì)發(fā)現(xiàn)沒有想象中那么簡(jiǎn)單,而是發(fā)生沖突了,報(bào)錯(cuò)如下:
error: could not revert 1642475... introduced a bug
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'
revert
僅僅是撤銷introduced a bug
這一commit的改動(dòng),默認(rèn)會(huì)生成一個(gè)新的commit提交,但在它之后還有commit 3
和commit 4
,它們的改動(dòng)不會(huì)被影響,依然保留在工作區(qū)中,因此產(chǎn)生了沖突。你可以手動(dòng)解決沖突后commit,但這卻是個(gè)麻煩且不優(yōu)雅的方式。因?yàn)?code>1642475、427d76b
和417e4a9
這幾個(gè)commit的改動(dòng)被一起合并在暫存區(qū)中,如果你修改的不止一個(gè)文件,那手動(dòng)解決沖突將會(huì)非常麻煩。解決方式是,默認(rèn) 不 生成新的commit,并按順序回滾。
先強(qiáng)制結(jié)束revert
git revert --abort
按順序回滾
git revert 417e4a9 --no-commit
git revert 427d76b --no-commit
git revert 1642475 --no-commit
git revert --continue
git revert --continue
,會(huì)生成帶默認(rèn)message的commit。更多參數(shù)說明詳見:git-revert-document
git reset
如果git revert
是以一個(gè)"安全""的方式來撤銷改動(dòng),那你可以認(rèn)為git reset
是一種 危險(xiǎn) 的方式。當(dāng)你使用git reset
后,將沒有辦法恢復(fù)原樣,它是一個(gè)永恒的撤銷,因?yàn)槟切ヽommits不再被任何ref
或reflog
引用。在使用這個(gè)工具時(shí)請(qǐng)務(wù)必謹(jǐn)慎,因?yàn)樗?code>git命令中唯一一個(gè)潛在的使你的努力付諸東流的命令。
git reset
是一個(gè)功能豐富的命令,它可以用于移除已提交的快照,但它更多的是用來撤銷暫存區(qū)和工作區(qū)的改動(dòng),另一種情況是,它應(yīng)該只用于撤銷本地的改動(dòng)(不應(yīng)該reset
那些已經(jīng)與其他開發(fā)者共享了的快照)。
用法
git reset <file>
從暫存區(qū)中移除指定的文件,但保留工作區(qū)不變。它unstage
了 一個(gè) 文件且沒有覆蓋任何改動(dòng)。
把文件加入暫存區(qū)叫做
stage
,文件修改過但還未使用git add
加入暫存區(qū)叫做unstage
git reset
重置暫存區(qū)匹配至最近的一次commit,但保留工作區(qū)不變。它unstage
了 所有 文件且沒有覆蓋任何改動(dòng),讓你有機(jī)會(huì)從頭開始重建暫存快照。
git reset --hard
重置暫存區(qū)和工作區(qū)匹配至最近的一次commit。除了unstage
所有文件外,-- hard
還告訴Git
也一并覆蓋工作區(qū)的所有改動(dòng),也就是說,這個(gè)操作撤銷了所有未提交的改動(dòng),所以在使用它前,請(qǐng)確定你是真的想丟棄本地的開發(fā)。
git reset <commit>
將當(dāng)前分支的HEAD移動(dòng)至<commit>
,重置暫存區(qū)匹配至<commit>
,但不包括工作區(qū)。從<commit>
開始的所有改動(dòng)會(huì)被駐留在工作區(qū),這讓你可以使用更干凈、更原子性的快照來重新提交項(xiàng)目歷史。
git reset --hard <commit>
將當(dāng)前分支的HEAD移動(dòng)至<commit>
以及重置暫存區(qū)和工作區(qū)匹配至<commit>
。它不僅撤銷了未提交的改動(dòng),還撤銷了<commit>
之后的所有commits。
討論
正如上面提及到的,git reset
是用來從一個(gè)代碼倉庫中移除改動(dòng)的。沒有-- hard
標(biāo)記時(shí),git reset
通過unstage
改動(dòng)或撤銷(uncommit)一系列已提交的快照來清理干凈代碼倉庫,然后重頭開始重建它們。當(dāng)一個(gè)試驗(yàn)已經(jīng)往可怕的方向發(fā)展時(shí),-- hard
標(biāo)記就派上用場(chǎng)了,你需要一個(gè)干凈的工作空間。
reset
是被設(shè)計(jì)來撤銷 本地 的改動(dòng)的,而revert
是被設(shè)計(jì)來安全地撤銷 公有 的commit的。出于完全不同的目的,這兩個(gè)命令的執(zhí)行結(jié)果也不同:reset
是完全地移除有改動(dòng)的地方,而revert
則是維持原來的改動(dòng),使用一個(gè)新的commit來達(dá)到撤銷的目的。
不要重置公有的歷史
當(dāng)<commit
后面的任一快照被推送到公有倉庫時(shí),你就不應(yīng)該使用git reset <commit>
,推送一個(gè)commit到公有倉庫后,就必須假設(shè)其他開發(fā)者是依賴于它的。刪除一個(gè)其他團(tuán)隊(duì)成員在此基礎(chǔ)上持續(xù)開發(fā)的commit會(huì)引發(fā)團(tuán)隊(duì)協(xié)作上的嚴(yán)重問題,當(dāng)他們嘗試與你的代碼倉庫同步時(shí),就像一大塊項(xiàng)目歷史突然地消失了。
下面的例子就是當(dāng)你嘗試reset
一個(gè)公有的commit時(shí)會(huì)發(fā)生的。
一旦你在reset
后新增一個(gè)commit,Git
會(huì)認(rèn)為你本地的歷史與origin/master
背道而馳了,當(dāng)合并commit時(shí),需要先同步你的代碼倉庫,這就有可能使你的團(tuán)隊(duì)感到迷惑和無助。
所以重點(diǎn)就是,你打算用git reset <commit>
來撤銷你那糟糕的試驗(yàn)時(shí),請(qǐng)確保它只作用于本地(還沒被推送至遠(yuǎn)程服務(wù)器)的改動(dòng)。如果你需要修復(fù)一個(gè)公有的commit,請(qǐng)使用git revert
,因?yàn)樗菫榱诉@個(gè)目的而被設(shè)計(jì)的。
示例
Unstage 一個(gè)文件
假設(shè)有兩個(gè)文件hello.py
和main.py
,已經(jīng)被添加到Git
倉庫中,修改這兩文件并進(jìn)行提交。
# Edit both hello.py and main.py
# Stage everything in the current directory
git add .
# Realize that the changes in hello.py and main.py
# should be committed in different snapshots
# Unstage main.py
git reset main.py
# Commit only hello.py
git commit -m "Make some changes to hello.py"
# Commit main.py in a separate snapshot
git add main.py
git commit -m "Edit main.py"
正如你所看到的,你可以使用git reset
來unstage
掉一些不小心加入暫存區(qū)但又與此次commit無關(guān)的文件,讓你的commits保持高度的專一。
移除本地的commits
接下來的例子展示了一個(gè)更高級(jí)的使用情況,它示范了你在一個(gè)新的試驗(yàn)上工作了一段時(shí)間并在提交了一些快照后,決定徹底拋棄它這整個(gè)過程究竟發(fā)生了什么。
# Create a new file called `foo.py` and add some code to it
# Commit it to the project history
git add foo.py
git commit -m "Start developing a crazy feature"
# Edit `foo.py` again and change some other tracked files, too
# Commit another snapshot
git commit -a -m "Continue my crazy feature"
# Decide to scrap the feature and remove the associated commits
git reset --hard HEAD~2
git reset HEAD~2
這句命令讓當(dāng)前分支回滾了兩個(gè)提交,實(shí)際上,從項(xiàng)目歷史上刪除了我們剛剛創(chuàng)建的兩個(gè)快照。請(qǐng)記住,這種類型的reset
應(yīng)該只用在未推送到遠(yuǎn)程服務(wù)器的commits上,絕不要在那些已經(jīng)被推送至公有倉庫的commits上執(zhí)行上面的操作。
git clean
git clean
從工作區(qū)移除未追蹤的文件。這的確是一個(gè)更方便的命令,因?yàn)樗褂?code>git status瑣細(xì)地查看哪些文件未追蹤,然后手動(dòng)刪除它們。就像普通的rm
命令一樣,git clean
是不可恢復(fù)的,所以在運(yùn)行它之前請(qǐng)確保你是真的想要?jiǎng)h除那些未追蹤的文件。
git clean
命令經(jīng)常和git reset --hard
一起被執(zhí)行,reset
僅僅影響已追蹤的文件,因此需要git clean
來單獨(dú)清理未追蹤的文件,這兩個(gè)命令相結(jié)合可以讓你的工作區(qū)回滾到一個(gè)特定的commit的確切狀態(tài)。
用法
git clean -n
執(zhí)行git clean
的“演習(xí)”。這向您展示哪個(gè)文件將會(huì)被刪除,但不會(huì)真正地執(zhí)行。
git clean -f
從當(dāng)前工作區(qū)中移除未追蹤的文件。-f(force)
標(biāo)記是必需的,除非clean.requireForce
選項(xiàng)被設(shè)為false
(默認(rèn)是true
)。這不會(huì)移除.gitignore
指定的未追蹤的文件。
git clean -f <path>
移除未追蹤的文件,但僅限于操作指定的路徑。
git clean -df
從當(dāng)前工作區(qū)中移除未追蹤的文件和目錄。
git clean -xf
從當(dāng)前工作區(qū)中移除未追蹤的文件,包括Git
忽略的文件。
討論
當(dāng)你在本地倉庫中做了一些令人尷尬的開發(fā)想要銷毀證據(jù)時(shí),git reset --hard
和git clean -f
會(huì)是你最好的朋友,運(yùn)行著兩個(gè)命令將會(huì)使你的工作區(qū)回滾至最近的一次commit,還你一個(gè)干凈的工作區(qū)。
git clean
在build
后清理工作區(qū)是很有用的,比如,你可以很容易地移除.o
和.exe
等C編譯器生成的二進(jìn)制文件,這是偶爾打包項(xiàng)目發(fā)布前的必要步驟,-x
選項(xiàng)達(dá)到這個(gè)目的特別方便。
記住,一起使用git reset
和git clean
是唯一一個(gè)具有潛在威脅的永久地刪除提交的命令,所以請(qǐng)謹(jǐn)慎使用。事實(shí)上,在使用git clean
時(shí),-f是必須的,
Git`的維護(hù)者甚至將它作為最基本的操作,而很多人會(huì)忘記的這一重要步驟,但這也預(yù)防了愚蠢行為而一不小心突然地刪除所有辛辛苦苦寫的代碼。
示例
下面的例子撤銷了工作區(qū)所有的改動(dòng),包括新增的文件。假設(shè)你已經(jīng)提交了一些快照,然后正在嘗試一些些新的開發(fā),但不知道自己做了什么導(dǎo)致了一些錯(cuò)誤,想要撤銷然后重新開始。
# Edit some existing files
# Add some new files
# Realize you have no idea what you're doing
# Undo changes in tracked files
git reset --hard
# Remove untracked files
git clean -df
運(yùn)行完reset/clean
一系列命令后,工作區(qū)和暫存區(qū)回滾到最近的commit,git status
將會(huì)告訴你這是一個(gè)干凈的工作區(qū),你現(xiàn)在可以準(zhǔn)備重新開始了。
注意,那些新增的文件沒有被加入暫存區(qū),它們不會(huì)被git reset --hard
影響,必須使用git clean
刪除它們。