Git原理笔记(一)

VCS: version control system

参考文档

 

一、底层命令(Plumbing)和高层命令(Procelain)

高层命令即我们常用的那些git命令。

除去这些命令,以UNIX风格使用或者由脚本调用的命令,一般称为底层命令。

 

我们这里主要学习底层命令。

当你在一个新目录或者已经建立了git的项目内执行git init的时候,都会创建一个.git目录。几乎所有的Git存储和操作的内容都位于该目录下;所以如果想备份或者复制一个库,基本上只需要拷贝该目录到其他地方就可以了。

 

目录结构:

Git原理笔记(一)

description文件仅供GitWeb使用,我们这里不关注。

 

config保存的项目特有的配置选项;

info保存的是不希望.gitignore文件中管理的忽略模式的全局可执行文件;

hooks保存的是客户端或服务端钩子脚本。

 

Git核心部分:HEAD、index、objects、refs

objects目录存储所有数据内容;

refs目录存储指向数据(分支)的提交对象的指针;

HEAD文件指向当前分支;

index文件保存了暂存区域信息。

 

二、Git对象

Git是一套内容寻址系统。

简单讲就是存储键值对(key-value);他允许插入任意类型的内容,并返回一个键值,通过该键值在任何时候再取出该内容。

 

举例如下:

我们初始化一个test目录:

mkdir test
cd test
git init
Initialized empty Git repository in F:/TV/test/.git/
 
find .git/objects
.git/objects/
.git/objects/info
.git/objects/pack
#确定objects目录是否为空
find .git/objects -type f
 

通过hash-object来插入一些数据:

echo 'test content' | git hash-obejct -w --stdin
#自动生成
d670460b4b4aece5915caf5c68d12f560a9fe3e4

-w表示存储(数据)对象,如果不指定这个参数的话仅仅返回键值;

--stdin表示从标准输入设备(stdin)读取内容,如果不指定这个参数需要指定一个要存储的文件的路径。

输出 一个40个字符的校验和,也就是SHA-1哈希值(唯一标识)。

 

查看如下:

find .git/objects -type f
#返回数据如下
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

Git原理笔记(一)

Git原理笔记(一)

这个便是git为每个内容生成的文件。前两个字符作为子目录的名称,后面的38个作为文件命名。

 

通过cat-file可以将数据内容取回,传入-p参数可以让该命令输出数据内容的类型:

git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
#返回内容如下
test content

【注意:存储的是文件的内容】

 

三、树(tree)对象

tree对象可以存储文件名,也可以存储一组文件。Git中所有的内容以tree或blob对象存储,其中tree对象对应于UNIX中的目录,blob对象则大致对应于inodes或文件内容。

我们以之前做过的某一项目查看tree对象的记录,如下图:

Git原理笔记(一)

每一条记录都包含:SHA-1指针、tree或blob类型、文件名、权限模式(第一列内容,644/755等);tree都是目录。

 

四、Commit(提交)对象

git commit和git add的时候做的操作,基本都是更新objects下的内容:保存修改了文件的blob;更新索引;创建tree对象;这之后才创建commit对象。

 

五、对象存储

通过Ruby脚本存储。

1.获得存储内容,例如:

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

2.Git以对象类型为起始内容构造一个文件头;然后增加一个空格,接着是数据内容的长度,最后是一个空字节,这里是blob类型:

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

3.Git将文件头和原始数据内容拼接起来,计算拼接后的新内容的SHA-1校验和。

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

4.Git然后用zlib对数据内容压缩:

>> require 'zlib'
=> true
>> zlib_content = Zlib::Deflate.deflate(store)
=> "x\234K\312\311OR04c(\317H,Q\310,V(-\320QH\311O\266\a\000_\034\a\235"

5.然后将压缩内容写入磁盘。将SHA-1的头两个字符作为子目录名,剩余38个字符作为文件名保存到子目录中:

>> 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

这样就创造了一个正确的blob对象,tree或者commit对象同理可以。

 

六、Git References

位于.git/refs目录

作用是保存头指针HEAD,为了查找SHA-1校验值,做的一个结点记录。

 

七、HEAD标记

HEAD文件指向你当前分支的引用标识符,他并不是SHA-1的值,而是指向另一个引用的指针。

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

他指向的是引用(references),而references才保存的SHA-1的校验值。

 

八、Tags

类似于commit的存在吧,但是多了一个tag的属性;它指向的是一个commit而不是一个tree。就像一个分支引用,但是不会改变-------永远指向同一个commit(commit有很多的...)。

 

九、Remotes

叫远程引用(remote reference),推送代码到远程remote之后,会记录到refs/remotes目录下;

Remote 引用和分支主要区别在于他们是不能被 check out 的。Git 把他们当作是标记了这些分支在服务器上最后状态的一种书签。

 

十、Packfiles

打包文件。

例如我们增加一个大文件,然后更改了这个大文件的一些内容,那么理论上git要保存两个版本的该文件,这会导致维护开销非常大,显然不是合理的解决方式。

解决的方式就是打包文件,git只保存一个完整的对象,并保存更改的差异。

 

Git往磁盘保存对象时,默认使用的格式叫松散对象(loose object)格式。Git会经常性的将这些对象打包至一个叫packfile的二进制文件来节省空间并提高效率。当仓库中有太多的松散对象,或者手工调用git gc命令,或者推送至远程服务器的时候,Git都会这样做。

 

具体做的策略:Git在打包对象时,会查找命名及尺寸相近的文件,并只保存文件不同版本之间的差异内容。而每个对应的差异版本都会有个SHA-1校验值标识;比较有趣的是最新的文件是最大的,之前的都是差异的内容保存文件,因为最新的也是访问最多的文件。

 

十一、The Refspec(引用格式)

格式: +<src>:<dst>

<src>是远端上的引用格式,<dst>是将要记录在本地的引用格式。可选的+号告诉Git在即使不能快速演进的情况下,也去强制更新它。

 

作用主要是为了在有很多分支的情况下,获取到本地的时候可以*更改或者其他操作,特别灵活使用。(这里包括拉到本地pull和推送到服务器push)

 

例如删除远端分支就是利用这种格式做到的:

Git原理笔记(一)

本来应该是:git push origin xx : topic

这里把src留空,就是把远端的topic分支变空,也就是删除它。

 

十二、传输协议

1.哑协议

Git的GET请求等基于HTTP的传输通常被称为哑协议,因为他在服务端(远端)不需要有针对Git特有的代码。

 

2.智能协议

a.上传数据

为了上传数据到远端,Git使用send-pack和receive-pack进程。send-pack进程运行在客户端,远端运行的是receive-pack进程。

 

b.下载数据

使用fetch-pack和upload-pack进程。

客户端启动fetch-pack进程,远端启动upload-pack进程,并协商后续数据传输过程。

 

协商就是协议的握手过程,例如:

Git原理笔记(一)

 

十三、维护及数据恢复

时不时的进行一些清理工作,清理导入的库,或者恢复丢失的数据等。

1.维护

系统会自动调用gc来清理松散对象(loose object)或packfile;

阈值为:7000个左右的loose project或者 50个左右的packfile就会自动执行gc。

 

2.数据恢复

强制删除一个分支后又想重新使用该分支;或者hard-reset了一个分支从而丢弃了分支的部分commit。这种需要恢复一下。

 

例如:

test仓库主分支进行hard-reset到一个老版本的commit,然后丢失了部分commit。

首先,查看当前仓库的状态:

$ git log --pretty=oneline
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

接着讲master分支移回到中间的一个commit:

$ git reset --hard 1a410efbd13591db07496601ebc7a059dd55cfe9
HEAD is now at 1a410ef third commit
$ git log --pretty=oneline
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

这样 ,最新的两个commit不见了,包括这两个commit的分支也不见了。

 

我们要做的是找到最新的commit的SHA值,并给它添加一个分支。

最快捷的方式是:git reflog

Git会在每次修改HEAD时悄悄记录下。然后当你提交或修改分支时,reflog就会更新。

所以我们通过git reflog来查看当前状态:

$ git reflog
1a410ef [email protected]{0}: 1a410efbd13591db07496601ebc7a059dd55cfe9: updating HEAD
ab1afef [email protected]{1}: ab1afef80fac8e34258ff41fc1b867c702daa24b: updating HEAD

如此,我们就知道最新的是la410ef这个SHA值了。

那么我们在这个commit的基础上创建分支就可以了。

 

3.移除对象

假如你在项目中commit了一个超级大的文件,但是后面将他删除了;但是因为记录存在,所以大文件其实是保存在了git的版本中。如果其他人要拉取的话,同样会拉取到这个版本,影响性能。这个时候,将该文件删除就非常有必要了。

 

删除的方式其实是重新提交该文件的之后的所有commit~