前言
最近在读《Pro Git》这本书,虽然距离第二版已经过去了好几年,Git 也在不断更新,但由于 Git 核心团队一直保持着良好的向后兼容性,所以书中关于 Git 的核心概念和命令依然有效。
本篇为我对 Git 的基本原理——分支与引用的理解。
基本原理
上一篇文章讲过 Git 的本质是内容寻址的文件系统,还有其内容对象如何存储。本篇文章将讲述 Git 如何通过分支等概念对其进行更好的管理,还有上文中未曾讲过的 Git 第四种对象类型。
Git 引用
我们可以使用类似于 git log 4a50341ad5fd8b672a7485b5a13490e9d9a7161c 这样的命令来浏览完整的提交历史,但是为了能从提交线上找到所有的提交对象,你仍然需要记住这个 40 位的 SHA-1 哈希值是最后一个提交。所以,我们需要一个文件来保存 SHA-1 值,并给文件起一个简单的名字,然后用这个名字指针来替代原始 SHA-1 值。
在 Git 里,这样的文件被称为“引用(references)”,它们大部分被存储在 .git/refs/ 目录中。在 Git 刚初始化时,这里并没有引用文件,只有简单的目录结构:
$ find .git/refs
.git/refs
.git/refs/heads
.git/refs/tags
Git 分支
简单来说,Git 分支就是 SHA-1 值的别名,其本质上就是一个指向某一系列提交之首的指针或引用。分支会被存储在 .git/refs/heads/ 文件夹中,由于在初始化时没有提交对象,所以 Git 并未真正创建分支。我们可以使用 update-ref 命令创建引用,并指定提交对象:
$ git update-ref refs/heads/master 4a50341ad5fd8b672a7485b5a13490e9d9a7161c
现在,就可以使用分支名代替对象查看 log:
$ git log --pretty=oneline master
4a50341ad5fd8b672a7485b5a13490e9d9a7161c (HEAD -> master) second
47d5e351638a282f5b2c3d70113e890a8b59ac0f first
通过 cat 等命令就可以直接查看引用文件内容:
$ cat .git/refs/heads/master
4a50341ad5fd8b672a7485b5a13490e9d9a7161c
当运行类似 git branch <branchname> 命令创建分支时,Git 实际上会运行 update-ref 命令,取得当前分支的引用对象(SHA-1 值),并将其加入你想要创建的任何新引用中。
HEAD
现在的问题是,当你执行 git branch <branchname> 时,Git 如何知道你的当前分支呢?答案就是 HEAD 文件。
HEAD 文件的作用就是告诉 Git 仓库当前在哪里;大部分时候都是处于一个分支之上,此时 HEAD 文件是一个“符号引用(symbolic refenerce)”,指向当前所在分支。符号引用类似于文件的快捷方式,它是一个指向其他引用的指针。HEAD 文件位于 .git 目录下:
$ cat .git/HEAD
ref: refs/heads/master
当我们执行 git commit 命令时,该命令会创建一个提交对象,并用 HEAD 文件中那个引用所指向的 SHA-1 值设置其父提交字段。如果所指向的引用文件不存在,则会将当前提交对象作为根提交对象,并根据当前地址,创建一个引用文件。
Git 同样有命令负责处理符号引用:
$ git symbolic-ref HEAD
refs/heads/master
$ git symbolic-ref HEAD refs/heads/test
$ git symbolic-ref HEAD
refs/heads/test
但是不能设置不符合格式的值:
$ git symbolic-ref HEAD test
fatal: Refusing to point HEAD outside of refs/
分离头指针
而有的时候,HEAD 文件在有些时候会处于分离头状态(detached HEAD),在此时,HEAD 会变为分离头指针,指向提交对象。在此时,你可以做一些实验性的修改和提交,并且可以在切换回一个分支时,丢弃在此状态下所做的提交而不对分支造成影响,也可以将修改写入当前提交,修改历史。
一般会在遇到合并冲突或重写提交时进入此状态,也可以直接 checkout 到一个提交对象中,进入此状态。
其他 HEAD 文件
- ORIG_HEAD:.git/ORIG_HEAD 记录了最近一次危险动作之前的提交对象,用于快速撤销。
- FETCH_HEAD: .git/FETCH_HEAD 是一个短期引用,表示刚刚从远程获取的分支的最新提交对象。
- REBASE_HEAD:.git/REBASE_HEAD 同样是短期引用,用于记录 rebase 命令执行时,当前进行的提交对象。
以上就是 Git 分支的基本原理,非常简单。通过分支表明不同快照流,使用 HEAD 表明当前分支。
标签引用
在 Git 中有两种类型的标签:轻量标签和附注标签。轻量标签就像分支一样指向提交对象,但它不会改变,仅仅只是一个固定的引用。创建方法与分支一致:
$ git update-ref refs/tags/v1.0 4a50341ad5fd8b672a7485b5a13490e9d9a7161c
附注标签要更复杂一些。如果创建一个附注标签,它会先创建一个标签对象(tag object),而附注标签则作为固定引用,指向标签对象。标签对象是 Git 中的第四种标签,与提交对象类似——它包含一个标签创建者的信息、一个日期、一段注释信息,以及一个指针。主要区别在于,标签对象通常指向一个提交对象,而不是一个树对象。
我们通过创建一个附注标签来验证:
$ git tag --annotate v1.1 4a50341ad5fd8b672a7485b5a13490e9d9a7161c -m "annotate tag"
$ git cat-file -p v1.1
object 4a50341ad5fd8b672a7485b5a13490e9d9a7161c
type commit
tag v1.1
tagger cnbailian <[email protected]> 1590104482 +0800
annotate tag
标签对象并非必须指向某个提交对象;你可以对任意类型的 Git 对象打标签,通常用于存储不需要出现于工作目录中的文件。例如,在 Git 源码中,项目维护者将他们的 GPG 公钥添加为一个数据对象,然后对这个对象打了一个标签。 可以克隆一个 Git 版本库,然后通过执行下面的命令来在这个版本库中查看上述公钥:
$ git cat-file blob junio-gpg-pub
远程引用
远程引用与分支引用类似,只是用来记录分支最后一次推送时的提交对象。文件位于 .git/refs/remotes/ 目录下,如果你添加了一个远程版本库并对其执行过推送操作,Git 就会记录下提交对象,并根据 remote 名称存入子目录。
远程引用也可以使用 git update-ref 来维护,但并不建议这么做,容易造成混乱,应该由 git push 等命令自动修改。
总结
本篇为 Git 基本原理的第二篇,主要讲述了 Git 如何通过分支等概念对提交对象进行更好的管理。
但仅有这些还不够,我们还需要明白在 Git 下的文件生命周期,以及基本的工作流程解析。