Git内部原理
Git内部原理
从根本上讲Git是一个内容寻址(content-addressable)文件系统,并在此之上提供了一个版本控制系统的用户界面。
内容寻址文件系统,意味着,Git的核心部分是一个简单的键值对数据库(key-value data store)。
.git目录包含了几乎所有Git存储和操作的对象。如若想备份或复制一个版本库,只需把这个目录拷贝至另一处即可。初始.git目录的结构如下:
$ ls -F1
HEAD
config
description
hooks/
info/
objects/
refs/
-
description
文件仅供GitWeb程序使用,我们无需关心。 -
config
文件包含项目特有的配置选项。 -
info
目录包含一个全局性排除(global exclude)文件,用以放置那些不希望被记录在.gitignore文件中的忽略模式(ignored patterns)。 -
hooks
目录包含客户端和服务端的钩子脚本(hooks scripts)。
以下四个条目是Git的核心组成部分:
-
HEAD
文件指示目前被检出(checkout
)的分支; -
index
文件(尚待创建)保存暂存区信息; -
objects
目录存储所有数据内容; -
refs
目录存储指向数据(分支)的提交对象的指针;
1 Git对象(Git Objects)
1.1 数据对象(blob object)
1.1.1 git hash-object -w
向数据库写入数据
底层命令hash-object
可将任意数据保存于.git
目录,并返回相应的键值。如向Git数据库存入一些文本:
$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4
该命令输出一个长度为40
个字符的校验和。这是一个SHA-1哈希值——一个将待存储的数据外加一个头部信息(header)一起做SHA-1校验运算而得的校验和。
可以在objects
目录下看到一个文件:
$ find .git/objects/ -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
这就是开始时Git存储内容的方式——一个文件对应一条内容,以该内容加上待定头部信息一起的SHA-1校验和为文件名。校验和的前两个
字符用于命名子目录,余下的38
个字符则用作文件名。
1.1.2 git cat-file -p
从数据库读取数据
可以通过cat-file -p
命令从Git那里取回数据。如:
$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content
记住文件的每一个版本所对应的SHA-1值并不现实;另一个问题是,在这个(简单的版本控制)系统中,文件名并没有被保存——我们仅保存了文件的内容。上述类型的对象我们称之为数据对象(blob object)。
1.1.3 git cat-file -t
返回对象类型
利用cat-file -t
命令,可以让Git告诉我们其内部存储的任何对象类型,只要给定该对象的SHA-1值:
$ git cat-file -t d670460b4b4aece5915caf5c68d12f560a9fe3e4
blob
1.2 树对象(tree object)
树对象(tree object)能解决文件名保存的问题,也允许我们将多个文件组织到一起。Git以一种类似于Unix文件系统的方式存储内容,但作了些简化。所有内容均以树对象和数据对象的形式存储,其中树对象对应了UNIX中的目录项,数据对象则大致上对应了inodes或文件内容。,一个树对象包含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的SHA-1指针,以及相应的模式、类型和文件名信息。
例如,某项目当前对应的最新树对象可能是这样的:
$ git cat-file -p master^{tree}
100644 blob 86cf30788913955d245d7165c825122c1c571f72 .gitignore
100644 blob 8e780fe4b4afe554f0a1d1b6b5c5677ba6fbb6da pom.xml
040000 tree f3a10fd0b4316258965971e229d68bdede5c9501 src
master^{tree}
语法表示master
分支上最新的提交所指向的树对象。请注意,src
子目录(所对应的那条树对象记录)并不是一个数据对象
,而是一个指针,其指向的是另一个树对象:
$ git cat-file -p f3a10fd0b4316258965971e229d68bdede5c9501
040000 tree f3fa3b134b5f0c5f0fdadd47a272f207babea336 main
040000 tree e1a60240db0e5cc21ba059a990d00b68f8eaab48 test
$ git cat-file -p f3fa3b134b5f0c5f0fdadd47a272f207babea336
040000 tree 6ed64b61504b4dc27ba6994943df0fcd518e9a84 java
$ git cat-file -p 6ed64b61504b4dc27ba6994943df0fcd518e9a84
040000 tree 48893e2148439d9142b9596081146edb89e5e221 net
$ git cat-file -p 48893e2148439d9142b9596081146edb89e5e221
040000 tree c8220a21f6913a1083d9b62d0bbc77be92c8f4a4 mrliuli
$ git cat-file -p c8220a21f6913a1083d9b62d0bbc77be92c8f4a4
100644 blob 442f52aef7dd865851762ee899f90f416ebdb24d App.java
100644 blob 9c184f6accd70ec3c6b71705760059bbe78fb22b JMSConsumer.java
100644 blob d47e2c65317098c2ae2728db88ee02c3ada74c52 JMSProducer.java
从概念上讲,Git内部存储的数据像这样:
图1:简化版的Git数据模型
通常,Git根据某一时刻暂存区(即index区域)所表示的状态创建并记录一个树对象,如此重复便可依次记录(某个时间段)一系列的树对象。
1.3 提交对象(commit object)
树对象代表了我们想要跟踪的不同项目快照。然而问题依旧:若想重用这些快照,你必须记住所有SHA-1哈希值。并且,你也完全不知道是谁保存了这些快照,在什么时刻保存的,以及为什么保存这些快照。而上以这些下量提交对象(commit object)为你保存的基本信息。
可以通过调用git commit-tree
命令创建一个提交对象,为些需要指定一个树对象的SHA-1值,以及该提交的父提交对象(如果有的话)。
以上介绍的几个底层操作便完成了一个Git提交历史的创建。这就是我们每次运行git add
和git commit
命令时,Git所做的实质工作——将被改写的文件保存为数据对象,更新暂存区,记录树对象,最后创建一个指明了顶层树对象和父提交的提交对象。这三种主要的Git对象——数据对象、树对象和提交对象——最初均以单独文件的形式保存在.git/objects
目录下。
图2:Git目录下的所有对象:
2 Git引用
我们可以借助类似于git log 1c0211a
这样的命令来浏览完整的提交历史,但为了能遍历那段历史从而找到所有相关对象,你仍须记住1c0211a
是最后一个提交。我们需要一个文件来保存SHA-1值,并给文件起一个简单的名字,然后用这个名字指针来代替原始的SHA-1值。在Git里,这样的文件被称为“引用(references,或缩写refs)”,可以在.git/refs
目录下找到这类含有SHA-1值的文件。
若要创建一个新引用来帮助记忆最新提交所在的位置,从技术上讲我们只需简单地做如下操作:
$ echo '1ae4c8490dda8cccea702f282c0ca96e7d488d1b' > .git/refs/heads/test
这基本上就是Git分支的本质:一个指向某一系列提交之首的指针或引用。
图3:包含分支引用的Git目录对象:
2.1 HEAD 引用
现在的问题是,当你执行git branch [branchname]
时,Git如何知道最新提交的SHA-1值呢?答案是HEAD文件。HEAD文件是一个符号引用(symbolic reference),指向目前所在的分支。所谓符号引用,意味着它并不像普通引用那样包含一个SHA-1值——它是一个指向其他引用的指针。如查看HEAD文件内容:
$ cat .git/HEAD
ref: refs/heads/master
当我们执行git commit
时,该命令会创建一个提交对象,并用HEAD文件中那个引用所指向的SHA-1值设置其交提交字段。
2.2 标签引用
标签对象(tag object)算是Git的第四种对象类型。标签对象(tag object)非常类似于一个提交对象——它包含一个标签创建者信息、一个日期、一段注释信息,以及一个指针。主要的区别在于,标签对象通常指向一个提交对象,而不是一个树对象。它像是一个永不移动的分支引用——永远指向同一个提交对象,只不过给这个提交对象加上一个更友好的名字罢了。
若要创建一个附注标签,Git 会创建一个标签对象,并记录一个引用来指向该标签对象,而不是直接指向提交对象。
2.3 远程引用
如果你添加了一个远程版本库并对其执行过推送操作,Git 会记录下最近一次推送操作时每一个分支所对应的值,并保存在 refs/remotes
目录下,这就是远程引用(remote reference)。
$ cat .git/refs/remotes/origin/dev_20180115
1c0211a17f8f53b16e1f4bc7d0f6565b6a19402c
远程引用和分支(位于 refs/heads
目录下的引用)之间最主要的区别在于,远程引用是只读的。虽然可以git checkout
到某个远程引用,但是 Git 并不会将 HEAD
引用指向该远程引用。因此,你永远不能通过 commit
命令来更新远程引用。Git 将这些远程引用作为记录远程服务器上各分支最后已知位置状态的书签来管理。
3 引用规格
假设你添加了这样一个远程版本库:
$ git remote add origin https://github.com/leonliu06/ActiveMQ-Study
上述命令会在 .git/config
文件中添加一个小节,并在其中指定远程版本库的名称(origin)、URL 和一个用于获取(fetch
)操作的引用规格(refspec):
[remote "origin"]
url = https://github.com/leonliu06/ActiveMQ-Study.git
fetch = +refs/heads/*:refs/remotes/origin/*
引用规格的格式由一个可选的 + 号和紧随其后的 : 组成,其中 是一个模式(pattern),代表远程版本库中的引用; 是那些远程引用在本地所对应的位置。+ 号告诉 Git 即使在不能快进的情况下也要(强制)更新引用。
默认情况下,引用规格由 git remote add
命令自动生成, Git 获取服务器中 refs/heads/
下面的所有引用,并将它写入到本地的 refs/remotes/origin/
中。
上一篇: git将远程仓库和本地仓库关联
下一篇: git 的内部原理
推荐阅读
-
java.sql.SQLException: 内部错误: Unable to construct a Datum from the specified input
-
Spring Cloud动态配置实现原理与源码分析
-
java中关于内部类的使用详解
-
前端开发之CSS原理详解
-
Android系统的智能指针(轻量级指针、强指针和弱指针)的实现原理分析【转】
-
EXEC(EXECUTE)函数访问INSERTED或DELETED的内部临时触发表
-
mysql 数据库中索引原理分析说明
-
浅谈Java中的atomic包实现原理及应用
-
HTML5实现WebSocket协议原理浅析
-
android 大图片拖拽并缩放实现原理