月知录

跳转到正文(jump to main content)

Git探秘

前言

TODO 为什么写?

适合哪些人看?

适合使用过Git,对Git有了一些了解,但是想进一步向底层探究Git怎么存储提交的内容、不同操作到底对存储的内容做了什么操作,以便在以后使用Git的过程中更得心应手、从容安心的那些人。

Git仓库内容

让我们在空目录下新建一个仓库,看一下Git都会生成什么内容。

$ git init .
$ ls -F1 .git/
branches/
config
description
HEAD
hooks/
info/
objects/
refs/

各目录/文件用途分别是:

  • description
    仅在GitWeb程序中使用。
  • config
    本项目相关的配置。
  • info
    有一个全局排除文件,用来保存不希望放到.gitignore文件的忽略模式。
  • hooks
    保存客户/服务侧的hook脚本。
  • objects
    保存所有的数据内容。
  • refs
    保存指向提交对象(commit object)数据(分支,标签,远程仓库等)的指针。
  • HEAD
    指向当前检出的分支。

后面随着在仓库下工作,Git还会生成别的文件,例如 index ,Git用来保存暂存区信息。

Git对象和引用

Git是一个内容寻址的文件系统,意思是它的核心是一个简单的 键值对数据存储 。 你可以插入任意类型的信息到Git仓库,Git会为此数据返回一个唯一键,后续可以用这个唯一键来获取插入的数据。
Git里的对象有下面几种类型。这篇文章就是要探究在不同的操作下,这些对象会不会有什么改变,以及怎么改变。

Blob对象

用来存储保存在Git仓库里的每个文件的内容的对象类型。Blob本身没有文件名信息,文件名是在tree对象中记录的。

Tree对象

Tree对象类似文件系统的目录树,保存Git仓库中目录和文件的关系。每个tree对象包含一个或者多个项,每一项对应一个blob对象或者一个子tree,及其模式,类型和文件名。

Commit对象

Commit对象是Git仓库的一个快照,记录了当时的文件层次结构以及文件内容。

TODO Tag对象

TODO 引用

Git常用操作探秘

现在开始探究Git的一些常用操作,看看这些操作会给仓库内容带来什么改变。

Git暂存区

暂存区暂存了什么

在空目录下初始化Git仓库,然后添加一个内容为"hello staging area"的文件test1.txt,此时仓库状态如下:

$ git status
位于分支 master

尚无提交
未跟踪的文件:
  (使用 "git add <文件>..." 以包含要提交的内容)        test1.txt

提交为空,但是存在尚未跟踪的文件(使用 "git add" 建立跟踪)

然后我们执行 git add test1.txt 将这个文件添加到暂存区。再看.git目录,发现多了个 index 文件,

$ ls -F1 .git/
branches/
config
description
HEAD
hooks/
index
info/
objects/
refs/

并且, .git/objects 多了 一个 文件,其sha1哈希值为78d3b5bd4c0bd6a28fc3760c8d48021355e9334e。

$ find .git/objects/
.git/objects/
.git/objects/78
.git/objects/78/d3b5bd4c0bd6a28fc3760c8d48021355e9334e
.git/objects/info
.git/objects/pack

而这个文件的内容就是test1.txt的内容,可以用下面的方式确认:

$ git cat-file -p 78d3b5bd4c0bd6a28fc3760c8d48021355e9334e
hello staging area

index 文件的内容可以用 git ls-files 查看:

$ git ls-files -s
100644 78d3b5bd4c0bd6a28fc3760c8d48021355e9334e 0       test1.txt

可以看到,index文件的内容就是上面我们添加的文件 test1.txt 以及模式,对应的blob对象等信息。

下面我们看一下取消文件暂存,index文件会怎么变化。

$ git rm --cached test1.txt 
rm 'test1.txt'
$ git status
位于分支 master

尚无提交
未跟踪的文件:
  (使用 "git add <文件>..." 以包含要提交的内容)        test1.txt

提交为空,但是存在尚未跟踪的文件(使用 "git add" 建立跟踪)

看下 .git/objects 目录,

$ find .git/objects/
.git/objects/
.git/objects/78
.git/objects/78/d3b5bd4c0bd6a28fc3760c8d48021355e9334e
.git/objects/info
.git/objects/pack

git add 添加的文件并不会因为取消暂存而被删除,但index文件中的记录会删除(但此时index文件并不会被删除,文件长度也不是0),可以再执行 git ls-files -s 来验证。

还有一个需要的点,因为暂存区是下次要提交的仓库快照,所以它会包含仓库中git记录的所有文件,而不仅仅是上次提交之后通过 git add 添加到暂存区的文件。

删除暂存区文件会发生什么

如果 git add test1.txt 之后不提交,而是把index文件删除,会发生什么呢?
删除之前,我们看下仓库的状态。

$ git status
位于分支 master

尚无提交
要提交的变更:  (使用 "git rm --cached <文件>..." 以取消暂存)        新文件:   test1.txt

现在我们把 .git/index 文件删除,再次查看仓库的状态。

$ git status
位于分支 master

尚无提交
未跟踪的文件:
  (使用 "git add <文件>..." 以包含要提交的内容)        test1.txt

提交为空,但是存在尚未跟踪的文件(使用 "git add" 建立跟踪)

又变为刚创建 test1.txt 文件时的状态了。

Git提交

接着暂存区暂存了什么,继续执行 git commit ,把test1.txt提交到仓库里,看会发生什么。

$ git add test1.txt
$ git commit -m 'first commit - test1.txt'
[master(根提交) fe655fb] first commit - test1.txt
 1 file changed, 1 insertion(+)
 create mode 100644 test1.txt

此时, .git/objects 一共会有三个文件(会多出来两个文件):

fe655fb1d5de1e068a1dbcb05ca628fa6076f643 :commit:
78d3b5bd4c0bd6a28fc3760c8d48021355e9334e :blob:
788d51e06017d18f7e13400805014bb62bf0fa4e :tree:

多出来的两个,一个是commit对象,另一个是commit对应的tree对象。查看tree对象的内容,看看是什么。

$ git cat-file -p 788d51e0
100644 blob 78d3b5bd4c0bd6a28fc3760c8d48021355e9334e    test1.txt

是test1.txt的信息,就是我们上面说过的模式、类型、哈希值以及文件名。

下面再添加一个子目录sub,并在其中创建一个文件test2.txt后提交看看仓库内容有什么变化。

$ mkdir sub
$ echo "hello test2" > sub/test2.txt
$ git add .
$ find .git/objects/
.git/objects/
.git/objects/fe
.git/objects/fe/655fb1d5de1e068a1dbcb05ca628fa6076f643
.git/objects/78
.git/objects/78/d3b5bd4c0bd6a28fc3760c8d48021355e9334e
.git/objects/78/8d51e06017d18f7e13400805014bb62bf0fa4e
.git/objects/info
.git/objects/pack
.git/objects/e6
.git/objects/e6/94444553266392e753e1fd4b168b3f04feb92a
$ git commit -m 'second commit - sub/test2.txt'
[master 6087737] second commit - sub/test2.txt
 1 file changed, 1 insertion(+)
 create mode 100644 sub/test2.txt

此时, .git/objects 下有如下这些文件:

608773728b3c86b5ea7f41e551895c96e60b9b91 :commit:
33a5993d35e13e62cc430ab0c3f842147f1d17e6 :tree:
fe655fb1d5de1e068a1dbcb05ca628fa6076f643 :commit:
51abbe01a27c194753550a3feadc49cac946d00a :tree:
78d3b5bd4c0bd6a28fc3760c8d48021355e9334e :blob:
788d51e06017d18f7e13400805014bb62bf0fa4e :tree:
e694444553266392e753e1fd4b168b3f04feb92a :blob:

它们之间的指向关系如下:

* 60877372 :commit:
** 33a5993d :tree:
*** 51abbe01 :tree:
    sub
**** e6944445 :blob:
     test2.txt
*** 78d3b5bd :blob:
    test1.txt
** fe655fb1 :commit:
*** 788d51e0 :tree:
**** 78d3b5bd :blob:
     test1.txt

层级关系对不同类型的对象来说含义不同。对于commit来说,内层commit是外层commit的父/祖先commit,例如 fe655fb 就是 60877372 的父commit;commit的下级tree表示此commit对应的tree对象;tree就好理解了,可以看成是目录树。

Git分支

Git合并

Git revert

Git rebase

Git cherry pick

Git reset

Git标签