Git本质上是一个内容寻址(content-addressable)的文件系统,根据文件内容的SHA-1哈希值来定位文件。Git核心部分是一个简单的键值对数据库(key-value data store)。向Git数据库插入任意类型的内容,会返回一个键值,经过返回的键值能够在任意时刻再次检索(retrieve)插入的内容。经过底层命令hash-object能够将任意数据保存到.git目录并返回相应的键值。
Git包含一套面向版本控制系统的工具集,包括高级命令和底层命令。高级命令主要由用户使用,底层命令能够窥探Git内部的工做机制,但多数底层命令并不面向最终用户,更适合做为新命令和自定义脚本的组成部分。
使用git init建立仓库时,Git会建立一个.git目录,其目录结构以下:
A、description文件仅供GitWeb程序使用。
B、config文件包含项目特有的配置选项。
C、info目录包含一个全局性排除(global exclude)文件,用于放置不但愿被记录在.gitignore文件中的忽略模式(ignored patterns)。
D、hooks目录包含客户端或服务端的钩子脚本(hook scripts)。
E、objects目录存储全部数据内容,内有info、pack子目录。
F、refs目录存储指向数据(分支)的提交对象的指针。
G、HEAD文件指示目前被检出的分支。
H、index(尚待建立)文件保存暂存区信息。
objects、refs、HEAD、index是Git仓库的四个核心部分。node
Git对象分为四种:数据对象(blob)、树对象(tree)、提交对象(commit)、标签对象(tag)。Git文件系统的设计思路与linux文件系统类似,即将文件的内容与文件的属性分开存储,文件内容存储在文件系统中,文件名、全部者、权限等文件属性信息则另外开辟区域进行存储。
Git利用SHA-1加密算法对其管理的每个文件生成一个惟一的16进制的40个字符长度的SHA-1哈希值来惟一标识对象。若是文件不变化,SHA-1哈希值不会改变;若是文件改变,会生成新的SHA-1哈希值。40位字符SHA-1哈希值的前两个字符做为目录名,后38个字符做为文件名,标识生成的Git对象。
Git对象的SHA-1哈希值计算公式以下:linux
header = "<type> " + content.length + "\0" hash = sha1(header + content)
Git在计算对象hash时,首先会在对象头部添加一个header。header由3部分组成:第一部分表示对象的类型,能够取值blob、tree、commit以分别表示数据对象、树对象、提交对象;第二部分是数据的字节长度;第三部分是一个空字节,用来将header和content分隔开。将header添加到content头部后,使用sha1算法计算出一个40位的hash值。
在手动计算Git对象的hash时须要注意:
A、header中第二部分关于数据长度的计算,必定是字节的长度而不是字符串的长度;
B、header + content的操做并非字符串级别的拼接,而是二进制级别的拼接。
各类Git对象的hash方法相同,不一样的在于:
A、头部类型不一样,数据对象是blob,树对象是tree,提交对象是commit;
B、数据内容不一样,数据对象的内容能够是任意内容,而树对象和提交对象的内容有固定的格式。
git cat-file能够用来实现全部Git对象的读取,包括数据对象、树对象、提交对象的查看。
git cat-file -p [hash-key] 能够查看已经存在的object对象内容
git cat-file -t [hash-key] 能够查看已经存在的object对象类型git
数据对象一般用于存储文件的内容,但不包括文件名、权限等信息。数据对象和其对应文件的所在路径、文件名是否改被更改都彻底没有关系。
Git会根据文件内容计算出一个SHA-1 hash值,以hash值做为索引将文件存储在Git文件系统中。因为相同的文件内容的hash值是同样的,所以Git将相同内容的文件只会存储一次。git hash-object命令能够用来计算文件内容的hash值,并将生成的数据对象存储到Git文件系统中。echo -en "hello,git" | git hash-object --stdin
f28ffa36cdf69904e516babfdb3005e108dddfb7
在echo后面使用-n选项,用来阻止自动在字符串末尾添加换行符,不然会致使实际传给git hash-object是hello,git\n
数据对象查看:
git show + 对象名(SHA1哈希值)算法
数据对象的内容格式以下:数据库
blob <content length><NULL><content>
使用git hash-object计算文本的SHA1哈希值echo -en "hello,git" | git hash-object --stdin
f28ffa36cdf69904e516babfdb3005e108dddfb7
使用openssl计算文本的SHA1哈希值:echo -en "blob 9\0hello,git" | openssl sha1
(stdin)= f28ffa36cdf69904e516babfdb3005e108dddfb7
若是文本中有中文时,必须注意数据长度的计算是字节数而不是字符数。可使用命令查看文本的字节数:echo -n "中文" | wc -c
安全
git init Test //初始化一个版本库 cd Test //进入Test find .git/objects //查找.git/objects目录下的内容
Git对objects目录进行初始化,并建立pack和info目录,但均为空。echo 'test content' | git hash-object -w --stdin //向Git数据库存入文本
d670460b4b4aece5915caf5c68d12f560a9fe3e4 //返回的键值
-w选项指示hash-object命令存储数据对象;若不指定此选项,则上述命令仅返回对应的键值。
--stdin选项则指示上述命令从标准输入读取内容,若不指定此选项,则须在命令尾部给出待存储文件的路径。
命令输出一个长度为40个字符的校验和,是一个SHA-1哈希值,一个将待存储的数据外加一个头部信息(header)一块儿作SHA-1校验运算而得的校验和。find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
能够在objects目录下看到一个文件。 Git存储内容的方式是一个文件对应一条内容,用内容加上特定头部信息一块儿的SHA-1校验和为文件命名。校验和的前两个字符用于命名子目录,余下的38个字符则用做文件名。
能够经过cat-file命令从Git数据库取回数据。指定-p选项可指示cat-file命令自动判断内容的类型,并显示格式友好的内容:git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content
服务器
经过对一个文件进行简单的版本控制揭示Git版本控制的原理。首先,建立一个新文件并将其内容存入Git数据库。ide
echo "version 1" > test //写入test文件内容 git hash-object -w test //存储test文件到Git数据库
83baae61804e65cc73a7201a7252750c76066a30
工具
echo 'version 2' > test //写入test文件新的内容 git hash-object -w test //再次将修改后的test文件存储到Git数据库
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
Git数据库记录了test文件的两个不一样版本。find .git/objects -type f
测试
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 .git/objects/83/baae61804e65cc73a7201a7252750c76066a30 .git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
恢复test文件到第一个版本:
git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test cat test //读取test文件内容
version 1
恢复test文件到第二个版本:
git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test cat test //读取test文件内容
version 2
上述对文件的版本控制中,记住文件的每个版本所对应的SHA-1值并不现实,而且文件名并无被保存。
利用cat-file -t命令,能够查看Git内部存储的任何对象类型,只要给定对象的SHA-1值。git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob
经过每个数据对象的hash值,能够访问Git文件系统中的任意数据对象,但记住数据对象的SHA-1哈希值显然是不现实的。数据对象只是解决了文件内容存储的问题,而文件名的存储则须要经过树对象实现。
树对象包含指向数据对象或是其它树对象的多个指针,用来表示内容之间的目录层次关系。
Git全部内容均以树对象和数据对象的形式存储,其中树对象对应UNIX中的目录项,数据对象对应inodes或文件内容。 一个树对象包含一条或多条树对象记录(tree entry),每条树对象记录含有一个指向数据对象或者子树对象的SHA-1指针以及相应的模式、类型、文件名信息。
某项目当前对应的最新树对象可使用以下命令查看:git cat-file -p master^{tree}
master^{tree}语法表示master分支上最新的提交所指向的树对象。 目录(所对应的树对象记录)并非一个数据对象,而是一个指针,其指向的是另外一个树对象。
树对象查看:
git show + 对象名/git ls-tree + 对象名
git ls-files --stage命令能够查看暂存区的内容。
树对象的内容格式以下:
tree <content length><NUL><file mode> <filename><NUL><item sha>...
item sha部分是二进制形式的sha1码,而不是十六进制形式的sha1码。git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test.txt
首先使用xxd把83baae61804e65cc73a7201a7252750c76066a30转换成为二进制形式,并将结果保存为sha1.txt以方便后面作追加操做。
echo -en "83baae61804e65cc73a7201a7252750c76066a30" | xxd -r -p > sha1.txt
构造content部分,并保存至文件content.txt
echo -en "100644 test.txt\0" | cat - sha1.txt > content.txt
计算content的长度cat content.txt | wc -c
生成SHA-1echo -en "tree 36\0" | cat - content.txt | openssl sha1
(stdin)= d8329fc1cc938780ffdd9f94e0d364e0ea74f579
Git根据某一时刻暂存区(即index文件)所表示的状态建立并记录一个对应的树对象,如此重复即可依次记录(某个时间段内)一系列的树对象。 所以,为建立一个树对象,首先须要经过暂存一些文件来建立一个暂存区。经过update-index为一个单独文件(test.txt文件)的首个版本建立一个暂存区。 利用update-index命令,能够把test文件的首个版本加入一个新的暂存区。
git update-index --add --cacheinfo 100644 \ 83baae61804e65cc73a7201a7252750c76066a30 test
--add表示新增文件名,若是第一次添加某一文件名,必须使用此选项;--cacheinfo mode object path是要添加的数据对象的模式、hash值和路径,path意味着为数据对象不只能够指定单纯的文件名,也可使用路径。另外要注意的是,使用git update-index添加完文件后,必定要使用git write-tree写入到Git文件系统中,不然只会存在于暂存区。
指定的文件模式为100644,代表是一个普通文件。 其它选择包括:100755,表示一个可执行文件;120000表示一个符号连接。
如今能够经过write-tree命令将暂存区内容写入一个树对象。无需指定-w 选项,若是某个树对象不存在,调用write-tree命令时会根据当前暂存区状态自动建立一个新的树对象。git write-tree
5bf35b145b6281c080d58b6d19a5113a47f782ed
git cat-file -p 5bf35b145b6281c080d58b6d19a5113a47f782ed
100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test
git cat-file -t 5bf35b145b6281c080d58b6d19a5113a47f782ed
tree
Git树对象是在commit的过程当中生成的,其生成会根据.git目录下的index文件的内容来建立。git add的操做就是将文件的信息保存到index文件中,在commit时,根据index的内容来生成树对象。
使用git update-index能够为数据对象指定名称和模式,而后使用git write-tree将树对象写入到Git文件系统中。
建立一个新的树对象,包括test.txt文件的第二个版本以及一个新的文件。
echo 'new file' > new.txt
git update-index --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值是第二版test.txt。 将第一个树对象加入第二个树对象,使其成为新的树对象的一个子目录。 经过调用read-tree命令能够把树对象读入暂存区。经过对 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的子目录,bak子目录包含test.txt文件的第一个版本。
树对象解决了文件名的问题,并且因为分阶段提交树对象,树对象能够看作是开发阶段源代码目录树的一次次快照,所以能够用树对象做为源代码版本管理。但须要记住每一个树对象的hash值,才能找到各阶段的源代码文件目录树。在源代码版本控制中,还须要知道谁提交了代码、何时提交的、提交的说明信息等,提交对象就是为了解决上述问题的。
提交对象指向一个树对象,而且带有相关的描述信息,标记项目某一个特定时间点的状态。提交对象包含一些关于时间点的元数据,如时间戳、最近一次提交的做者、指向上次提交的指针等等。
提交对象查看以下:
git show / git log + -s + --pretty=raw +对象名
提交对象是用来保存提交的做者、时间、说明这些信息的,可使用git commit-tree来将提交对象写入到Git文件系统中。echo 'first commit' | git commit-tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
162f9174ac6bb4c5d41bfc00fcb5147e2d62b839
commit-tree除了要指定提交的树对象,也要提供提交说明,但提交的做者和时间则根据环境变量自动生成,并不须要指定。因为提交的做者和时间不一样,提交对象的SHA-1哈希值也不相同。
提交对象的查看可使用git cat-file。git cat-file -p 162f9174ac6bb4c5d41bfc00fcb5147e2d62b839
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 author scorpio <642960662@qq.com> 1536497938 +0800 committer scorpio <642960662@qq.com> 1536497938 +0800 first commit
非首次提交须要指定使用-p指定父提交对象,使代码版本才能成为一条时间线。echo 'second commit' | git commit-tree 0155eb -p 162f9174ac6bb4c5d41bfc00fcb5147e2d62b839
f6bbc9d4e8de1b35ad66c2115aa8519587c26100
git cat-file查看一下新的提交对象,看到相比于第一次提交,多了parent部分。
第三次提交:echo 'third commit' | git commit-tree 3c4e9c -p f6bbc9d4e8de1b35ad66c2115aa8519587c26100
第三次提交查看:git log --stat 26a72965aa9c1bdab9fe5972012bd903f501f006 --pretty=oneline
26a72965aa9c1bdab9fe5972012bd903f501f006 third commit bak/test.txt | 1 + 1 file changed, 1 insertion(+) f6bbc9d4e8de1b35ad66c2115aa8519587c26100 second commit new.txt | 1 + test.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) 162f9174ac6bb4c5d41bfc00fcb5147e2d62b839 first commit test.txt | 1 + 1 file changed, 1 insertion(+)
最终提交对象的结构图:
合并的提交(merge commits)可能会有不仅一个父对象。若是一个提交对象没有父对象,称为根提交(root commit),表明项目最初的一个版本(revision)。每一个项目必须有至少有一个根提交(root commit)。
提交对象的内容格式以下:
commit <content length><NUL>tree <tree sha> parent <parent sha> [parent <parent sha> if several parents from merges] author <author name> <author e-mail> <timestamp> <timezone> committer <author name> <author e-mail> <timestamp> <timezone> <commit message>
第一次提交的提交对象的内容以下:git cat-file -p 162f9174ac6bb4c5d41bfc00fcb5147e2d62b839
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 author scorpio <642960662@qq.com> 1536497938 +0800 committer scorpio <642960662@qq.com> 1536497938 +0800 first commit
使用openssl计算SHA-1
echo -n "commit 165\0 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 author scorpio <642960662@qq.com> 1536497938 +0800 committer scorpio <642960662@qq.com> 1536497938 +0800 first commit" | openssl sha1
Git中的数据对象解决了数据存储的问题,树对象解决了文件名存储问题,提交对象解决了提交信息的存储问题。
Git对象(数据对象、树对象和提交对象)都存储在.git/objects目录下。
Git对象的40位SHA-1哈希值分为两部分:前两位做为目录名称,后38位做为对象文件名。
Git对象的存储路径规则为:.git/objects/hash[0, 1]/hash[2, 40]
Git对象存储的算法步骤:
A、计算content长度,构造header;
B、将header添加到content前面,构造Git对象;
C、使用sha1算法计算Git对象的40位hash码;
D、使用zlib的deflate算法压缩Git对象;
E、将压缩后的Git对象存储到.git/objects/hash[0, 2]/hash[2, 40]路径下;
使用Nodejs来实现git hash-object -w的功能,即计算Git对象的hash值并存储到Git文件系统中:
const fs = require('fs') const crypto = require('crypto') const zlib = require('zlib') function gitHashObject(content, type) { // 构造header const header = `${type} ${Buffer.from(content).length}\0` // 构造Git对象 const store = Buffer.concat([Buffer.from(header), Buffer.from(content)]) // 计算hash const sha1 = crypto.createHash('sha1') sha1.update(store) const hash = sha1.digest('hex') // 压缩Git对象 const zlib_store = zlib.deflateSync(store) // 存储Git对象 fs.mkdirSync(`.git/objects/${hash.substring(0, 2)}`) fs.writeFileSync(`.git/objects/${hash.substring(0, 2)}/${hash.substring(2, 40)}`, zlib_store) console.log(hash) } // 调用入口 gitHashObject(process.argv[2], process.argv[3])
测试:node index.js 'hello, world' blob
8c01d89ae06311834ee4b1fab2f0414d35f01102
git cat-file -p 8c01d89ae06311834ee4b1fab2f0414d35f01102
hello, world
Git操做中常常须要浏览完整的提交历史,但为了能遍历提交历史从而找到全部相关对象,必须记住最后一个提交对象的SHA1哈希值。所以,须要一个文件来保存SHA-1值,并给文件起一个简单名字,而后用名字来替代原始的 SHA-1值。能够在.git/refs目录下找到含有SHA-1值的文件。find .git/refs
.git/refs .git/refs/heads .git/refs/tags
若是须要建立一个新引用来帮助记录最新提交所在的位置,从技术上只需将最新提交对象的SHA1哈希值写入引用文件内:
echo "524fd8729bbee740392739d22f64784ec81a9804" > .git/refs/heads/test
而后就能够在Git命令中使用刚建立的新引用来代替SHA-1值。git log --pretty=oneline test
一般,不提倡直接编辑引用文件。 若是想更新某个引用,Git提供了一个更加安全的命令update-ref来编辑引用。
Git分支的本质上是一个指向某一系列提交之首的指针或引用。 若想在某个提交对象上建立一个分支,能够进行以下操做:git update-ref refs/heads/newbranchname commit_id
当执行git branch (branchname)时,Git经过HEAD文件获取最新提交对象的SHA-值。HEAD文件是一个符号引用(symbolic reference),不像普通引用包含一个SHA-1值,而是一个指向其它引用的指针,指向当前所在的分支。 能够查看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值设置其父提交字段。
标签对象(tag object)相似于一个提交对象,包含一个标签建立者信息、一个日期、一段注释信息以及一个指针。主要的区别在于,标签对象一般指向一个提交对象,而不是一个树对象。 标签对象永远指向同一个提交对象,并给指向的提交对象加上一个更友好的名字。
若是添加了一个远程版本库并对其执行过推送操做,Git会记录下最近一次推送操做时每个分支所对应的值,并保存在refs/remotes目录下。能够添加一个叫作origin的远程版本库,而后把master分支推送到远程仓库。
若是查看refs/remotes/origin/master文件,能够发现origin远程版本库的 master分支所对应的SHA-1值就是最近一次与服务器通讯时本地master分支所对应的SHA-1值。
远程引用和分支(位于refs/heads目录下的引用)之间最主要的区别在于,远程引用是只读的。虽然能够git checkout 到某个远程引用,但Git并不会将 HEAD引用指向该远程引用。所以,永远不能经过commit命令来更新远程引用。 Git将远程引用做为记录远程服务器上各分支最后已知位置状态的书签来管理。
Git最初向磁盘中存储对象时所使用的格式被称为“松散(loose)”对象格式。 但Git会时不时地将多个松散对象打包成一个称为“包文件(packfile)”的二进制文件,以节省空间和提升效率。当版本库中有太多的松散对象,或者手动执行git gc命令,或者向远程服务器执行推送时,Git都会对对象打包。 要看到打包过程,能够手动执行git gc命令让Git对对象进行打包。git gc
Counting objects: 47126, done. Delta compression using up to 4 threads. Compressing objects: 100% (16945/16945), done. Writing objects: 100% (47126/47126), done. Total 47126 (delta 29923), reused 46986 (delta 29783)