前言
通过对 Git 底层 API 的使用来了解其存储结构与工作原理,通过了解工作原理可以帮助我们更好地解决各类 Git 代码版本管理操作上的问题。
介绍
Git 是一个内容寻址(content-addressable)文件系统,Git 的核心部分是一个简单的键值对数据库(key-value data store)。你可以向该数据库插入任意类型的内容,它会返回一个键值,通过该键值可以在任意时刻再次检索(retrieve)该内容.
当在一个新目录或已有目录执行 git init 时,Git 会创建一个 .git 目录。 这个目录包含了几乎所有 Git 存储和操作的东西。 如若想备份或复制一个版本库,只需把这个目录拷贝至另一处即可。
FETCH_HEAD // 远程所有分支的head指针
HEAD // 当前分支head指针
ORIG_HEAD // 危险操作的回退指针
config // config 文件包含项目特有的配置选项
description // 仅供 GitWeb 程序使用,我们无需关心
hooks/ // hooks 目录包含客户端或服务端的钩子脚本(hook scripts)
index // 暂存区tree指针
info/ // info 目录包含一个全局性排除(global exclude)文件, 用以放置那些不希望被记录在 .gitignore 文件中的忽略模式(ignored patterns
logs/ // 记录所有操作,包含checkout merge reset等
objects/ // 内容文件夹
refs/ //refs 目录存储指向数据(分支、远程仓库和标签等)的提交对象的指针
config // 项目设置
hash
Git 用以计算校验和的机制叫做 SHA-1 散列(hash,哈希)。 这是一个由 40 个十六进制字符(0-9 和 a-f)组成的字符串,基于 Git 中文件的内容或目录结构计算出来。 SHA-1 哈希看起来是这样:
24b9da6552252987aa493b52f8696cd6d3b00373
规则**header + content**
- 头部类型不同,数据对象是
blob,树对象是tree,提交对象是commit; - 数据内容不同,数据对象的内容可以是任意内容,而树对象和提交对象的内容有固定的格式。
文件存储
由hash前两位做目录,后38位做文件名存储在objects下面
ps: 如果真的计算的hash值一致的,就忽略
数据对象 blob
存储内容 hash-object
底层命令 git hash-object 可将任意数据保存于 .git/objects 目录(即 对象数据库),并返回指向该数据对象的唯一的键
echo 'aaa' | git hash-object -w --stdin // 从命令行输入内容存储
echo 'test.ts' | git hash-object -w --stdin-paths // 从已知文件输入内容存储
echo 'version 1' > test.txt & git hash-object -w test.txt // 从已知文件输入内容存储
取出内容 cat-file
git cat-file [hash] // -p(转换二进制到content) -s(查看size)-t(查看类型)
树对象 tree
Git 以一种类似于 UNIX 文件系统的方式存储内容,但作了些许简化。 所有内容均以树对象和数据对象的形式存储,其中树对象对应了 UNIX 中的目录项,数据对象则大致上对应了 inodes 或文件内容。 一个树对象包含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的 SHA-1 指针,以及相应的模式、类型、文件名信息.
加入暂存区
git update-index v1.ts
git update-index --add v2.ts // 没有索引的要加入索引
// git status 查看
写入树对象
从当前的暂存区创建树对象
git write-tree // 输入 tree-hash
git cat-file [hash] -p // 查看树对象内容
git cat-file [hash] -t // 查看类型
读入树对象
git read-tree [tree-hash]
git read-tree --prefix=subdir [tree-hash] // 读入新目录
提交对象 commit-tree
提交对象的格式很简单:
- 顶层树对象,代表当前项目快照; 然后是可能存在的父提交
- 作者/提交者信息
- 留空一行,最后是提交注释。
提交树对象
// 输入commit-hash
echo 'v1' | git commit-tree [tree-hash]
// -p 为提交对象添加父节点 可添加多个父节点
echo 'v2' | git commit-tree [tree-hash] -p [commit-hash]
模拟git commit
// 输入commit-hash
echo 'v1' | git commit-tree [tree-hash] -p [parent-commit-hash]
// 更新HEAD指针
git update-ref HEAD [commit-hash]
模拟git merge
echo 'v3' > v3.txt & git update-index --add v3.txt
git write-tree
git commit-tree
git update-ref HEAD [commit-hash]
查看所有对象
git cat-file --batch-check --batch-all-objects
git rev-list
git rev-list --all // 获取commit列表
git rev-parse
git rev-parse HEAD // 获取当前分支的commitID
git rev-parse --default HEAD --revs-only // 获取当前分支的commitID
git rev-parse --all // 获取所有分支,标签,远程分支commitID
git rev-parse --short [commit-hash] // 获取前7位的hash值
git rev-parse [branch]^{commit} // 获取branch的commit-hash
git rev-parse [branch] // 获取branch的commit-hash
git rev-parse "[commit-hash]^1" // ^1获取父节点的hash ^0获取当前节点的hash
git rev-parse "[commit-hash]^@" // 获取commit-hash的父节点(多个父节点)
# git rev-parse --verify
在这里是找到commitid的树对象的名称
// 找到commit-hash对应的tree-hash
git rev-parse --verify "[commit-hash]^{tree}"
// 检测hash的type和指定的type是否匹配
// type: object || commit || tree || blob 不匹配会报错
git rev-parse -q --verify "[hash]^{type}"
Git 引用
git update-ref
模拟git reset
// git reset 153c6e3673a7f15e9d57aa0d554f899669a1e91e
.git/refs/heads/master // 保存了master的ref
// 直接文件更新
echo '153c6e3673a7f15e9d57aa0d554f899669a1e91e' > .git/refs/heads/master
// update 安全更新
git update-ref refs/heads/master 153c6e3673a7f15e9d57aa0d554f899669a1e91e
模拟git checkout
// git checout fix/1
echo 'ref: refs/heads/fix/1' > .git/HEAD
标签引用
标签引用存储在 .git/refs/tags
Pack
Git 每一个不同版本的快照记录下来,每一个文件存储成 Blob 对象,无论是与之前的一个文件有细微的差别,对于 Git 来说都是与之前的一个不同版本,,所以会原封不动的储存下来,这样的方式使我们切换版本速度更快、构成 Git 可分配式的基础等等,但可以长期下来控制 Git的资料库不免笨重起来,特别是长期稳定的大文件,每次仅仅修改一点点的时候。
Git 会不定时地自动运行一个叫做 “auto gc” 「垃圾收集」的命令。 大多数时候,这个命令并不会产生效果。 然而,如果有太多松散对象(不在包文件中的对象)或者太多包文件--7,000 个左右的loose object(即没有被压缩成packfile 的Git Object)或是50 个packfile,Git 会运行一个完整的 git gc 命令。 这个命令会做以下事情:收集所有松散对象并将它们放置到包文件中, 将多个包文件合并为一个大的包文件,移除与任何提交都不相关的陈旧对象
-
将所有有被参考的loose objects 封装成packfiles
-
将较小的packfiles 合并成一个大的packfiles
-
将所有的Git Reference 档案合并成
packed-refs档案 -
将不被参考的Git Objects 删除,像是:
- 没有被任何Tree Object 参考的Blob Object
- 没有被任何Commit Object 和Tree Object 参考的Tree Object
- 没有被任何Git Reference 参考的Commit Object
git gc
两个文件名和大小都差不多的情况下执行git gc进行打包,避免git占内存
执行git gc后,.git/objects下只剩下悬空节点(没有被Reference的commit节点 ),所有的Object都被打包到pack文件夹中
git verify-pack
查看打包后的.pack文件
git verify-pack .git/objects/pack/pack-62be68f9db548aedcaab7 -v
- 第一列表示hash
- 第二列是对象的类型
- 第三列是文件大小
- 最后是引用文件
可以看出2daa旧版本,e069新版本,git在记录的方式是完整的保留新版本,用新版本做基础来patch,原始的版本反而是以差异方式保存的——这是因为大部分情况下需要快速访问文件的最新版本
Git 时常会自动对仓库进行重新打包以节省空间。当然你也可以随时手动执行 git gc 命令来这么做
提交丢失
如果提交已经丢失了(git reset操作)——没有分支指向这些提交且记不起这些提交的hash值
git reflog
// reflog会找找出位于 .git/log/refs下的文件,这个文件会记录你对HEAD的所有操作
// 也可以直接去.git/log/refs/[branch]去找
git reflog
git log -g // 详细信息
这个提交只是ref被删除了,log日志还在的时候,如果log日志也不在了,hh
git fsck
git fsck 会检查数据库的完整性。 如果使用一个 --full 选项运行它,它会向你显示出所有没有被其他对象指向的对象(悬空节点)
错误查询
有时候我们会遇到这么一种情况,大家一起合作开发一个项目,你完成一个功能后commit了,过了好一段时间,你再去验证这个功能时,发现不工作了(应该是别人的某次提交导致的)。what?提交代码后就没有再修改那个功能模块的代码,它怎么能不work了呢?当然也有可能你不知道修改了什么导致的。我想这时候的你肯定是崩溃的,so,git bisect能帮你快速定位到是哪一次commit导致这个功能出问题的。然后你看下这次commit改了些啥就很快能找到问题了。
git biset
将代码提交的历史,按照两分法不断缩小定位。所谓"两分法",就是将代码历史一分为二,确定问题出在前半部分,还是后半部分,不断执行这个过程,直到范围缩小到某一次代码提交, 最后该分支会定格在出错的commit上
// git bisect start [终点] [起点]
git bisect start HEAD 4d83cf // 开始查错
git bisect good // 标识该段提交不包含错误提交
git bisect bad // 标识该段提交包含错误提交
git bisect reset // 差错结束后会停留在错误commit,reset回到最原始commit