Git 的内部工作原理

news2024/11/28 14:45:02

简介

通过本次分享学习 Git 的内部工作原理和实现方式。

学习这部分内容对于理解 Git 的用途和强大至关重要。

首先要弄明白一点,从根本上来讲 Git 是一个内容寻址(content-addressable)文件系统,并在此之上提供了一个版本控制系统的用户界面。

底层命令与上层命令

在日常使用git的经历中,必然会用到过 checkoutbranchremote 等约 30 个 Git 的子命令。

然而,由于 Git 最初是一套面向版本控制系统的工具集,而不是一个完整的、用户友好的版本控制系统, 所以它还包含了一部分用于完成底层工作的子命令。

这些命令被设计成能以 UNIX 命令行的风格连接在一起,抑或藉由脚本调用,来完成工作。 这部分命令一般被称作“底层(plumbing)”命令,而那些更友好的命令则被称作“上层(porcelain)”命令。

底层命令得以窥探 Git 内部的工作机制,也有助于说明 Git 是如何完成工作的,以及它为何如此运作。

多数底层命令并不面向最终用户:它们更适合作为新工具的组件和自定义脚本的组成部分。

.git目录结构

当在一个新目录或已有目录执行 git init 时,Git 会创建一个 .git 目录。 这个目录包含了几乎所有 Git 存储和操作的东西。 如若想备份或复制一个版本库,只需把这个目录拷贝至另一处即可。 接下来探讨的所有内容,均位于这个目录内。 新初始化的 .git 目录的典型结构如下:

$ ls -F1
config
description
HEAD
hooks/
info/
objects/
refs/

随着 Git 版本的不同,该目录下可能还会包含其他内容。 不过对于一个全新的 git init 版本库,这将是你看到的默认结构。

  • description 文件:仅供 GitWeb 程序使用,无需太多关心。

  • config 文件:包含项目特有的配置选项。

  • info 目录: 包含一个全局性排除(global exclude)文件, 用以放置那些不希望被记录在 .gitignore 文件中的忽略模式(ignored patterns)。

  • hooks 目录:包含客户端或服务端的钩子脚本(hook scripts)。

剩下的四个条目很重要

  • HEAD 文件:指向目前被检出的分支

  • index 文件:文件保存暂存区信息

  • objects 目录: 存储所有数据内容

  • refs 目录:存储指向数据(分支、远程仓库和标签等)的提交对象的指针

它们都是 Git 的核心组成部分。我们将详细地逐一检视这四部分,来理解 Git 是如何运转的。

Git 对象

Git 是一个内容寻址文件系统,这是什么意思呢?

这意味着,Git 的核心部分是一个简单的键值对数据库(key-value data store)。

你可以向 Git 仓库中插入任意类型的内容,它会返回一个唯一的键,通过该键可以在任意时刻再次取回该内容。

数据对象

可以通过底层命令 git hash-object 来演示上述效果。

该命令可将任意数据保存于 .git/objects 目录(即 对象数据库),并返回指向该数据对象的唯一的键。

首先,我们需要初始化一个新的 Git 版本库,并确认 objects 目录为空:

$ git init test
Initialized empty Git repository in /tmp/test/.git/
$ cd test
$ find .git/objects
.git/objects
.git/objects/info
.git/objects/pack
$ find .git/objects -type f

可以看到 Git 对 objects 目录进行了初始化,并创建了 pack 和 info 子目录,但均为空。

hash-object

我们用 git hash-object 创建一个新的数据对象并将它手动存入你的新 Git 数据库中:

$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4

在这种最简单的形式中,git hash-object 会接受你传给它的东西,而它只会返回可以存储在 Git 仓库中的唯一键。

-w 选项会指示该命令不要只返回键,还要将该对象写入数据库中。

--stdin 选项则指示该命令从标准输入读取内容;若不指定此选项,则须在命令尾部给出待存储文件的路径。

此命令输出一个长度为 40 个字符的校验和。 这是一个 SHA-1 哈希值。

一个将待存储的数据content + 一个头部信息(header)一起做 SHA-1 校验运算而得的校验和。

hash = SHA-1(header + content)

后文会简要讨论该头部(header)信息。

现在我们可以查看 Git 是如何存储数据的:

$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

如果你再次查看 objects 目录,那么可以在其中找到一个与新内容对应的文件。 这就是开始时 Git 存储内容的方式。

一个文件对应一条内容, 以该内容加上特定头部信息一起的hash为文件命名。 hash的前两个字符用于命名子目录,余下的 38 个字符则用作文件名。

一旦你将内容存储在了对象数据库中,那么可以通过 cat-file 命令从 Git 那里取回数据。

cat-file 指定 -p 选项可指示该命令自动判断内容的类型,并为我们显示大致的内容:

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

至此,你已经掌握了如何向 Git 中存入内容,以及如何将它们取出。

我们同样可以将这些操作应用于文件中的内容。

例如,可以对一个文件进行简单的版本控制。

  1. 首先,创建一个新文件并将其内容存入数据库:
$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30
  1. 接着,向文件里写入新内容,并再次将其存入数据库:
$ echo 'version 2' > test.txt
$ git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
  1. 对象数据库记录下了该文件的两个不同版本,当然之前我们存入的第一条内容也还在:
$ find .git/objects -type f
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

现在可以在删掉 test.txt 的本地副本,然后用 Git 从对象数据库中取回它的第一个版本:

$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt
$ cat test.txt
version 1

或者第二个版本:

$ git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test.txt
$ cat test.txt
version 2

然而,记住文件的每一个版本所对应的 SHA-1 值并不现实;另一个问题是,在这个(简单的版本控制)系统中,文件名并没有被保存——我们仅保存了文件的内容。

上述类型的对象我们称之为 数据对象(blob object)。利用 git cat-file -t 命令,可以让 Git 告诉我们其内部存储的任何对象类型,只要给定该对象的 SHA-1 值:

$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob

树对象

接下来要探讨的 Git 对象类型是树对象(tree object),它能解决文件名保存的问题,也允许我们将多个文件组织到一起。

Git 以一种类似于 UNIX 文件系统的方式存储内容,但作了些许简化。

所有内容均以树对象和数据对象的形式存储,其中树对象对应了 UNIX 中的目录项,数据对象则大致上对应了 inodes 或文件内容。

一个树对象包含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的 SHA-1 指针,以及相应的模式、类型、文件名信息。

例如,某项目当前对应的最新树对象可能是这样的:

$ git cat-file -p master^{tree}
100644 blob 8178c76d627cade75005b40711b92f4177bc6cfc    README
100644 blob fa40f3c76a4e81143dad1610efa97702d48132fc    Rakefile
040000 tree 9d99701ca3d90acd5ab846f6164d7dc377ec2c71    lib

master^{tree} 语法表示 master 分支上最新的提交所指向的树对象。 请注意,lib 子目录(所对应的那条树对象记录)并不是一个数据对象,而是一个指针,其指向的是另一个树对象:

$ git cat-file -p 9d99701ca3d90acd5ab846f6164d7dc377ec2c71    
100644 blob 898bfe3e20e7c54438e3d481fa7960876d363168      simplegit.rb

从概念上讲,Git 内部存储的数据有点像这样:

简化版的 Git 数据模型。

update-index

通常,Git 根据某一时刻暂存区(即 index 区域)所表示的状态创建并记录一个对应的树对象, 如此重复便可依次记录(某个时间段内)一系列的树对象。

因此,为创建一个树对象,首先需要通过暂存一些文件来创建一个暂存区。

可以通过底层命令 git update-index 为一个单独文件——我们的 test.txt 文件的首个版本——创建一个暂存区。

利用该命令,可以把 test.txt 文件的首个版本人为地加入一个新的暂存区。 必须为上述命令指定 --add 选项,因为此前该文件并不在暂存区中(我们甚至都还没来得及创建一个暂存区呢);

同样必需的还有 --cacheinfo 选项,因为将要添加的文件位于 Git 数据库中,而不是位于当前目录下。 同时,需要指定文件模式、SHA-1 与文件名:

$ git update-index --add --cacheinfo \
100644 \
83baae61804e65cc73a7201a7252750c76066a30 \
test.txt

本例中,我们指定的文件模式为 100644,表明这是一个普通文件。 其他选择包括:100755,表示一个可执行文件;120000,表示一个符号链接。 这里的文件模式参考了常见的 UNIX 文件模式,但远没那么灵活——上述三种模式即是 Git 文件(即数据对象)的所有合法模式(当然,还有其他一些模式,但用于目录项和子模块)。

write-tree

现在,可以通过 git write-tree 命令将暂存区内容写入一个树对象。 此处无需指定 -w 选项——如果某个树对象此前并不存在的话,当调用此命令时, 它会根据当前暂存区状态自动创建一个新的树对象 (d8329fc1cc938780ffdd9f94e0d364e0ea74f579):

$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30      test.txt

不妨用之前见过的 git cat-file 命令验证一下它确实是一个树对象:

$ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree

接着我们来创建一个新的树对象,它包括 test.txt 文件的第二个版本,以及一个新的文件:

$ echo 'new file' > new.txt
$ git update-index --add --cacheinfo 100644 \
  1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
$ git update-index --add new.txt

暂存区现在包含了 test.txt 文件的新版本,和一个新文件:new.txt。 记录下这个目录树(将当前暂存区的状态记录为一个树对象),然后观察它的结构:

$ git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341
$ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt

我们注意到,新的树对象包含两条文件记录,同时 test.txt 的 SHA-1 值(1f7a7a)是先前值的“第二版”。 只是为了好玩:你可以将第一个树对象加入第二个树对象,使其成为新的树对象的一个子目录。 通过调用 git read-tree 命令,可以把树对象读入暂存区。 本例中,可以通过对该命令指定 --prefix 选项,将一个已有的树对象作为子树读入暂存区:

$ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git write-tree
3c4e9cd789d88d8d89c1073707c3585e41b0e614
$ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614
040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579      bak
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt

如果基于这个新的树对象创建一个工作目录,你会发现工作目录的根目录包含两个文件以及一个名为 bak 的子目录,该子目录包含 test.txt 文件的第一个版本。 可以认为 Git 内部存储着的用于表示上述结构的数据是这样的:

当前 Git 的数据内容结构。

$ find .git/objects/ -type f
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 #tree-2
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 #tree-3
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
.git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 #tree-1
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92

如果你做完了以上所有操作,那么现在就有了三个树对象,分别代表我们想要跟踪的不同项目快照。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3cOQKlhu-1669597594496)(C:\Users\Administrator\AppData\Roaming\marktext\images\2022-11-23-17-16-43-image.png)]

提交对象

然而问题依旧:若想重用这些快照,你必须记住所有三个tree对象的 SHA-1 哈希值。

并且,你也完全不知道:

  • 谁保存了这些快照,

  • 在什么时刻保存的,

  • 以及为什么保存这些快照。

而以上这些,正是提交对象(commit object)能为你保存的基本信息。

commit-tree

可以通过调用 commit-tree 命令创建一个提交对象,为此需要指定一个树对象(tree)的 SHA-1 值,以及该提交的父提交对象(可选)。

我们从之前创建的第一个树对象开始:

$ echo 'first commit' | git commit-tree d8329f
b712f832c6807974ff7f38c6576d0fb23f1eda30

由于创建时间和作者数据不同,你现在会得到一个不同的散列值。 请将本章后续内容中的提交和标签的散列值替换为你自己的校验和。

现在可以通过 git cat-file 命令查看这个新提交对象:

$ git cat-file -p b712f8
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author wuzh <wuzh@pobo.net.cn> 1669257861 +0800
committer wuzh <wuzh@pobo.net.cn> 1669257861 +0800

first commit

提交对象的格式很简单:

  • 它先指定一个顶层树对象,代表当前项目快照;

  • 然后是可能存在的父提交(前面描述的提交对象并不存在任何父提交);

  • 之后是作者/提交者信息(依据你的 user.name 和 user.email 配置来设定,外加一个时间戳);

  • 留空一行,最后是提交注释。

接着,我们将创建另两个提交对象,它们分别引用各自的上一个提交(作为其父提交对象):

$ echo 'second commit' | git commit-tree 0155eb -p b712f8
70958d49d5f19f0d2e4a0230b8369ba6f755ae78
$ echo 'third commit'  | git commit-tree 3c4e9c -p 70958d
dd1bdcc4d20ba5fd3297c318561dd15e7ec08585

这三个提交对象分别指向之前创建的三个树对象快照中的一个。

现在,如果对最后一个提交的 SHA-1 值运行 git log 命令查看的 Git 提交历史了:

$ git log --stat dd1bdc
commit dd1bdcc4d20ba5fd3297c318561dd15e7ec08585
Author: wuzh <wuzh@pobo.net.cn>
Date:   Thu Nov 24 10:47:15 2022 +0800

    third commit

 bak/test.txt | 1 +
 1 file changed, 1 insertion(+)

commit 70958d49d5f19f0d2e4a0230b8369ba6f755ae78
Author: wuzh <wuzh@pobo.net.cn>
Date:   Thu Nov 24 10:46:52 2022 +0800

    second commit

 new.txt  | 1 +
 test.txt | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

commit b712f832c6807974ff7f38c6576d0fb23f1eda30
Author: wuzh <wuzh@pobo.net.cn>
Date:   Thu Nov 24 10:44:21 2022 +0800

    first commit

 test.txt | 1 +
 1 file changed, 1 insertion(+)

到这里,我们没有借助任何上层命令,仅凭几个底层操作便完成了一个 Git 提交历史的创建。

这就是每次我们运行 git add 和 git commit 命令时,Git 所做的工作实质就是将被改写的文件保存为数据对象, 更新暂存区,记录树对象,最后创建一个指明了顶层树对象和父提交的提交对象。

这三种主要的 Git 对象

  • 数据对象、

  • 树对象、

  • 提交对象

最初均以单独文件的形式保存在 .git/objects 目录下。

下面列出了目前示例目录内的所有对象,辅以各自所保存内容的注释:

$ find .git/objects -type f
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2
.git/objects/ec/d09b43056db92b5a8625fb5f71a499c0abc63c # 重复的commit 3
.git/objects/dd/1bdcc4d20ba5fd3297c318561dd15e7ec08585 # commit 3
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1
.git/objects/0d/87e17232ac6ed69ea385587fcb2b42804d4043 # 重复的commit 2
.git/objects/70/958d49d5f19f0d2e4a0230b8369ba6f755ae78 # commit 2
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content'
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
.git/objects/b7/12f832c6807974ff7f38c6576d0fb23f1eda30 # commit 1

如果跟踪所有的内部指针,将得到一个类似下面的对象关系图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c1vvkBov-1669597594496)(C:\Users\Administrator\AppData\Roaming\marktext\images\2022-11-24-11-02-53-image.png)]

对象存储

前文曾提及,你向 Git 仓库提交的所有对象都会有个头部信息一并被保存。 让我们略花些时间来看看 Git 是如何存储其对象的。

通过在 Ruby 脚本语言中交互式地演示,你将看到一个数据对象——本例中是字符串“what is up, doc?”——是如何被存储的。

可以通过 irb 命令启动 Ruby 的交互模式:

$ irb
>> content = "what is up, doc?"
=> "what is up, doc?"

Git 首先会以识别出的对象的类型作为开头来构造一个头部信息,本例中是一个“blob”字符串。 接着 Git 会在头部的第一部分添加一个空格,随后是数据内容的字节数,最后是一个空字节(null byte):

>> header = "blob #{content.length}\0"
=> "blob 16\u0000"

Git 会将上述头部信息和原始数据拼接起来,并计算出这条新内容的 SHA-1 校验和。 在 Ruby 中可以这样计算 SHA-1 值——先通过 require 命令导入 SHA-1 digest 库, 然后对目标字符串调用 Digest::SHA1.hexdigest()

>> store = header + content
=> "blob 16\u0000what is up, doc?"
>> require 'digest/sha1'
=> true
>> sha1 = Digest::SHA1.hexdigest(store)
=> "bd9dbf5aae1a3862dd1526723246b20206e5fc37"

我们来比较一下 git hash-object 的输出。 这里使用了 echo -n 以避免在输出中添加换行。

$ echo -n "what is up, doc?" | git hash-object --stdin
bd9dbf5aae1a3862dd1526723246b20206e5fc37

Git 会通过 zlib 压缩这条新内容。在 Ruby 中可以借助 zlib 库做到这一点。 先导入相应的库,然后对目标内容调用 Zlib::Deflate.deflate()

>> require 'zlib'
=> true
>> zlib_content = Zlib::Deflate.deflate(store)
=> "x\x9CK\xCA\xC9OR04c(\xCFH,Q\xC8,V(-\xD0QH\xC9O\xB6\a\x00_\x1C\a\x9D"

最后,需要将这条经由 zlib 压缩的内容写入磁盘上的某个对象。 要先确定待写入对象的路径:

  • hash的前两个字符作为子目录名称,

  • hash的后38 个字符则作为子目录内文件的名称。

  • 如果该子目录不存在,可以通过 Ruby 中的 FileUtils.mkdir_p() 函数来创建它。

  • 通过 File.open() 打开这个文件。

  • 对上一步中得到的文件句柄调用 write() 函数,以向目标文件写入之前那条 zlib 压缩过的内容:

>> path = '.git/objects/' + sha1[0,2] + '/' + sha1[2,38]
=> ".git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37"
>> require 'fileutils'
=> true
>> FileUtils.mkdir_p(File.dirname(path))
=> ".git/objects/bd"
>> File.open(path, 'w') { |f| f.write zlib_content }
=> 32

我们用 git cat-file 查看一下该对象的内容:

$ git cat-file -p bd9dbf5aae1a3862dd1526723246b20206e5fc37
what is up, doc?

就是这样——你已创建了一个有效的 Git 数据对象。

所有的 Git 对象均以这种方式存储,区别仅在于类型标识。

另两种对象类型的头部信息以字符串“commit”或“tree”开头,而不是“blob”。 另外,虽然数据对象的内容几乎可以是任何东西,但提交对象和树对象的内容却有各自固定的格式。

Git 引用

如果你对仓库中从一个提交(比如 dd1bdc)开始往前的历史感兴趣,那么可以运行 git log dd1bdc 这样的命令来显示历史,不过你需要记得 dd1bdc 是你查看历史的起点提交。 如果我们有一个文件来保存 SHA-1 值,而该文件有一个简单的名字, 然后用这个名字指针来替代原始的 SHA-1 值的话会更加简单。

在 Git 中,这种简单的名字被称为“引用(references,或简写为 refs)”。 你可以在 .git/refs 目录下找到这类含有 SHA-1 值的文件。 在目前的项目中,这个目录没有包含任何文件,但它包含了一个简单的目录结构:

$ find .git/refs
.git/refs
.git/refs/heads
.git/refs/tags
$ find .git/refs -type f

若要创建一个新引用来帮助记忆最新提交所在的位置,从技术上讲我们只需简单地做如下操作:

$ echo dd1bdcc4d20ba5fd3297c318561dd15e7ec08585 > .git/refs/heads/master

现在,你就可以在 Git 命令中使用这个刚创建的新引用来代替 SHA-1 值了:

$ git log master
commit dd1bdcc4d20ba5fd3297c318561dd15e7ec08585 (HEAD -> master)
Author: wuzh <wuzh@pobo.net.cn>
Date:   Thu Nov 24 10:47:15 2022 +0800

    third commit

commit 70958d49d5f19f0d2e4a0230b8369ba6f755ae78 (test)
Author: wuzh <wuzh@pobo.net.cn>
Date:   Thu Nov 24 10:46:52 2022 +0800

    second commit

commit b712f832c6807974ff7f38c6576d0fb23f1eda30
Author: wuzh <wuzh@pobo.net.cn>
Date:   Thu Nov 24 10:44:21 2022 +0800

    first commit

update-ref

我们不提倡直接编辑引用文件。 如果想更新某个引用,Git 提供了一个更加安全的命令 update-ref 来完成此事:

$ git update-ref refs/heads/master dd1bdcc4d20ba5fd3297c318561dd15e7ec08585 

这基本就是 Git 分支的本质:一个指向某一系列提交之首的指针或引用。 若想在第二个提交上创建一个分支,可以这么做:

$ git update-ref refs/heads/test 70958d

这个分支将只包含从第二个提交开始往前追溯的记录:

$ git log test
commit 70958d49d5f19f0d2e4a0230b8369ba6f755ae78 (test)
Author: wuzh <wuzh@pobo.net.cn>
Date:   Thu Nov 24 10:46:52 2022 +0800

    second commit

commit b712f832c6807974ff7f38c6576d0fb23f1eda30
Author: wuzh <wuzh@pobo.net.cn>
Date:   Thu Nov 24 10:44:21 2022 +0800

    first commit

至此,我们的 Git 数据库从概念上看起来像这样:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fCHA6gK2-1669597594497)(C:\Users\Administrator\AppData\Roaming\marktext\images\2022-11-24-11-14-32-image.png)]

当运行类似于 git branch <branch> 这样的命令时,Git 实际上会运行 update-ref 命令, 取得当前所在分支最新提交对应的 SHA-1 值,并将其加入你想要创建的任何新引用中。

HEAD 引用

现在的问题是,当你执行 git branch <branch> 时,Git 如何知道最新提交的 SHA-1 值呢? 答案是 HEAD 文件。

HEAD 文件通常是一个符号引用(symbolic reference),指向目前所在的分支。 所谓符号引用,表示它是一个指向其他引用的指针。

然而在某些罕见的情况下,HEAD 文件可能会包含一个 git 对象的 SHA-1 值。 当你在检出一个标签、提交或远程分支,让你的仓库变成 “分离 HEAD”状态时,就会出现这种情况。

如果查看 HEAD 文件的内容,通常我们看到类似这样的内容:

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

如果执行 git checkout test,Git 会像这样更新 HEAD 文件:

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

当我们执行 git commit 时,该命令会创建一个提交对象,并用 HEAD 文件中那个引用所指向的 SHA-1 值设置其父提交字段。

symbolic-ref

你也可以手动编辑该文件,然而同样存在一个更安全的命令来完成此事:git symbolic-ref。 可以借助此命令来查看 HEAD 引用对应的值:

$ git symbolic-ref HEAD
refs/heads/master

同样可以设置 HEAD 引用的值:

$ git symbolic-ref HEAD refs/heads/test
$ cat .git/HEAD
ref: refs/heads/test

不能把符号引用设置为一个不符合引用规范的值:

$ git symbolic-ref HEAD test
fatal: Refusing to point HEAD outside of refs/

标签引用

前面我们刚讨论过 Git 的三种主要的对象类型(数据对象树对象 和 提交对象 ),然而实际上还有第四种。 标签对象(tag object) 非常类似于一个提交对象。

它包含:

  • 一个标签创建者信息、

  • 一个日期、

  • 一段注释信息,

  • 以及一个指针。

主要的区别在于,标签对象通常指向一个提交对象,而不是一个树对象。

它像是一个永不移动的分支引用——永远指向同一个提交对象,只不过给这个提交对象加上一个更友好的名字罢了。

存在两种类型的标签:

  • 附注(anotation)标签

  • 轻量(light-weight)标签。

轻量标签

可以像这样创建一个轻量标签:

$ git update-ref refs/tags/v1.0 b712f832c6807974ff7f38c6576d0fb23f1eda30

这就是轻量标签的全部内容——一个固定的引用。

附注标签

一个附注标签则更复杂一些。 若要创建一个附注标签,Git 会创建一个标签对象,并记录一个引用来指向该标签对象,而不是直接指向提交对象。 可以通过创建一个附注标签来验证这个过程(使用 -a 选项):

$ git tag -a v1.1 70958d49d5f19f0d2e4a0230b8369ba6f755ae78 -m 'test tag'

下面是上述过程所建标签对象的 SHA-1 值:

$ cat .git/refs/tags/v1.1
093f7c80761b9045e762a362e91ef42dd67ff0eb

现在对该 SHA-1 值运行 git cat-file -p 命令:

$ git cat-file -p 093f7c80761b9045e762a362e91ef42dd67ff0eb
object 70958d49d5f19f0d2e4a0230b8369ba6f755ae78
type commit
tag v1.1
tagger wuzh <wuzh@pobo.net.cn> 1669260318 +0800

test tag

我们注意到,object 条目指向我们打了标签的那个提交对象的 SHA-1 值。

另外要注意的是,标签对象并非必须指向某个提交对象;你可以对任意类型的 Git 对象打标签。

例如,在 Git 源码中,项目维护者将他们的 GPG 公钥添加为一个数据对象,然后对这个对象打了一个标签。 可以克隆一个 Git 版本库,然后通过执行下面的命令来在这个版本库中查看上述公钥:

$ git cat-file blob junio-gpg-pub

Linux 内核版本库同样有一个不指向提交对象的标签对象——首个被创建的标签对象所指向的是最初被引入版本库的那份内核源码所对应的树对象。

远程引用

我们将看到的第三种引用类型是远程引用(remote reference)。 如果你添加了一个远程版本库并对其执行过推送操作,Git 会记录下最近一次推送操作时每一个分支所对应的值,并保存在 refs/remotes 目录下。 例如,你可以添加一个叫做 origin 的远程版本库,然后把 master 分支推送上去:

$ git remote add origin https://github.com/git/git

此时,如果查看 refs/remotes/origin/master 文件,可以发现 origin 远程版本库的 master 分支所对应的 SHA-1 值,就是最近一次与服务器通信时本地 master 分支所对应的 SHA-1 值:

$ cat .git/refs/remotes/origin/master
c000d916380bb59db69c78546928eadd076b9c7d

远程引用和分支(位于 refs/heads 目录下的引用)之间最主要的区别在于:

远程引用是只读的。

虽然可以 git checkout 到某个远程引用,但是 Git 并不会将 HEAD 引用指向该远程引用

因此,你永远不能通过 commit 命令来更新远程引用。

Git 将这些远程引用作为记录远程服务器上各分支最后已知位置状态的书签来管理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/41845.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

WPF基础知识系例

WPF系列-绘图和动画1、WPF绘图1.1 直线1.2 矩形1.3 椭圆1.4 路径1、WPF绘图 这里将从静态图像绘制入手&#xff0c;进而学习动画效果制作。WPF 拥有自己的一套图形 API &#xff0c;使用这套 API 不但可以轻松地绘制出精美的图形&#xff0c;还可以方便地对图形进行滤镜、变形…

致敬逆行者网页设计作品 大学生抗疫感动专题网页设计作业模板 疫情感动人物静态HTML网页模板下载

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

RPC 对比 HTTP

初识RPC RPC 远程方法调用&#xff08;Remote Procedure Call&#xff09;大多用在服务治理领域 基本都是分布式环境中的注册中心来负责新服务的注册、服务续约、服务下线、服务销毁国内 Dubbo 是应用最广的 RPC 框架国外 gRPC RPC协议 无论哪种RPC方案&#xff0c;都要自定义…

你好,法语!A2知识点总结(3)

3.句型 3.1疑问 注意1 主谓倒装的疑问句式&#xff0c;主语必须是代词。 •主语是名词&#xff0c;不能直接倒装&#xff0c;要人为加进去一个代替主语的代词放在动词后面&#xff0c;“-”连接 •EX: Votre pre est professeur? •Est-votre pre professeur? •Votre pre e…

Kafka的设计原理

Kafka的设计原理和使用场景一、Kafka简介二、Kafka的架构2.1、Kafka 一些重要概念2.2、工作流程2.3、副本原理2.4、分区和主题的关系2.5、生产者2.5.1、分区可以水平扩展2.5.2、分区策略2.6、消费者2.6.1、消费方式2.6.2、分区分配策略2.7、数据可靠性保证2.7.1、副本数据同步策…

Linux基础知识

目录 流行的Linux发行版本 Linux系统的启动顺序 禁止使用ping命令 鉴定故障解决办法 user 不在 sudoers 文件中。此事将被报告解决办法 修改文件或目录的所属用户与权限 1、chgrp&#xff1a;更改文件属组 2、chown&#xff1a;更改文件属主&#xff0c;也可以同时更改文…

Agent 与对象的辨析

如果说对象是70年代以来软件界最激动人心的革新之一&#xff0c;那么&#xff0c;Agent的相关理论和技术&#xff0c;为分布式开放系统的设计与实现提供了新的途径&#xff0c;可以称之为软件开发的又一重大突破。讨论问题的前提是概念的澄清&#xff0c;什么是对象&#xff1f…

【学习笔记49】JavaScript的this指向

一、this指向 每一个函数内部都有一个关键字thisthis的值, 和函数的调用有关, 与函数书写无关 1. 普通调用方式 一个普通的全局函数, 在正常调用的情况下, this window function fn() {console.log(this); }fn() // this window2. 函数放置到对象 如果将函数放置到对象…

图数据技术护航网络安全 - Neo4j 大中华区总经理 方俊强

网络数字化及人工智能为许多行业来革命性的进步&#xff0c;但与此同时&#xff0c;网络安全领域产生的问题也日益严峻。人工智能进攻性风险和网络威胁领域的发展正在重新定义企业安全&#xff0c;从而给企业带来了更高的挑战。如何守护网络安全&#xff0c;成为企业深化数字化…

CSDN第十一场竞赛

目录 1.比赛详情 2.竞赛详情 3.原题回顾 (1).圆小艺 思路模拟 代码实现 (2). K皇把妹 思路模拟 代码实现 (3).筛选宝物 思路模拟 代码实现 (4).圆桌 思路模拟 代码实现 4.总结 1.比赛详情 报名地址&#xff1a;https://edu.csdn.net/contest/detail/24?utm_sou…

目标检测论文解读复现之二十:基于改进Yolov5的地铁隧道附属设施与衬砌表观病害检测方法

前言 此前出了目标改进算法专栏&#xff0c;但是对于应用于什么场景&#xff0c;需要什么改进方法对应与自己的应用场景有效果&#xff0c;并且多少改进点能发什么水平的文章&#xff0c;为解决大家的困惑&#xff0c;此系列文章旨在给大家解读最新目标检测算法论文&#xff0…

14个SpringBoot优化小妙招

1. 定义配置文件信息 有时候我们为了统一管理会把一些变量放到 yml 配置文件中 例如 用 ConfigurationProperties 代替 Value 使用方法 定义对应字段的实体 Data // 指定前缀 ConfigurationProperties(prefix "developer") Component public class DeveloperPro…

力扣(LeetCode)813. 最大平均值和的分组(C++)

动态规划 本题需要用到前缀和&#xff0c;前缀和需要将下标 iii 后移一位&#xff0c;所以我们将状态的下标 iii 也后移一位&#xff1b;由于状态转移依赖于 j−1j-1j−1 &#xff0c;我们将 jjj 的下标后移一位。 class Solution { public:double largestSumOfAverages(vecto…

2022天梯赛练习集(2022.9-2022.10)

使用函数判断完全平方数 没有加&#xff08;int&#xff09;过不了 int IsSquare(int n){if((int)sqrt(n) * sqrt(n) ! n) return 0;else return 1; } 使用函数求余弦函数的近似值 double funcos(double e, double x){double sum 1, item 1;for(int i 0; fabs(item) >…

Tmuxs -高效使用Linux terminal

Tmuxs -高效使用Linux terminal前言what&#xff1f;重要概念安装Tmux 常用命令Tmux 常用内部命令窗口&#xff08;window&#xff09;指令&#xff1a;面板&#xff08;pane&#xff09;指令&#xff1a;如何用鼠标调整pane大小配置生效参考配置参考前言 这个利器绝对可以提升…

eMMC编程基础 -(二)eMMC基础介绍

eMMC编程基础 -&#xff08;二&#xff09;eMMC基础介绍1 eMMC 简介1.1 eMMC系统概述1.2 eMMC 的整体架构如下图片所示&#xff1a;2 Flash Memory1 eMMC 简介 eMMC 是 embedded MultiMediaCard 的简称。 eMMC 是对 MMC 的一个拓展&#xff0c;以满足更高标准的性能、成本、体…

文件包含笔记

很多语言支持使用包含文件&#xff0c;这样允许开发者把可重复使用的代码存入单个文件中&#xff0c;在未来需要使用时&#xff0c;将它包含在其他代码文件中即可使用。 如果是像 C/C 这种编译语言&#xff0c;即使可以包含任意文件&#xff0c;若没有调用其中函数也不会有什么…

【从零开始学微服务】05.微服务的优势和不足

大家好&#xff0c;欢迎来到万猫学社&#xff0c;跟我一起学&#xff0c;你也能成为微服务专家。 没有“银弹” 在一些电影中&#xff0c;“银弹”被视作能迅速杀死狼人的武器&#xff0c;是杀死狼人的灵丹妙药。“银弹”常被比喻为解决复杂问题的良方或高招。 由于软件的复杂…

11.27

一.进制转换 这道题的思路就是先把每个数模对应的进制.再除以,就得到对应的, 因为可能会有16进制,所以直接弄一个字符串数组,按照"0123456789ABCDEF"顺序存储,再用模找里面对应的就可以了 但是有可能是负数,所以我们需要在此之前判断一下 如果用字符串临时拼接会产…

2008年武汉高校630操作系统真题B卷

操作系统————核心系统软件 竞争计算机系统资源的基本单位————进程 UNIX————分时操作系统 操作系统中必不可少的调度————进程调度 进程和程序的本质区别————前者是动态后者是静态 磁带————顺序存储文件 某进程在运行过程中需要等待从磁盘上读入数…