Git底层原理与分析模型

https://www.cnblogs.com/liqinglucky/p/git.html

一、git版本管理

1.1 背景--从写毕业论文讲文档备份

让我们从写毕业论文的经历讲起。通常开始写论文之前,先在一个开阔的空间创建了一个文件夹用于保存将来的论文稿。然后就开始了我们的 “毕业论文版本管理”。

这样管理存在的问题:

  1. 看不出每一个版本都更改了什么东西
  2. 合并多个文档版本的不同段落需要逐个打开手动复制。
  3. 文档副本很多的时候,和容易忘记那个才是自己的最终版本。
  4. 文档手滑删除了,只能再写一遍。

当然毕业论文至多就多复制几个文件备份就好了,但如果是上万个代码文件的项目工程如何管理呢?

1.2 版本控制系统

备份策略通常包括版本控制,或者叫“对变更进行追踪管理”。不管是集中式的 CVS、SVN 还是分布式的 Git 工具,实际上都是一种版本控制系统,我们可以通过他们很方便的管理我们的文件、代码等。

1.2.1 集中式与分布式

集中式版本控制系统[1]。版本库是集中存放在中央服务器的,开发者都是在自己本地电脑,先从中央服务器下载最新的版本,然后开始干活。干完活了,再把自己的活推送给中央服务器。集中式版本控制系统最大的毛病就是必须联网才能工作,中央服务器要是出了问题,所有人都没法干活了。

布式版本控制系统。分布式版本控制系统根本没有 “中央服务器”,每个人的电脑上都是一个完整的版本库。 开发者之间可以把各自的修改直接推送给对方。分布式版本控制系统的安全性要高很多,因为每个人电脑里都有完整的版本库,某一个人的电脑坏掉了不要紧,随便从其他人那里复制一个就可以了。

在实际使用分布式版本控制系统的时候,其实很少在两人之间的电脑上推送版本库的修改。因此,分布式版本控制系统通常也有一台充当 “中央服务器” 的电脑,但这个服务器的作用仅仅是用来方便 “交换” 大家的修改,没有它大家也一样干活,只是交换修改不方便而已。

1.3 git历史

Git 是当前流行的分布式版本控制管理工具,最初由 Linus Torvalds (Linux 创始人) 创造,于 2005 年发布[2]
[3]

[!quote] Git版本控制管理, Jon Loeliger, Mattbew McCullougb
Git诞生之前,Linux内核开发过程中使用BitKeeper来作为版本控制系统(VCS),然而在2005年春天,BitKeeper对免费版加入额外限制时,Linux社区意识到BitKeeper不再是长期可行的方案。
Linux 创始人Linus Torvalds也开始寻找替代方案。首先他回避再次使用商业解决方案。但当时现有的开源方案中的一些限制和缺陷也使得他放弃。

1.4 git特性

  1. 分布式开发。允许开发人员在自己本地离线并行开发,不需要与中心版本库时刻同步。
  2. 能够胜任上千开发人员协同。
  3. 性能优异。网络传输操作,需要使用“压缩”和“差异比较”技术。
  4. 保持完整性和可靠性。一个分布式版本控制系统,要能绝对保证数据的完整性和不被篡改。通过安全散列函数(SHA1)命名和识别数据库中的对象。
  5. 强化责任。能够定位谁改动了文件与改动说明。
  6. 原子事务。相关操作要么全部执行要么都不执行。
  7. 支持并鼓励基于分支的开发。
  8. 完整的版本库。每个人的版本库中都有一份完整历史修订记录。
  9. 清晰的内部设计。
  10. 免费自由。

二、git底层原理--数据结构

其实只要我们掌握几个基本的git命令其实就够了,但还是很有必要理解git的实现逻辑。[4]

[!tip]
工科很多东西都有一套底层逻辑,得出结论更多靠的是“推导”而非“记忆”。

2.1 仓库-- git初始化一个仓库

如果我们打算对该项目进行版本管理,第一件事就是使用 git init 命令,进行初始化

$ git init

git init 命令只会做一件事,就是在项目的根目录下创建一个 .git 的子目录,用来保存当前项目的一些版本信息。

2.2 三大分区--追踪文件管理状态

  • 工作区(Working Directory):能直接编辑文件。这个区位置最简单,就是我们的所有源代码目录。新增的文件和修改的文件修改状态为红色
  • 暂存区(stage,index):暂时存放的文件数据。可以理解为数据进入本地代码仓库之前存放的区域。git add操作将文件副本加入暂存区。文件修改状态为绿色。
  • 仓库区(commit History):纳入版本管理的文件数据。可以理解为一个本地的代码仓库。暂存区git commit的文件会被放入仓库区。文件修改状态清除。[5][6]

2.3 对象类型(objects)--commit,tree,blob

Git 可以通过一种算法可以得到任意文件的 “指纹”(40 位 16 进制数字),然后通过文件指纹存取数据,存取的数据都位于 objects 目录。

块(blob: binary large object):文件内容本身,不包含文件名。[7]
目录树(tree):记录blob标识、路径名和目录里的所有文件。
提交(commit): 保存版本库中提交时刻的快照。

一个commit表示了什么?
A: 每个commit索引出此刻完整工作区源文件
B: 每个commit索引出此次所有工作区新增文件内容

[!quote] Git版本控制管理 4.1.5节, Jon Loeliger, Mattbew McCullougb
git内部数据库存储文件的每个版本,而不是差异
git用blob之间的区别计算历史,
git需要创建一个工作目录时,对文件系统说“我有这样大的一个blob数据,应该放在路径tree下,你能重新构造?”
直接存储每个版本的完整内容是否效率太低了?如果只添加一行到文件,git是否要存两个版本的全部内容?不是,不完全是。git的打包文件存储机制,定位内容相似的全部文件,然后只存储一份全部内容。之后计算文件间差异并只存差异。如果你只更改一行内容,git会存储新版本的全部内容,然后记录一行的差异,存储在包里。
打包文件跟对象中其他对象存储在一起。也用于网络版本库中的高效数据传输。

梳理 git管理文件版本的原理:
1 每个源文件内容本身映射成blob对象,而源文件名与文件路径映射成tree。指纹(SHA1)算法保证了对象的全局唯一性。
2 用commit索引tree, blob来追踪还原工作区的所有文件。一个commit就是一个完整的版本副本。
3 commit会自动追加成commit链还原了版本的全部历史。

三、git分析模型--有向无环图

理解了commit的实质基本原理后,建立commit的基本分析模型[8][9]:有向无环图(DAG)。有向无环图是不会回到起点的图。一个commit表示一个节点,一个节点表示了所有文件的一次版本。节点有父链接(历史改动生成的指纹)指向上一个commit,只要根据git的抽象模型分析git的操作就够了。只需关注commit id的变化分析。

3.1 commit与reset操作

一次commit就是整个仓库所有代码的改动。

git add a.txt
git commit -m "add a.txt"

git log看commit链。

# git log
commit dea03e51887ee93dbe862d8c6a4e5f64ca586d60 (HEAD -> master)
Author: Your Name <you@example.com>
Date:   Tue Jun 4 03:17:26 2024 +0000

    add b.txt

commit 966737211d560207bc6d7e01be267707adc22ca6
Author: Your Name <you@example.com>
Date:   Tue Jun 4 03:12:04 2024 +0000

    add a.txt

通过git diff可以看到改动了哪些内容。
比较两次commit id的diff

git diff commit_id1 commit_id2

3.2 分支与merge操作

分支就是在某个commit发生分叉的commit链。
merge就是将两个commit链合并。

查看该 commit object 后可以看到,执行 merge 操作之后,会将原本版本链基础上衍生出一个新的 commit,并且该 commit 拥有两个 parent 父指针:
1 两个分支合并,不冲突的时候,git计算合并结果,并创建一个新提交
2 有冲突是,通常出现在同一个文件,同一行两个分支都各自进行了改动。git自己不解决冲突,而是留给开发人员处理,然后开发人员做一次commit。

如何避免 git 冲突?
“各位同仁们,接下来的四五个小时内我即将提交一笔代码,恳请大家在这段时间不要merge任何代码,求求大家了,晚上给你们买奶茶!”
只要是多人协作开发,文件的冲突是不可避免的,剩下的问题只是根据git提示手动解决冲突。

3.3 push与pull操作

对于处在远端的中央仓库,我们每次尝试通过 push 向远端推送一个 commit 时,远端仓库都会对提交版本的正确性进行校验,校验方式是沿拟提交 commit object 的 parent 指针向前遍历,倘若能找到某个 parent commit object 和远端分支上最后一个 commit object 的 key 值相同,才可能允许这次 push 行为,以此保证版本链的连贯性.

追踪远端分支

git checkout -b dev --track origin/dev

3.4 子模块(submodule)

子模块是一个链接,就像文件夹的快捷方式。
git submodule可以看到记录子模块的文件.gitmodule,文件的内容就是记录的子模块的那个仓库的最新commit id。

四、操作演示

4.1 分区与对象

// 实时观察.git目录的变化
$ watch -n .5 tree .git

// .git目录
.git
├── branches
├── COMMIT_EDITMSG
├── config
├── description
├── HEAD     <<< 指向当前commit
├── index    <<< 暂存区文件
├── info
│   └── exclude
├── logs
│   ├── HEAD
│   └── refs
│       └── heads
│           └── master
├── objects
│   ├── 96
│   │   └── 6737211d560207bc6d7e01be267707adc22ca6
│   ├── bc
│   │   └── 9a8c7d02d20e99c0481003176a906d4c6e0cf3
│   ├── c9
│   │   └── b2240e42509686a034104179629ad74f72d3f4
│   ├── cc
│   │   └── 147aadf9175a075ea6f2c455692d074b45c329
│   ├── de
│   │   └── a03e51887ee93dbe862d8c6a4e5f64ca586d60
│   ├── info
│   └── pack
└── refs
    ├── heads
    │   └── master   <<< 指向当前分支commit
    └── tags

objects

// 对象类型:blob
# git cat-file -t c9b224
blob
// 对象内容: 文件内容
# git cat-file -p c9b224
1 hello
2 git

// 对象类型:tree
# git cat-file -t cc147a
tree
// 对象内容: 文件内容
# git cat-file -p cc147a
100644 blob c9b2240e42509686a034104179629ad74f72d3f4    a.txt
100644 blob c9b2240e42509686a034104179629ad74f72d3f4    b.txt

// 对象类型:commit
# git cat-file -t dea03e
commit
// 对象内容: 文件内容
# git cat-file -p dea03e
tree cc147aadf9175a075ea6f2c455692d074b45c329
parent 966737211d560207bc6d7e01be267707adc22ca6
author Your Name <you@example.com> 1717471046 +0000
committer Your Name <you@example.com> 1717471046 +0000

add b.txt

index

# git ls-files --stage
100644 c9b2240e42509686a034104179629ad74f72d3f4 0       a.txt
100644 c9b2240e42509686a034104179629ad74f72d3f4 0       b.txt
// 当前分支
.git# cat HEAD
ref: refs/heads/master

# git cat-file -t HEAD
commit
root@ubuntu:/home/git# git cat-file -p HEAD
tree cc147aadf9175a075ea6f2c455692d074b45c329
parent 966737211d560207bc6d7e01be267707adc22ca6
author Your Name <you@example.com> 1717471046 +0000
committer Your Name <you@example.com> 1717471046 +0000

add b.txt

# git cat-file -t refs/heads/master
commit
# git cat-file -p refs/heads/master
tree cc147aadf9175a075ea6f2c455692d074b45c329
parent 966737211d560207bc6d7e01be267707adc22ca6
author Your Name <you@example.com> 1717471046 +0000
committer Your Name <you@example.com> 1717471046 +0000

add b.txt

logs

# cat .git/logs/HEAD
0000000000000000000000000000000000000000 966737211d560207bc6d7e01be267707adc22ca6 Your Name <you@example.com> 1717470724 +0000  commit (initial): add a.txt

966737211d560207bc6d7e01be267707adc22ca6 dea03e51887ee93dbe862d8c6a4e5f64ca586d60 Your Name <you@example.com> 1717471046 +0000  commit: add b.txt

4.2 远端仓库

git clone ssh://root@server/home/git

遇到的问题:push到远端失败

Writing objects: 100% (3/3), 308 bytes | 154.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0

remote: You can set the 'receive.denyCurrentBranch' configuration variable
remote: to 'ignore' or 'warn' in the remote repository to allow pushing into
remote: its current branch; however, this is not recommended unless you
remote: arranged to update its work tree to match what you pushed in some
remote: other way.

解决:
root@ubuntu:/home/git/.git# cat config
[core]
        repositoryformatversion = 0
        filemode = true
        bare = false
        logallrefupdates = true
[receive]                        <<< 增加策略
        denyCurrentBranch = ignore

4.3 子模块

git submodule add ssh://root@server/home/plugin

子模块

git/plugin# git log
commit cbb1dd676b4d269e6cf02a4eadb4946391adbfa1 (HEAD -> master, origin/master, origin/HEAD)
Author: Your Name <you@example.com>
Date:   Sat Jun 1 13:03:18 2024 +0000

    c.txt


git/plugin# cd ../
git# git status
On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   .gitmodules
        new file:   plugin

git# git submodule
 cbb1dd676b4d269e6cf02a4eadb4946391adbfa1 plugin (heads/master)

git# cat .gitmodules
[submodule "plugin"]
        path = plugin
        url = ssh://root@server/home/plugin

参考


  1. Git原理入门解析 (qq.com) ↩︎

  2. Git版本控制管理, Jon Loeliger, Mattbew McCullougb ↩︎

  3. Git-内部原理 pro-git在线文档 ↩︎

  4. [万字串讲git版本控制底层原理及实战分享]( https://www.bilibili.com/video/BV1Gu4y1u7ut/, https://zhuanlan.zhihu.com/p/670878449) ↩︎

  5. .git目录 ↩︎

  6. 手撕Git,告别盲目记忆 ↩︎

  7. objects ↩︎

  8. 动画图解Git的10大命令 ↩︎

  9. git图解 ↩︎

posted @ 2024-06-08 22:56  liqinglucky  阅读(442)  评论(0编辑  收藏  举报