Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Git内部原理之Git引用 - jingsam #60

Open
eightHundreds opened this issue Mar 15, 2022 · 0 comments
Open

Git内部原理之Git引用 - jingsam #60

eightHundreds opened this issue Mar 15, 2022 · 0 comments

Comments

@eightHundreds
Copy link
Owner

这篇文章本应该在 6 月份就完成,拖了 4 个月之后,终于鼓起勇气捡起来,实在惭愧。坚持写文章就像长跑,途中跑起来基本是靠惯性,如果停下来再起跑就很累很困难。

闲话不多说,本篇继续承接前文讲一讲 Git 内部原理,本篇的主题是 Git 引用的原理。

首先来搞清楚什么是 Git 引用,前文讲了 Git 提交对象的哈希、存储原理,理论上我们只要知道该对象的 hash 值,就能往前推出整个提交历史,例如:

$ git log --pretty=oneline 3ac728ac62f0a7b5ac201fd3ed1f69165df8be31
3ac728ac62f0a7b5ac201fd3ed1f69165df8be31 third commit
d4d2c6cffb408d978cb6f1eb6cfc70e977378a5c second commit
db1d6f137952f2b24e3c85724ebd7528587a067a first commit

现在问题来了,提交对象的这 40 位 hash 值不好记忆,Git 引用相当于给 40 位 hash 值取一个别名,便于识别和读取。Git 引用对象都存储在.git/refs目录下,该目录下有 3 个子文件夹headstagsremotes,分别对应于 HEAD 引用、标签引用和远程引用,下面分别讲一讲每种引用的原理。

HEAD 引用是用来指向每个分支的最后一次提交对象,这样切换到一个分支之后,才能知道分支的 “尾巴” 在哪里。HEAD 引用存储在.git/refs/heads目录下,有多少个分支,就有相应的同名 HEAD 引用对象。例如代码库里面有mastertest两个分支,那么.git/refs/heads目录下就存在mastertest两个文件,分别记录了分支的最后一次提交。

HEAD 引用的内容就是提交对象的 hash 值,理论上我们可以手动地构造一个 HEAD 引用:

$ echo "3ac728ac62f0a7b5ac201fd3ed1f69165df8be31" > .git/refs/heads/master

Git 提供了一个专有命令update-ref,用来查看和修改 Git 引用对象,当然也包括 HEAD 引用:

$ git update-ref refs/heads/master 3ac728ac62f0a7b5ac201fd3ed1f69165df8be31
$ git update-ref refs/heads/master
3ac728ac62f0a7b5ac201fd3ed1f69165df8be31

上面的命令我们将master分支的 HEAD 指向了3ac728ac62f0a7b5ac201fd3ed1f69165df8be31,现在用git log查看下master的提交历史,可以发现最后一次提交就是所更新的 hash 值:

$ git log --pretty=oneline master
3ac728ac62f0a7b5ac201fd3ed1f69165df8be31 (HEAD -> master) third commit
d4d2c6cffb408d978cb6f1eb6cfc70e977378a5c second commit
db1d6f137952f2b24e3c85724ebd7528587a067a first commit

同理,可以使用同样的方法更新test分支的 HEAD:

$ git update-ref refs/heads/test d4d2c6cffb408d978cb6f1eb6cfc70e977378a5c
$ git log --pretty=oneline test
d4d2c6cffb408d978cb6f1eb6cfc70e977378a5c (test) second commit
db1d6f137952f2b24e3c85724ebd7528587a067a first commit

.git/refs/heads目录下存储了每个分支的 HEAD,那怎么知道代码库当前处于哪个分支呢?这就需要一个代码库级别的 HEAD 引用。.git/HEAD这个文件就是整个代码库级别的 HEAD 引用。我们先查看一下.git/HEAD文件的内容:

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

我们发现.git/HEAD文件的内容不是 40 位 hash 值,而像是指向.git/refs/heads/master。尝试切换到test

$ git checkout test
$ cat .git/HEAD
ref: refs/heads/test

切换分支后,.git/HEAD文件的内容也跟着指向.git/refs/heads/test.git/HEAD也是 HEAD 引用对象,与一般引用不同的是,它是 “符号引用”。符号引用类似于文件的快捷方式,链接到要引用的对象上。

Git 提供专门的命令git symbolic-ref ,用来查看和更新符号引用:

$ git symbolic-ref HEAD refs/heads/master
$ git symbolic-ref HEAD refs/heads/test

至此,我们分析了两种 HEAD 引用,一种是分支级别的 HEAD 引用,用来记录各分支的最后一次提交,存储在.git/refs/heads目录下,使用git update-ref来维护;一种是代码库级别的 HEAD 引用,用来记录代码库所处的分支,存储在.git/HEAD文件,使用git symbolic-ref来维护。

标签引用,顾名思义就是给 Git 对象打标签,便于记忆。例如,我们可以将某个提交对象打 v1.0 标签,表示是 1.0 版本。标签引用都存储在.git/refs/tags里面。

标签引用和 HEAD 引用本质是 Git 引用对象,同样使用git update-ref来查看和修改:

$ git update-ref refs/tags/v1.0 d4d2c6cffb408d978cb6f1eb6cfc70e977378a5c
$ cat .git/refs/tags/v1.0
d4d2c6cffb408d978cb6f1eb6cfc70e977378a5c

还有一种标签引用称为 “附注引用”,可以为标签添加说明信息。上面的标签引用打了一个v1.0的标签表示发布 1.0 版本,有时候发布软件的时候除了版本号信息,还要写更新说明。附注引用就是用来实现打标签的同时,也可以附带说明信息。

附注引用是怎么实现的呢?与常规标签引用不同的是,它不直接指向提交对象,而是新建一个 Git 对象存储到.git/objects中,用来记录附注信息,然后附注标签指向这个 Git 对象。

使用git tag建立一个附注标签:

$ git tag -a v1.1 3ac728ac62f0a7b5ac201fd3ed1f69165df8be31 -m "test tag"
$ cat .git/refs/tags/v1.1
8be4d8e4e8e80711dd7bae304ccfa63b35a6eb8c

使用git cat-file来查看附注标签所指向的 Git 对象:

$ git cat-file -p 8be4d8e4e8e80711dd7bae304ccfa63b35a6eb8c
object 3ac728ac62f0a7b5ac201fd3ed1f69165df8be31
type commit
tag v1.1
tagger jingsam <jing-sam@qq.com> 1529481368 +0800

test tag

可以看到,上面的 Git 对象存储了我们填写的附注信息。

总之,普通的标签引用和附注引用同样都是存储的是 40 位 hash 值,指向一个 Git 对象,所不同的是普通的标签引用是直接指向提交对象,而附注标签是指向一个附注对象,附注对象再指向具体的提交对象。

另外,本质上标签引用并不是只可以指向提交对象,实际上可以指向任何 Git 对象,即可以给任何 Git 对象打标签。

远程引用,类似于.git/refs/heads中存储的本地仓库各分支的最后一次提交,在.git/refs/remotes是用来记录多个远程仓库各分支的最后一次提交。

我们可以使用git remote来管理远程分支:

$ git remote add origin git@github.com:jingsam/git-test.git

上面添加了一个origin远程分支,接下来我们把本地仓库的master推送到远程仓库上:

$ git push origin master
Counting objects: 9, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (9/9), 720 bytes | 360.00 KiB/s, done.
Total 9 (delta 0), reused 0 (delta 0)
To github.com:jingsam/git-test.git
 * [new branch]      master -> master

这时候在.git/refs/remotes中的远程引用就会更新:

$ cat .git/refs/remotes/origin/master
3ac728ac62f0a7b5ac201fd3ed1f69165df8be31

和本地仓库的master比较一下,发现是一模一样的,表示远程分支和本地分支是同步的:

$ cat .git/refs/heads/master
3ac728ac62f0a7b5ac201fd3ed1f69165df8be31

由于远程引用也是 Git 引用对象,所以理论上也可以使用git update-ref来手动维护。但是,我们需要先把代码与远程仓库进行同步,在远程仓库中找到对应分支的 HEAD,然后使用git update-ref进行更新,过程比较麻烦。而我们在执行git pullgit push这样的高层命令的时候,远程引用会自动更新。

到这里,三种 Git 引用都已分析完毕。总的来说,三种 Git 引用都统一存储到.git/refs目录下,Git 引用中的内容都是 40 位的 hash 值,指向某个 Git 对象,这个对象可以是任意的 Git 对象,可以是数据对象、树对象、提交对象。三种 Git 引用都可以使用git update-ref来手动维护。

三种 Git 引用对象所不同的是,分别存储于.git/refs/heads.git/refs/tags.git/refs/remotes, 存储的文件夹不同,赋予了引用对象不同的功能。HEAD 引用用来记录本地分支的最后一次提交,标签引用用来给任意 Git 对象打标签,远程引用正式用来记录远程分支的最后一次提交。
https://jingsam.github.io/2018/10/12/git-reference.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant