Git通讲-第二章(3):分支模型
前言
呜呜呜🫡🫡🫡也是到第三篇了,我打算在该篇主要讲git的本地分支的相关内容,而远程分支则留给下一篇分布式版本控制。由于.git/ref/
文件下还有tag,那就同时补充介绍一下之前一直忽视的tag吧,我个人感觉是没啥用。
其实这一块内容我6月份的一篇文章中已经讲解了具体的命令操作([[Git本地与远程]]),本篇则更加关注于概念和原理。
分支模型
Git 的分支模型是一种灵活且高效的版本控制机制,通过指针和哈希引用管理分支,能够轻松支持并行开发、功能隔离和协作。这种实现方式既节省空间,又使得分支操作非常迅速。以下是 Git 分支模型的详细讲解:
1. 分支的基本概念
- 分支是 Git 的核心特性,它允许你从主线代码中分离出独立的开发路径。分支的创建、切换和合并速度很快,使得开发人员可以并行进行新功能的开发、修复 bug 或实验,而不会影响主分支。
- 默认情况下,Git 会有一个主分支,通常称为
main
或master
(弗洛伊德之死黑命贵运动席卷全球后由于master有主人的意义,为避免种族歧视嫌疑,git现在默认的主分支名改为了main,github也是如此,当然分支的默认名称可以通过config设置成任何)。
2. 基于指针的分支实现
利用不可变数据对象
之前的文章已经介绍过了Git 的对象存储分为四种类型:
- Blob(数据对象):存储文件内容的对象。
- Tree(树对象):表示一个目录结构,包含了该目录下所有文件和子目录的 Blob 和 Tree 的引用。
- Commit(提交对象):保存一次提交的快照,包括提交者信息、时间戳、提交信息,以及指向父提交和 Tree 对象的指针。
- Tag(标签对象):用于对某个提交创建标记(在后面文章会提到,和commit很像,辅助标记重要的commit用的)。
每个对象都用其 SHA-1 哈希值标识和引用,这种不可变的哈希标识是 Git 能够实现高效分支管理的基础。
分支的本质是指针
而在 Git 中分支的本质,一个分支其实是一个指向某个提交对象的指针(引用)
- 引用(Refs):Git 将分支存储在
.git/refs/heads/
目录中,每个分支名称对应一个文件,文件内容是一个 40 字符长的 SHA-1 哈希值,这个值指向该分支的最后一次提交。(远程分支则存储在.git/refs/remotes/
) - HEAD 指针:HEAD 是一个特殊的指针,它指向当前被检出的分支。通过切换分支,Git 会更新 HEAD 的指向,使其指向新的分支文件。
- Git 的分支本质上是一个指向提交对象的可移动指针。每个分支指针都指向该分支的最新提交对象(commit)。
- Git 使用一个特殊的
HEAD
指针来表示当前活动分支,HEAD
始终指向当前分支的最新提交对象。例如:上面的结构表示1
2main --> C3
HEAD --> mainmain
分支指向提交C3
,而HEAD
指向main
,代表我们当前位于main
分支上。
分支的创建与切换
创建分支:当你使用 git branch <branch-name>
创建一个新分支时,在 Git 中创建分支时,实际上只是创建了一个新指针,并不会复制文件或创建新的目录。相反,它会:
- 在
.git/refs/heads/
目录下创建一个新的分支文件。 - 将当前分支的最后一个提交的哈希值写入新分支文件中。
由于分支文件只是一个包含 SHA-1 哈希的文本文件,因此创建分支的速度非常快、占用存储空间极少。
比如,从main
分支的C3
提交创建一个feature
分支,Git 只会创建一个名为feature
的指针,也指向C3
:切换分支:切换分支时,Git 会将1
2main --> C3
feature --> C3HEAD
指针指向新分支,而HEAD
所指向的内容即为工作目录中的代码版本。
例如,从main
切换到feature
分支后:切换到1
2
3main --> C3
feature --> C3
HEAD --> featurefeature
后,HEAD
指向feature
,接下来的所有操作都将记录在feature
分支上,不影响main
。
提交与分支指针的前移
- 在分支上提交更改:当你在分支上提交新的更改时,Git 会生成一个新的提交对象,并将该分支的指针更新到新提交的哈希值。其他分支的指针不会受到影响。
- 分支的移动:分支实际上是一个移动的指针,每次提交都会使当前分支指针从原来的提交指向新的提交。
假设在feature
分支上进行了一个新提交C4
,Git 会让feature
指针指向C4
,而main
仍然停留在C3
,从而保持分支的独立性:1
2
3main --> C3
feature --> C4
HEAD --> feature
合并分支
开发完成后,通常需要将新分支合并回主分支,Git 会生成一个新的合并提交对象,包含两个父提交对象,确保变更历史完整。合并分支(git merge
)将不同分支的更改整合到一起:
- 快进合并:当目标分支的 HEAD 可以直接指向源分支的最新提交时,Git 会直接移动指针,称为快进合并(Fast-Forward)。
- 三方合并:如果分支出现分叉,Git 会基于两条分支的最新提交及其最近的共同祖先生成一个新的合并提交,称为三方合并(Three-Way Merge)。
比如,将feature
分支合并到main
后,Git 会生成合并提交C5
,连接了C3
和C4
:此时1
2
3
4main --> C5
/ \
C3 C4
feature --> C4main
分支指向C5
,而feature
分支保持在C4
。
分支的删除
删除分支时,Git 只会删除该分支的指针,不会删除任何提交记录,因为所有提交仍然可以通过其 SHA-1 哈希访问到。只有在提交对象没有任何引用指向时,才会通过 Git 的垃圾回收机制(git gc
)最终被清理。
例如删除 feature
分支只是删除指针 feature
,不影响提交 C4
和其他内容。
分支切换的过程
- 更新 HEAD:切换分支时,Git 会更新 HEAD,使其指向新的分支。
- 更新工作区和暂存区:Git 会将工作区和暂存区内容更新为新分支指向的提交对象中的内容。
3. Git 分支的轻量级实现优势
- 高效:分支操作速度快,仅为指针操作,因此创建、切换、删除等操作不会耗费太多资源。
- 节省空间:Git 不会为分支创建完整的代码快照,而是通过哈希引用追踪更改的部分,这极大节省了存储空间。
- 历史记录完整:每次提交都会生成一个唯一的 SHA-1 哈希值,确保了提交内容的完整性和安全性。
分支指令
1. 分支的创建和管理
- 使用命令
git branch <branch-name>
创建新分支。 - 切换到指定分支使用
git checkout <branch-name>
或者结合创建和切换的git checkout -b <branch-name>
。 - 查看所有分支可以使用
git branch
命令。
2. 合并分支
- 开发完成后,可以将分支的更改合并回主分支。使用
git merge <branch-name>
进行合并。 - 合并时可能会产生冲突,需要手动解决冲突后再提交合并结果。
3. 分支策略
- Git Flow:一种常用的分支策略,适用于需要稳定版本的项目。包括:
master
:生产环境代码。develop
:开发分支,用于合并所有特性分支。feature
:用于新功能开发的分支。release
:准备发布的分支。hotfix
:快速修复生产环境问题的分支。
- GitHub Flow:更简单的策略,通常用于持续交付的项目。包括:
- 从
main
创建特性分支进行开发。 - 提交完成后通过 Pull Request 将更改合并回
main
。
- 从
4. 分支的删除
- 使用
git branch -d <branch-name>
删除本地分支。 - 使用
git push origin --delete <branch-name>
删除远程分支。
5. 常见的命令
- 创建新分支:
git branch <branch-name>
- 切换分支:
git checkout <branch-name>
- 合并分支:
git merge <branch-name>
- 删除分支:
git branch -d <branch-name>
- 查看分支:
git branch
- 查看所有远程分支:
git branch -r
Tag
在Git中,tag(标签)用于给特定的提交(commit)打上标记,通常用于标识重要的版本或里程碑。标签和提交类似于“快照”,一旦创建就不会改变,这使它非常适合在代码的历史记录中标记版本,如发布版本v1.0.0、v2.1.3等。
标签的引用会被记录在.git/refs/tags/
目录中。每个标签的引用会对应一个文件,文件内容就是该标签所指向的提交哈希值。
Git中的标签主要分为两种:
- 轻量级标签(Lightweight Tag):这是一个简单的指向特定提交的引用,本质上类似一个不会移动的分支,不包含额外的元数据。
- 注释标签(Annotated Tag):这是更正式的标签类型。它包含标签名、日期、作者、签名(可选)、注释等信息,适合用在需要明确说明版本的场景中。
在Git中,标签(tag)会生成一个唯一的哈希值。这个哈希值由Git生成,用于唯一标识该标签,就像提交(commit)对象一样。
标签的生成机制和Git内部对象存储的方式有关。Git中所有的数据(包括标签、提交、树、文件)都是对象,每个对象都有一个唯一的哈希值,使用了SHA-1算法生成。标签的类型决定了它如何被存储和引用:
Git标签的实现机制
Git标签本质上是一个Git对象,可以在仓库中找到并引用。标签对象的生成和类型不同,生成的哈希值也稍有不同。
两种不同的标签
1. 轻量级标签(Lightweight Tag)
轻量级标签仅仅是一个指向提交对象的“符号引用”,它没有独立的标签对象。它更像是给某个提交“起了个别名”。轻量级标签不会额外存储元数据(如作者、日期、注释等),只是简单地将标签名称指向指定的提交。
其存储方式为简单的指针,不生成独立的标签对象,因此不会创建额外的Git对象或哈希值,仅通过标签名称引用对应的提交哈希值。
2. 注释标签(Annotated Tag)
注释标签是更正式的标签类型,Git会为其生成一个单独的“标签对象”(tag object)。创建注释标签时,Git生成了一个新对象,用于保存以下信息:
- 标签名称
- 标签的作者(tagger)
- 标签的创建时间
- 标签的注释内容
- 标签所指向的提交对象
注释标签的生成过程如下:
- 创建标签对象:Git使用SHA-1算法生成标签的哈希值,并将标签的元信息和所指向的提交信息存储在标签对象中。
- 指向提交对象:标签对象包含指向特定提交的指针,因此这个标签对象与目标提交对象形成一个关联。
- 存储在对象数据库:标签对象连同其生成的哈希值会存储在Git对象数据库中,以确保其不可更改性和唯一性。
注释标签的哈希值可通过git show <tag>
查看。此哈希值唯一对应标签对象,而标签指向的提交对象则会有自己的哈希值。
标签对象的实现结构
标签对象的具体内容结构如下(可以通过git cat-file -p <tag>
查看):
1 | object <commit-hash> # 指向的提交哈希 |
这个标签对象和提交对象是相互独立的,它存储在.git/objects/
目录中,单独占用一个哈希值。
标签哈希值的作用
标签的哈希值可以确保每个标签的唯一性,尤其是在注释标签中,这个哈希值标识了标签对象的内容和元信息,确保了即便同名标签在内容不同情况下仍具有唯一性。这种机制使得Git标签成为项目版本控制中的不可变标记,为项目的发布和历史追溯提供了可靠依据。
标签的创建与管理操作
1. 创建轻量级标签
轻量级标签只需指定标签名和要标记的提交即可,Git默认会将标签创建在当前的HEAD指针所指向的提交上(即当前分支的最新提交):
1 | git tag 标签名 |
若需为指定的提交打标签,可以在命令后加上提交哈希:
1 | git tag 标签名 提交哈希 |
2. 创建注释标签
注释标签带有更多信息,用 -a
参数创建,推荐为正式的发布版本添加注释标签。
1 | git tag -a 标签名 -m "标签注释" |
其中,-m
后的内容是标签的注释。
3. 查看标签
用 git tag
查看当前仓库中所有的标签:
1 | git tag |
可以使用 git show 标签名
查看指定标签的详细信息:
1 | git show 标签名 |
4. 删除标签
删除标签时使用 -d
选项:
1 | git tag -d 标签名 |
5. 推送标签到远程仓库
标签默认不会自动推送到远程仓库,需要手动执行:
1 | git push origin 标签名 |
推送所有标签到远程仓库:
1 | git push origin --tags |
6. 删除远程标签
先删除本地标签,然后将删除操作推送到远程:
1 | git tag -d 标签名 |
总结
Git标签为项目提供了灵活的版本管理方式,适合标记不同阶段的代码。轻量级标签简单快速,而注释标签提供了详细的版本信息,是常用的版本控制工具之一。
后记
本篇就是简单的介绍了一下Git的分支模型的实现原理,其实都是基于前两篇文章提到的不可变数据模型,经过前两篇文章的洗礼,这一篇理解起来应该没什么难度🫠。git分支本质是指针引用,指针的信息记录在.git/ref
中,顺带的介绍了下tag。下一篇我将讲解Git的如何实现分布式合作了。