深入理解Git
.git
目录
object
文件夹
hooks
文件夹
和其它版本控制系统一样,Git 能在特定的重要动作发生时触发自定义脚本。
有两组这样的钩子:客户端的(由诸如提交和合并这样的操作所调用)和服务器端的(作用于诸如接收被推送的提交这样的联网操作)。
钩子都被存储在 Git 目录下的 hooks
子目录中。
当使用 git init
初始化一个新版本库时,Git 默认会在这个目录中放置一些示例脚本。这些脚本除了本身可以被调用外,它们还透露了被触发时所传入的参数。 所有的示例都是 shell 脚本,其中一些还混杂了 Perl 代码,不过,任何正确命名的可执行脚本都可以正常使用 —— 可以用 Ruby 或 Python,或其它语言编写它们。 这些示例的名字都是以 .sample
结尾,如果想启用它们,得先移除这个后缀。
客户端钩子
提交工作流钩子
pre-commit
:在键入提交信息前运行, 用于检查即将提交的快照prepare-commit-msg
:在启动提交信息编辑器之前,默认信息被创建之后运行, 允许编辑提交者所看到的默认信息commit-msg
:可以用来在提交通过前验证项目状态或提交信息post-commit
:在整个提交过程完成后运行, 一般用于通知之类的事情
电子邮件工作流钩子
由 git am
命令调用
applypatch-msg
: 用来确保提交信息符合格式,或直接用脚本修正格式错误pre-applypatch
:运行于应用补丁之后产生提交之前,用来在提交前检查快照post-applypatch
:运行于提交产生之后,是在git am
运行期间最后被调用的钩子,可以用来把结果通知给一个小组或所拉取的补丁的作者
其他钩子
-
pre-rebase
:运行于变基之前,以非零值退出可以中止变基的过程, 可以使用这个钩子来禁止对已经推送的提交变基 -
post-rewrite
:被那些会替换提交记录的命令调用,比如git commit --amend
和git rebase
(不过不包括git filter-branch
), 用途很大程度上跟post-checkout
和post-merge
差不多 -
post-checkout
:在git checkout
成功运行后会被调用, 可以根据项目环境用来调整工作目录, 其中包括放入大的二进制文件、自动生成文档或进行其他类似这样的操作 -
post-merge
:在git merge
成功运行后会被调用, 可以用来恢复 Git 无法跟踪的工作区数据,比如权限数据,也可以用来验证某些在 Git 控制之外的文件是否存在,这样就能在工作区改变时,把这些文件复制进来 pre-push
:会在git push
运行期间更新了远程引用但尚未传送对象时被调用,可以在推送开始之前,用它验证对引用的更新操作pre-auto-gc
:会在垃圾回收开始之前被调用,可以用来提醒现在要回收垃圾了,或者依情形判断是否要中断回收
服务器端钩子
pre-receive
:处理来自客户端的推送操作时最先被调用,可以用这个钩子阻止对引用进行非快进(non-fast-forward)的更新,或者对该推送所修改的所有引用和文件进行访问控制。update
:和pre-receive
脚本十分类似,不同之处在于它会为每一个准备更新的分支各运行一次。 假如推送者同时向多个分支推送内容,pre-receive
只运行一次,相比之下update
则会为每一个被推送的分支各运行一次。post-receive
:在整个过程完结以后运行,可以用来更新其他系统服务或者通知用户。 它接受与pre-receive
相同的标准输入数据。 它的用途包括给某个邮件列表发信,通知持续集成(continous integration)的服务器,或者更新问题追踪系统(ticket-tracking system) —— 甚至可以通过分析提交信息来决定某个问题(ticket)是否应该被开启,修改或者关闭
Git对象
Git 用以计算校验和的机制叫做 SHA-1 散列(hash,哈希)。 这是一个由 40 个十六进制字符(0-9 和 a-f)组成字符串,基于 Git 中文件的内容或目录结构计算出来。
Git 数据库中保存的信息都是以文件内容的哈希值来索引,而不是文件名。
提交对象
- 这类对象表示修订版本
- 每个提交由一组包含0个或多个父提交的首部(键-值对)构成,实际上包含一系列链接的树对象表示版本库内容的快照——项目的顶层目录
- 用户可以使用底层命令
git commit-tree
或者只是简单地使用git commit
命令创建一个包含给定树对象的提交,将它作为一个修订的快照
树对象
- 这些对象用于表示文件目录
- 每个树对象是一个根据文件名排序的实体列表
- 每个实体由复合权限和类型、文件或者目录名,给定路径的已连接对象的一个链接(即SHA-1标识符),或者树对象(表示子目录)、blob对象(表示文件内容),又或者只是提交对象(表示子模块)等元素构成
- 如果不同修订之间包含一个子目录的相同内容它只会存储一次
git cat-file -p HEAD^{tree}
-
The special notation HEAD^{tree} means that from the reference given, HEAD recursively dereferences the object at the reference until a tree object is found
-
A generic form of the notation is
<rev>^<type>
, and will return the first object of<type>
, searching recursively from<rev>
二进制大对象(BLOB)
- 这些对象用于存储文件内容的给定版本
- 可以使用底层命令
git hash-object -w
创建一个这样的对象 - 如果不同修订版本包含的文件内容是一样的它只会存储一次
标签对象
- 这些对象表示附注标签,签名标签属于特例
- 标签对象还包含一系列的首部信息(此外还指向了一个标签对象)和一个标签信息
- 用户可以使用底层命令
git mktag
或者简单地使用git tag
命令创建一个标签
ls -lh # 获取字节数
# 用"blob <字节数>\0<字符串>" | shasum 来获取哈希值
# "\0"是Linux中的字符串终止符
echo "blob 10\0hello git" | shasum # "hello git"加上"\n"一共是10
# 通过哈希值获取内容
import zlib
compressed_contents = open(<哈希值>, 'rb').read()
zlib.decompress(compressed_contents)
暂存和提交
暂存操作会为每一个文件计算校验和(使用SHA-1哈希算法),然后会把当前版本的文件快照保存到 Git 仓库中(Git 使用 blob 对象来保存它们),最终将校验和加入到暂存区域等待提交
当使用 git commit
进行提交操作时,Git 会先计算每一个子目录的校验和,然后在 Git 仓库中这些校验和保存为树对象。 随后,Git 便会创建一个提交对象,它除了包含上面提到的那些信息外,还包含指向这个树对象(项目根目录)的指针。如此一来,Git 就可以在需要的时候重现此次保存的快照。
非首次提交对象会包含一个指向上次提交对象(父对象)的指针。
分支
Git 的分支,其实本质上仅仅是指向提交对象的可变指针。 Git 的默认分支名字是 master
。 在多次提交操作之后,你其实已经有一个指向最后那个提交对象的 master
分支。 它会在每次的提交操作中自动向前移动。
Git 的 “master” 分支并不是一个特殊分支。 它就跟其它分支完全没有区别。 之所以几乎每一个仓库都有 master 分支,是因为
git init
命令默认创建它,并且大多数人都懒得去改动它。
HEAD 是一个对当前检出记录的符号引用 —— 也就是指向你正在其基础上进行工作的提交记录。 HEAD 总是指向当前分支上最近一次提交记录。大多数修改提交树的 Git 命令都是从改变 HEAD 的指向开始的。 HEAD 通常情况下是指向分支名的(如 bugFix)。在你提交时,改变了 bugFix 的状态,这一变化通过 HEAD 变得可见。