Git reset,git checkout,和git revert命令是Git工具箱中最有用的幾個工具之一。他們都用來撤銷倉庫中的某種修改,其中前兩個命令可以用來撤銷針對提交或者單個文件的修改。
因為如此相似,在特定開發場景下很容易出現不知道該使用那個命令的情況。在本文中我們會比較git reset,git checkout和git revert命令最常見的使用方式。希望在本文結束時讀者能夠在自己的項目中胸有成竹地使用對應的命令。
為了深入理解,我們需要考慮每個命令在Git倉庫的三種狀態管理機制中所產生的不同效果:工作目錄,暫存快照,和提交歷史。有時候這些狀態被稱為Git的三棵樹。在閱讀本文過程中請謹記這三種不同的工作機制。
checkout操作會將當前HEAD指針指向指定的提交。請參見下圖:
上圖展示了main分支中一系列的提交。此時的HEAD指針和main分支的當前指針都指向提交d。接下來執行git checkout b
這個操作會影響到「提交歷史」樹。git checkout命令可以用於提交,甚至在文件層級上執行。對於文件進行checkout操作會改變該文件的內容到某一次指定提交。
revert操作撤銷指定提交並創建一個新的提交,其內容為指定提交的所有逆向修改。git revert只能運行在提交層面,不能對指定文件操作。
reset操作接受一次commit作為參數,並將git的三棵樹狀態重置到指定的這次commit的相同狀態。reset操作可以在三棵樹的不同狀態下執行。
checkout和reset通常用於本地或者私有分支的撤銷操作。修改之後的提交歷史,在推送到共享的遠程倉庫時會引發衝突。反之revert操作的「公共撤銷」通常被認為是安全的。因為revert操作會為撤銷動作創建一次提交,而這個撤銷歷史也會被其他人得到,並且revert操作也不會覆蓋團隊其他成員可能依賴的提交歷史。
Git Reset vs Revert vs Checkout
下表總結了這些命令的常用場景。
命令 |
影響範圍 |
常見實用場景 |
git reset |
提交 |
丟棄私有分支或者未提交的變更 |
git reset |
文件 |
反暫存一個文件 |
git checkout |
提交 |
切換分支或者查看歷史快照 |
git checkout |
文件 |
丟棄工作目錄下的變更 |
git revert |
提交 |
撤銷公共分支的提交 |
git revert |
文件 |
(N/A) |
關於提交的操作
向git reset和git checkout命令傳遞的參數決定了其影響範圍。使用命令時不含文件路徑則會讓操作作用於整個提交。接下來這部分我們會主要討論相關內容。注意git revert沒有文件層面的操作。
重置指定提交
在提交層面,重置可以移動分支頂端到其他提交。基於這一特性,重置可以用於刪除分支中的提交。比如,下面的命令將hotfix分支的頂端向前移動了兩個提交。
git checkout hotfix git reset HEAD~2
hotfix分支的最後兩個提交現在稱為孤立的提交。這意味著下次Git執行垃圾回收時會刪除他們。換句話說,如此操作意味著你要丟棄這些提交。這一過程可以通過下圖表示:
如此使用git reset撤銷那些還未與他人共享過的變更相當簡便。如果你開始開發一個功能做了幾次提交之後,突然發現「臥槽,我在幹嘛?從頭來吧。」的時候可以直接使用這個命令。
除了移動當前分支以外,你還可以傳遞以下選項,用git reset來變更暫存快照或者工作目錄:
- --soft – The staged snapshot and working directory are not altered in any way.
- --soft – 暫存快照和工作目錄不會改變
- --mixed – The staged snapshot is updated to match the specified commit, but the working directory is not affected. This is the default option.
- --mixed – 暫存快照更新為指定提交,但是工作目錄不受影響。這是默認選項。
- --hard – The staged snapshot and the working directory are both updated to match the specified commit.
- --hard – 暫存快照和工作目錄都被更新為指定提交。
checkout舊提交
git checkout命令用於更新倉庫狀態到指定的項目提交歷史。當傳遞的參數是一個分支名稱,則用於切換分支。
git checkout hotfix
在命令內部,以上所有命令都是移動HEAD指針到不同分支,並相應的更新工作目錄。由於這一操作具有潛在的覆蓋本地變更的可能性,因此Git會強制你在checkout操作之前執行commit或者stash命令,以便存儲可能由於checkout丟失的變更。與git reset不同,git checkout不會移動分支本身的指針。
你也可以通過傳遞提交引用作為參數,checkout出指定提交而不是分支。其內部執行方式與checkout分支一摸一樣:移動HEAD指針到指定提交。舉個例子,下面的命令會checkout出當前提交的祖父節點。
git checkout HEAD~2
這一操作經常用於查看某一個舊版本的項目快照。然而由於當前HEAD指針並不指向任何分支,這一操作會讓你處於游離HEAD狀態。由於在這個狀態下提交新的更新之後,當切換為其他分支之後無法在回到新的提交,所以在游離狀態下新建提交是危險的。基於這個原因,如果希望在游離狀態下做新的提交,應該先基於此提交創建新的分支。
使用revert撤銷公共提交
revert命令通過新建一個提交來撤銷之前的一個提交。因為這一操作不會重寫提交歷史所以被認為是一種安全的撤銷操作。比如下面的例子中,Git會搞清楚倒數第二次提交的內容,然後創建一個新的提交用於撤銷這些內容,並且將新提交的撤銷動作提交到當前的項目中。
git checkout hotfix git revert HEAD~2
此過程圖示如下:
與git reset相反,git revert沒有改變已有提交歷史。基於此,git revert應該被用於撤銷公共分支上的變更,而git reset應該被限制於撤銷私有分支的變更。
你也可以理解為git revert用於撤銷已提交的變更,git reset用於撤銷未提交的變更。
與git checkout一樣,git revert操作也會導致潛在的文件覆蓋,所以Git也會要求在revert之前先進行commit或者stash操作。
關於文件的操作
git reset和git checkout命令也接受文件路徑作為可選參數。這也讓其行為與上面所介紹的功能完全不一樣。相比於操作整個快照,附加的文件路徑參數限制相應操作的影響範圍到單個文件。
Git-reset指定文件
當附加了文件路徑作為參數時,git reset會根據指定提交更新暫存快照。比如下面的命令會獲取foo.py文件在倒數第二次提交時的快照,根據快照內容變更文件,並暫存它,等待下一次提交:
git reset HEAD~2 foo.py
相比於針對提交的git reset命令,上面的命令更多地用於HEAD。執行git reset HEAD foo.py會取消foo.py的暫存,但其中的改變仍然在工作目錄中。
--soft,--mixed和--hard選項在文件層面的git reset操作沒有任何作用,因為暫存快照總是最新的,並且工作目錄總是不更新的。
Git checkout 文件
checkout一個文件與使用git reset命令傳遞文件路徑類似,除了checkout更新的是工作目錄,而不是更新暫存快照。另外,與執行checkout命令關於提交的操作不同,checkout一個提交會改變HEAD的指向,而checkout文件不改變HEAD,僅改變文件內容。也就意味著執行這個命令不會切換分支。
比如下面的命令更新工作目錄中的foo.py文件,將其內容同步為倒數第二次提交時的樣子。
git checkout HEAD~2 foo.py
就像操作提交時使用git checkout,這也可以用於查看項目的舊版本——不過這次是查看指定文件的舊版本。
如果你暫存並提交了checkout出來的舊版本文件,其執行結果也含有將指定文件revert到舊版本的效果。不過請注意這一操作也同時移除了該文件從舊版本之後的所有後續變更歷史,然而revert命令僅撤銷指定提交的變更。
就像git reset,這種情況也通常與HEAD搭配使用。比如,git checkout HEAD foo.py執行的結果就含有丟棄foo.py文件未暫存的變更的效果。這個行為類似於git reset HEAD --hard,只不過影響範圍僅限於指定文件。
總結
到現在為止,你應該已經擁有用於在Git倉庫中撤銷變更所需的所有知識了。git reset,git checkout,和git revert命令容易混淆,但是當你考慮到他們分別在工作目錄,暫存快照和提交歷史上的可能產生的影響,就不難在開發中分辨出應該使用哪個命令。
原文地址:https://www.atlassian.com/git/tutorials/resetting-checking-out-and-reverting