概述
這次主要來講講Git
的反悔操作羽杰,自己平時在寫代碼的過程中經(jīng)常會出現(xiàn)想要棄用所有的改動或回滾到上一次commit
的情況。Git
上的反悔操作有reset
、rebase
泽腮、revert
等像吻,每個操作各有區(qū)別和對應的使用場景峻汉,這里做下總結(jié)。
Git
的反悔操作有兩大類:
- 撤銷改動 ( Undoing Change )
- 重寫歷史 ( Rewriting History )
文章大部分翻譯于 Undoing Changes 和 Rewriting history亡脑,并結(jié)合了自己的一些理解和補充喷斋。
撤銷改動(Undoing Change)
git checkout
git checkout
有三個不同的功能:切換分支锅风、回滾至某個commit边器、回滾一個文件至某個commit训枢。切換分支是git checkout
最常見的功能,不做介紹忘巧,這里主要介紹下它在撤銷文件改動上的應用恒界。
回滾至某個commit
git chekcout <commit>
上面的命令是回滾到工作目錄中指定的 commit 上,這是一個 只讀 操作砚嘴,不會影響到當前工作區(qū)的狀態(tài)十酣,它在你查看舊版本的文件時不會損壞你的代碼倉庫。通常际长,HEAD
指向master分支或其他本地分支耸采,當使用git checkout
回滾到以前的 commit 時,HEAD
就不再指向某個分支了工育,而是直接指向一個commit虾宇,這時就叫做detached HEAD
狀態(tài)。
切換到detached HEAD
狀態(tài)時如绸,會有一個警告嘱朽。
這個警告是告訴你,你現(xiàn)在做的所有事情與你開發(fā)項目的其余工作區(qū)是分離的竭沫,即所有的改動與本地倉庫的任一分支都無關(guān)燥翅,不會影響到其他的分支的狀態(tài)。如果你準備在detached HEAD
狀態(tài)下開發(fā)新的feature蜕提,那將會沒有分支允許你回退這里森书,當你不可避免地切換到其他分支時,將沒有任何辦法引用到這個feature谎势。你可以把detached HEAD
狀態(tài)看作是正在一個未命名的分支上凛膏。
HEAD 和 detached HEAD 的區(qū)別可以參考 How can I reconcile detached HEAD with master/origin?
將英文翻譯為中文經(jīng)常會詞不達意,很難把握脏榆,建議還是看英文原文:)猖毫。
示例
假設(shè)你正在進行一次瘋狂的重構(gòu),但現(xiàn)在你不確定是否要繼續(xù)下去须喂。這時你想要看一下開始這次重構(gòu)之前項目原來的樣子吁断,首先你需要找到你想要查看的版本的ID趁蕊。
git log --oneline
假設(shè)你的項目歷史看起來像下面這樣:
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)仔役。你可以查看文件掷伙、編譯項目、運行測試用例又兵,甚至編輯文件任柜,完全不用擔心丟失項目“當前”的狀態(tài),你在這里做的所有修改都不會被保存到項目中沛厨。當你想要繼續(xù)那次瘋狂的重構(gòu)時宙地,你需要回到項目的“當前”狀態(tài)。
git checkout master
回滾一個文件至某個commit
git checkout <commit> <file>
回滾一個文件到以前的一個版本逆皮,這個操作會 影響 當前工作區(qū)的狀態(tài)宅粥。
你可以在一個新的快照中重新提交這個舊版本,當然也包含其他任何文件电谣。實際上粹胯,checkout
的這個用法和revert
類似,只不過是僅針對一個文件辰企。
示例
如果你只對單個文件感興趣,你可以使用 git checkout
獲取到該文件的舊版本况鸣。比如牢贸,如果你只想要看看 某次commit下的hello.py
文件,可以使用下面的命令:
git checkout a1e8fb5 hello.py
記住镐捧,不像切換commit潜索,這會影響當前項目的狀態(tài)。這個舊版本的文件的狀態(tài)會變?yōu)?Change to be committed
懂酱,給你一個機會將該文件恢復到先前的版本竹习。
如果你決定不需要保留這個舊版本了,你可以切換到最近的版本列牺,如下:
git checkout HEAD hello.py
git revert
git revert
可以撤銷一個已提交的快照(snapshot)整陌,但它解決的是如何撤銷已提交的被引入的改動,并生成內(nèi)容來追加一個新的提交瞎领,而不是從項目的歷史中移除這個提交泌辫,這避免了丟失歷史記錄,這對于項目的每一次修改的歷史記錄的完整性來說非常重要九默,并這是服務于可靠的多人協(xié)作開發(fā)的震放。
git revert <commit>
這句命令會撤銷這次<commit>所有被引入的改動,生成一個新的commit驼修,并應用在當前分支上殿遂。
當你想從你的項目歷史中移除一個完整的commit時诈铛,就應該使用git revert
。比如墨礁,你正在追蹤一個Bug并發(fā)現(xiàn)它是在一次單一的commit中被引入的幢竹,你可以手動進行修改,刪除有Bug的代碼來修復它饵溅,然后提交一個新的快照妨退,但這樣很麻煩,效率也很低蜕企,你更應該做的是咬荷,使用git revert
自動完成,撤銷這次commit所有被引入的改動轻掩。
Reverting vs. Resetting
很重要的一點幸乒,revert
是對一次單一的commit的撤銷,并不是真正意義上的回滾唇牧。它不是通過移除項目中一次commit后面的所有提交來“回滾”之前的狀態(tài)罕扎,實際上那樣的操作在Git
上被叫做reset
,而不是revert
丐重。
比起reset
腔召,revert
有兩個重要的好處:
revert
不會改變項目的歷史。如果那些commits已經(jīng)推到了共享的代碼倉庫扮惦,它會是一個“安全”的操作臀蛛。為什么改變共享代碼倉庫的歷史是危險的,請看后面的git reset
的介紹崖蜜。revert
可以作用于歷史中 任意 的單一的commit節(jié)點浊仆,然而reset
只能做到從當前 最新 的commit開始回滾。比如說豫领,如果你想要只撤銷一次舊的指定的commit抡柿,使用git reset
,你則必須移除該commit和該commit之后出現(xiàn)的所有commits等恐,然后再把那些隨后的commit重新提交洲劣。毫無疑問,這種撤銷的方式一點都不優(yōu)雅鼠锈。
示例1
下面的例子是git revert
的一個簡單示例闪檬,提交了一個快照,然后立即使用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仍然被保留在項目歷史中,git revert
新增了一個新的commit來撤銷它的改動同欠,而不是刪除它样傍。結(jié)果就是横缔,第3次和第5次commit的代碼是完全一樣的,第4次commit依然保留在歷史中衫哥,以防我們想要重新回滾到這里茎刚。
示例2
假設(shè)你發(fā)現(xiàn)在某次commit中引入了一個bug,你想使用 git revert
來回滾撤逢。查看歷史:
git log --oneline
項目歷史如下:
417e4a9 commit 4
427d76b commit 3
1642475 introduced a bug
71d3ef7 commit 1
bf4f6f6 git initial
使用 revert
回滾到 1642475
git revert 1642475
但你會發(fā)現(xiàn)沒有想象中那么簡單膛锭,而是發(fā)生沖突了,報錯如下:
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的改動蚊荣,默認會生成一個新的commit提交初狰,但在它之后還有commit 3
和commit 4
,它們的改動不會被影響互例,依然保留在工作區(qū)中奢入,因此產(chǎn)生了沖突。你可以手動解決沖突后commit媳叨,但這卻是個麻煩且不優(yōu)雅的方式腥光。因為1642475
、427d76b
和417e4a9
這幾個commit的改動被一起合并在暫存區(qū)中糊秆,如果你修改的不止一個文件武福,那手動解決沖突將會非常麻煩。解決方式是痘番,默認 不 生成新的commit艘儒,并按順序回滾。
先強制結(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
夫偶,會生成帶默認message的commit。更多參數(shù)說明詳見:git-revert-document
git reset
如果git revert
是以一個"安全""的方式來撤銷改動觉增,那你可以認為git reset
是一種 危險 的方式兵拢。當你使用git reset
后,將沒有辦法恢復原樣逾礁,它是一個永恒的撤銷,因為那些commits不再被任何ref
或reflog
引用说铃。在使用這個工具時請務必謹慎,因為它是git
命令中唯一一個潛在的使你的努力付諸東流的命令嘹履。
git reset
是一個功能豐富的命令腻扇,它可以用于移除已提交的快照,但它更多的是用來撤銷暫存區(qū)和工作區(qū)的改動砾嫉,另一種情況是幼苛,它應該只用于撤銷本地的改動(不應該reset
那些已經(jīng)與其他開發(fā)者共享了的快照)。
用法
git reset <file>
從暫存區(qū)中移除指定的文件焕刮,但保留工作區(qū)不變舶沿。它unstage
了 一個 文件且沒有覆蓋任何改動墙杯。
把文件加入暫存區(qū)叫做
stage
,文件修改過但還未使用git add
加入暫存區(qū)叫做unstage
git reset
重置暫存區(qū)匹配至最近的一次commit括荡,但保留工作區(qū)不變高镐。它unstage
了 所有 文件且沒有覆蓋任何改動,讓你有機會從頭開始重建暫存快照畸冲。
git reset --hard
重置暫存區(qū)和工作區(qū)匹配至最近的一次commit嫉髓。除了unstage
所有文件外,-- hard
還告訴Git
也一并覆蓋工作區(qū)的所有改動邑闲,也就是說算行,這個操作撤銷了所有未提交的改動,所以在使用它前监憎,請確定你是真的想丟棄本地的開發(fā)纱意。
git reset <commit>
將當前分支的HEAD移動至<commit>
,重置暫存區(qū)匹配至<commit>
鲸阔,但不包括工作區(qū)池户。從<commit>
開始的所有改動會被駐留在工作區(qū)炎辨,這讓你可以使用更干凈、更原子性的快照來重新提交項目歷史。
git reset --hard <commit>
將當前分支的HEAD移動至<commit>
以及重置暫存區(qū)和工作區(qū)匹配至<commit>
缀匕。它不僅撤銷了未提交的改動,還撤銷了<commit>
之后的所有commits奕枢。
討論
正如上面提及到的夭问,git reset
是用來從一個代碼倉庫中移除改動的。沒有-- hard
標記時晃痴,git reset
通過unstage
改動或撤銷(uncommit)一系列已提交的快照來清理干凈代碼倉庫残吩,然后重頭開始重建它們。當一個試驗已經(jīng)往可怕的方向發(fā)展時倘核,-- hard
標記就派上用場了泣侮,你需要一個干凈的工作空間。
reset
是被設(shè)計來撤銷 本地 的改動的紧唱,而revert
是被設(shè)計來安全地撤銷 公有 的commit的活尊。出于完全不同的目的,這兩個命令的執(zhí)行結(jié)果也不同:reset
是完全地移除有改動的地方漏益,而revert
則是維持原來的改動蛹锰,使用一個新的commit來達到撤銷的目的。
不要重置公有的歷史
當<commit
后面的任一快照被推送到公有倉庫時绰疤,你就不應該使用git reset <commit>
铜犬,推送一個commit到公有倉庫后,就必須假設(shè)其他開發(fā)者是依賴于它的。刪除一個其他團隊成員在此基礎(chǔ)上持續(xù)開發(fā)的commit會引發(fā)團隊協(xié)作上的嚴重問題翎苫,當他們嘗試與你的代碼倉庫同步時权埠,就像一大塊項目歷史突然地消失了。
下面的例子就是當你嘗試reset
一個公有的commit時會發(fā)生的煎谍。
一旦你在reset
后新增一個commit攘蔽,Git
會認為你本地的歷史與origin/master
背道而馳了,當合并commit時呐粘,需要先同步你的代碼倉庫满俗,這就有可能使你的團隊感到迷惑和無助。
所以重點就是作岖,你打算用git reset <commit>
來撤銷你那糟糕的試驗時唆垃,請確保它只作用于本地(還沒被推送至遠程服務器)的改動。如果你需要修復一個公有的commit痘儡,請使用git revert
辕万,因為它正是為了這個目的而被設(shè)計的。
示例
Unstage 一個文件
假設(shè)有兩個文件hello.py
和main.py
沉删,已經(jīng)被添加到Git
倉庫中渐尿,修改這兩文件并進行提交。
# 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
接下來的例子展示了一個更高級的使用情況殴穴,它示范了你在一個新的試驗上工作了一段時間并在提交了一些快照后凉夯,決定徹底拋棄它這整個過程究竟發(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
這句命令讓當前分支回滾了兩個提交采幌,實際上劲够,從項目歷史上刪除了我們剛剛創(chuàng)建的兩個快照。請記住休傍,這種類型的reset
應該只用在未推送到遠程服務器的commits上再沧,絕不要在那些已經(jīng)被推送至公有倉庫的commits上執(zhí)行上面的操作。
git clean
git clean
從工作區(qū)移除未追蹤的文件尊残。這的確是一個更方便的命令,因為它使用git status
瑣細地查看哪些文件未追蹤淤堵,然后手動刪除它們寝衫。就像普通的rm
命令一樣,git clean
是不可恢復的拐邪,所以在運行它之前請確保你是真的想要刪除那些未追蹤的文件慰毅。
git clean
命令經(jīng)常和git reset --hard
一起被執(zhí)行,reset
僅僅影響已追蹤的文件扎阶,因此需要git clean
來單獨清理未追蹤的文件汹胃,這兩個命令相結(jié)合可以讓你的工作區(qū)回滾到一個特定的commit的確切狀態(tài)婶芭。
用法
git clean -n
執(zhí)行git clean
的“演習”。這向您展示哪個文件將會被刪除着饥,但不會真正地執(zhí)行犀农。
git clean -f
從當前工作區(qū)中移除未追蹤的文件。-f(force)
標記是必需的宰掉,除非clean.requireForce
選項被設(shè)為false
(默認是true
)呵哨。這不會移除.gitignore
指定的未追蹤的文件。
git clean -f <path>
移除未追蹤的文件轨奄,但僅限于操作指定的路徑孟害。
git clean -df
從當前工作區(qū)中移除未追蹤的文件和目錄。
git clean -xf
從當前工作區(qū)中移除未追蹤的文件挪拟,包括Git
忽略的文件挨务。
討論
當你在本地倉庫中做了一些令人尷尬的開發(fā)想要銷毀證據(jù)時,git reset --hard
和git clean -f
會是你最好的朋友玉组,運行著兩個命令將會使你的工作區(qū)回滾至最近的一次commit谎柄,還你一個干凈的工作區(qū)。
git clean
在build
后清理工作區(qū)是很有用的球切,比如谷誓,你可以很容易地移除.o
和.exe
等C編譯器生成的二進制文件,這是偶爾打包項目發(fā)布前的必要步驟吨凑,-x
選項達到這個目的特別方便捍歪。
記住,一起使用git reset
和git clean
是唯一一個具有潛在威脅的永久地刪除提交的命令鸵钝,所以請謹慎使用糙臼。事實上,在使用git clean
時恩商,-f是必須的变逃,
Git`的維護者甚至將它作為最基本的操作,而很多人會忘記的這一重要步驟怠堪,但這也預防了愚蠢行為而一不小心突然地刪除所有辛辛苦苦寫的代碼揽乱。
示例
下面的例子撤銷了工作區(qū)所有的改動,包括新增的文件粟矿。假設(shè)你已經(jīng)提交了一些快照凰棉,然后正在嘗試一些些新的開發(fā),但不知道自己做了什么導致了一些錯誤陌粹,想要撤銷然后重新開始撒犀。
# 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
運行完reset/clean
一系列命令后,工作區(qū)和暫存區(qū)回滾到最近的commit,git status
將會告訴你這是一個干凈的工作區(qū)或舞,你現(xiàn)在可以準備重新開始了荆姆。
注意,那些新增的文件沒有被加入暫存區(qū)映凳,它們不會被git reset --hard
影響胆筒,必須使用git clean
刪除它們。