Git探秘: 深度解析版本控制神器

上篇Git入门中介绍了一些Git基本操作过程,其中提到了Git版本库的概念,Git到底是如何管理版本信息的呢?常用的git add、git commit、git branch命令在git版本库中会产生什么内容?Gitlab服务器是如何存放项目内容的?看完本篇文章,相关疑惑你都会得到解答

一、基本介绍

.git目录也可以称为”版本库”(Version Control Repository)。该目录包含了Git版本控制系统所需的所有信息,包括历史提交、分支、标签、配置等。它是Git用来管理和追踪项目版本的核心组成部分。通过.git目录,Git可以跟踪和记录项目中每个文件的变化,以及各个提交版本之间的关系。因此,可以将.git目录视为整个项目的版本库,用于管理和存储项目的版本历史。当我们使用git init 命令的时候,就会创建一个新的Git仓库。执行该命令后,Git会在当前目录下生成一个.git目录,该目录用来存储Git仓库的元数据和对象。

admin@admindeMacBook-Pro Workspace % mkdir LearnGit
admin@admindeMacBook-Pro Workspace % cd LearnGit
admin@admindeMacBook-Pro LearnGit % ls
admin@admindeMacBook-Pro LearnGit % git init
Initialized empty Git repository in /Users/admin/Workspace/LearnGit/.git/
admin@admindeMacBook-Pro LearnGit % ls -la
total 0
drwxr-xr-x@  3 admin  staff   96 11  2 18:08 .
drwxr-xr-x  14 admin  staff  448 11  2 18:08 ..
drwxr-xr-x@  9 admin  staff  288 11  2 18:08 .git

二、初始化

2.1 目录结构

查看.git 仓库目录结构

admin@admindeMacBook-Pro LearnGit % tree .git
.git
├── HEAD
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-merge-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   ├── push-to-checkout.sample
│   └── update.sample
├── info
│   └── exclude
├── objects
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

9 directories, 17 files
  1. HEAD文件:该文件指向当前分支的引用。它通常包含了一个指向refs/heads/目录下某个分支的引用。
  2. config文件:该文件存储了Git仓库的配置信息。它包含了仓库级别的配置项,如用户名、邮箱、远程仓库等
  3. description文件是一个可选的文件,用于给Git仓库提供一个简短的描述。该文件的内容通常是一个简单的文本字符串,用于描述该Git仓库的用途或特点。这
  4. hooks/目录:该目录包含了Git的钩子脚本。你可以在这里编写自定义脚本,用于在特定的Git操作(如提交、合并等)前后执行一些自定义的操作。
  5. info/目录是一个可选的目录,用于存储一些Git相关的辅助信息。在info/目录中,最常见的文件是info/exclude。exclude文件用于指定Git仓库中不希望被跟踪的文件或目录。可以在exclude文件中添加模式匹配规则,告诉Git忽略特定的文件或目录。
  6. objects/目录:该目录存储了Git仓库中的对象。Git使用对象来存储文件的内容和元数据。每个对象都有一个唯一的SHA-1哈希值作为标识。对象可以是文件内容、提交历史、分支、标签等。
  7. refs/目录:该目录存储了Git仓库中的引用。引用是指向某个提交对象的指针,它们可以是分支、标签或其他的引用。refs/heads/目录存储分支引用,refs/tags/目录存储标签引用。

2.2 文件变动

新增了一个文件,并添加到暂存区

admin@admindeMacBook-Pro LearnGit % echo 'test' > file
admin@admindeMacBook-Pro LearnGit % git add file

此时查看.git 目录结构,发现多了个index文件,且objects新增了一个名为9d的文件夹。index文件:该文件存储了暂存区(也称为索引)的内容。暂存区是Git用来暂存将要提交的文件修改的区域。

.git
├── HEAD
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-merge-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   ├── push-to-checkout.sample
│   └── update.sample
├── index
├── info
│   └── exclude
├── objects
│   ├── 9d
│   │   └── aeafb9864cf43055ae93beb0afd6c7d144bfa4
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

2.3 git 存储机制

使用file命令查看objects 里存储的文件格式,发现是zlib压缩后的内容

admin@admindeMacBook-Pro LearnGit % file .git/objects/9d/aeafb9864cf43055ae93beb0afd6c7d144bfa4
.git/objects/9d/aeafb9864cf43055ae93beb0afd6c7d144bfa4: zlib compressed data

想查看zlib压缩后的内容具体是什么,需要先解压缩查看,下面命令通过Python的zlib库解压缩查看

admin@admindeMacBook-Pro LearnGit % python3 -c "import zlib; import sys; data = open('.git/objects/9d/aeafb9864cf43055ae93beb0afd6c7d144bfa4', 'rb').read(); print(zlib.decompress(data))"

b'blob 5x00testn'

从结果可以看出,这个文件记录了之前通过 git add 命令添加的 file 文件的相关信息,包括文件的类型、大小和内容。具体地说,文件类型为 blob,大小为 4,内容则是 test。9d/aeafb9864cf43055ae93beb0afd6c7d144bfa4 这个目录和文件名是怎么来的 目录和文件名其实是基于内容的 sha1 哈希值生成的。通过对 zlib 压缩的数据进行 sha1sum 处理,我们就可以得到这样的值。Git在存储内容时,取其前两个字符作为文件夹名(如 9d),余下的部分作为文件名。这种方式是为了确保在 objects 文件夹中不会有过多的文件,从而使文件系统保持高效。

admin@admindeMacBook-Pro LearnGit % python3 -c "import zlib, hashlib; print(hashlib.sha1(zlib.decompress(open('.git/objects/9d/aeafb9864cf43055ae93beb0afd6c7d144bfa4', 'rb').read())).hexdigest())"

9daeafb9864cf43055ae93beb0afd6c7d144bfa4

当时上面用Python的zlib库这种方式查看zlib压缩后的内容比较麻烦,Git专门提供了git cat-file命令进行查看 git cat-file命令的一般用法是:

git cat-file <options> <object>

其中,是命令的选项,

  • -t:显示对象的类型(blob、tree、commit等)。
  • -s:显示对象的大小(以字节为单位)。
  • -p:以人类可读的方式显示对象的内容。备注:必须要引用从根对象文件夹开始的完整文件名,也就是前面一定要加上9d,否则会出现如下报错
admin@admindeMacBook-Pro 9d % git cat-file -s 9daeafb9864cf43055ae93beb0afd6c7d144bfa4
5
admin@admindeMacBook-Pro 9d % git cat-file -t 9daeafb9864cf43055ae93beb0afd6c7d144bfa4
blob
admin@admindeMacBook-Pro 9d % git cat-file -p 9daeafb9864cf43055ae93beb0afd6c7d144bfa4
test
admin@admindeMacBook-Pro 9d % ls -la
total 8
drwxr-xr-x@ 3 admin  staff   96 11  2 18:17 .
drwxr-xr-x@ 5 admin  staff  160 11  2 18:17 ..
-r--r--r--@ 1 admin  staff   20 11  2 18:17 aeafb9864cf43055ae93beb0afd6c7d144bfa4
admin@admindeMacBook-Pro 9d % git cat-file -p aeafb9864cf43055ae93beb0afd6c7d144bfa4
fatal: Not a valid object name aeafb9864cf43055ae93beb0afd6c7d144bfa4

三、文件提交

上面一步介绍了执行git add命令,增加一个文件到暂存区,.git 下的文件有哪些变化。那么执行commit 提交后,又会发生哪些变化呢?

admin@admindeMacBook-Pro LearnGit % git commit -m "first commit"
[main (root-commit) 89b57e3] first commit
 1 file changed, 1 insertion(+)
 create mode 100644 file
admin@admindeMacBook-Pro LearnGit % tree .git
.git
├── COMMIT_EDITMSG
├── HEAD
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-merge-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   ├── push-to-checkout.sample
│   └── update.sample
├── index
├── info
│   └── exclude
├── logs
│   ├── HEAD
│   └── refs
│       └── heads
│           └── main
├── objects
│   ├── 89
│   │   └── b57e32d3250db54dc189ad0a9a1c81a41ea724
│   ├── 9d
│   │   └── aeafb9864cf43055ae93beb0afd6c7d144bfa4
│   ├── e1
│   │   └── b8ecbb1f19709f3a4867a0ffe08bb2e07acf19
│   ├── info
│   └── pack
└── refs
    ├── heads
    │   └── main
    └── tags

首先有一个新文件 COMMIT_EDITMSG,它保存了最新的提交信息。另外新增了一个 logs 目录,git 通过它来记录所有的提交变动。在此,可以查看所有引用(refs)及 HEAD 的提交记录。object 文件夹也发生了些变化,另外 refs/heads 目录,里面出现有一个 main 文件。这是 main 分支的引用。查看内容发现其指向了一个新的对象commit。查看commit对象的内容时,显示提交的作者和提交者信息,另外内容里返回了一个e1b8ecbb1f19709f3a4867a0ffe08bb2e07acf19 tree对象

admin@admindeMacBook-Pro LearnGit % cat .git/refs/heads/main
89b57e32d3250db54dc189ad0a9a1c81a41ea724
admin@admindeMacBook-Pro LearnGit % git cat-file -t 89b57e32d3250db54dc189ad0a9a1c81a41ea724
commit
admin@admindeMacBook-Pro LearnGit % git cat-file -p 89b57e32d3250db54dc189ad0a9a1c81a41ea724
tree e1b8ecbb1f19709f3a4867a0ffe08bb2e07acf19
author 洋洋 <yangyang@test.com> 1698931679 +0800
committer 洋洋 <yangyang@test.com> 1698931679 +0800

first commit

也可以使用 git cat-file -t refs/heads/main  命令查看 查看tree对象的内容,发现这个文件指向了我们在执行 git add file 时加入的那个初始对象。

admin@admindeMacBook-Pro LearnGit %  git cat-file -p e1b8ecbb1f19709f3a4867a0ffe08bb2e07acf19
100644 blob 9daeafb9864cf43055ae93beb0afd6c7d144bfa4        file

四、文件修改

让对文件file内容进行修改,新增一行aaa内容,

admin@admindeMacBook-Pro LearnGit % echo "aaa" > file
admin@admindeMacBook-Pro LearnGit % git add file
admin@admindeMacBook-Pro LearnGit % git commit -m "add new line"
[main cc5ad72] add new line
1 file changed, 1 insertion(+), 1 deletion(-)
admin@admindeMacBook-Pro LearnGit % tree .git
.git
├── COMMIT_EDITMSG
├── HEAD
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-merge-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   ├── push-to-checkout.sample
│   └── update.sample
├── index
├── info
│   └── exclude
├── logs
│   ├── HEAD
│   └── refs
│       └── heads
│           └── main
├── objects
│   ├── 72
│   │   └── 943a16fb2c8f38f9dde202b7a70ccc19c52f34
│   ├── 89
│   │   └── b57e32d3250db54dc189ad0a9a1c81a41ea724
│   ├── 9d
│   │   └── aeafb9864cf43055ae93beb0afd6c7d144bfa4
│   ├── cc
│   │   └── 5ad72bced41c7e9ebea0a2adb85ba5b26dd344
│   ├── e1
│   │   └── b8ecbb1f19709f3a4867a0ffe08bb2e07acf19
│   ├── f6
│   │   └── 722b5327845e79fbc2ce8a8e008d417566dcb3
│   ├── info
│   └── pack
└── refs
  ├── heads
  │   └── main
  └── tags

18 directories, 28 files

总的来说,新增了三个对象。一个是含有文件新内容的 blob 对象,还有一个是 tree 对象,以及一个 commit 对象。再次从 HEAD 或 refs/heads/main 开始追踪这些对象。

admin@admindeMacBook-Pro LearnGit % git cat-file -p refs/heads/main
tree f6722b5327845e79fbc2ce8a8e008d417566dcb3
parent 89b57e32d3250db54dc189ad0a9a1c81a41ea724
author 洋洋 <yangyang@test.com> 1698932923 +0800
committer 洋洋 <yangyang@test.com> 1698932923 +0800

add new line
admin@admindeMacBook-Pro LearnGit % git cat-file -p f6722b5327845e79fbc2ce8a8e008d417566dcb3
100644 blob 72943a16fb2c8f38f9dde202b7a70ccc19c52f34        file
admin@admindeMacBook-Pro LearnGit %  git cat-file -p 72943a16fb2c8f38f9dde202b7a70ccc19c52f34
aaa

commit 对象现在有了一个额外的键名为 parent,它链接到上一个提交,因为当前提交是基于上一个提交创建的。

五、创建分支

创建一个新的分支。执行 git branch test 命令

├── logs
│   ├── HEAD
│   └── refs
│       └── heads
│           ├── main
│           └── test
└── refs
  ├── heads
  │   ├── main
  │   └── test
  └── tags

此操作会在 refs/heads 目录下加入一个新的文件。该文件的名称就是我们新建的分支名,而内容则是最新的提交标识 id。

admin@admindeMacBook-Pro LearnGit % cat .git/refs/heads/test
cc5ad72bced41c7e9ebea0a2adb85ba5b26dd344
admin@admindeMacBook-Pro LearnGit % git log
commit cc5ad72bced41c7e9ebea0a2adb85ba5b26dd344 (HEAD -> main, test)
Author: 洋洋 <yangyang@test.com>
Date:   Thu Nov 2 21:48:43 2023 +0800

  add new line

commit 89b57e32d3250db54dc189ad0a9a1c81a41ea724
Author: 洋洋 <yangyang@test.com>
Date:   Thu Nov 2 21:27:59 2023 +0800

  first commit

在 git 中,标签的创建也是类似的操作,但它们是被创建在 refs/tags 目录下。在 logs 目录下也新增了一个文件,该文件用于记录与 main 分支类似的提交历史信息。当创建一个新的分支,Git 会在当前的提交基础上创建一个指针,指向新的分支。

六、执行推送

对本地 git 仓库进行了一系列操纵之后,需要把本地仓库推送到远程。远程 git 仓库会接收哪些数据?提前在gitlab仓库上建立一个仓库,用git remote命令做远程关联,然后把代码推送到远程

admin@admindeMacBook-Pro  LearnGit % git remote add origin git@git.github.com:yangyang/git-learn.git
admin@admindeMacBook-Pro  LearnGit % git push -u origin --all 
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 10 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (6/6), 452 bytes | 452.00 KiB/s, done.
Total 6 (delta 0), reused 0 (delta 0), pack-reused 0
remote:
remote: To create a merge request for test, visit:
remote:   https://git.github.com/yangyang/git-learn/-/merge_requests/new?merge_request[source_branch]=test
remote:
To git.github.com:yangyang/git-learn.git
● [new branch]      main -> main
● [new branch]      test -> test
branch 'main' set up to track 'origin/main'.
branch 'test' set up to track 'origin/test'.

此时查看,发现新增了一个新的 refs/remotes 目录,这是用来存储不同远程仓库相关信息的。

tree .git
└── refs
├── heads
│   ├── main
│   └── test
├── remotes
│   └── origin
│       ├── main
│       └── test
└── tags

实际上传送到远程 git 仓库的数据是什么呢?那就是 objects 文件夹内的所有数据,以及明确推送的 refs 下的分支和标签。PS:类似常用的Gitlab 管理平台,其背后存储的项目就是.GIT 目录

七、.Git 还原

通过上面的介绍,可以了解到.git目录包含了几乎所有 Git 存储和操作的东西。如若想备份或复制一个版本库,只需把这个目录拷贝至另一处即可。那么我们应该也可以把.git 文件目录结构给还原,如何还原呢?直接把.git 目录拷贝到 LearnGit2 目录,执行git checkout . 命令即可还原

admin@admindeMacBook-Pro  Workspace % cp -r LearnGit/.git LearnGit2 
admin@admindeMacBook-Pro  Workspace % cd LearnGit2 
admin@admindeMacBook-Pro  LearnGit2 % ls -la 
total 0
drwxr-xr-x@  3 admin  staff   96 11  3 10:48 .
drwxr-xr-x  15 admin  staff  480 11  3 10:48 ..
drwxr-xr-x@ 12 admin  staff  384 11  3 10:48 .git
admin@admindeMacBook-Pro  LearnGit2 % git checkout . 
Updated 1 path from the index
admin@admindeMacBook-Pro  LearnGit2 % ls 
file

八、总结

Git 本质上是一个内容寻址文件系统。在 Git 中,每个文件和目录都被视为一个对象,每个对象都有一个唯一的 SHA-1 哈希值作为其标识符。Git 使用这些哈希值来寻址和识别文件和目录。当对文件进行更改时,Git 会根据文件内容生成一个新的哈希值,并将其作为新的对象存储在 Git 仓库中。Git 的内容寻址机制使得文件的历史版本可以被有效地管理和追踪。每个提交(commit)都包含一个指向根目录的指针,这个根目录指针又指向其他子目录和文件,形成了一个有向无环图(DAG)的结构。这种结构可以让我们轻松地查看、回溯和恢复文件的历史版本。另外,Git 还使用了一种称为“对象存储”的方式来存储文件和目录对象。这些对象被压缩和存储在 Git 仓库的 .git/objects 目录下。通过使用哈希值作为对象的标识符,Git 可以通过快速的哈希查找来读取和写入这些对象。另外阅读了本文,你会发现.git/hooks 目录下的文件作用并没有讲到,那一个个如pre-commit.sample文件是什么意思,有什么作用?下一篇会单独再讲一下.git/hooks下的文件作用。

参考链接

  • https://git-scm.com/book/zh/v2/Git-内部原理-Git-对象
  • https://jvns.ca/blog/2023/09/14/in-a-git-repository–where-do-your-files-live-/
  • https://blog.meain.io/2023/what-is-in-dot-git/?continueFlag=83c2a4620ff11f71fb87e48f91cc14f5


原文始发于微信公众号(洋洋自语):Git探秘: 深度解析版本控制神器

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/273085.html

(0)
明月予我的头像明月予我bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!