目录
- 1. 三分钟上手! 一文看懂 Git 的底层工作原理
- 1.1. Git 目录结构
- 1.2. Git 三大对象
- 1.3. Git Brach 和 Tag
1. 三分钟上手! 一文看懂 Git 的底层工作原理
1.1. Git 目录结构
Git 的本质是一个文件系统(很重要, 记住这句话, 理解这句话), 工作目录中的所有文件的历史版本以及提交记录(commit)都是以文件对象的方式保存在 .git 目录中的。
我们先来创建一个名为 git-demo 空目录, 并采用 git init 命令初始化 Git 仓库。该命令会在工作目录下生成一个 .git 目录, 该目录将用于保存工作区中所有的文件历史的历史版本, commit, branch, tag 等所有信息。
$ mkdir git-demo
$ cd git-demo
$ git init
其目录结构如下:
待会我们重点关注下这几个目录:
- HEAD: 工作目录当前状态对应的 commit, 一般来说是当前 branch 的 head, HEAD 也可以通过 git checkout 命令被直接设置到一个特定的 commit 上, 这种情况被称之为 detached HEAD
- objects: 这里是真正保存 Git 对象的目录, 包括三类对象 commit, tree 和 blob(具体这三类对象是什么, 慢慢往下看就知道了)
- refs: 用来保存 branch 和 tag 对应的 commit
1.2. Git 三大对象
目前 Objects 目录中还没有任何内容, 我们创建一个文件并提交:
$ git:(master) echo "my project" > README
$ git:(master) mkdir src
$ git:(master) echo "hello world" > src/file1.txt
添加并提交:
$ git:(master) git add .
$ git:(master) git commit -m "init commit"
从打印输出可以看到, 上面的命令创建了一个 commit 对象, 该 commit 包含两个文件。查看 .git/objects
目录, 可以看到该目录下增加了 5 个子目录 06
, 3b
, 82
, c5
, ca
, 每个子目录下有一个以一长串字母数字命令的文件:
这一大串是什么?
Git Object 目录中存储了三种对象: Commit, Tree 和 Blob, Git 会为对象生成一个文件, 并根据文件信息生成一个 SHA-1 哈希值作为文件内容的校验和, 创建以该校验和前两个字符为名称的子目录, 并以 (校验和) 剩下 38 个字符为文件命名 , 将该文件保存至子目录下。
可以通过 git cat-file -t 哈希值 命令查看对象类型, 通过 git cat-file -p 哈希值 命令查看对象中的内容, 哈希值就是目录名+文件名, 在没有歧义的情况下, 命令可以不用输入整个哈希值, 输入前几位即可。
我们挨个看下:
065bca(blob):
3b18e(blob):
824244(tree):
c5bc98(commit):
ca96(tree):
认真看图, 大家看完也就差不多清楚了 commit、blob、tree 这几大对象是什么东西了
从 commit 对象(c5bc98)入手, commit 对象中保存了 commit 的作者, commit 的描述信息, 签名信息以及该 commit 中包含哪些 tree 对象和 blob 对象。从上图可知包含了 tree 对象(ca96)。
可以把 tree 对象看成这次提交相关的所有文件的根目录, 可以看到 ca96 这个 tree 对象中包含了一个 blob 对象(065bca), 即 README 文件, 以及一个 tree 对象(824244), 即 src 目录。而 blob 对象存储的就是真正的内容。
这几个对象的对应关系如下图所示:
1.3. Git Brach 和 Tag
现在来看下 HEAD 中的内容, 前面说过, HEAD 中存储的是工作目录当前状态对应的 commit:
$ git:(master) cat .git/HEAD
ref: refs/heads/master
$ git:(master) cat .git/refs/heads/master
c5bc98b8990bedd7444da537320559e601eba87b
c5bc98 正是我们最近的这次 commit!
master 是一个分支名, 所以分支(branch)的本质是一个指向 commit 的指针
我们切一个新分支 feat/work:
查看下 refs/heads/master 和 refs/heads/feat/work 中的 commit 值:
从其内容可以看到, feat/work 这个 branch 并没有创建任何新的版本文件, 和 master 一样指向了 c5bc98 这个 commit。
从上面的实验可以看出, 一个 branch 其实只是一个 commit 对象的应用, Git 并不会为每个 branch 存储一份拷贝, 因此在 git 中创建 branch 几乎没有任何代价。
接下来我们在 feat/work 这个 branch 上进行一些修改, 然后提交:
$ git:(feat/work) echo "new line" >> src/file1.txt
$ git:(feat/work) echo "do nothing" >> License
$ git:(feat/work) git add .
$ git:(feat/work) git commit -m "some change"
查看当前的 HEAD:
可以看到 HEAD 指向了 feat/work 这个 branch, 而 feat/work branch 则指向了 8a442 这个 commit, master branch 指向的 commit 未变化, 仍然是 c5bc98。
查看 8a442 这个 commit 对象的内容:
可以看到 commit 有一个 parent 字段, 指向了前一个 commit c5bc98。还包含了一个 tree 对象(2a9dd):
可以观察到, 由于 README 没有变化, 还是指向的 065bca 这个 blob 对象。License 是一个新建的 blob 对象, src 和 file1.txt 则指向了新版本的对象。
增加了这次 commit 后, Git 中各个对象的关系如下图所示:
Tag 和 branch 类似, 也是指向某个 commit 的指针。不同的是 tag 创建后其指向的 commit 不能变化, 而 branch 创建后, 其指针会在提交新的 commit 后向前移动。