progit笔记:Git内部原理
从根本上来讲 Git 是一个内容寻址(content-addressable)文件系统,并在此之上提供了一个版本控制系统的用户界面。
.git目录结构:
description
文件仅供 GitWeb 程序使用,我们无需关心。config
文件包含项目特有的配置选项。info
目录包含一个全局性排除(global exclude)文件,用以放置那些不希望被记录在 .gitignore文件中的忽略模式(ignored patterns)。hooks
目录包含客户端或服务端的钩子脚本(hook scripts)。
重要:
objects
目录存储所有数据内容。refs
目录存储指向数据(分支、远程仓库和标签等) 的提交对象的指针。HEAD
文件指向目前被检出的分支。index
文件保存暂存区信息。
Git对象
# 可将任意数据保存于 .git/objects 目录(即对象数据库)并返回指向该数据对象的唯一的键
$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4
# 'test content' 对应的新生成文件
$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
# git cat-file -p 选项可指示该命令自动判断内容的类型,并为我们显示大致的内容:
$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content
# git cat-file -t 命令让 Git 告诉我们其内部存储的任何对象类型
$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob
version 2
树对象
能解决文件名保存的问题,也允许我们将多个文件组织到一起。Git 以一种类似于 UNIX 文件系统的方式存储内容,但作了些许简化。 所有内容均以树对象和数据对象的形式存储,其中树对象对应了 UNIX 中的目录项,数据对象则大致上对应了 inodes 或文件内容。 一个树对象包含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的 SHA-1 指针,以及相应的模式、类型、文件名信息。
通常,Git 根据某一时刻暂存区(即 index 区域,下同)所表示的状态创建并记 录一个对应的树对象, 如此重复便可依次记录(某个时间段内)一系列的树对象。
提交对象
提交对象的格式很简单:它先指定一个顶层树对象,代表当前项目快照; 然后是可能存在的父提交(前面描述的提交对象并不存在任何父提交); 之后是作者/提交者信息(依据你的 user.name 和 user.email 配置来设定,外加一个时间戳); 留空一行,最后是提交注释。
git add
和 git commit
:Git 所做的工作实质就是将被改写的文件保存为数据对象,更新暂存区,记录树对象,最后创建一个指明了顶层树对象和父提交的提交对象。
Git引用
Git 分支的本质:一个指向某一系列提交之首的指针或引用。
git branch <branch>
这样的命令时,Git 实际上会运行 update-ref
命令, 取得当前所在分支最新提交对应的 SHA-1 值,并将其加入你想要创建的任何新引用中。
HEAD 引用
当你执行 git branch <branch>
时,Git 如何知道最新提交的 SHA-1 值呢? 答案是 HEAD 文件。
# HEAD 文件的内容
$ cat .git/HEAD
ref: refs/heads/master
标签引用
标签对象(tag object) 非常类似于一个提交对象——它包含一个标签创建者信息、一个日期、一段注释信息,以及一个指针。 主要的区别在于,标签对象通常指向一个提交对象,而不是一个树对象。 它像是一个永不移动的分支引用——永远指向同一个提交对象,只不过给这个提交对象加上一个更友好的名字罢了。
远程引用
如果添加了一个远程版本库并对其执行过推送操作,Git 会记录下最近一次推送操作时每一个分支所对应的值,并保存在 refs/remotes
目录下。
包文件
Git 最初向磁盘中存储对象时所使用的格式被称为“松散(loose)”对象格式(每个版本保存一份文件)。
但是,Git 会时不时地将多个这些对象打包成一个称为“包文件(packfile)”的二进制文件,以节省空间和提高效率。 当版本库中有太多的松散对象,或者你手动执行 git gc
命令,或者你向远程服务器执行推送时,Git 都会这样做。
引用规范
[remote "origin"]
url = https://github.com/schacon/simplegit-progit
fetch = +refs/heads/*:refs/remotes/origin/*
引用规范的格式由一个可选的 + 号和紧随其后的 : 组成, 其中 是一个模式(pattern),代表远程版本库中的引用; 是本地跟踪的远程引用的位置。 + 号告诉 Git 即使在不能快进的情况下也要(强制)更新引用。
维护与数据恢复
维护
# 自动垃圾回收:
$ git gc --auto
如果你执行了 git gc
命令,refs
目录中将不会再有这些文件(refs/heads/main、 refs/heads/testing)。 为了保证效率 Git 会将它们移动到名为 .git/packed-refs
的文件中。
数据恢复
问题:删除了正在工作的分支,或者硬重置了一个分支,找回想要的提交?
使用一个名叫 git reflog
的工具。 当你正在工作时,Git 会默默地记录每一次你改变 HEAD 时它的值。 每一次你提交或改变分支,引用日志都会被更新。 引用日志(reflog)也可以通过 git update-ref
命令更新,我们在 Git 引用有提到使用这个命令而不是是直接将 SHA-1 的值写入引用文件中的原因。
$ git reflog
1a410ef HEAD@{0}: reset: moving to 1a410ef
ab1afef HEAD@{1}: commit: modified repo.rb a bit
484a592 HEAD@{2}: commit: added repo.rb
再通过创建一个新的分支指向这个提交来恢复它
$ git branch recover-branch ab1afef
或者使用 git fsck
, 如果使用一个 --full
选项运行它,它会向你显示出所有没有被其他对象指向的对象:
$ git fsck --full
Checking object directories: 100% (256/256), done.
Checking objects: 100% (18/18), done.
dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4
dangling commit ab1afef80fac8e34258ff41fc1b867c702daa24b
dangling tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9
dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293
# 以标准日志的格式输出引用日志:
$ git log -g
移除对象
问题:某个版本引入了一个大文件,现在需要从仓库中永久删除。
警告:这个操作对提交历史的修改是破坏性的。
在.idx文件中找出大文件,
# 输出内容的第三列(即文件大小)进行排序,找出大文件:
$ git verify-pack -v .git/objects/pack/pack-29…69.idx \
| sort -k 3 -n \
| tail -3
dadf7258d699da2c8d89b09ef6670edb7d5f91b4 commit 229 159 12
033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob 22044 5792 4977696
82c99a3e86bb1267b236a4b6eff7868d97489af1 blob 4975916 4976258 1438
# rev-list 找出具体那个文件:
$ git rev-list --objects --all | grep 82c99a3
82c99a3e86bb1267b236a4b6eff7868d97489af1 git.tgz
# 查看哪些提交对这个文件产生改动:
$ git log --oneline --branches -- git.tgz
dadf725 oops - removed large tarball
7b30847 add git tarball
从 Git 历史中完全移除这个文件
$ git filter-branch --index-filter \
'git rm --ignore-unmatch --cached git.tgz' -- 7b30847^..
删除引用日志和其他文件对这个文件的引用
$ rm -Rf .git/refs/original
$ rm -Rf .git/logs/
$ git gc
执行git count-objects -v
,可以从 size 的值看出,这个大文件还在你的松散对象中,并没有消失;但是它不会在推送或接下来的克隆中出现,这才是最重要的。
如果真的想要删除它,可以通过有 --expire
选项的 git prune
命令来完全地移除那个对象:
$ git prune --expire now