Git学习之~工作区、暂存区、HEAD

首先看一下git-demo的提交日志,使用git log查看提交日志(附加--stat参数可以看到每次提交的文件变更统计)。

1
$ git log --stat

git log stat

通过日志可以看到第一次提交对文件welcome.txt有一行的变更,而第二次提交因为使用了--allow-empty参数进行的空提交,所以提交说明中看不到任何对实质内容的修改。

修改不能直接提交?

首先更改welcome.txt文件的内容,在文件后面追加一行。

1
$ echo "很高兴见到你。" >> welcome.txt

这时就可以通过git diff命令看到修改后的文件和版本库中文件的差异。实质和本地比较的不是版本库,而是一个中间状态的文件。

1
$ git diff

git diff

接下来执行提交,看能否提交成功。

1
$ git commit -m "追加一行:很高兴见到你。"

git no commit

提交成功了吗?好像没有。
提交没有成功的证据:

  • 先看看提交日志,如果提交成功就会有新的提交记录。
1
$ git log --pretty=oneline

git log oneline
使用了精简输出来显示日志,以便更简洁和清晰地看到提交历史。可以看到版本库中只有两个提交,针对刚才修改文件的提交没有成功!

  • 执行git diff可以看到和之前相同的差异输出,这也说明提交没有成功。
  • 执行git status查看文件状态,可以看到文件处于未提交状态。
  • 执行git status -s可以显示精简格式的状态输出。
    图片5

提交为什么会失败呢?原因是没有对修改的welcome.txt文件执行git add命令,将修改的文件添加到“提交任务”,然后才能提交。
现在就将修改的文件“添加”到提交任务。

1
$ git add welcome.txt

现在再执行一些Git命令,看看当执行“添加”后会发生什么变化。

  • 执行git diff没有输出。
  • 执行git diff headgit diff master进行比较会发现有差异,这个差异是正常的,因为还没有提交。
1
$ git diff head
  • 执行git status,状态输出和之前不一样了。
1
$ git status

图片6
对新的Git状态输出做下翻译:

1
2
3
4
5
位于分支master上
下列的修改将被提交:
(如果你后悔了,可以使用“git reset HEAD <file>...”命令将下列改动撤出提交任务(提交暂存区,stage),
否则执行提交命令将会提交)
已修改: welcome.txt

不得不说,Git太人性化了,它把各种情况下可以使用到的命令都告诉了用户,虽然这显得有点啰嗦。
如果不要这么啰嗦,可以使用简洁方式:

1
2
$ git status -s
M welcome.txt

图片7
上面精简状态输出与执行git add之前的精简状态的输出相比,有细微的差别。

  • 虽然都是M(Modified)标识,但是位置不同。在执行git add之前,这个M位于第二列(第一列是个空格),执行完git add之后,字符M位于第一列(第二列是空白)。
  • 位于第一列的字符M的含义:版本库中的文件和处于中间状态–提交任务(提交暂存区,stage)中的文件相比有改动;
  • 位于第二列的字符M的含义:工作区当前的文件和处于中间状态–提交任务(提交暂存区,stage)中的文件相比有改动。
    这时如果直接提交git commit,加入提交任务的welcome.txt文件的更改就被提交入库了。但是先不忙着提交,再进行一些操作,看看能否被彻底搞糊涂。
  • 继续修改welcome.txt文件(在文件后面追加一行)。
1
$ echo "Good-bye" >> welcome.txt
  • 然后执行git status查看一下状态:
1
git status

图片8
状态输出中居然是之前出现的两种不同状态的输出。

  • 使用简洁模式输出,也会看到两种精简输出的杂合。
1
git status -s

图片9
上面Git状态的输出可以这么理解:不但版本库中最新提交的文件和处于中间状态(提交暂存区,stage)中文件
相比有改动,而且工作区当前的文件和处于中间状态(提交暂存区,stage)中的文件相比也有改动。
即现在welcome.txt中有三个不同的版本:一个在工作区,一个在暂存区,一个是版本库中最新版本。通过
不同的参数调用git diff命令可以看到不同版本库之间的差异。

  • 不带任何选项和参数调用git diff显示工作区最新改动,即工作区和提交暂存区中相比的差异。
    图片10
  • 将工作区和HEAD(当前工作分支)相比,会看到更多差异。
    图片11
  • 通过参数--cached或者--staged参数调用git diff命令,看到的是提交暂存区和版本库中文件的差异。
    图片12

好了,现在提交下看看。

1
2
3
$ git commit -m "哪个版本的数据会被提交?"
[master a67c6fd] 哪个版本的数据会被提交?
1 file changed, 1 insertion(+)

这次提交终于成功了,查看下日志:

1
2
3
4
$ git log --pretty=oneline
a67c6fdd0a0e809340944432a3bf7d78e7e10f31 (HEAD -> master) 哪个版本的数据会被提交?
d3617dadd10c274e6e2b6caf47635d5771b9034c 这是谁提交的?
d3e0684a1c510f4effd155b01591019e2c5d7812 初始化的第一次提交

查看精简状态输出:

1
2
$ git status -s
M welcome.txt

状态输出中文件名的前面出现了一个字母M,即只位于第二列的字母M。那么第一列的M去哪了?很明显被提交了。
即提交任务(提交暂存区,stage)中的内容被提交到版本库,所以第一列因为提交暂存区和版本库中的状态一致,所以显示空白。
图片13
那提交的welcome.txt是哪个版本呢?通过执行git diff或者git diff head命令查看差异。虽然命令
git diffgit diff head的比较过程并不同,但是会看到下面相同的差异输出结果。

1
2
$ git diff
$ git diff head

图片14

理解Git暂存区(stage)

Git暂存区(stage,或称为index)的设计是Git最成功的设计之一,也是最难理解的一个设计。
在版本库.git目录下,有一个index文件,下面针对这个文件做个实验。
首先执行git checkout命令,撤销工作区中welcome.txt文件尚未提交的修改。

1
2
$ git checkout -- welcome.txt
$ git status -s

图片15
通过状态输出可以看到工作区已经没有改动了,查看下.git/index文件的时间戳: 2019-06-10 22:00:43

1
$ ls --full-time .git/index

图片16

再次执行git status命令,然后显示.git/index文件的时间戳:2019-06-10 22:00:43,和上面的一样。

1
2
$ git status -s 
$ ls --full-time .git/index

图片17
现在更改一下welcome.txt文件的时间戳,但不改变它的内容。然后再执行git status命令,然后查看.git/index文件时间戳为:22:06:32

1
2
3
$ touch welcome.txt
$ git status -s
$ ls --full-time .git/index

图片18
可以看到时间戳改变了。
这个实验说明当执行git status或者git diff命令扫描工作区改动的时候,先依据.git/index文件中记录的
(工作区跟踪文件)时间戳、长度等信息判断工作区文件是否改变。如果工作区的文件时间戳改变,说明文件的
内容可能被改变了,需要打开文件,读取文件内容,和更改前的原始文件相比,判断文件内容是否被更改。如果
文件内容没有改变,则将该文件新的时间戳记录到.git/index文件中。因为判断文件是否更改使用的是时间戳、
文件长度等信息进行比较,要比通过文件内容比较快的多,所以GIt这样的实现方式可以让工作区状态扫描更快
执行,这也是Git高效的因素之一。
文件.git/index实际上就是一个包含文件索引的目录树,像是一个虚拟的工作区。在这个虚拟工作区的目录树中,
记录了文件名、文件的状态信息(时间戳、文件长度等)。文件的内容并不存储其中,而是保存在Git对象库
.git/objects目录中,文件索引建立了文件和对象库中对象实体之间的对应。
下面这张图展示了工作区、版本库中的暂存区和版本库之间的关系。

图片19

  • 图中左侧为工作区,右侧为版本库。在版本库中标记为index的区域是暂存区(stage,亦称index),标记为master的是master分支所代表的目录树。
  • 图中可以看出此时HEAD实际上是指向master分支的一个“游标”,所以图示的命令中出现HEAD的地方可以用master替换。
  • 图中的objects标识的区域为Git的对象库,实际位于.git/objects目录下。
  • 当对工作区修改(或新增)的文件执行git add命令时,暂存区的目录树被更新,同时工作区修改(或新增)的文件
    内容被写入到对象库中的一个新的对象中,而对象的ID被记录在暂存区的文件索引中。
  • 当执行提交操作时,暂存区的目录树写到版本库(对象库)中,master分支会做出相应的更新。即master最新指向的目录树就是提交时原暂存区的目录树。
  • 当执行git reset HEAD命令时,暂存区的目录树会被重写,被master分支指向的目录树所替换,但是工作区不受影响。
  • 当执行git rm --cached <file>命令时,会直接从暂存区删除文件,工作区则不做改变。
  • 当执行git checkout . 或者git checkout -- <file>命令时,会用暂存区全部或指定的文件替换工作区的文件,。
    这个操作很危险,会清除工作区中未添加到暂存区的改动。
  • 当执行git checkout HEAD .或者git checkout HEAD <file>命令时,会用HEAD指向的master分支中的全部或者
    部分文件替换暂存区和工作区中的文件。这个命令也极具危险性,因为不但会清除工作区中未提交的改动,也会清除暂存区中未提交的改动。

Git Diff 魔法

暂存区目录树浏览

对于HEAD(版本库中当前提交)指向的目录树,可以使用git ls-tree来查看。

1
2
$ git ls-tree -l HEAD 
100644 blob d7230d74c5f8d61ff3c1adbfd3b035c09cb62c16 33 welcome.txt

图片20

  • 使用-l参数,可以显示文件大小。上面welcome.txt大小33字节。
  • 输出的条目从左至右,第一个字段是文件的属性(rw-r–r–),第二个字段说明是Git对象库中的一个blob对象(文件),
    第三个字段则是该文件在对象库中对应的ID–一个40位的SHA1哈希值格式的ID,第四个字段是文件大小,第五个字段是文件名。

在浏览暂存区中的目录树之前,首先清除工作区当中的改动。通过 git clean -fd命令清除当前工作区中没有加入
版本库的文件和目录(非跟踪文件和目录),然后执行git checkout .命令,用暂存区刷新工作区。

1
2
$ git clean -fd
$ git checkout .

然后在工作区中做出一些修改(修改welcome.txt,增加一个子目录和文件),然后添加到暂存区。最后再对工作区做出修改。

1
2
3
4
5
6
$ echo "( ^_^ )/~~拜拜" >> welcome.txt
$ mkdir -p a/b/c
$ echo "Hello." >> a/b/c/hello.txt
$ git add .
$ echo "ヾ( ̄▽ ̄)Bye~Bye~" >> a/b/c/hello.txt
$ git status -s

图片21
上面的命令运行完毕后,通过精简的状态输出,可以看出工作区、暂存区、版本库当前分支的最新版本(HEAD)各不相同。先来看看工作区中文件的大小:

1
2
3
$ find . -path ./.git -prune -o -type f -printf "%-20p\t%s\n"
./a/b/c/hello.txt 30
./welcome.txt 52

图片22

要显示暂存区的目录树,可以使用git ls-files命令。

1
2
3
$ git ls-files -s
100644 18832d35117ef2f013c4009f5b2128dfaeff354f 0 a/b/c/hello.txt
100644 a2b90cc2992cf3ff4e0d6beb0c9461158124c2b6 0 welcome.txt

图片23
这个输出和之前使用git ls-tree命令的输出不一样,如果想要使用git ls-tree命令,需要先将暂存区的
目录树写入Git对象库git write-tree命令,然后在针对git write-tree命令写入的tree执行git ls-tree

1
2
3
4
5
$ git write-tree 
1e9d611d50af21a2f0ec7bce6e270fd170a8f557
$ git ls-tree -l
040000 tree 53583ee687fbb2e913d18d508aefd512465b2092 - a
100644 blob a2b90cc2992cf3ff4e0d6beb0c9461158124c2b6 50 welcome.txt

图片24
从上面的命令可以看出:

  • 到处都是40位的SHA1哈希值格式的ID,可以用于指代文件内容(blob),用于指代目录树(tree),还可以用于指代提交。
  • git write-tree的输出就是写入Git对象库中的Tree ID,这个ID将作为下一条命令的输入。
  • git ls-tree命令中,没有把40位的ID写全,而是使用了前几位,实际上只要不和其他的对象ID冲突,可以随心所欲的使用缩写ID。
  • git ls-tree的输出显示的第一条是一个tree对象,即刚才创建的一级目录a

如果想要递归显示目录内容,则使用-r参数调用。使用参数-t可以把递归过程中遇到的每棵树都显示出来,而不是只显示最终的文件。下面执行递归操作显示目录树的内容。

1
2
3
4
5
6
$ git write-tree | xargs git ls-tree -l -r -t
040000 tree 53583ee687fbb2e913d18d508aefd512465b2092 - a
040000 tree 514d729095b7bc203cf336723af710d41b84867b - a/b
040000 tree deaec688e84302d4a0b98a1b78a434be1b22ca02 - a/b/c
100644 blob 18832d35117ef2f013c4009f5b2128dfaeff354f 7 a/b/c/hello.txt
100644 blob a2b90cc2992cf3ff4e0d6beb0c9461158124c2b6 50 welcome.txt

图片25

通过使用不同的参数调用git diff命令,可以对工作区、暂存区、HEAD中的内容两两比较。下面的图展示了git diff命令的作用范围。
图片26

通过上面的图就不难理解下面命令的输出。

  • 工作区和暂存区比较。
1
$ git diff

图片27

  • 暂存区和HEAD比较。
1
$ git diff --cached

图片28

  • 工作区和HEAD比较。
1
$ git diff HEAD

图片29

不要使用git commit -a

实际上Git的提交命令git commit可以带上-a参数,对本地所有变更的文件执行提交操作,包括本地修改的文件,删除的文件,但不包括未被版本库跟踪的文件。
这个命令的确可以简化一些操作,但同时会丢掉Git暂存区带给用户最大的好处:对提交内容进行控制的能力。

本文标题:Git学习之~工作区、暂存区、HEAD

文章作者:王洪博

发布时间:2018年05月24日 - 22:05

最后更新:2019年09月12日 - 10:09

原始链接:http://whb1990.github.io/posts/2c9fc045.html

▄︻┻═┳一如果你喜欢这篇文章,请点击下方"打赏"按钮请我喝杯 ☕
0%