Git是一个非常优秀的内容版本控制工具。从本质上来讲,Git是一个内容寻址(content addressable)文件系统,在此基础上才提供了一个版本控制系统。

Git仓库目录

初始化一个本地Git仓库,可以看到Git创建了一个.git目录,这个目录存储了一切Git仓库的信息。

.
|---a.txt
|---.git
| |---config            # 项目特有的配置选项,与.gitignore格式一致
| |---objects           # Git对象数据库存放目录,包含本地仓库所有的文件内容和指针
| | |---3b
| | | |---18e512dba79e4c8300dd08aeb37f8e728b8dad
| | |---...
| | |---info
| |---rr-cache
| |---HEAD              # 记录目前被checkout出的分支
| |---info              # 包含一个全局性排除文件,放置不希望记录在.gitignore文件中的忽略模式
| | |---exclude
| |---logs
| | |---HEAD
| | |---refs            # 存放本地和远程仓库的分支,标签,头指针等信息
| | | |---heads
| | | | |---master
| |---description       # GitWeb程序使用的描述文件
| |---hooks             # 钩子脚本目录,自带一些示例脚本,可以在Git运行的各个环节执行自定义的操作
| | |---commit-msg.sample
| | |---...
| |---refs              # 存储指向数据(分支)的提交对象的指针
| | |---heads
| | | |---master
| | |---tags
| |---index             # 保存暂存区信息
| |---branches
| |---MERGE_RR
| |---COMMIT_EDITMSG

以上是Git目录的简单介绍,更加详细的介绍可以参考gitrepository-layout

在介绍后面的内容之前需要了解Git命令的一个分类。Git的命令有两大分类:

  • 高层命令(porcelain):checkout、branch、remote等30多个日常使用的Git命令。
  • 底层命令(plumbing):hash-object、cat-file等一些底层工作的命令,平时并不常用。

数据对象(Blob Object)

Git的核心部分可以看作是一个简单的key-value数据库,可以向数据库插入任意类型的内容,然后会返回一个key,通过返回的这个key可以检索到之前插入的value。Git所有的数据存放在。.git/objects目录下,也就是数据库的位置。数据库的SET和GET操作可以用以下两个命令完成:

  • hash-object:SET操作,将待存储的数据外加一个头部信息一起做SHA-1校验运算得到一个40字符长度的校验和并输出。
    • -w:存储数据对象
    • --stdin:从标准输入读取内容,不指定这个的话需要在后面给出待存储文件路径。
  • cat-file:GET操作
    • -p:自动判断内容类型,并以友好的格式显示
    • -t:返回内部存储对象的类型

SET操作

$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4
$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

可以看到.git/objects目录下有个文件,在Git中一个文件对应一个目录。

GET操作

创建一个新文件,并存入数据库。

$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30

接着修改文件继续存入。

$ echo 'version 2' > test.txt
$ git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

可以看到这样并不会覆盖之前的文件,这个文件每次修改的版本都在数据库里。

$ find .git/objects -type f
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

可以用cat-file命令将文件恢复到第一个版本。

$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt
$ cat test.txt
version 1

上面的一些操作仅保存了文件的内容,文件名还没有保存。这种保存了文件内容的对象在Git中叫做数据对象(blob object)。

树对象(Tree Object)

树对象解决的就是文件名保存的问题,允许我们将多个文件组织在一起。Git中所有的内容均以树对象和数据对象的形式存储,其中树对像就跟Linux文件系统中的目录一样,数据对象对应了inodes或文件内容。一个树对象包含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的SHA-1指针,以及相应的模式、类型、文件名信息。

通常Git根据某一时刻暂存区所表示的状态创建并记录一个对应的树对象,如此重复便可依次记录(某个时间段内)一系列的树对象。所以树对象的创建主要有两步,首先将文件添加到暂存区,然后将暂存区的内容写入树对象,这里主要有三个命令。

  • update-index:更新暂存区命令
    • --add:添加文件到暂存区
    • —remove:从暂存区移除文件
    • --cacheinfo <mode> <object> <path>:插入特定信息到暂存区,包括文件模式,对象SHA-1值,路径
  • write-tree:根据当前暂存区内容创建一个新的树对象,然后写入。
  • read-tree:将树对象读入暂存区
    • —prefix:将已有的树对象作为子树读入暂存区
$ git update-index --add --cacheinfo 100644 83baae61804e65cc73a7201a7252750c76066a30 test.txt
$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30      test.txt
$ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree
$ echo 'new file' > new.txt
$ git update-index --cacheinfo 100644 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
$ git update-index --add new.txt
$ git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341
$ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt
$ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git write-tree
3c4e9cd789d88d8d89c1073707c3585e41b0e614
$ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614
040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579      bak
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt

此时的Git数据内容结构如下图:

data-model-2.png

提交对象(Commit Object)

已经解决了文件版本,文件名以及目录的问题,但是如果想重用这些版本快照,我们却只有几个SHA-1哈希值,不知道关于这些快照保存的内容以及原因等信息,非常不方便,所以我们需要提交对象(Commit Object)。提交对象使用commit-tree命令创建,创建时需要指定一个树对象的哈希值,以及该提交对象的父提交对象(如果有的话)。

$ echo 'first commit' | git commit-tree d8329f
fdf4fc3344e67ab068f836878b6c4951e3b15f3d
$ git cat-file -p fdf4fc3 # 查看这个刚刚创建的提交对象
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author Scott Chacon <[email protected]> 1243040974 -0700
committer Scott Chacon <[email protected]> 1243040974 -0700

first commit
$ echo 'second commit' | git commit-tree 0155eb -p fdf4fc3
cac0cab538b970a37ea1e769cbbde608743bc96d
$ echo 'third commit'  | git commit-tree 3c4e9c -p cac0cab
1a410efbd13591db07496601ebc7a059dd55cfe9

此时Git目录下的所有对象如下图:

data-model-3.png

此刻,已经通过一些底层命令完成了git addgit commit的操作。 Git 所做的实质工作——将被改写的文件保存为数据对象,更新暂存区,记录树对象,最后创建一个指明了顶层树对象和父提交的提交对象。 这三种主要的 Git 对象——数据对象、树对象、提交对象——最初均以单独文件的形式保存在 .git/objects 目录下。 下面列出了目前示例目录内的所有对象,辅以各自所保存内容的注释:

$ find .git/objects -type f
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2
.git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1
.git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content'
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
.git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1

参考资料