引用的基本原理
Git使用SHA-1来追踪各个版本的文件和提交。但是SHA-1是一组无序字母数字,不变记忆,所以Git提供了引用,用简短的有意义的字母或者单词来代替SHA-1。那么字母或者单词与SHA-1需要一个映射关系。这个映射关系就是引用。它们被记录在.git/refs文件夹中。
下面使用一个全新的git工程来说明引用。
$ git init
Initialized empty Git repository in F:/97_example/git/ByHand/RefTestProject/.git/
确认一下此时refs文件夹的内容
$ find .git/refs
.git/refs
.git/refs/heads
.git/refs/tags
$ find .git/refs -type f
建立一个test.txt文件,改变一些内容,提交三次。
$ git log --pretty=oneline master
169c899e7e2d1a57e809d97998e541da01ba34e9 (HEAD -> master) 3rd commit
03787b6b681ede0bdf5f0f39d170a7dfcc22f6ec 2nd commit
5d2af1e8d157e2d54aa067f109fc2058607b64c2 1st commit
现在确认一下refs的内容,发现多了一个叫做master的文件。master就是引用名。
$ find .git/refs -type f
.git/refs/heads/master
用文本编辑器打开这个文件,会看到里面是一个SHA-1(169c899e7e2d1a57e809d97998e541da01ba34e9),它就是引用值。并且指向最新的提交。我们可以手动更改这个文件,来让它指向其他提交。比如手动改为第二个提交
$ git log --pretty=oneline master
03787b6b681ede0bdf5f0f39d170a7dfcc22f6ec (HEAD -> master) 2nd commit
5d2af1e8d157e2d54aa067f109fc2058607b64c2 1st commit
这和重置(reset)的效果是一样的。当然,并不推荐手动编辑引用文件。如果想使用底层命令完成相同的操作,可以使用如下命令
$ git update-ref refs/heads/master 169c899e7e2d1a57e809d97998e541da01ba34e9
确认当前状态
$ git log --pretty=oneline master
169c899e7e2d1a57e809d97998e541da01ba34e9 (HEAD -> master) 3rd commit
03787b6b681ede0bdf5f0f39d170a7dfcc22f6ec 2nd commit
5d2af1e8d157e2d54aa067f109fc2058607b64c2 1st commit
发现master又指向第三个提交了。
如果想实现创建分支的功能,可以使用如下命令
$ git update-ref refs/heads/test 03787b6b681ede0bdf5f0f39d170a7dfcc22f6ec
这个示例实现了,以第二个提交创建一个新的分支的功能。确认refs的内容会发现多了一个test文件。文件的内容就是第二个提交的SHA-1
$ find .git/refs -type f
.git/refs/heads/master
.git/refs/heads/test
确认它的状态发现,它只包含第一和第二两个提交。因为它是基于第二个提交生成的。
$ git log --pretty=oneline test
03787b6b681ede0bdf5f0f39d170a7dfcc22f6ec (HEAD -> test) 2nd commit
5d2af1e8d157e2d54aa067f109fc2058607b64c2 1st commit
HEAD指针
之前的章节中介绍过HEAD指针,它始终指向当前分支的最新提交。它也是一个引用,隐含在.git文件夹中。接上例,我们切换到了test分支,那么此时的HEAD的内容如下:
$ cat .git/HEAD
ref: refs/heads/test
它指向了test引用,test引用又指向了test分支的最新提交。那么HEAD指针当前指向的就是test分支的最新提交。
此时如果使用commit命令,会创建一个对象,并且将HEAD指向的提交作为父提交。HEAD文件是不可见的,但是可以使用一下命令来编辑它
$ git symbolic-ref HEAD refs/heads/master
$ cat .git/HEAD
ref: refs/heads/master
结果是当前分支切换回了master,而且HEAD指向的最新提交是master的第三次提交。
标签引用
refs文件夹中出了heads文件夹还有一个tags文件夹。标签文件的内容,是设定标签时的提交的SHA-1。比如很久以前的GitTestProject示例
$ find .git/refs -type f
.git/refs/heads/master
.git/refs/heads/testing
.git/refs/remotes/gtNew/master
.git/refs/remotes/origin/HEAD
.git/refs/remotes/origin/master
.git/refs/tags/v0.9
.git/refs/tags/v1.0
最下面的两行就是标签引用。
v1.0文件中的内容是
78f82e3198a308895c0c5b3d0bed395ac1c9cbd8
确认一下当前分支状态
$ git log --pretty=oneline master
42427244030cc65299d7eba8796ae6264ecb8ed5 (HEAD -> master) add lib folder
013c1ed1006d469d59cf72b80925cba15ee051ca git log
4416a2c9d6c5c010d42a2326170c4a9ddd54cbd3 (origin/master, origin/HEAD) fix conflict
7957b99d73b8e2782aed8c327359ade6aaf7f6eb (testing) changed by testing branch
c7a8ec8a7a0941ec3301859d715dae6e9f6ab8ba changed by master branch
812b2893979d5fa0f6c615e903ae9ebe88d0e5cc made a change
6fa2268d1f6f1db13d3f233982fdd274af5c1381 (tag: v1.0) combine commit second time
c4bea312bd0676dcdf00a40bb38fb2930bc82afe (tag: v0.9) First commit
6e2fa0997c2ce0bd01c289000c70795d232676ca (gtNew/master) Initial commit
发现分支中标记为v1.0的提交的SHA-1和标签引用中的并不一样。这是因为标签引用中的SHA-1指向一个记录了标签信息的隐含文件。看一下这个文件的内容:
$ git cat-file -p 78f82e3198a308895c0c5b3d0bed395ac1c9cbd8
object 6fa2268d1f6f1db13d3f233982fdd274af5c1381
type commit
tag v1.0
tagger kutilion <kutilion@gmail.com> 1554209566 +0800
my version 1.0
文件中记录的SHA-1才是v1.0标签标记的提交的SHA-1。
远程引用
如果你添加了一个远程版本库并对其执行过推送操作,Git 会记录下最近一次推送操作时每一个分支所对应的值,并保存在 refs/remotes 目录下。
还是GitTestProject示例:
$ find .git/refs -type f
.git/refs/heads/master
.git/refs/heads/testing
.git/refs/remotes/gtNew/master
.git/refs/remotes/origin/HEAD
.git/refs/remotes/origin/master
.git/refs/tags/v0.9
.git/refs/tags/v1.0
中间有三行remotes的记录,就是远程引用所生成的文件。HEAD
远程引用和分支(位于 refs/heads 目录下的引用)之间最主要的区别在于,远程引用是只读的。 虽然可以git checkout 到某个远程引用,但是 Git 并不会将 HEAD 引用指向该远程引用。因此,你不能通过commit 命令来更新远程引用。