| 時間 | 更新備注 |
|---|---|
| 2018-03-02 | 新建文章 |
| 2018-06-10 | 添加和revert&checkout的對比 |
| 2019-01-18 | 更新鏈接 |
目錄
- Git 筆記系列(一)—— Git簡介
- Git 筆記系列(二)—— Git工作流程
- Git 筆記系列(三)—— Git常用命令-一覽
- Git 筆記系列(四)—— Git常用命令-Checkout
- Git 筆記系列(五)—— Git常用命令-Branch
- Git 筆記系列(六)—— Git常用命令-Reset
- Git 筆記系列(七)—— Git常用命令-Rebase
- Git 筆記系列(八)—— Git常用命令-Stash等
- Git 筆記系列(九)—— Git進階
引言
在提交層面上,reset將一個分支的末端指向另一個提交。這可以用來移除當前分支的一些提交。比如,下面這兩條命令讓 hotfix 分支向后回退了兩個提交。
git checkout hotfix
git reset HEAD~2
hotfix 分支末端的兩個提交現(xiàn)在變成了懸掛提交。也就是說,下次 Git 執(zhí)行垃圾回收的時候,這兩個提交會被刪除。換句話說,如果你想扔掉這兩個提交,你可以這么做。reset 操作如下圖所示:


如果你仔細研究reset命令本身就知道,它本身做的事情就是重置HEAD(當前分支的版本頂端)到另外一個commit。
Reset解惑
讓我們跟著 reset 看看它都做了什么。它以一種簡單可預(yù)見的方式直接操縱這三棵樹。它做了三個基本操作。 第 1 步:移動 HEAD
reset 做的第一件事是移動 HEAD 的指向。這與改變 HEAD 自身不同(checkout 所做的); reset 移動 HEAD 指向的分支。這意味著如果 HEAD 設(shè)置為 master 分支(例如,你正在 master 分支上),運行 git reset 9e5e64a將會使master指向9e5e64a。

reset補充
總的來說,git reset命令是用來將當前branch重置到另外一個commit的,而這個動作可能會將index以及work tree同樣影響。比如如果你的master branch(當前checked out)是下面這個樣子:
- A - B - C (HEAD, master)
HEAD和master branch是在一起的,而你希望將master指向到B,而不是C,那么你執(zhí)行
git reset B以便移動master branch到``B那個commit:
- A - B (HEAD, master) # - C is still here, but there's no branch pointing to it anymore
注意:git reset和checkout是不一樣的。如果你運行g(shù)it checkout B,那么你講得到:
- A - B (HEAD) - C (master)
這時HEAD和master branch就不在一個點上了,你進入detached HEAD State. HEAD,work tree,index都指向了B,但是master branch卻依然指向C。如果在這個點上,你執(zhí)行一個新的commit D,那么你講得到下面(當然這可能并不是你想要的,你可能想要的是創(chuàng)一個branch做bug fix):
- A - B - C (master)
\
D (HEAD)

記住git reset不會產(chǎn)生commits,它僅僅更新一個branch(branch本身就是一個指向一個commit的指針)指向另外一個commit(Head和branch Tip同時移動保持一致).其他的僅剩對于index和work tree(working directory)有什么影響。git checkout xxxCommit則只影響HEAD,如果xxxCommit和一個branch tip是一致的話,則HEAD和branch相匹配,如果xxxCommit并不和任何branch tip相一致,則git進入detached HEAD 狀態(tài)。
reset用法
重置命令(git reset)是Git最常用的命令之一,也是最危險,最容易誤用的命令。來看看git reset命令的用法。
用法一:git reset [-q] [<commit>] [--] <paths>...
用法二:git reset [--soft | --mixed | --hard | --merge | --keep] [-q] [<commit>]
上面列出了兩個用法,其中<commit>都是可選項,可以使用引用或者提交ID,如果省略 <commit>則相當于使用了HEAD的指向作為提交ID。
上面列出的兩種用法的區(qū)別在于,第一種用法在命令中包含路徑<paths>。為了避免路徑和引用(或者提交ID)同名而沖突,可以在<paths>前用兩個連續(xù)的短線(減號)作為分隔。
第一種用法(包含了路徑<paths>的用法)不會重置引用,更不會改變工作區(qū),而是用指定提交狀態(tài)(<commit>)下的文件(<paths>)替換掉暫存區(qū)中的文件。例如命令git reset HEAD <paths>相當于取消之前執(zhí)行的git add <paths>命令時改變的暫存區(qū)。
第二種用法(不使用路徑<paths>的用法)則會重置引用。根據(jù)不同的選項,可以對暫存區(qū)或者工作區(qū)進行重置。參照下面的版本庫模型圖,來看一看不同的參數(shù)對第二種重置語法的影響。
reset注意
當你傳入HEAD 以外的其他提交的時候要格小心,因為 reset 操作會重寫當前分支的歷史。正如 rebase 黃金法則所說的,在公共分支上這樣做可能會引起嚴重的后果。
當執(zhí)行 “git reset HEAD” 命令時,暫存區(qū)的目錄樹會被重寫,被 master 分支指向的目錄樹所替換,但是工作區(qū)不受影響。
reset Parameters
- soft
reset 做的第一件事是移動 HEAD 的指向。
--soft參數(shù)告訴Git重置HEAD到另外一個commit,但也到此為止。如果你指定--soft參數(shù),Git將停止在那里而什么也不會根本變化。這意味著index,working copy都不會做任何變化,所有的在original HEAD和你重置到的那個commit之間的所有變更集都放在stage(index)區(qū)域中。


- mixed(default默認選項)
接下來,reset 會用 HEAD 指向的當前快照的內(nèi)容來更新索引。
--mixed是reset的默認參數(shù),也就是當你不指定任何參數(shù)時的參數(shù)。它將重置HEAD到另外一個commit,并且重置index以便和HEAD相匹配,但是也到此為止。working copy不會被更改。所有該branch上從original HEAD(commit)到你重置到的那個commit之間的所有變更將作為local modifications保存在working area中,(被標示為local modification or untracked via git status),但是并未staged的狀態(tài),你可以重新檢視然后再做修改和commit


- hard
reset 要做的的第三件事情就是讓工作目錄看起來像索引。如果使用 --hard 選項,它將會繼續(xù)這一步。
Git reflog

--hard參數(shù)將會blow out everything.它將重置HEAD返回到另外一個commit(取決于~12的參數(shù)),重置index以便反映HEAD的變化,并且重置working copy也使得其完全匹配起來。這是一個比較危險的動作,具有破壞性,數(shù)據(jù)因此可能會丟失!如果真是發(fā)生了數(shù)據(jù)丟失又希望找回來,那么只有使用:git reflog命令了。makes everything match the commit you have reset to.你的所有本地修改將丟失。如果我們希望徹底丟掉本地修改但是又不希望更改branch所指向的commit,則執(zhí)行git reset --hard = git reset --hard HEAD. i.e. don't change the branch but get rid of all local changes.另外一個場景是簡單地移動branch從一個到另一個commit而保持index/work區(qū)域同步。這將確實令你丟失你的工作,因為它將修改你的work tree!


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重置范圍
前面講述了 reset 基本形式的行為,不過你還可以給它提供一個作用路徑。若指定了一個路徑,reset 將會跳 過第 1 步,并且將它的作用范圍限定為指定的文件或文件集合。這樣做自然有它的道理,因為 HEAD 只是一個指 針,你無法讓它同時指向兩個提交中各自的一部分。不過索引和工作目錄 可以部分更新,所以重置會繼續(xù)進行 第2、3步。
現(xiàn)在,假如我們運行git reset file.txt(這其實是git reset --mixed HEAD file.txt的簡寫形 式,因為你既沒有指定一個提交的 SHA-1 或分支,也沒有指定 --soft 或 --hard),它會:
- 移動 HEAD 分支的指向 (已跳過)
- 讓索引看起來像 HEAD (到此處停止)
所以它本質(zhì)上只是將 file.txt 從 HEAD 復(fù)制到索引中。

更進一步,我們可以不讓 Git 從 HEAD 拉取數(shù)據(jù),而是通過具體指定一個提交來拉取該文件的對應(yīng)版本。我們只需運行類似 于git reset eb43bf file.txt的命令即可。

壓縮提交
我們來看看如何利用這種新的功能來做一些有趣的事情 - 壓縮提交。
假設(shè)你的一系列提交信息中有 “oops.”、“WIP” 和 “forgot this file”,聰明的你就能使用 reset 來輕松快 速地將它們壓縮成單個提交,也顯出你的聰明。(壓縮提交 展示了另一種方式,不過在本例中用 reset 更簡 單。)
假設(shè)你有一個項目,第一次提交中有一個文件,第二次提交增加了一個新的文件并修改了第一個文件,第三次提
交再次修改了第一個文件。由于第二次提交是一個未完成的工作,因此你想要壓縮它。

那么可以運行git reset --soft HEAD~2來將HEAD分支移動到一個舊一點的提交上(即你想要保留的第 一個提交):

注意,這時候因為是在上次提交的后,的Index暫存區(qū)和Working Directory是保持一致的,所以可以直接提交。
然后只需再次運行git commit:

現(xiàn)在你可以查看可到達的歷史,即將會推送的歷史,現(xiàn)在看起來有個 v1 版 file-a.txt 的提交,接著第二個提 交將 file-a.txt 修改成了 v3 版并增加了 file-b.txt。包含 v2 版本的文件已經(jīng)不在歷史中了。
reset和checkout
checkout這個命令做的不過是將HEAD移到一個新的分支,然后更新工作目錄。因為這可能會覆蓋本地的修改,Git 強制你提交或者緩存工作目錄中的所有更改,不然在 checkout 的時候這些更改都會丟失。和 git reset 不一樣的是,git checkout 沒有移動這些分支。這對于快速查看項目舊版本來說非常有用。
checkout對工作目錄是安全的,它會通過檢查來確保不會將已更改的文件吹走。
- checkout不會去修改你在Working Directory里修改過的文件
- checkout則把HEAD移動到另一個分支
- reset會不做檢查把working directory里的所有內(nèi)容都更新掉
- reset把branch移動到HEAD指向的地方

reset和revert
- git revert可以用在公共分支上,git reset應(yīng)該用在私有分支上.
git revert用于記錄一些新的提交以反轉(zhuǎn)一些早期提交的影響(通常只是一個錯誤的提交)。如果你想扔掉工作目錄中所有未提交的更改,你應(yīng)該看到git-reset,特別是--hard選項。如果你想提取特定文件,就像在另一個提交中那樣,你應(yīng)該看到git-checkout,特別是git checkout <commit> -- <filename>語法。請謹慎使用這些替代方法,因為它們都會丟棄工作目錄中的未提交更改。
reset是用來修改提交歷史的,想象這種情況,如果你在2天前提交了一個東西,突然發(fā)現(xiàn)這次提交是有問題的。
這個時候你有兩個選擇,要么使用git revert(推薦),要么使用git reset。

上圖可以看到git reset是會修改版本歷史的,他會丟棄掉一些版本歷史。
而git revert是根據(jù)那個commit逆向生成一個新的commit,版本歷史是不會被破壞的。
相比git reset,它不會改變現(xiàn)在的提交歷史。因此,git revert可以用在公共分支上,git reset應(yīng)該用在私有分支上。
你也可以把git revert當作撤銷已經(jīng)提交的更改,而git reset HEAD用來撤銷沒有提交的更改。
就像git checkout 一樣,git revert 也有可能會重寫文件。所以,Git會在你執(zhí)行revert之前要求你提交或者緩存你工作目錄中的更改。
如果你的更改還沒有共享給別人,git reset 是撤銷這些更改的簡單方法。當你開發(fā)一個功能的時候發(fā)現(xiàn)「糟糕,我做了什么?我應(yīng)該重新來過!」時,reset 就像是 go-to 命令一樣。
除了在當前分支上操作,你還可以通過傳入這些標記來修改你的緩存區(qū)或工作目錄:
- --soft – 緩存區(qū)和工作目錄都不會被改變
- --mixed – 默認選項。緩存區(qū)和你指定的提交同步,但工作目錄不受影響
- --hard – 緩存區(qū)和工作目錄都同步到你指定的提交
把這些標記想成定義 git reset 操作的作用域就容易理解多了
相比 git reset,它不會改變現(xiàn)在的提交歷史。因此,git revert 可以用在公共分支上,git reset 應(yīng)該用在私有分支上。
總結(jié)
簡單總結(jié)一下,其實就是--soft 、--mixed以及--hard是指代三個不同的恢復(fù)等級。使用--soft就僅僅將Head頭指針恢復(fù),已經(jīng)add的緩存以及工作空間的所有東西都不變。如果使用--mixed,就將Head頭指針恢復(fù)掉,已經(jīng)add的緩存也會丟失掉,工作空間的代碼什么的是不變的。如果使用--hard,那么一切就全都恢復(fù)了,Head頭指針變,add的緩存消失,本地工作區(qū)的代碼的也恢復(fù)到指定之前版本的狀態(tài)。
| 命令 | 作用域 | 常用情景 |
|---|---|---|
| git reset | 提交層面 | 在私有分支上舍棄一些沒有提交的更改 |
| git reset | 文件層面 | 將文件從緩存區(qū)中移除 |
| git checkout | 提交層面 | 切換分支或查看舊版本 |
| git checkout | 文件層面 | 舍棄工作目錄中的更改 |
| git revert | 提交層面 | 在公共分支上回滾更改 |
| git revert | 文件層面 | (然而并沒有) |
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
參考
- git reset soft,hard,mixed之區(qū)別深解
- Git - git-reset Documentation
- 代碼回滾:git reset、git checkout和git revert區(qū)別和聯(lián)系 - houpy - 博客園
- Pro Git(中文版)