Git内部原理

内部原理

初始化一个Git目录

$ git init 
Initialized empty Git repository in D:/VSCode/testGit/.git/	

## 新初始化的 .git 目录的典型结构如下:
$ ls -Fl
total 7
-rw-r--r-- 1 xuyuansheng 197121 130 10月 10 21:44 config     
-rw-r--r-- 1 xuyuansheng 197121  73 10月 10 21:44 description
-rw-r--r-- 1 xuyuansheng 197121  23 10月 10 21:44 HEAD       
drwxr-xr-x 1 xuyuansheng 197121   0 10月 10 21:44 hooks/     
drwxr-xr-x 1 xuyuansheng 197121   0 10月 10 21:44 info/      
drwxr-xr-x 1 xuyuansheng 197121   0 10月 10 21:44 objects/   
drwxr-xr-x 1 xuyuansheng 197121   0 10月 10 21:44 refs/
  • description 文件仅供 GitWeb 程序使用,我们无需关心。
  • config 文件包含项目特有的配置选项。
  • info 目录包含一个全局性排除(global exclude)文件, 用以放置那些不希望被记录在 .gitignore 文件中的忽略模式(ignored patterns)。
  • hooks 目录包含客户端或服务端的钩子脚本(hook scripts), 在 Git 钩子 中这部分话题已被详细探讨过。

剩下的四个条目很重要HEAD 文件、(尚待创建的)index 文件,和 objects 目录、refs 目录。 它们都是 Git 的核心组成部分。

  • objects 目录存储所有数据内容;
  • refs 目录存储指向数据(分支、远程仓库和标签等)的提交对象的指针;
  • HEAD 文件指向目前被检出的分支;
  • index 文件保存暂存区信息。

objects目录中的Git对象

概述

Git 是一个内容寻址文件系统,听起来很酷。但这是什么意思呢? 这意味着,Git 的核心部分是一个简单的键值对数据库(key-value data store)。 你可以向 Git 仓库中插入任意类型的内容,它会返回一个唯一的键,通过该键可以在任意时刻再次取回该内容。

可以通过底层命令 git hash-object 来演示上述效果——该命令可将任意数据保存于 .git/objects 目录(即 对象数据库),并返回指向该数据对象的唯一的键。

首先,我们需要初始化一个新的 Git 版本库,并确认 objects 目录为空:可以看到 Git 对 objects 目录进行了初始化,并创建了 packinfo 子目录,但均为空。

$ git init 
Initialized empty Git repository in D:/VSCode/test/.git/
$ find .git/objects 
.git/objects
.git/objects/info
.git/objects/pack
user@NAME MINGW64 /d/VSCode/test (master)
$ find .git/objects -type f 

写入数据到Git中 git hash-object

我们用 git hash-object 创建一个新的数据对象并将它手动存入你的新 Git 数据库中:

user@NAME MINGW64 /d/VSCode/test (master)
$ echo 'test content' | git hash-object -w --stdin 
d670460b4b4aece5915caf5c68d12f560a9fe3e4

在这种最简单的形式中,git hash-object 会接受你传给它的东西,而它只会返回可以存储在 Git 仓库中的唯一键。

-w 选项会指示该命令不要只返回键,还要将该对象写入数据库中。

--stdin 选项则指示该命令从标准输入读取内容;若不指定此选项,则须在命令尾部给出待存储文件的路径。

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

查看 Git 是如何存储数据的:

##  发现在目录中已经有一个文件了
$ find .git/objects/ -type f 
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

如果你再次查看 objects 目录,那么可以在其中找到一个与新内容对应的文件。 这就是开始时 Git 存储内容的方式——一个文件对应一条内容, 以该内容加上特定头部信息一起的 SHA-1 校验和为文件命名。 校验和的前两个字符用于命名子目录,余下的 38 个字符则用作文件名。

cat-file查看Git文件

一旦你将内容存储在了对象数据库中,那么可以通过 cat-file 命令从 Git 那里取回数据。 这个命令简直就是一把剖析 Git 对象的瑞士军刀。 为 cat-file 指定 -p 选项可指示该命令自动判断内容的类型,并为我们显示大致的内容:

user@NAME MINGW64 /d/VSCode/test (master)
## -p 查看Git文件内容
$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4 
test content

对Git中文件进行操作

至此,你已经掌握了如何向 Git 中存入内容,以及如何将它们取出。 我们同样可以将这些操作应用于文件中的内容。 例如,可以对一个文件进行简单的版本控制。 首先,创建一个新文件并将其内容存入数据库:

新建文件,写入Git中

## 新建文件
user@NAME MINGW64 /d/VSCode/test (master)
$ echo 'version 1' > test.txt 
## 写入Git中
user@NAME MINGW64 /d/VSCode/test (master)
$ git hash-object -w test.txt 
warning: LF will be replaced by CRLF in test.txt.
The file will have its original line endings in your working directory
83baae61804e65cc73a7201a7252750c76066a30
## 更新文件内容
user@NAME MINGW64 /d/VSCode/test (master)
$ echo 'version 2' > test.txt 
## 再次写入Git中
user@NAME MINGW64 /d/VSCode/test (master)
$ git hash-object -w test.txt 
warning: LF will be replaced by CRLF in test.txt.
The file will have its original line endings in your working directory
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

查看objects中的Git对象

$ find .git/objects -type f
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

## 现在可以在删掉 `test.txt` 的本地副本,然后用 Git 从对象数据库中取回它的第一个版本:
$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt
$ cat test.txt
version 1

## 或者第二个版本:
$ git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test.txt
$ cat test.txt
version 2

数据对象(blob object)

然而,记住文件的每一个版本所对应的 SHA-1 值并不现实;另一个问题是,在这个(简单的版本控制)系统中,文件名并没有被保存——我们仅保存了文件的内容。 上述类型的对象我们称之为 数据对象(blob object)。 利用 git cat-file -t 命令,可以让 Git 告诉我们其内部存储的任何对象类型,只要给定该对象的 SHA-1 值:

$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob

树对象

准备数据

## 在Git仓库中添加如下结构文件,.git目录除外。
$ find  . !  -path "./.git/*"
.
./.git
./lib
./lib/simplegit.rb
./Rakefile        
./README

概述

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

user@NAME MINGW64 /d/VSCode/test (master)
$ git cat-file -p "master^{tree}"
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a    README  
100644 blob e81307b7ab679109c26d10e9aa95f661d5be82e5    Rakefile
040000 tree e8b3a5262abc80e4cebaeab256f90f6f5a7d25f9    lib 

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

你可能会在某些 shell 中使用 master^{tree} 语法时遇到错误。在 Windows 的 CMD 中,字符 ^ 被用于转义,因此你必须双写它以避免出现问题:git cat-file -p master^^{tree}。 在 PowerShell 中使用字符 {} 时则必须用引号引起来,以此来避免参数解析错误:git cat-file -p 'master^{tree}'

在 ZSH 中,字符 ^ 被用在通配模式(globbing)中,因此你必须将整个表达式用引号引起来:git cat-file -p "master^{tree}"

$  git cat-file -p e8b3a5262abc80e4cebaeab256f90f6f5a7d25f9
100644 blob ffeec7aa12599e6ba43b19db8a6440114e136362    simplegit.rb

目录结构

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

创建自己的树对象

通常,Git 根据某一时刻暂存区(即 index 区域,下同)所表示的状态创建并记录一个对应的树对象, 如此重复便可依次记录(某个时间段内)一系列的树对象。 因此,为创建一个树对象,首先需要通过暂存一些文件来创建一个暂存区。

创建暂存区 git update-index

可以通过底层命令 git update-index 为一个单独文件——我们的 test.txt 文件的首个版本——创建一个暂存区。 利用该命令,可以把 test.txt 文件的首个版本人为地加入一个新的暂存区。

必须为上述命令指定 参数

  1. --add 选项,因为此前该文件并不在暂存区中(我们甚至都还没来得及创建一个暂存区呢); 同样必需的还有
  2. --cacheinfo 选项,因为将要添加的文件位于 Git 数据库中,而不是位于当前目录下。
  3. 文件模式: 100644
  4. SHA-1: Git库中Blob对象,83baae61804e65cc73a7201a7252750c76066a30、这是前面例子中的内容为"version 1" 的版本
  5. 文件名: test.txt
## 添加文件
user@NAME MINGW64 /d/VSCode/test (master)
git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt
## 添加到暂存区
$ git update-index --add --cacheinfo 100644 83baae61804e65cc73a7201a7252750c76066a30 test.txt 
## 仓库状态
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   test.txt

本例中,我们指定的文件模式为 :

  1. 100644,表明这是一个普通文件。
  2. 100755,表示一个可执行文件;
  3. 120000,表示一个符号链接。

这里的文件模式参考了常见的 UNIX 文件模式,但远没那么灵活——上述三种模式即是 Git 文件(即数据对象)的所有合法模式(当然,还有其他一些模式,但用于目录项和子模块)。

将暂存区内容写入一个树对象

现在,可以通过 git write-tree 命令将暂存区内容写入一个树对象。 此处无需指定 -w 选项——如果某个树对象此前并不存在的话,当调用此命令时, 它会根据当前暂存区状态自动创建一个新的树对象:

$ git write-tree 
2ea0cbfea85132056156e6613383fb9d52211eb5

$ git cat-file -t  2ea0cbfea85132056156e6613383fb9d52211eb5
tree

user@NAME MINGW64 /d/VSCode/test (master)
$ git cat-file -p  2ea0cbfea85132056156e6613383fb9d52211eb5 
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a    README  
100644 blob e81307b7ab679109c26d10e9aa95f661d5be82e5    Rakefile
040000 tree e8b3a5262abc80e4cebaeab256f90f6f5a7d25f9    lib     
100644 blob 83baae61804e65cc73a7201a7252750c76066a30    test.txt

接着我们来创建一个新的树对象,它包括 test.txt 文件的第二个版本,以及一个新的文件:

$ git update-index --add --cacheinfo 100644 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a  test.txt    
## 本地目录中的文件不需要 --cacheinfo
user@NAME MINGW64 /d/VSCode/test (master)
$ git update-index --add new.txt 

暂存区现在包含了 test.txt 文件的新版本,和一个新文件:new.txt。 记录下这个目录树(将当前暂存区的状态记录为一个树对象),然后观察它的结构:

user@NAME MINGW64 /d/VSCode/test (master)
$ git write-tree
cf6d62e6207872255144f487d580616eba46cc2d

user@NAME MINGW64 /d/VSCode/test (master)
$ git cat-file -p cf6d62e6207872255144f487d580616eba46cc2d 
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a    README  
100644 blob e81307b7ab679109c26d10e9aa95f661d5be82e5    Rakefile
040000 tree e8b3a5262abc80e4cebaeab256f90f6f5a7d25f9    lib     
100644 blob fa49b077972391ad58037050f2a75f74e3671e92    new.txt 
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a    test.txt

我们注意到,新的树对象包含 test.txt 的 SHA-1 值(1f7a7a)是先前值的“第二版”。 只是为了好玩:你可以将第一个树对象加入第二个树对象,使其成为新的树对象的一个子目录。 通过调用 git read-tree 命令,可以把树对象读入暂存区。 本例中,可以通过对该命令指定 --prefix 选项,将一个已有的树对象作为子树读入暂存区:

posted @ 2023-02-12 20:29  菜阿  阅读(36)  评论(0编辑  收藏  举报