4
头图

一. 引言

什么是Git引用和分支?比如我在 Github 上一个项目的 .git/refs目录下:

├─heads
│      dev
│      master
│
├─remotes
│  └─origin
│          master
│
└─tags

refs 目录下包含了 heads、remote、tags 三个子目录,每个子目录下都有对应的文件
打开 heads/master 文件,查看其内容:

$ cat heads/master
1b41db435c03fe80fa965dc77442261708deb16d

上述这段编码,其实就是 SHA-1 值,再来看看其类型和内容:

$ git cat-file -p 1b41d
tree 03073e441d5360400b758257647c2a0250ee38c7
parent 90157be126d9204779c63aaadf3a95f5fa207fc4
author haohuin <huins...@gmil.com> 1623935829 +0800
committer haohuin <huins...@gmil.com> 1623935829 +0800
系统介绍Readme

这个 SHA-1 值就指向 Git 仓库中的某个提交对象 ID。既然能用 SHA-1 值来区分不同的提交对象,为何要创建一个存放 SHA-1 值的文件呢?因为 SHA-1 值过于长,而且没有规律,如果利用简单的文件名来引用对原来的提交对象,就不用记住复杂且无规律的 SHA-1 值了。
因此在 Git 中,像这种只含有 SHA-1 值的文件,就是 Git 的引用(Reference)。而分支,就是一个指向某一系列提交之首的指针或引用。如下图所示:
image.png
下面就讲解一下 Git 中的引用和分支的具体内容

二. Git引用

在引言中提到过,Git 引用内部存储着 SHA-1 值,来跟踪和引用不同的 Git 对象,从类型上讲,可以分成 HEAD 引用,分支引用(branch),标签引用(tag)和远程引用(remote):

2.1. HEAD引用

.git根目录中,有一个 HEAD 文件,我们先来查看一下 HEAD 文件中的内容:

$ cat .git/HEAD
ref: refs/heads/master

发现 HEAD 中的内容指向了一个引用 refs.heads/master,当执行 git checkout dev,将当前版本切换至 dev 分支后,HEAD 文件内容会变成如下引用值:

$ git checkout dev
Switched to branch 'dev'

$ cat .git/HEAD
ref: refs/heads/dev

此时 HEAD 指向的分支成了 dev,说明 HEAD 的内容就是指向当前 Git 仓库所在的分支。HEAD 文件通常是一个符号引用(symbolic reference),指向目前所在的分支或提交。 所谓符号引用,表示它是一个指向其他引用的指针。让我们首先来看看指向分支的 HEAD:

2.1.1 指向分支

如下图所示,是 HEAD 指向某个分支:
image.png
我们可 通过 git checkout <分支名>来使得 HEAD 在不同分支中进行切换。
此外有一种特殊情况,会导致 HEAD 中的内容直接为某个提交对象的 SHA-1 ID 值:

2.1.2 指向某个提交对象

当使用 git checkout检出某个非分支引用指向下的提交对象时:

//先查看所有的提交对象
 $ git log
commit 1b41db435c03fe80fa965dc77442261708deb16d (HEAD -> dev, origin/master, master)
Author: haohuin <huins...@gmil.com> 
Date:   Thu Jun 17 21:17:09 2021 +0800

    系统介绍Readme

commit 90157be126d9204779c63aaadf3a95f5fa207fc4
Author: haohuin <huins...@gmil.com>
Date:   Thu Jun 17 21:08:07 2021 +0800

    基本功能完成

commit f7d98432d5bc8c1a67e4c4f75795544cb8a7f4d0
Author: haohuin <huins...@gmil.com>
Date:   Thu Jan 21 21:26:48 2021 +0800

    init repository

//将HEAD指向最早的提交-init repository
$ git checkout f7d98432d5bc
Note: switching to 'f7d98432d5bc'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at f7d9843 init repository

而后出现了一大段的描述,其实现在就处于"detached HEAD"(分离头指针)状态,我们再来看看此时 HEAD 中的内容:

$ cat .git/HEAD
f7d98432d5bc8c1a67e4c4f75795544cb8a7f4d0

此时 HEAD 指向了一个具体的提交 ID,而不是一个分支引用。
在这种情况下,如果再次进行提交操作,创建了一个新的提交,但是此时的 master 和 dev 分支引用都不会向前移动。HEAD 也不会指向这个新提交,那么因为没有引用指向该提交,Git 的垃圾回收机制可能最后会清理掉这个提交:
image.png
但是分离头指针的这个特性,也有助于创建临时分支的情景。
总结来说,HEAD 引用有以下作用:

  • 当 HEAD 指向当前所处的分支或提交,它表示正在工作的位置。当再执行 Git 操作时,比如提交,检出分支等时,Git 根据 HEAD 的位置确定在哪个分支上进行操作。
  • 当 HEAD 指向某一个具体的提交时,会进入分离头指针状态。在这种状态下,可以在历史提交上工作,创建新的提交,但不会更新任何分支。这在实验性工作、查看旧版本代码或创建临时分支时很有用。
  • 当 HEAD 执行分支时,可以使用 ^ 符号用于引用上一次提交。例如,HEAD^ 表示 HEAD 的父提交,HEAD^^ 表示 HEAD 的父提交的父提交,以此类推。这在查看提交历史、比较版本之间的差异等操作中很有用。
  • 当存在合并冲突时,HEAD 在解决冲突时起到重要作用。HEAD 指向当前分支的最新提交,同时也是合并操作的目标分支。可以通过编辑冲突文件并执行合适的操作,来解决冲突后进行提交。

2.2. 分支引用

在Git中,分支引用是指向特定提交(commit)的可变指针。它们用于跟踪各个分支在代码库中的位置,以便在不同的提交之间进行切换和管理。

2.2.1 本地分支

分支引用存储在 .git/refs/heads 目录下,每个分支引用对应该目录下的一个文件。比如在当前仓库下的 .git/refs/heads目录下有两个分支 dev 和 master,其内部存储的值如下:

$ cat .git/refs/heads/master
1b41db435c03fe80fa965dc77442261708deb16d
$ cat .git/refs/heads/dev
1b41db435c03fe80fa965dc77442261708deb16d
$ git cat-file -p 1b41db4
tree 03073e441d5360400b758257647c2a0250ee38c7
parent 90157be126d9204779c63aaadf3a95f5fa207fc4
author haohuin <huins...@gmil.com> 1623935829 +0800
committer haohuin <huins...@gmil.com> 1623935829 +0800

系统介绍Readme

两个分支文件内部的内容就是指向某个提交对象,此处两个分支指向的提交对象一致。
分支引用通常是指向最新提交的指针。当在分支上进行提交时,分支引用会自动更新以指向新的提交。这样,Git就能够跟踪分支的进展和历史记录。
除了本地分支引用,还有远程分支引用。

2.2.2 远程分支

远程分支引用跟踪远程仓库中的分支。这些引用存储在 .git/refs/remotes/ 目录下。
如果你添加了一个远程版本库并对其执行过推送操作,Git 会记录下最近一次推送操作时每一个分支所对应的值,并保存 refs/remotes 目录下。在我的项目中有远程提交的仓库 orgin,可以通过 git remote showw 来看看当前远程仓库:

$ git remote show
origin
$ cat .git/refs/remotes/origin/master
1b41db435c03fe80fa965dc77442261708deb16d

此时远程仓库 origin中的分支 master对应的 SHA-1 值就是本地仓库中最新的提交对象。

远程引用和分支(位于 refs/heads 目录下的引用)之间最主要的区别在于,远程引用是只读的。 虽然可以 git checkout 到某个远程引用,但是 Git 并不会将 HEAD 引用指向该远程引用。因此,你永远不能通过 commit 命令来更新远程引用。 Git 将这些远程引用作为记录远程服务器上各分支最后已知位置状态的书签来管理。

在远程引用中,可能会涉及到引用规范的内容,具体可以看这篇文章Git - 引用规范

2.3. 标签引用

除了 HEAD 引用和分支引用,在 Git 对象中还有标签对象,从前面的深入剖析Git对象底层原理我们知道,标签对象有两种:

  • 轻量标签:只有一个固定的指向提交对象的引用值,类似于分支引用
  • 附注标签:会创建一个标签对象,其中记录一个引用来指向该标签对象

标签对象类似于分支引用,但与分支引用不同的是,标签对象是不可变的,一旦创建就不能更改。
标签对象通常用于指定软件版本、发布快照或重要里程碑。它们提供了一个有意义的名称来标识特定的提交或代码状态。
标签对象存储在 .git/refs/tags/ 目录下,每个标签对应该目录下的一个文件。例如,如果有一个名为 firstTag 的标签,对应的标签对象文件就是 .git/refs/tags/firstTag
在我的仓库中分别存储了两种标签,可以看到其对应的标签,以及其引用的对象:

$ find .git/refs/tags/
.git/refs/tags/
.git/refs/tags/firstTag
.git/refs/tags/secondTag

//1.查看轻量标签,其内部引用值指向提交对象
$ cat .git/refs/tags/firstTag
2b2af66549827bd6a466fe43081f406c2a12900b

$ git cat-file -p firstTag
tree 2503e9e0c4f774fc5ce298f4972f0e6d3a800d6f
parent 7b34a1e750918570ed610ee1f228e83b43a1192e
author haohuin <huins...@gmil.com> 1705458723 +0800
committer haohuin <huins...@gmil.com> 1705458723 +0800

second commit

//2.查看附注标签,其内部引用值指向标签对象
$ cat .git/refs/tags/secondTag
9cb2c0bd900b37a05f7c531fdd07cece35097045

$ git cat-file -p secondTag
object 8a4678fae181c16c6f4ff0e6a618991128d86da2
type commit
tag secondTag
tagger haohuin <huins...@gmil.com> 1705480524 +0800

secondTag

三. 总结

总体来说,引用机制让Git可以通过简单但唯一的ID来识别和追踪对象,管理项目版本以及支持分布式协作开发模式。Git中的引用主要包括

  1. HEAD引用,指向当前检出的分支或提交对象。HEAD管理 Git 当前操作所在的分支或提交状态。
  2. 分支引用,存储在.git/refs/heads目录下,每个分支对应一个引用文件。指向某个提交对象,跟踪分支历史记录和进度。本地分支和远程分支分别保存在不同目录下。
  3. 标签引用,存储在.git/refs/tags目录下,每个标签对应一个文件或对象。用于标记特定版本,不可变更。分支和标签都是引用的一种,但功能不同。分支指导开发顺序,标签标记具体版本。

参考资料

Git - Git 引用
《Git 权威指南》


归思君
1.2k 声望209 粉丝

阿里云社区专家博主,华为云社区云享专家,一个会点前端的java工程师。