## 重置揭密
在繼續了解更專業的工具前,我們先討論一下?`reset`?與?`checkout`。 在你初次遇到的 Git 命令中,這兩個是最讓人困惑的。 它們能做很多事情,所以看起來我們很難真正地理解并恰當地運用它們。 針對這一點,我們先來做一個簡單的比喻。
### [三棵樹](http://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E9%87%8D%E7%BD%AE%E6%8F%AD%E5%AF%86#三棵樹)
理解?`reset`?和?`checkout`?的最簡方法,就是以 Git 的思維框架(將其作為內容管理器)來管理三棵不同的樹。 “樹” 在我們這里的實際意思是 “文件的集合”,而不是指特定的數據結構。 (在某些情況下索引看起來并不像一棵樹,不過我們現在的目的是用簡單的方式思考它。)
Git 作為一個系統,是以它的一般操作來管理并操縱這三棵樹的:
| 樹 | 用途 |
| --- | --- |
HEAD 上一次提交的快照,下一次提交的父結點
|
|
Index
|
預期的下一次提交的快照
|
|
Working Directory
|
沙盒
|
#### HEAD
HEAD 是當前分支引用的指針,它總是指向該分支上的最后一次提交。 這表示 HEAD 將是下一次提交的父結點。 通常,理解 HEAD 的最簡方式,就是將它看做?**你的上一次提交**?的快照。
其實,查看快照的樣子很容易。 下例就顯示了 HEAD 快照實際的目錄列表,以及其中每個文件的 SHA-1 校驗和:
~~~
$ git cat-file -p HEAD
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
author Scott Chacon 1301511835 -0700
committer Scott Chacon 1301511835 -0700
initial commit
$ git ls-tree -r HEAD
100644 blob a906cb2a4a904a152... README
100644 blob 8f94139338f9404f2... Rakefile
040000 tree 99f1a6d12cb4b6f19... lib
~~~
`cat-file`?與?`ls-tree`?是底層命令,它們一般用于底層工作,在日常工作中并不使用。不過它們能幫助我們了解到底發生了什么。
#### 索引
索引是你的?**預期的下一次提交**。 我們也會將這個概念引用為 Git 的 “暫存區域”,這就是當你運行?`git commit`?時 Git 看起來的樣子。
Git 將上一次檢出到工作目錄中的所有文件填充到索引區,它們看起來就像最初被檢出時的樣子。 之后你會將其中一些文件替換為新版本,接著通過?`git commit`?將它們轉換為樹來用作新的提交。
~~~
$ git ls-files -s
100644 a906cb2a4a904a152e80877d4088654daad0c859 0 README
100644 8f94139338f9404f26296befa88755fc2598c289 0 Rakefile
100644 47c6340d6459e05787f644c2447d2595f5d3a54b 0 lib/simplegit.rb
~~~
再說一次,我們在這里又用到了?`ls-files`?這個幕后的命令,它會顯示出索引當前的樣子。
確切來說,索引并非技術上的樹結構,它其實是以扁平的清單實現的。不過對我們而言,把它當做樹就夠了。
#### 工作目錄
最后,你就有了自己的工作目錄。 另外兩棵樹以一種高效但并不直觀的方式,將它們的內容存儲在`.git`?文件夾中。 工作目錄會將它們解包為實際的文件以便編輯。 你可以把工作目錄當做?**沙盒**。在你將修改提交到暫存區并記錄到歷史之前,可以隨意更改。
~~~
$ tree
.
├── README
├── Rakefile
└── lib
└── simplegit.rb
1 directory, 3 files
~~~
### [工作流程](http://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E9%87%8D%E7%BD%AE%E6%8F%AD%E5%AF%86#工作流程)
Git 主要的目的是通過操縱這三棵樹來以更加連續的狀態記錄項目的快照。

Figure 7-2.
讓我們來可視化這個過程:假設我們進入到一個新目錄,其中有一個文件。 我們稱其為該文件的?**v1**版本,將它標記為藍色。 現在運行?`git init`,這會創建一個 Git 倉庫,其中的 HEAD 引用指向未創建的分支(`master`?還不存在)。

Figure 7-3.
此時,只有工作目錄有內容。
現在我們想要提交這個文件,所以用?`git add`?來獲取工作目錄中的內容,并將其復制到索引中。

Figure 7-4.
接著運行?`git commit`,它首先會移除索引中的內容并將它保存為一個永久的快照,然后創建一個指向該快照的提交對象,最后更新?`master`?來指向本次提交。

Figure 7-5.
此時如果我們運行?`git status`,會發現沒有任何改動,因為現在三棵樹完全相同。
現在我們想要對文件進行修改然后提交它。 我們將會經歷同樣的過程;首先在工作目錄中修改文件。 我們稱其為該文件的?**v2**?版本,并將它標記為紅色。

Figure 7-6.
如果現在運行?`git status`,我們會看到文件顯示在 “Changes not staged for commit,” 下面并被標記為紅色,因為該條目在索引與工作目錄之間存在不同。 接著我們運行?`git add`?來將它暫存到索引中。

Figure 7-7.
此時,由于索引和 HEAD 不同,若運行?`git status`?的話就會看到 “Changes to be committed” 下的該文件變為綠色 ——也就是說,現在預期的下一次提交與上一次提交不同。 最后,我們運行?`git commit`?來完成提交。

Figure 7-8.
現在運行?`git status`?會沒有輸出,因為三棵樹又變得相同了。
切換分支或克隆的過程也類似。 當檢出一個分支時,它會修改?**HEAD**?指向新的分支引用,將?**索引**?填充為該次提交的快照,然后將?**索引**?的內容復制到?**工作目錄**?中。
### [重置的作用](http://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E9%87%8D%E7%BD%AE%E6%8F%AD%E5%AF%86#重置的作用)
在以下情景中觀察?`reset`?命令會更有意義。
為了演示這些例子,假設我們再次修改了?`file.txt`?文件并第三次提交它。 現在的歷史看起來是這樣的:

Figure 7-9.
讓我們跟著?`reset`?看看它都做了什么。 它以一種簡單可預見的方式直接操縱這三棵樹。 它做了三個基本操作。
#### 第 1 步:移動 HEAD
`reset`?做的第一件事是移動 HEAD 的指向。 這與改變 HEAD 自身不同(`checkout`?所做的);`reset`?移動 HEAD 指向的分支。 這意味著如果 HEAD 設置為?`master`?分支(例如,你正在?`master`?分支上),運行?`git reset 9e5e64a`?將會使?`master`?指向?`9e5e64a`。

Figure 7-10.
無論你調用了何種形式的帶有一個提交的?`reset`,它首先都會嘗試這樣做。 使用?`reset --soft`,它將僅僅停在那兒。
現在看一眼上圖,理解一下發生的事情:它本質上是撤銷了上一次?`git commit`?命令。 當你在運行`git commit`?時,Git 會創建一個新的提交,并移動 HEAD 所指向的分支來使其指向該提交。 當你將它?`reset`?回?`HEAD~`(HEAD 的父結點)時,其實就是把該分支移動回原來的位置,而不會改變索引和工作目錄。 現在你可以更新索引并再次運行?`git commit`?來完成?`git commit --amend`?所要做的事情了(見?[修改最后一次提交](http://git-scm.com/book/zh/v2/ch00/_git_amend))。
#### 第 2 步:更新索引(--mixed)
注意,如果你現在運行?`git status`?的話,就會看到新的 HEAD 和以綠色標出的它和索引之間的區別。
接下來,`reset`?會用 HEAD 指向的當前快照的內容來更新索引。

Figure 7-11.
如果指定?`--mixed`?選項,`reset`?將會在這時停止。 這也是默認行為,所以如果沒有指定任何選項(在本例中只是?`git reset HEAD~`),這就是命令將會停止的地方。
現在再看一眼上圖,理解一下發生的事情:它依然會撤銷一上次?`提交`,但還會?*取消暫存*?所有的東西。 于是,我們回滾到了所有?`git add`?和?`git commit`?的命令執行之前。
#### 第 3 步:更新工作目錄(--hard)
`reset`?要做的的第三件事情就是讓工作目錄看起來像索引。 如果使用?`--hard`?選項,它將會繼續這一步。

Figure 7-12.
現在讓我們回想一下剛才發生的事情。 你撤銷了最后的提交、`git add`?和?`git commit`?命令**以及**工作目錄中的所有工作。
必須注意,`--hard`?標記是?`reset`?命令唯一的危險用法,它也是 Git 會真正地銷毀數據的僅有的幾個操作之一。 其他任何形式的?`reset`?調用都可以輕松撤消,但是?`--hard`?選項不能,因為它強制覆蓋了工作目錄中的文件。 在這種特殊情況下,我們的 Git 數據庫中的一個提交內還留有該文件的?**v3**?版本,我們可以通過?`reflog`?來找回它。但是若該文件還未提交,Git 仍會覆蓋它從而導致無法恢復。
#### 回顧
`reset`?命令會以特定的順序重寫這三棵樹,在你指定以下選項時停止:
1. 移動 HEAD 分支的指向?*(若指定了?`--soft`,則到此停止)*
2. 使索引看起來像 HEAD?*(若未指定?`--hard`,則到此停止)*
3. 使工作目錄看起來像索引
### [通過路徑來重置](http://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E9%87%8D%E7%BD%AE%E6%8F%AD%E5%AF%86#通過路徑來重置)
前面講述了?`reset`?基本形式的行為,不過你還可以給它提供一個作用路徑。 若指定了一個路徑,`reset`?將會跳過第 1 步,并且將它的作用范圍限定為指定的文件或文件集合。 這樣做自然有它的道理,因為 HEAD 只是一個指針,你無法讓它同時指向兩個提交中各自的一部分。 不過索引和工作目錄?*可以部分更新*,所以重置會繼續進行第 2、3 步。
現在,假如我們運行?`git reset file.txt`?(這其實是?`git reset --mixed HEAD file.txt`?的簡寫形式,因為你既沒有指定一個提交的 SHA-1 或分支,也沒有指定?`--soft`?或?`--hard`),它會:
1. 移動 HEAD 分支的指向?*(已跳過)*
2. 讓索引看起來像 HEAD?*(到此處停止)*
所以它本質上只是將?`file.txt`?從 HEAD 復制到索引中。

Figure 7-13.
它還有?*取消暫存文件*?的實際效果。 如果我們查看該命令的示意圖,然后再想想?`git add`?所做的事,就會發現它們正好相反。

Figure 7-14.
這就是為什么?`git status`?命令的輸出會建議運行此命令來取消暫存一個文件。 (查看?[取消暫存的文件](http://git-scm.com/book/zh/v2/1-git-basics/_unstaging)?來了解更多。)
我們可以不讓 Git 從 HEAD 拉取數據,而是通過具體指定一個提交來拉取該文件的對應版本。 我們只需運行類似于?`git reset eb43bf file.txt`?的命令即可。

Figure 7-15.
它其實做了同樣的事情,也就是把工作目錄中的文件恢復到?**v1**?版本,運行?`git add`?添加它,然后再將它恢復到?**v3**?版本(只是不用真的過一遍這些步驟)。 如果我們現在運行?`git commit`,它就會記錄一條“將該文件恢復到?**v1**?版本”的更改,盡管我們并未在工作目錄中真正地再次擁有它。
還有一點同?`git add`?一樣,就是?`reset`?命令也可以接受一個?`--patch`?選項來一塊一塊地取消暫存的內容。 這樣你就可以根據選擇來取消暫存或恢復內容了。
### [壓縮](http://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E9%87%8D%E7%BD%AE%E6%8F%AD%E5%AF%86#壓縮)
我們來看看如何利用這種新的功能來做一些有趣的事情 - 壓縮提交。
假設你的一系列提交信息中有 “oops.”、“WIP” 和 “forgot this file”, 聰明的你就能使用`reset`?來輕松快速地將它們壓縮成單個提交,也顯出你的聰明。 ([壓縮提交](http://git-scm.com/book/zh/v2/ch00/_squashing)?展示了另一種方式,不過在本例中用?`reset`?更簡單。)
假設你有一個項目,第一次提交中有一個文件,第二次提交增加了一個新的文件并修改了第一個文件,第三次提交再次修改了第一個文件。 由于第二次提交是一個未完成的工作,因此你想要壓縮它。

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

Figure 7-17.
然后只需再次運行?`git commit`:

Figure 7-18.
現在你可以查看可到達的歷史,即將會推送的歷史,現在看起來有個 v1 版?`file-a.txt`?的提交,接著第二個提交將?`file-a.txt`?修改成了 v3 版并增加了?`file-b.txt`。 包含 v2 版本的文件已經不在歷史中了。
### [檢出](http://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E9%87%8D%E7%BD%AE%E6%8F%AD%E5%AF%86#檢出)
最后,你大概還想知道?`checkout`?和?`reset`?之間的區別。 和?`reset`?一樣,`checkout`?也操縱三棵樹,不過它有一點不同,這取決于你是否傳給該命令一個文件路徑。
#### 不帶路徑
運行?`git checkout [branch]`?與運行?`git reset --hard [branch]`?非常相似,它會更新所有三棵樹使其看起來像?`[branch]`,不過有兩點重要的區別。
首先不同于?`reset --hard`,`checkout`?對工作目錄是安全的,它會通過檢查來確保不會將已更改的文件吹走。 其實它還更聰明一些。它會在工作目錄中先試著簡單合并一下,這樣所有*還未修改過的*文件都會被更新。 而?`reset --hard`?則會不做檢查就全面地替換所有東西。
第二個重要的區別是如何更新 HEAD。?`reset`?會移動 HEAD 分支的指向,而?`checkout`?只會移動 HEAD 自身來指向另一個分支。
例如,假設我們有?`master`?和?`develop`?分支,它們分別指向不同的提交;我們現在在`develop`?上(所以 HEAD 指向它)。 如果我們運行?`git reset master`,那么?`develop`?自身現在會和?`master`?指向同一個提交。 而如果我們運行?`git checkout master`?的話,`develop`?不會移動,HEAD 自身會移動。 現在 HEAD 將會指向?`master`。
所以,雖然在這兩種情況下我們都移動 HEAD 使其指向了提交 A,但*做法*是非常不同的。?`reset`?會移動 HEAD 分支的指向,而?`checkout`?則移動 HEAD 自身。

Figure 7-19.
#### 帶路徑
運行?`checkout`?的另一種方式就是指定一個文件路徑,這會像?`reset`?一樣不會移動 HEAD。 它就像?`git reset [branch] file`?那樣用該次提交中的那個文件來更新索引,但是它也會覆蓋工作目錄中對應的文件。 它就像是?`git reset --hard [branch] file`(如果?`reset`?允許你這樣運行的話)- 這樣對工作目錄并不安全,它也不會移動 HEAD。
此外,同?`git reset`?和?`git add`?一樣,`checkout`?也接受一個?`--patch`?選項,允許你根據選擇一塊一塊地恢復文件內容。
### [總結](http://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E9%87%8D%E7%BD%AE%E6%8F%AD%E5%AF%86#總結)
希望你現在熟悉并理解了?`reset`?命令,不過關于它和?`checkout`?之間的區別,你可能還是會有點困惑,畢竟不太可能記住不同調用的所有規則。
下面的速查表列出了命令對樹的影響。 “HEAD” 一列中的 “REF” 表示該命令移動了 HEAD 指向的分支引用,而“HEAD” 則表示只移動了 HEAD 自身。 特別注意?*WD Safe?*?一列 - 如果它標記為**NO**,那么運行該命令之前請考慮一下。
HEAD | Index | Workdir | 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**
|
- 前言
- Scott Chacon 序
- Ben Straub 序
- 獻辭
- 貢獻者
- 引言
- 1. 起步
- 1.1 關于版本控制
- 1.2 Git 簡史
- 1.3 Git 基礎
- 1.4 命令行
- 1.5 安裝 Git
- 1.6 初次運行 Git 前的配置
- 1.7 獲取幫助
- 1.8 總結
- 2. Git 基礎
- 2.1 獲取 Git 倉庫
- 2.2 記錄每次更新到倉庫
- 2.3 查看提交歷史
- 2.4 撤消操作
- 2.5 遠程倉庫的使用
- 2.6 打標簽
- 2.7 Git 別名
- 2.8 總結
- 3. Git 分支
- 3.1 分支簡介
- 3.2 分支的新建與合并
- 3.3 分支管理
- 3.4 分支開發工作流
- 3.5 遠程分支
- 3.6 變基
- 3.7 總結
- 4. 服務器上的 Git
- 4.1 協議
- 4.2 在服務器上搭建 Git
- 4.3 生成 SSH 公鑰
- 4.4 配置服務器
- 4.5 Git 守護進程
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 第三方托管的選擇
- 4.10 總結
- 5. 分布式 Git
- 5.1 分布式工作流程
- 5.2 向一個項目貢獻
- 5.3 維護項目
- 5.4 總結
- 6. GitHub
- 6.1 賬戶的創建和配置
- 6.2 對項目做出貢獻
- 6.3 維護項目
- 6.4 管理組織
- 6.5 腳本 GitHub
- 6.6 總結
- 7. Git 工具
- 7.1 選擇修訂版本
- 7.2 交互式暫存
- 7.3 儲藏與清理
- 7.4 簽署工作
- 7.5 搜索
- 7.6 重寫歷史
- 7.7 重置揭密
- 7.8 高級合并
- 7.9 Rerere
- 7.10 使用 Git 調試
- 7.11 子模塊
- 7.12 打包
- 7.13 替換
- 7.14 憑證存儲
- 7.15 總結
- 8. 自定義 Git
- 8.1 配置 Git
- 8.2 Git 屬性
- 8.3 Git 鉤子
- 8.4 使用強制策略的一個例子
- 8.5 總結
- 9. Git 與其他系統
- 9.1 作為客戶端的 Git
- 9.2 遷移到 Git
- 9.3 總結
- 10. Git 內部原理
- 10.1 底層命令和高層命令
- 10.2 Git 對象
- 10.3 Git 引用
- 10.4 包文件
- 10.5 引用規格
- 10.6 傳輸協議
- 10.7 維護與數據恢復
- 10.8 環境變量
- 10.9 總結
- A. 其它環境中的 Git
- A1.1 圖形界面
- A1.2 Visual Studio 中的 Git
- A1.3 Eclipse 中的 Git
- A1.4 Bash 中的 Git
- A1.5 Zsh 中的 Git
- A1.6 Powershell 中的 Git
- A1.7 總結
- B. 將 Git 嵌入你的應用
- A2.1 命令行 Git 方式
- A2.2 Libgit2
- A2.3 JGit
- C. Git 命令
- A3.1 設置與配置
- A3.2 獲取與創建項目
- A3.3 快照基礎
- A3.4 分支與合并
- A3.5 項目分享與更新
- A3.6 檢查與比較
- A3.7 調試
- A3.8 補丁
- A3.9 郵件
- A3.10 外部系統
- A3.11 管理
- A3.12 底層命令