玖叶教程网

前端编程开发入门

Git 进阶之基本原理——分支与引用

前言

最近在读《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 下的文件生命周期,以及基本的工作流程解析。

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言