三棵树
了解 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
Git 的 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
再说一次,咱们在这里又用到了 git ls-files
这个幕后的命令,它会显示出索引以后的样子。
确切来说,索引在技术上并非树结构,它其实是以扁平的清单实现的。不过对咱们而言,把它当做树就够了。
工作目录
最初,你就有了本人的 工作目录 (通常也叫 工作区 )。另外两棵树以一种高效但并不直观的形式,将它们的内容存储在 .git
文件夹中。工作目录会将它们解包为理论的文件以便编辑。你能够把工作目录当做 沙盒。在你将批改提交到暂存区并记录到历史之前,能够随便更改。
$ tree
.
├── README
├── Rakefile
└── lib
└── simplegit.rb
1 directory, 3 files
工作流程
经典的 Git 工作流程是通过操纵这三个区域来以更加间断的状态记录我的项目快照的。
让咱们来可视化这个过程:假如咱们进入到一个新目录,其中有一个文件。咱们称其为该文件的 v1 版本,将它标记为蓝色。当初运行 git init
,这会创立一个 Git 仓库,其中的 HEAD 援用指向未创立的 master
分支。
此时,只有工作目录有内容。
当初咱们想要提交这个文件,所以用 git add
来获取工作目录中的内容,并将其复制到索引中。
接着运行 git commit
,它会获得索引中的内容并将它保留为一个永恒的快照,而后创立一个指向该快照的提交对象,最初更新 master
来指向本次提交。
此时如果咱们运行 git status
,会发现没有任何改变,因为当初三棵树完全相同。
当初咱们想要对文件进行批改而后提交它。咱们将会经验同样的过程;首先在工作目录中批改文件。咱们称其为该文件的 v2 版本,并将它标记为红色。
如果当初运行 git status
,咱们会看到文件显示在“Changes not staged for commit”上面并被标记为红色,因为该条目在索引与工作目录之间存在不同。接着咱们运行 git add
来将它暂存到索引中。
此时,因为索引和 HEAD 不同,若运行 git status
的话就会看到“Changes to be committed”下的该文件变为绿色 ——也就是说,当初预期的下一次提交与上一次提交不同。最初,咱们运行 git commit
来实现提交。
当初运行 git status
会没有输入,因为三棵树又变得雷同了。
切换分支或克隆的过程也相似。当检出一个分支时,它会批改 HEAD 指向新的分支援用,将 索引 填充为该次提交的快照,而后将 索引 的内容复制到 工作目录 中。
重置的作用
在以下情景中察看 reset
命令会更有意义。
为了演示这些例子,假如咱们再次批改了 file.txt
文件并第三次提交它。当初的历史看起来是这样的:
让咱们跟着 reset
看看它都做了什么。它以一种简略可预感的形式间接操纵这三棵树。它做了三个基本操作。
第 1 步:挪动 HEAD
reset
做的第一件事是挪动 HEAD 的指向。这与扭转 HEAD 本身不同(checkout
所做的);reset
挪动 HEAD 指向的分支。这意味着如果 HEAD 设置为 master
分支(例如,你正在 master
分支上),运行 git reset 9e5e6a4
将会使 master
指向 9e5e6a4
。
无论你调用了何种模式的带有一个提交的 reset
,它首先都会尝试这样做。应用 reset --soft
,它将仅仅停在那儿。
当初看一眼上图,了解一下产生的事件:它实质上是撤销了上一次 git commit
命令。当你在运行 git commit
时,Git 会创立一个新的提交,并挪动 HEAD 所指向的分支来使其指向该提交。当你将它 reset
回 HEAD~
(HEAD 的父结点)时,其实就是把该分支挪动回原来的地位,而不会扭转索引和工作目录。当初你能够更新索引并再次运行 git commit
来实现 git commit --amend
所要做的事件了(见 批改最初一次提交)。
第 2 步:更新索引(–mixed)
留神,如果你当初运行 git status
的话,就会看到新的 HEAD 和以绿色标出的它和索引之间的区别。
接下来,reset
会用 HEAD 指向的以后快照的内容来更新索引。
如果指定 --mixed
选项,reset
将会在这时进行。这也是默认行为,所以如果没有指定任何选项(在本例中只是 git reset HEAD~
),这就是命令将会进行的中央。
当初再看一眼上图,了解一下产生的事件:它仍然会撤销一上次 提交
,但还会 勾销暂存 所有的货色。于是,咱们回滚到了所有 git add
和 git commit
的命令执行之前。
第 3 步:更新工作目录(–hard)
reset
要做的的第三件事件就是让工作目录看起来像索引。如果应用 --hard
选项,它将会持续这一步。
当初让咱们回忆一下方才产生的事件。你撤销了最初的提交、git add
和 git commit
命令 以及 工作目录中的所有工作。
必须留神,--hard
标记是 reset
命令惟一的危险用法,它也是 Git 会真正地销毁数据的仅有的几个操作之一。其余任何模式的 reset
调用都能够轻松吊销,然而 --hard
选项不能,因为它强制笼罩了工作目录中的文件。在这种非凡状况下,咱们的 Git 数据库中的一个提交内还留有该文件的 v3 版本,咱们能够通过 reflog
来找回它。然而若该文件还未提交,Git 仍会笼罩它从而导致无奈复原。
回顾
reset
命令会以特定的程序重写这三棵树,在你指定以下选项时进行:
- 挪动 HEAD 分支的指向 (若指定了
--soft
,则到此进行) - 使索引看起来像 HEAD (若未指定
--hard
,则到此进行) - 使工作目录看起来像索引
通过门路来重置
后面讲述了 reset
根本模式的行为,不过你还能够给它提供一个作用门路。若指定了一个门路,reset
将会跳过第 1 步,并且将它的作用范畴限定为指定的文件或文件汇合。这样做天然有它的情理,因为 HEAD 只是一个指针,你无奈让它同时指向两个提交中各自的一部分。不过索引和工作目录 能够局部更新,所以重置会持续进行第 2、3 步。
当初,如果咱们运行 git reset file.txt
(这其实是 git reset --mixed HEAD file.txt
的简写模式,因为你既没有指定一个提交的 SHA-1 或分支,也没有指定 --soft
或 --hard
),它会:
- 挪动 HEAD 分支的指向 (已跳过)
- 让索引看起来像 HEAD (到此处进行)
所以它实质上只是将 file.txt
从 HEAD 复制到索引中。
它还有 勾销暂存文件 的实际效果。如果咱们查看该命令的示意图,而后再想想 git add
所做的事,就会发现它们正好相同。
这就是为什么 git status
命令的输入会倡议运行此命令来勾销暂存一个文件。(查看 勾销暂存的文件 来理解更多。)
咱们能够不让 Git 从 HEAD 拉取数据,而是通过具体指定一个提交来拉取该文件的对应版本。咱们只需运行相似于 git reset eb43bf file.txt
的命令即可。
它其实做了同样的事件,也就是把工作目录中的文件复原到 v1 版本,运行 git add
增加它,而后再将它复原到 v3 版本(只是不必真的过一遍这些步骤)。如果咱们当初运行 git commit
,它就会记录一条“将该文件复原到 v1 版本”的更改,只管咱们并未在工作目录中真正地再次领有它。
还有一点同 git add
一样,就是 reset
命令也能够承受一个 --patch
选项来一块一块地勾销暂存的内容。这样你就能够依据抉择来勾销暂存或复原内容了。
压缩
咱们来看看如何利用这种新的性能来做一些乏味的事件——压缩提交。
假如你的一系列提交信息中有“oops.”“WIP”和“forgot this file”,聪慧的你就能应用 reset
来轻松疾速地将它们压缩成单个提交,也显出你的聪慧。(压缩提交 展现了另一种形式,不过在本例中用 reset
更简略。)
假如你有一个我的项目,第一次提交中有一个文件,第二次提交减少了一个新的文件并批改了第一个文件,第三次提交再次批改了第一个文件。因为第二次提交是一个未实现的工作,因而你想要压缩它。
那么能够运行 git reset --soft HEAD~2
来将 HEAD 分支挪动到一个旧一点的提交上(即你想要保留的最近的提交):
而后只需再次运行 git commit
:
当初你能够查看可达到的历史,行将会推送的历史,当初看起来有个 v1 版 file-a.txt
的提交,接着第二个提交将 file-a.txt
批改成了 v3 版并减少了 file-b.txt
。蕴含 v2 版本的文件曾经不在历史中了。
检出
最初,你大略还想晓得 checkout
和 reset
之间的区别。和 reset
一样,checkout
也操纵三棵树,不过它有一点不同,这取决于你是否传给该命令一个文件门路。
不带门路
运行 git checkout [branch]
与运行 git reset --hard [branch]
十分类似,它会更新所有三棵树使其看起来像 [branch]
,不过有两点重要的区别。
首先不同于 reset --hard
,checkout
对工作目录是平安的,它会通过查看来确保不会将已更改的文件弄丢。其实它还更聪慧一些。它会在工作目录中先试着简略合并一下,这样所有 还未修改过的 文件都会被更新。而 reset --hard
则会不做查看就全面地替换所有货色。
第二个重要的区别是 checkout
如何更新 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 本身。
带门路
运行 checkout
的另一种形式就是指定一个文件门路,这会像 reset
一样不会挪动 HEAD。它就像 git reset [branch] file
那样用该次提交中的那个文件来更新索引,然而它也会笼罩工作目录中对应的文件。它就像是 git reset --hard [branch] file
(如果 reset
容许你这样运行的话),这样对工作目录并不平安,它也不会挪动 HEAD。
此外,同 git reset
和 git add
一样,checkout
也承受一个 --patch
选项,容许你依据抉择一块一块地复原文件内容。
总结
心愿你当初相熟并了解了 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 |
HEAD | YES | YES | YES |
File Level | ||||
reset [commit] |
NO | YES | NO | YES |
checkout [commit] |
NO | YES | YES | NO |