前言
本文章主要记录学习git底层原理时的一些知识点
文章参考
- MIT missing class
- How to explain git in simple words?
- The anatomy of a Git commit
- Pro Git
Git的数据模型
- blob: 压缩并保存所有文件内容的数据结构
blob = array<byte>
- tree: 存储目录的结构,保存所属文件和tree(子目录)
tree = map<string, tree | blob>
,(这里的blob实际上是一个哈希值)。永远存在一个root树。 - commit: commit是指向tree的数据结构,每个提交都有自己的哈希值,它还会保存您的消息、时间戳和您的信息。当改变了追踪的文件blob1并且提交后,git会将更改文件后的内容存储在新的blob2中,这时commit会创建一个新的tree,tree也会通过一个新的hash值指向blob2,(但指向其它没有更改的blob的哈希是没有变化的,也就是依然指向原blob) 这样,通过两次commit,git repo中就保存了两个不同版本的的snapshot。
// 伪代码
type commit = struct {
parents: array<commit>
author: string
message: string
snapshot: tree
}
- Objects : 一个Object是tree/blob/commit中的统一表达。很显然,如果要让tree中直接包含blob/tree本身或者一个副本,commit包含tree的全部内容(递归地继续包含blob和tree),磁盘开销会很大。所以实际上,当tree指向blob/tree时或者commit指向tree时,实际上是通过引用它们的hash值(SHA-1)实现的。也就是说一个commit指向的tree和一个tree中包含的内容实际上都是哈希值,这些哈希值指向blob或者tree,并且这个tree本身通过哈希可以被commit或者tree引用。所有Object通过SHA-1哈希寻址。
// 伪代码
type object = blob | tree | commit
objects = map<string, object>
def store(object):
id = sha1(object)
objects[id] = object
def load(id):
return objects[id]
- references: 实际上是指向commit的指针,因为用哈希值去让人交互非常难记,所以用reference代替.例如,master reference 通常指向开发主分支中的最新提交。在 Git 中,“我们当前所在的位置”是一个名为“HEAD”的特殊引用。
references = map<string, string>
def update_reference(name, id):
references[name] = id
def read_reference(name):
return references[name]
def load_reference(name_or_id):
if name_or_id in references:
return load(references[name_or_id])
else:
return load(name_or_id)
git对一个目录的表示(最上层是root tree)
working directory | Stage OR Index
working directory:工作目录。当用checkout切换commit时,工作目录也会切换到commit对应的snapshot
This the git directory in which you are working. When you check-out a commit, your whole directory with all files is changed/replaced to match that commit (except ignored files)
Stage OR Index:暂存区。在git commit保存到git repo前的缓存区,可以在这个缓存区中决定哪些改动过的文件需要被添加进repo中。试想一下如果没有Stage,那么每次提交就直接是工作目录到git repo的提交,有一些还没有完成的工作也会不得不添加进去。
详情参考:The anatomy of a Git commit
History
每次commit都会生成一个commit object,每个commit实际上会指向它之前的commit(parent),但每个commit的parent有可能有多个,参考之前的伪代码,这是因为当并行开发两个独立的功能,彼此独立,之后merge,新生成的commit就会指向多个parent。
经过多次提交后,commit组成的链就形成了,这个链上的每个commit都保留了那一次提交时的root tree的哈希值。如果某个文件改变了,那么那个文件对应的哈希值会改变,所以需要一个新的blob存储,同时存储它的tree由于需要保存指向它的哈希值,所以tree的内容也改变,那么tree作为object的哈希也改变。最后自底向上就体现在根目录的哈希发生改变。通过这样形成保存的snapshot。
branch
branch实际上是一个可移动的指针,它指向的内容是当前commit的分支中最新的那一个,每次提交时,它都会移动到新提交的内容。 Git 将默认分支命名为 master。您当前加载的分支或提交由 HEAD 跟踪。 Head 始终指向您当时所在的提交或分支
Tag
可以把tag当成git history中的一个书签,它是一个不可移动的,指向一个确定的commit的书签。标签对于指定版本很有用。
一些个人理解
Object的设计感觉有点像EXT文件系统,用指针(哈希)而不是直接存储blob的原因就是为了避免每次使用直接载入时需要载入全部的子节点的blob,但其实用不到而且内存开销太大。要用的时候再载入内存。