文件结构
当使用git init
创建一个新的仓库时,Git 会创建一个.git
目录。目录结构如下:
1 | HEAD |
- HEAD 当前被检出分支的指针
- config 项目特有的配置
- description 描述文件
- hooks 钩子脚本
- index 保存暂存区信息,在首次
git add
后才会生成 - info 包含一个全局性排除文件
- objects 存储所有数据内容
- refs 所有分支的提交对象的指针
Working Directory
除.git 目录的其他目录和文件,不包含.gitignore 排除的目录及文件
对象结构
Git 是一个内容寻址文件系统。其核心部分时一个简单的键值对数据库。你可以想 Git 仓库插入任意类型的内容,它会返回一个唯一的键,通过该键可以在任意时刻再次取回内容。
Git 有数据对象(blob),树对象(tree),引用(refs)
blob
每个文件的数据内容以二进制的形式存储在 Git 仓库中,我们可以使用命令git hash-object -w <file>
的方式,手动向 git 数据库中插入该数据(即在.git/objects
目录下,写入该对象),而它只会返回存储在 Git 仓库的唯一键。此命令输出一个长度为 40 的 sha1 哈希值,前 2 位字符用于命名子目录,后 38 位则用作文件名 例如:
1 | find .git/objects/ -type f |
git 使用 sha1 时,在可区分唯一 sha1 时最少可只用前四位就可以
一开始.git/objects
下没有存储任何数据对象,当我们使用git hash-object
命令存入一条内容时,可以观察到.git.objects
目录下,生成了 sha1 相对应的子目录和文件名。通过git cat-file -p <sha1>
查看数据对象时,我们可以看到之前存入的文本内容
我们使用git add <file>
时,就是 Git 内部做了git hash-object
的命令,将<file>
的内容存入到.git/objects
中。
1 | echo 'version 1' > 1.txt |
使用git add
后,我们可以看到index
文件的生成
1 | find .git/ -type f |
我们可以看到 index 保存了工作区 add 的 blob 对象的 sha1
blob 对象是针对数据内容的,不区分文件
例如:
1 | echo 'version 1' > 2.txt |
tree
Git 以一种类似于 UNIX 文件系统的方式存储内容,但作了些许简化。 所有内容均以树对象和数据对象的形式存储,其中树对象对应了 UNIX 中的目录项,数据对象则大致上对应了 inodes 或文件内容。 一个树对象包含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的 SHA-1 指针,以及相应的模式、类型、文件名信息。
当我们使用git commit
后,我们可以看到
1 |
|
commit 工作原理
commit 对象指向的 tree 对象,遍历 tree 对象查找到的所有 blob 对象,与缓存区中的所有 blob 进行 diff 对比,若有差异,则可以进行下一次 commit。
refs
如果你对仓库中从一个提交(比如 9f1224)开始往前的历史感兴趣,那么可以运行 git log 9f1224 开始往前的历史感兴趣,那么可以运行这样的命令来显示历史,不过你需要记得 9f1224b 是你查看历史的起点提交。 如果我们有一个文件来保存 SHA-1 值,而该文件有一个简单的名字, 然后用这个名字指针来替代原始的 SHA-1 值的话会更加简单。
在 Git 中,这种简单的名字被称为“引用(references,或简写为 refs)”。 你可以在 .git/refs 目录下找到这类含有 SHA-1 值的文件。 在目前的项目中,这个目录没有包含任何文件,但它包含了一个简单的目录结构:
1 | find .git/refs -type f |
HEAD 引用
HEAD 文件通常是一个符号引用(symbolic reference),指向目前所在的分支。 所谓符号引用,表示它是一个指向其他引用的指针。我们通常使用git checkout
来更改 HEAD 的内容
1 | more .git/HEAD |
标签引用
标签对象(tag object) 非常类似于一个提交对象——它包含一个标签创建者信息、一个日期、一段注释信息,以及一个指针。 主要的区别在于,标签对象通常指向一个提交对象,而不是一个树对象。 它像是一个永不移动的分支引用——永远指向同一个提交对象,只不过给这个提交对象加上一个更友好的名字罢了。
1 | git tag V1.0 9f1224b -m 'commit 1 tag v1.0' |
远程引用
如果你添加了一个远程版本库并对其执行过推送操作,Git 会记录下最近一次推送操作时每一个分支所对应的值,并保存在 refs/remotes 目录下。
git gc
当使用git gc
或者git push
时,git 会自动将.git/objects/
下文件进行压缩,在.git/objects/pack/
下生成.idx
和.pack
文件。
分支
git 的 commit 是一个有向无环图,其只包含父类 commit 的 sha1,分支仅仅是一个指针其值为 commit 的 sha1
查看.git 内的文件可以看到生成了.git/refs/heads/master,可以发现其指向 commit 1
1 | more .git/refs/heads/master |
我们可以通过查看.git/logs/refs/heads/master,查看分支的历史记录,这也是git log
命令的原理
1 | more .git/logs/refs/heads/master |
我们新增子目录以及子文件,并添加到 stage 区
1 | mkdir dir |
git diff
git diff --cache
是比较 index 和 HEAD 的差异
1 | 我们查看HEAD的内容,发现HEAD指向master的commit 1 |
提交 commit 2
1 | git commit -m 'commit 2' |
从概念上讲,此时 Git 内部存储的数据有点像这样:
git diff <commit1> <commit2>
的原理都是差不多的
切换分支
切换分支本质上就是将 HEAD 的引用指向对应的分支
1 | git sw -b dev |
回退
回退工作区
1 | 修改2.txt |
git diff 是用来比较缓存区和工作区的差异的,但仅比较已在 git 版本控制下的文件,即仅比较缓存区已经有的文件
1 | git diff |
拉取别的分支或 commit 的文件
git checkout <branch> <file>
或git checkout <branch> <file>
,分支的引用最终也指向一个 commit 对象,因此这两种方法都是类似的。
1 | more 1.txt |
撤销(reset)
git reset 首先是将 HEAD 指向的引用的 commit 对象修改为新的 commit 对象,然后根据参数( --soft
,--mixed(default)
,--hard
)的不同,决定是否同步缓存区和工作区的内容。
大致的方式如下图所示
我们通过实例验证
1 | echo 'D' > state |