深入理解Git:从原理到实践的全面指南

news/2025/1/3 14:05:58/文章来源:https://www.cnblogs.com/potatso/p/18644042

Hello World

深入理解Git:从原理到实践的全面指南

本文深入剖析Git的核心概念和底层实现原理,包括对象模型、分支管理、合并策略等关键特性。通过理解Git的数据结构和工作机制,帮助开发者更好地处理版本控制中的各种复杂场景,从基础使用者进阶为Git专家。

在日常开发中,很多开发者在使用Git时往往只是停留在基本操作层面,特别是在遇到复杂的合并冲突时,常常感到无所适从。要想真正掌握Git,成为Git专家,关键在于深入理解其底层原理。本文将帮助你:

  • 深入理解Git的核心概念和工作原理
  • 学会分析和解决各类合并冲突
  • 掌握Git的高级特性和最佳实践

通过本文的学习,你将能够在日常工作中更加自如地使用Git,处理各种复杂场景。

1. 基础概念 git的三种基本对象

1.1 Blob

Blob对象是Git中最基础的存储单元。它的工作原理是:

  1. 将文件内容进行SHA-1哈希计算,得到一个唯一的哈希值作为文件名
  2. 使用zlib压缩算法压缩文件内容,生成blob对象

这种设计的巧妙之处在于利用了哈希算法的"雪崩效应":即使文件内容只改变一个字符,产生的哈希值也会完全不同。这样,Git只需要比较文件的哈希值,就能立即知道文件是否被修改,而不需要逐字节地比较文件内容,大大提高了效率。

1.2 Tree

Tree对象相当于Git中的目录结构,用于组织和管理文件。它的工作原理是:

  1. 记录目录下所有内容的信息:
    • 文件对应的blob对象
    • 子目录对应的tree对象
  2. 为每个目录生成一个唯一的哈希值,这样任何目录结构的变化都能被追踪

这种层级结构让Git能够高效地管理从单个文件到整个项目的所有内容变化。

1.3 Commit

保存每次提交的信息。

Git Structure

1.4 分支

分支其实就是一个指向commit的指针(文件内容是commit的hash值)。当你提交新的更改时,这个指针会自动指向新的commit。这就是为什么我们能轻松地在不同版本之间切换:本质上就是在移动这个指针。

Git分支原理

2. 完整的一次提交过程

在这里我们模拟解释一次git add/ commit的过程,来了解git的工作原理。

2.1 git add 文件

假设我们修改了readme.md文件,执行git add时会发生以下操作:

  1. Git读取readme.md的内容
  2. 将内容用zlib算法压缩,创建一个blob对象
  3. 用SHA1算法计算这个blob对象的哈希值
  4. 用这个哈希值作为文件名,将blob对象保存到Git仓库中

这样,每个文件版本都会有一个唯一的标识(哈希值),Git可以精确地追踪文件的每次变化。

2.2 git commit

执行git commit时,Git会按以下步骤创建新的提交:

  1. 创建新的Tree对象:

    • 读取当前仓库的Tree对象(保存了所有文件的目录结构)
    • git add创建的新Blob对象更新到Tree中
    • 为这个新的目录结构创建一个新的Tree对象
  2. 创建新的Commit对象:

    • 记录新Tree对象的哈希值
    • 记录父Commit的哈希值
    • 保存提交信息(commit message)
    • 保存提交者信息和时间戳
  3. 更新分支指针:

    • 将当前分支指向这个新的Commit对象

这样就完成了一次完整的提交,保存了项目的一个新版本。
Git提交过程

相当于,我们每次提交,都会完整的把项目中每个文件通过zlib压缩,完整地保存下来。而对于没有修改的文件,git会通过类似于快捷方式,或者软链接等方式,直接指向上一次提交的版本。

Commit对象包含以下重要信息:

  1. tree:指向项目根目录的Tree对象
  2. parent:指向一个或多个父Commit
    • 常规提交只有一个父Commit
    • 合并提交会有多个父Commit,记录了所有被合并的分支
  3. author/committer:提交者信息
  4. message:提交信息

这种设计让Git能够完整记录项目的历史,并支持复杂的分支合并操作。

在git commit时,会获取git add创建的blob对象。为了优化设计,git在某处地方存放了一个列表,里面写满了

3. 常见概念以及命令解析

3.1 暂存区(Stage/Index)

暂存区是Git中的一个重要概念,它是一个临时的Tree对象,默认与仓库的HEAD指针指向的Tree对象相同,内容也相同。如果有文件提交,那么临时Tree对象只修改对应的blob,指向新的blob。现在不理解不要紧,下面在使用git底层命令模拟提交的时候会通过例子去详细解释:

  1. 当执行git add时:

    • 创建新的Blob对象
    • 更新暂存区的Tree对象,指向这些新的Blob
  2. 默认情况下:

    • 暂存区的Tree对象与最近一次提交的Tree对象结构相同
    • 只有执行git add后,相应的文件引用才会更新
  3. 特殊操作:

    • 执行git reset --soft时,只移动HEAD指针,暂存区保持不变

3.2 使用git底层命令模拟commit

既然git add是创建新的Blob对象,那么我们理论上也可以用git提供的底层命令去完成这个操作。

MS-7B23:~/repo$ git hash-object -w -t blob hello.txt
ce013625030ba8dba906f756967f9e9ca394464a

在这里,我们可以看到git hash-object命令的输出,它生成了一个新的Blob对象,并且返回了这个对象的哈希值。我们直接使用zlib命令,在.git文件夹中找到这个blob文件并解压。

zlib-flate -uncompress < .git/objects/ce/013625030ba8dba906f756967f9e9c
a394464a 
blob 6hello

接下来,我们需要将这个新创建的blob对象添加到暂存区的tree对象中。这个过程包含两步:

  1. 获取当前的tree对象并创建副本
    注意

    1. 如果你此时仓库为空,例如刚初始化,git中没有任何东西。那你并不需要获取当前的tree对象。跳到第二步即可
    2. 但是如果你此时仓库中已经被初始化,那就需要获取当前的tree对象。需要使用git read-tree HEAD # 读取当前HEAD的树到暂存区
  2. 在新的tree对象中更新hello.txt的引用,指向我们刚创建的blob对象

git update-index --add --cacheinfo 100644 ce013625030ba8dba906f756967f9e9ca394464a hello.txt

然后再把这个新的tree也保存到文件,我们需要执行git write-tree,这个命令会返回这个tree的哈希值。

git write-tree
aaa96ced2d9a1c8e72c56b253a0e2fe78393feb7

到这里,我们已经创建了文件的blob对象和目录结构的tree对象。最后一步是创建commit对象,它将包含:

  • 这个tree对象的引用
  • 提交信息
  • 提交者信息
  • 时间戳
    当然默认git会读取git config中的user.name和user.email,但是也可以在git commit中自己指定

这个临时的树,就是git中大名鼎鼎的暂存区。你可以使用git ls-tree $tree_sha1来查看它的内容(ls-tree可以查看任意tree对象的内容),也可以使用git diff $tree_sha1来查看它的变化。

我们现在提交这个树到仓库,并包含commit信息。

echo "Project structure" |  git commit-tree aaa96ced2d9a1c8e72c56b253a0e2fe78393feb7
f2d4b8622737f02c226f16594f32c423b2c39c5c

我们直接解压这个commit对象,可以看到它的内容。

zlib-flate -uncompress < .git/objects/f2/d4b8622737f02c226f16594f32c423b2c39c5c 
commit 212tree aaa96ced2d9a1c8e72c56b253a0e2fe78393feb7
author lll <abc@mail.com> 1735620034 +0800
committer lll <abc@mail.com> 1735620034 +0800Project structure

到这里还不算完,还需要更新HEAD指针,才算一次真正的提交

git update-ref refs/heads/master f2d4b8622737f02c226f16594f32c423b2c39c5c

这样,我们就完成了一次完整的提交,保存了项目的一个新版本。使用git log命令,可以查看本次提交的记录

git log
commit f2d4b8622737f02c226f16594f32c423b2c39c5c (HEAD -> master)
Author: liang_zhibang <liang_zhibang@venusgroup.com.cn>
Date:   Tue Dec 31 12:40:34 2024 +0800Project structure

完整图例

Git底层命令提交流程

3.3 git soft三种模式的区分

通过前面的学习,我们已经了解到HEAD和暂存区实际上都是指向特定Git对象的指针。默认暂存区指针与HEAD指针指向同一Tree对象。
现在,让我们通过一个具体的例子来深入理解git reset的三种模式(--soft、--mixed、--hard)的区别。

假设我们有一个项目,已经进行了三次提交,我们将以这个场景为例:
Git Structure

3.3.1 git reset --soft

如果我们执行git reset --soft HEAD^1, 那么Git会做以下操作:
更新HEAD指针,指向上一次提交的commit对象, 暂存区指针不动,依旧指向本次commit提交的tree对象

Git Reset Soft模式

这种状态下,如果我们执行新的提交,会产生一个有趣的效果:

  1. 暂存区保留了最新的文件状态(指向最新的Tree对象)
  2. HEAD指向了上一个commit
  3. 当我们提交时,会用暂存区的内容创建一个新的commit

这就是为什么它被称为"soft"(软)重置:

  • 只移动HEAD指针,不改变任何文件内容
  • 保留了暂存区的所有改动
  • 常用场景:
    1. 修改最近一次的提交信息
    2. 将多个提交合并为一个提交
    3. 在不丢失任何改动的情况下重新组织提交

我们可以使用Git底层命令来模拟git reset --soft的效果。首先使用git rev-parse HEAD获取当前commit的引用,然后通过git rev-parse HEAD^1获取父commit的hash值,最后使用git update-ref refs/heads/master HEAD^1来更新分支指针。这样就实现了只移动HEAD指针而保持暂存区不变的效果。

3.3.2 git reset --mixed

如果我们执行git reset --mixed HEAD^1, 那么Git会做以下操作:
更新HEAD指针,指向上一次提交的commit对象, 暂存区同时也更新,指向上一次提交的tree对象。但是工作区不动。工作区就是你当前在磁盘系统上的项目文件。

Git Reset Mixed模式

这种模式的一个典型应用场景是:当你不小心提交了一些不应该提交的文件,或者不该提交的代码,但又不想丢失工作区的修改时。通过git reset --mixed,你可以重置暂存区到上一个状态,重新选择要提交的文件,同时保留工作区的所有改动。这就是为什么它被称为"mixed"(混合)重置 —— 它在保留工作区的同时,让你能重新组织暂存区的内容。

通过上面的描述,我们可以使用git read-tree 命令来模拟git reset --soft的效果。核心要点在于修改HEAD和暂存区指针,但不改变工作区的内容。

3.3.3 git reset --hard

如果我们执行git reset --hard HEAD^1, 那么Git会做以下操作:
更新HEAD指针,指向上一次提交的commit对象, 暂存区同时也更新,指向上一次提交的tree对象,同时把Tree对象中的文件,解压覆盖工作区。
Git Reset Hard模式

3.4 git 怎么从暂存区复制文件到工作区

要从暂存区复制文件到工作区,需要用到两个底层命令:

3.4.1 git read-tree

这个命令用于读取和更新暂存区的状态。它是git checkoutgit restore等命令的基础,主要功能是:

  • 读取Git对象库中的tree对象,更新暂存区的状态
  • 在合并时,可以将多个tree对象的内容合并到暂存区

3.4.2 git checkout-index

这个命令用于从暂存区复制文件到工作区。它的工作过程是:

  1. 读取暂存区中的文件信息(包含blob对象的引用)
  2. 从对象库中读取对应的blob对象
  3. 将blob对象的内容解压到工作区

Git检出文件过程

如图所示,当我们需要从暂存区恢复一个文件到工作区时,Git会先通过read-tree命令读取暂存区的信息,找到文件对应的blob对象,然后使用checkout-index命令将blob对象的内容解压到工作区。这就是git checkout -- <file>git restore <file>命令背后的工作原理。

3.5 合并冲突与快速合并

在Git中,一个commit可以有多个父commit,这个特性正是分支合并的基础。当我们合并分支时,本质上是将两个不同分支上的树对象(tree objects)合并成一个新的树对象,并创建一个新的commit来指向这个合并后的树对象。

3.5.1快速合并(Fast-forward)

当一个分支的所有提交都是另一个分支的直接后继时,Git可以执行快速合并。这意味着两个分支的文件树之间没有任何冲突,因为一个分支的历史完全包含了另一个分支的历史。在这种情况下,Git只需要将当前分支的指针"快进"到目标分支的位置即可,无需创建新的合并提交。

Git快速合并

3.5.2 合并冲突

当两个分支同时修改了同一个文件的同一个位置时,Git就无法自动决定应该使用哪个版本的内容,这就产生了合并冲突。与快速合并不同,这种情况下Git需要我们手动介入来解决冲突。

Git合并冲突

比如上图所示的情况:

  • 基础版本的文件内容是"Hello World"
  • master分支将其修改为"Hello Git World"
  • feature分支将其修改为"Hello World"并添加了新行"Welcome!"
  • 当我们尝试合并这两个分支时,Git会在文件中标记出冲突的位置:
    • <<<<<<< HEAD 标记当前分支(master)的修改内容
    • ======= 分隔两个分支的修改
    • >>>>>>> feature 标记要合并的分支(feature)的修改内容

要解决冲突,我们需要:

  1. 打开发生冲突的文件
  2. 找到并分析冲突内容
  3. 决定最终要保留的内容
  4. 删除所有的冲突标记(<<<<<<< HEAD,=======,>>>>>>> feature)
  5. 保存文件
  6. 使用git add标记冲突已解决
  7. 使用git commit完成合并

这样就完成了一次冲突解决,新的commit将会同时包含两个分支的历史。

Git也提供了一些自动解决冲突的策略。通过在git merge命令中使用-X参数,我们可以指定合并策略选项:

  • git merge -X ours 在冲突时优先使用当前分支(我们的)的修改
  • git merge -X theirs 在冲突时优先使用要合并分支(他们的)的修改

这些策略可以帮助我们在某些场景下自动化解决冲突,但要谨慎使用,因为它们可能会忽略掉重要的改动。建议只在确定优先级的情况下使用这些选项。

3.6 撤销commit

有时我们可能需要撤销历史中的某个特定提交,比如要撤销第三个commit。但是git reset命令有一个局限性:它只能从最新的提交开始,连续地撤销commit。也就是说,如果要撤销第三个提交,就必须先撤销第五个和第四个提交,这显然不是我们想要的结果。这种情况下,我们需要使用其他的Git命令来实现目标。

Git的核心设计理念之一是保持完整的历史记录。因此,撤销一个提交并不是简单地删除它,而是通过创建一个新的提交来"反转"之前的修改。这样做可以确保所有的变更都有迹可循,同时也让其他协作者能够清楚地看到这些变更的来龙去脉。这就是为什么Git提供了git revert这样的命令,而不是直接删除提交记录。

我们同样可以使用Git提供的底层命令来模拟git revert

  1. 使用git rev-parse HEAD~2获取要撤销的commit的引用(假设是倒数第三个提交)
  2. 使用git rev-parse HEAD~3获取该commit的父commit引用
  3. 使用git diff HEAD~3 HEAD~2命令获取这两个commit之间的差异,并生成一个patch文件:git diff HEAD~3 HEAD~2 > changes.patch
  4. 使用git apply --reverse changes.patch命令反向应用这个patch,相当于撤销这个commit的修改
  5. 最后使用git add .git commit -m "Revert commit xxx"来提交这个撤销操作

通过--reverse参数,我们告诉Git要反向应用这个patch,也就是撤销而不是应用这些修改。这样就实现了撤销特定commit的效果,同时保留了完整的操作记录。

3.6.1 处理合并提交的撤销

理解了撤销commit的原理后,我们来看一个特殊情况:合并提交(merge commit)的撤销。合并提交的特殊之处在于它有多个父commit。这时,如果直接使用git revert,Git会遇到一个问题:它不知道应该基于哪个父commit来生成反向修改。

例如,当我们撤销一个合并提交时,我们需要明确告诉Git:

  1. 是要撤销合并本身
  2. 还是要保留其中一个父分支的修改

这就是为什么在处理合并提交时,我们需要使用git revert -m <parent-number>命令,其中parent-number指定要保留哪个父分支的修改(1表示第一个父分支,2表示第二个父分支)。

Git合并提交撤销

如图所示,当我们需要撤销一个合并提交时,必须选择要保留哪个父分支的修改。这是因为合并提交包含了来自两个分支的改动,Git需要知道我们想要撤销哪些改动。

3.7 git rebase

git rebase是一个强大的命令,它可以帮助我们重新组织提交历史。它的工作原理是通过提取修改并以patch形式重放,这使得它能够:

3.7.1 合并分支

merge不同,rebase通过改变提交的基础(base)来合并分支。从底层实现来看,rebase的过程是这样的:

  1. 找到两个分支的共同祖先
  2. 对比当前分支相对于祖先的历次提交,提取每次提交的差异(patch)
  3. 将当前分支指向目标分支的最新提交
  4. 按照提交的顺序,将之前保存的patch依次应用到当前分支

这种方式相当于把我们的修改在目标分支上重新"重放"一遍,使提交历史呈现出一条直线,更加清晰易读。虽然最终的代码改动是一样的,但是提交历史会被重写。

Git Rebase Patch过程

如图所示,rebase的核心是通过生成patch文件来保存修改,然后将这些修改重新应用到新的基础上。patch文件记录了具体的改动内容(增加、删除或修改的行),这就是为什么即使在新的位置重放,也能保持相同的改动效果。

3.7.2 修改提交历史

正是因为rebase是基于patch的重放机制,而不是直接修改Git对象,所以我们可以在重放过程中对这些patch进行编辑。通过交互模式(git rebase -i),我们可以在重放之前对提交进行多种操作:

  • 重新排序提交
  • 合并多个提交
  • 修改提交信息
  • 删除某些提交
  • 将一个提交拆分成多个

这种能力使得我们可以在代码推送到远程仓库之前,整理和优化我们的提交历史,使其更加清晰和有意义。但需要注意的是,一旦提交已经推送到公共仓库,就应该避免使用rebase来修改历史,因为这会给其他开发者带来麻烦。

Git Rebase Squash Patch过程

如图所示,当我们使用squash命令合并提交时,Git实际上是将每个提交的改动提取为patch文件,然后将这些patch合并成一个新的patch,最后将合并后的patch应用到目标位置。这就是为什么我们能够保持改动的完整性,同时简化提交历史。

通过本文的学习,你已经深入了解了Git的核心概念和底层实现原理。这些知识将帮助你:

  1. 理解Git的数据结构和存储模型

    • Blob、Tree和Commit对象的作用和关系
    • 分支和指针的工作机制
  2. 掌握复杂操作的原理

    • 分支合并和冲突解决的底层逻辑
    • Rebase和Reset等高级操作的工作方式
  3. 培养问题解决思维

    • 学会从底层原理分析问题
    • 能够处理更复杂的版本控制场景

记住,理解Git的核心概念和数据结构比记住具体命令更重要。这些知识将帮助你以更系统的方式思考和解决版本控制中的各种挑战。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/861925.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

墨天轮国产数据库排行榜年终总结-2024年

本文对2024年墨天轮中国数据库流行度排行榜进行了年终盘点,包含多个维度的详细分析整理,欢迎大家阅读交流。图片说明:按照墨天轮中国数据库流行度排行榜分数比例生成 前言: 岁月不居,时节如流。岁末年终,忽焉已至。墨天轮平台已于2024年12月1日公布了中国数据库流行度排行榜…

CISSP备考经验分享2024年12月

https://www.cnblogs.com/iAmSoScArEd/p/18644021 我超怕的 CISSP备考经验分享2024年12月 考试过了,以下是我的备考经历,不一定适合所有人,所以请按照自己的习惯备考。 考试感受 考试形式:CAT模式、中文(感兴趣可以先了解下考试模式:https://www.isc2china.org/cissp-cat…

Gradle下载Plugins插件连接超时 failed: Connection timed out: connect的解决方法

可以去gradle官方plugin仓库看看插件是否存在。 出现超时的问题多半都是网络因素,可能是复杂的网络导致你访问不了吧,配置下plugins可访问使用的仓库即可。 需要注意buildscript在plugins之上的位置。 buildscript {repositories {mavenLocal()maven { url https://maven.ali…

定义通用返回包装类

定义通用返回包装类 包装返回正确的数据格式,返回类型示例 {"code":200,"message":"success","data":[] }通用包装类 import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;/*** @author zhangsh…

dolphinscheduler相关

任务状态和事件驱动时序图

蓝牙设备过多导致Win11出现不定时死机现象

10月份之后发现自己的ROG FLOW X16笔记本开始不定时死机了。具体表现是正常使用过程突然死机,各种地方点不动,不能注销,不能重启,不能关机,我的电脑打不开,任务管理器打开是一片空白。仅有一些窗口最大化和最小化之类的功能可以点击。并且没有任何表征,就是突然给你出现…

绝对不能错过的:Air201看门狗定时器!

看门狗技术(Watchdog Timer)是嵌入式系统中一种重要的稳定性增强技术,用于监测和恢复系统的正常运行状态。当系统因程序跑飞、死机或受到电磁干扰而无法正常工作时,看门狗能够及时检测并采取相应措施,从而避免系统长时间处于故障状态。 本文将带你了解看门狗的工作原理、应…

关于Chat2DB的吐槽

最近心血来潮准备支援原子一波、看着多出来一个选项联合会员chat2db、于是纳闷chat2db是个啥东西于是下载下来试用了一下,怎么说呢、不好评价 ​ SQL优化功能:一股浓浓的AI味,跟你直接问ChatGpt差不多。 ​ sql提示也没想象的好、自然语言转sql更是难用、不如直接自己写 ​ …

[转] 认知负荷才是关键

Title: cognitive-load/README.zh-cn.md at main zakirullin/cognitive-load URL Source: https://github.com/zakirullin/cognitive-load/blob/main/README.zh-cn.md简介(Introduction)这世上有如此多的“流行语”和“最佳实践”,但是让我们把注意力转向更基础的方面。即——…

7、RabbitMQ队列之远程调用(RPC)【RabbitMQ官方教程】

在第二个教程中,我们学习了如何使用工作队列在多个工作人员之间分配耗时的任务。 但是,如果我们需要在远程计算机上运行一个函数并等待结果呢?好吧,那是另一回事。这种模式通常被称为远程过程调用或RPC。 在本教程中,我们将使用RabbitMQ构建一个RPC系统:一个客户端和一个…

Dictionary 添加重复的键值对

Dictionary 添加重复的键值对| Id | Title | DateAdded | SourceUrl | PostType | Body | BlogId | Description | DateUpdated | IsMarkdown | EntryName | CreatedTime | IsActive | AutoDesc | AccessPermission | | -------------| -------------| -------------| --------…