欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Git内部原理

程序员文章站 2022-07-13 16:19:48
...

Git内部原理


  从根本上讲Git是一个内容寻址(content-addressable)文件系统,并在此之上提供了一个版本控制系统的用户界面。

  内容寻址文件系统,意味着,Git的核心部分是一个简单的键值对数据库(key-value data store)。

  .git目录包含了几乎所有Git存储和操作的对象。如若想备份或复制一个版本库,只需把这个目录拷贝至另一处即可。初始.git目录的结构如下:

$ ls -F1
HEAD
config
description
hooks/
info/
objects/
refs/
  • description文件仅供GitWeb程序使用,我们无需关心。
  • config文件包含项目特有的配置选项。
  • info目录包含一个全局性排除(global exclude)文件,用以放置那些不希望被记录在.gitignore文件中的忽略模式(ignored patterns)。
  • hooks目录包含客户端和服务端的钩子脚本(hooks scripts)。

  以下四个条目是Git的核心组成部分:

  • HEAD文件指示目前被检出checkout)的分支;
  • index文件(尚待创建)保存暂存区信息;
  • objects目录存储所有数据内容;
  • refs目录存储指向数据(分支)的提交对象的指针;

1 Git对象(Git Objects)

 1.1 数据对象(blob object)

  1.1.1 git hash-object -w向数据库写入数据

  底层命令hash-object可将任意数据保存于.git目录,并返回相应的键值。如向Git数据库存入一些文本:

$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4

  该命令输出一个长度为40个字符的校验和。这是一个SHA-1哈希值——一个将待存储的数据外加一个头部信息(header)一起做SHA-1校验运算而得的校验和。

  可以在objects目录下看到一个文件:

$ find .git/objects/ -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

  这就是开始时Git存储内容的方式——一个文件对应一条内容,以该内容加上待定头部信息一起的SHA-1校验和为文件名。校验和的前两个字符用于命名子目录,余下的38个字符则用作文件名。

  1.1.2 git cat-file -p从数据库读取数据

  可以通过cat-file -p命令从Git那里取回数据。如:

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

  记住文件的每一个版本所对应的SHA-1值并不现实;另一个问题是,在这个(简单的版本控制)系统中,文件名并没有被保存——我们仅保存了文件的内容。上述类型的对象我们称之为数据对象blob object)。

  1.1.3 git cat-file -t返回对象类型

  利用cat-file -t命令,可以让Git告诉我们其内部存储的任何对象类型,只要给定该对象的SHA-1值:

$ git cat-file -t d670460b4b4aece5915caf5c68d12f560a9fe3e4
blob

 1.2 树对象(tree object)

  树对象tree object)能解决文件名保存的问题,也允许我们将多个文件组织到一起。Git以一种类似于Unix文件系统的方式存储内容,但作了些简化。所有内容均以树对象和数据对象的形式存储,其中树对象对应了UNIX中的目录项,数据对象则大致上对应了inodes或文件内容。,一个树对象包含了一条或多条树对象记录tree entry),每条记录含有一个指向数据对象或者子树对象的SHA-1指针,以及相应的模式、类型和文件名信息。
  例如,某项目当前对应的最新树对象可能是这样的:

$ git cat-file -p master^{tree}
100644 blob 86cf30788913955d245d7165c825122c1c571f72    .gitignore
100644 blob 8e780fe4b4afe554f0a1d1b6b5c5677ba6fbb6da    pom.xml
040000 tree f3a10fd0b4316258965971e229d68bdede5c9501    src

  master^{tree}语法表示master分支上最新的提交所指向的树对象。请注意,src子目录(所对应的那条树对象记录)并不是一个数据对象,而是一个指针,其指向的是另一个树对象:

$ git cat-file -p f3a10fd0b4316258965971e229d68bdede5c9501
040000 tree f3fa3b134b5f0c5f0fdadd47a272f207babea336    main
040000 tree e1a60240db0e5cc21ba059a990d00b68f8eaab48    test

$ git cat-file -p f3fa3b134b5f0c5f0fdadd47a272f207babea336
040000 tree 6ed64b61504b4dc27ba6994943df0fcd518e9a84    java

$ git cat-file -p 6ed64b61504b4dc27ba6994943df0fcd518e9a84
040000 tree 48893e2148439d9142b9596081146edb89e5e221    net

$ git cat-file -p 48893e2148439d9142b9596081146edb89e5e221
040000 tree c8220a21f6913a1083d9b62d0bbc77be92c8f4a4    mrliuli

$ git cat-file -p c8220a21f6913a1083d9b62d0bbc77be92c8f4a4
100644 blob 442f52aef7dd865851762ee899f90f416ebdb24d    App.java
100644 blob 9c184f6accd70ec3c6b71705760059bbe78fb22b    JMSConsumer.java
100644 blob d47e2c65317098c2ae2728db88ee02c3ada74c52    JMSProducer.java

  从概念上讲,Git内部存储的数据像这样:

  图1:简化版的Git数据模型
  Git内部原理

  通常,Git根据某一时刻暂存区(即index区域)所表示的状态创建并记录一个树对象,如此重复便可依次记录(某个时间段)一系列的树对象。

 1.3 提交对象(commit object)

  树对象代表了我们想要跟踪的不同项目快照。然而问题依旧:若想重用这些快照,你必须记住所有SHA-1哈希值。并且,你也完全不知道是谁保存了这些快照,在什么时刻保存的,以及为什么保存这些快照。而上以这些下量提交对象commit object)为你保存的基本信息。

  可以通过调用git commit-tree命令创建一个提交对象,为些需要指定一个树对象的SHA-1值,以及该提交的父提交对象(如果有的话)。

  以上介绍的几个底层操作便完成了一个Git提交历史的创建。这就是我们每次运行git addgit commit命令时,Git所做的实质工作——将被改写的文件保存为数据对象,更新暂存区,记录树对象,最后创建一个指明了顶层树对象和父提交的提交对象。这三种主要的Git对象——数据对象、树对象和提交对象——最初均以单独文件的形式保存在.git/objects目录下。

  图2:Git目录下的所有对象:
  Git内部原理

2 Git引用

  我们可以借助类似于git log 1c0211a这样的命令来浏览完整的提交历史,但为了能遍历那段历史从而找到所有相关对象,你仍须记住1c0211a是最后一个提交。我们需要一个文件来保存SHA-1值,并给文件起一个简单的名字,然后用这个名字指针来代替原始的SHA-1值。在Git里,这样的文件被称为“引用(references,或缩写refs)”,可以在.git/refs目录下找到这类含有SHA-1值的文件。

  若要创建一个新引用来帮助记忆最新提交所在的位置,从技术上讲我们只需简单地做如下操作:

$ echo '1ae4c8490dda8cccea702f282c0ca96e7d488d1b' > .git/refs/heads/test

  这基本上就是Git分支的本质一个指向某一系列提交之首的指针或引用

  图3:包含分支引用的Git目录对象:
  Git内部原理

 2.1 HEAD 引用

  现在的问题是,当你执行git branch [branchname]时,Git如何知道最新提交的SHA-1值呢?答案是HEAD文件。HEAD文件是一个符号引用(symbolic reference),指向目前所在的分支。所谓符号引用,意味着它并不像普通引用那样包含一个SHA-1值——它是一个指向其他引用的指针。如查看HEAD文件内容:

$ cat .git/HEAD
ref: refs/heads/master

  当我们执行git commit时,该命令会创建一个提交对象,并用HEAD文件中那个引用所指向的SHA-1值设置其交提交字段。

 2.2 标签引用

  标签对象(tag object)算是Git的第四种对象类型。标签对象tag object)非常类似于一个提交对象——它包含一个标签创建者信息、一个日期、一段注释信息,以及一个指针。主要的区别在于,标签对象通常指向一个提交对象,而不是一个树对象。它像是一个永不移动的分支引用——永远指向同一个提交对象,只不过给这个提交对象加上一个更友好的名字罢了。

  若要创建一个附注标签,Git 会创建一个标签对象,并记录一个引用来指向该标签对象,而不是直接指向提交对象。

 2.3 远程引用

  如果你添加了一个远程版本库并对其执行过推送操作,Git 会记录下最近一次推送操作时每一个分支所对应的值,并保存在 refs/remotes 目录下,这就是远程引用remote reference)。

$ cat .git/refs/remotes/origin/dev_20180115
1c0211a17f8f53b16e1f4bc7d0f6565b6a19402c

  远程引用和分支(位于 refs/heads 目录下的引用)之间最主要的区别在于,远程引用是只读的。虽然可以git checkout 到某个远程引用,但是 Git 并不会将 HEAD 引用指向该远程引用。因此,你永远不能通过 commit 命令来更新远程引用。Git 将这些远程引用作为记录远程服务器上各分支最后已知位置状态的书签来管理。

3 引用规格

  假设你添加了这样一个远程版本库:

$ git remote add origin https://github.com/leonliu06/ActiveMQ-Study

  上述命令会在 .git/config 文件中添加一个小节,并在其中指定远程版本库的名称(origin)、URL 和一个用于获取(fetch)操作的引用规格refspec):

[remote "origin"]
    url = https://github.com/leonliu06/ActiveMQ-Study.git
    fetch = +refs/heads/*:refs/remotes/origin/*

  引用规格的格式由一个可选的 + 号和紧随其后的 : 组成,其中 是一个模式(pattern),代表远程版本库中的引用; 是那些远程引用在本地所对应的位置。+ 号告诉 Git 即使在不能快进的情况下也要(强制)更新引用。
  默认情况下,引用规格由 git remote add 命令自动生成, Git 获取服务器中 refs/heads/ 下面的所有引用,并将它写入到本地的 refs/remotes/origin/ 中。

相关标签: git