git的reset和checkout的區別
Git裡有三個區域很重要
- HEAD 指向最近一次commit裡的所有snapshot
- Index 快取區域,只有Index區域裡的東西才可以被commit
- Working Directory 使用者操作區域
下圖解釋了這三個區域的狀態的變化過程:
初始狀態
當你checkout分支的時候,git做了這麼三件事情
- 將HEAD指向那個分支的最後一次commit
- 將HEAD指向的commit裡所有檔案的snapshot替換掉Index區域裡原來的內容
- 將Index區域裡的內容填充到Working Directory裡
所以你可以發現,HEAD、Index、Working Directory這個時候裡的內容都是一模一樣的。
注意:一般會誤解為,Index中的內容是空的,只有git add後才會有東西。實際上不是,Index裡一直是有東西的。
所以,Git的所有操作就是對這三個區域的狀態(或內容)的操作。
Changed
如果你在Working Directory裡修改了檔案,git會發現Working Directory裡的內容和Index區域裡的內容不一致了。
這個時候git status的結果是:
# Changes not staged for commit:
Staged
一個檔案僅僅changed是不能被commit的,Git要求只能提交Index裡的東西。
所以需要git add。這個命令的意思是,把Changed的檔案的內容同步到Index區域裡。這樣Working Directory和Index區域的內容就一致了。這個過程被稱之為stage
這個時候git status的結果是:
# Changes to be committed:
Committed
最後,你就可以提交了
git commit
這樣,就把HEAD的狀態和Index以及Working Directory形成一致了。
reset
reset是用來修改提交歷史的,想象這種情況,如果你在2天前提交了一個東西,突然發現這次提交是有問題的。
這個時候你有兩個選擇,要麼使用git revert(推薦),要麼使用git reset。
上圖可以看到git reset是會修改版本歷史的,他會丟棄掉一些版本歷史。
而git revert是根據那個commit逆向生成一個新的commit,版本歷史是不會被破壞的。
已經push到遠端倉庫的commit不允許reset
上面已經講了,git reset是會丟棄掉commit的。
如果commit已經被push到遠端倉庫上了,也就意味著其他開發人員就可能基於這個commit形成了新的commit,這時你去reset,就會造成其他開發人員的提交歷史莫名其妙的丟失,或者其他災難性的後果。
因此,一旦commit已經被push到遠端倉庫,那麼是堅決不允許去reset它的。
不帶檔案引數的reset
前面章節已經說道Git有三個區域,Git的所有操作實際上是在操作這三個區域的狀態(或內容)。
git reset配合不同的引數,對這三個區域會產生不同的影響。
reset實際上有3個步驟,根據不同的引數可以決定執行到哪個步驟(--soft
, --mixed
, --hard
)。
- 改變HEAD所指向的commit(
--soft
) - 執行第1步,將Index區域更新為HEAD所指向的commit裡包含的內容(
--mixed
) - 執行第1、2步,將Working Directory區域更新為HEAD所指向的commit裡包含的內容(
--hard
)
注意
–mixed
是預設引數,也就是說執行reset的時候不給就認為是--mixed
。
下表說明了三種形式的git reset所產生的不同效果。
target代表想要將git指向到哪個commit
working index HEAD target working index HEAD
----------------------------------------------------
A B C D --soft A B D
--mixed A D D
--hard D D D
--merge (disallowed)
working index HEAD target working index HEAD
----------------------------------------------------
A B C C --soft A B C
--mixed A C C
--hard C C C
--merge (disallowed)
帶檔案引數的reset
上面講到的git reset實際上不帶引數的,如果帶上檔案引數,那麼效果會是怎樣的?
- HEAD不會動
- 將那個commit的snapshot裡的那個檔案放到Index區域中
需要注意的是帶檔案引數的git reset沒有--hard, --soft這兩個引數。只有--mixed引數。
unstage
下面這兩個命令是一樣的,都是reset到HEAD上。
git reset file.txt
git reset --mixed HEAD file.txt
這個例子的意義在於,unstage file,仔細想一想是不是這樣?當你把一個檔案stage到Index區域裡後後悔了,那麼只需要把Index區域裡的這個檔案恢復到最近一次commit的狀態(也就是HEAD),那就相當於unstage了。
恢復到歷史版本
下面這個命令就是將某個檔案恢復到歷史版本上。
reset eb43bf file.txt
這個例子的意思在於,把某個檔案恢復到Index區域裡,然後直接commit,這樣就等於把這個檔案恢復到歷史版本了,這樣依賴你都不需要去改動Working Directory了。
checkout
前面講到checkout是會修改HEAD的指向,變更Index區域裡的內容,修改Working Directory裡的內容。
這看上去很像reset --hard
,但和reset --hard
相比有兩個重要的差別
- reset會把working directory裡的所有內容都更新掉
- checkout不會去修改你在Working Directory裡修改過的檔案
- reset把branch移動到HEAD指向的地方
- checkout則把HEAD移動到另一個分支
第二個區別可能有點難以理解,舉例來說:假設你有兩個分支master和develop,這兩個分支指向不一樣的commit,我們現在在develop分支上(HEAD指向的地方)
如果我們git reset master
,那麼develop就會指向master所指向的那個commit。
如果我們git checkout master
,那麼develop不會動,只有HEAD會移動。HEAD會指向master。看圖:
帶檔案引數
當執行git checkout [branch] file時,checkout幹了這件事情:
- 更新了index區域裡file檔案的內容
- 更新了working directory裡file檔案的內容
總結reset和checkout
head index work dir wd safe
Commit Level
reset --soft [commit] REF NO NO YES
reset [commit] REF YES NO YES
reset --hard [commit] REF YES YES NO
checkout [commit] HEAD YES YES YES
File Level
reset (commit) [file] NO YES NO YES
checkout (commit) [file] NO YES YES NO
“head”一列中的“REF”表示該命令移動了HEAD指向的分支引用,而“HEAD”則表示只移動了HEAD自身。 特別注意 “wd safe?” 一列,YES表示不會懂你在work dir的修改,NO代表會動你在work dir的修改。