Skip to content

深入理解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 --amendgit rebase(不过不包括 git filter-branch), 用途很大程度上跟 post-checkoutpost-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 数据库中保存的信息都是以文件内容的哈希值来索引,而不是文件名。

img

git cat-file -t <hash> # 类型
git cat-file -p <hash> # 内容
git cat-file -s <hash> # 大小

提交对象

  • 这类对象表示修订版本
  • 每个提交由一组包含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 变得可见。

工作区和索引区

git ls-files # 列出索引区文件
git ls-files -s # 打印权限,哈希值, ,文件名