7.从底层讲清楚Git仓库

从最底层开始,把 Git 讲明白:以 Gitee 为例看懂 blob、tree、commit、分支与远程仓库

刚用 Gitee 时,很多人会被这一串词劝退:

本地仓库、远程仓库、本地分支、远程分支、.git、commit、blob、tree、哈希、暂存区、HEAD......

它们看起来互不相关,其实是一套由小到大、层层包裹的结构。本文就用一条"自底向上"的主线,把它们一次性捋顺。

先给你一张"地图",读的时候可以随时回来看自己到了哪一层:

text 复制代码
第 1 层  哈希值       → 给任意内容发"身份证号"
第 2 层  blob         → 存"一个文件的内容"
第 3 层  tree         → 存"目录里有哪些文件/子目录"
第 4 层  commit       → 存"某一刻整个项目的快照"
─────────────────────────────────────
第 5 层  .git 目录     → 上面这些对象都装在这里
第 6 层  分支          → 一个指向最新 commit 的"书签"
第 7 层  HEAD         → 标记"我现在站在哪个分支"
第 8 层  暂存区 index  → 提交前挑选"哪些改动要进下一版"
─────────────────────────────────────
第 9 层  本地仓库      → 你电脑里的 .git
第 10 层 远程仓库      → Gitee 上的另一份 .git
第 11 层 本地/远程分支 → 两份数据库各自的分支记录
第 12 层 clone/fetch/pull/push/merge → 在两份库之间搬运

第 1 层:哈希值 ------ Git 世界的"身份证号"

Git 给每一个对象命名的方式,不是"文件名",而是"内容的哈希值"。

所谓哈希,就是一套计算规则(哈希函数)把任意内容变成一串固定长度的字符(哈希值):

text 复制代码
原始内容
   ↓  哈希函数(Git 传统用 SHA-1)
哈希值(40 位十六进制,如 a1b2c3...)

记住三个词的差别:

  • 哈希函数:计算规则,例如 SHA-1;
  • 哈希值 :计算结果,例如 a1b2c3d4...
  • 哈希:口语简称,有时指函数,有时指结果。

Git 的关键特性是内容寻址 :内容完全相同,得到的哈希值通常也相同;内容哪怕只改一个标点,哈希值通常就变成完全不同的一串。所以 Git 能用哈希值唯一定位一个对象------这也正是后面 blob、tree、commit 都被哈希值"串"起来的原因。


第 2 层:Blob ------ Git 怎么存"一个文件的内容"

一句话理解:blob 存的是"文件的内容本身",哈希值只是这个内容的"门牌号"。

Git 处理 README.md(内容 Hello Git)时,实际做了两件事:

  1. 把"内容 + 一个很小的类型/长度头"压缩后存进对象库,这就是 blob 对象------它装的是内容,不是哈希;
  2. 用 SHA-1 对"blob 头 + 内容"算出哈希值,把这个哈希当作"文件名",把 blob 存放在 .git/objects/<前2位>/<后38位> 里。

所以准确地说:blob 体内 = 文件内容;哈希值 = 这个 blob 在对象库里的地址/身份证号,并不是 blob 里面装的东西。

两点特性:

  1. blob 只认内容,不认名字和路径。 文件名、目录位置它一概不管。
  2. 内容相同就复用。 比如 a.txtb.txt 内容都是 Hello,它们会指向同一个 blob,Git 不会存两份。

第 3 层:Tree ------ Git 怎么存"目录长什么样"

blob 只知道"文件内容",却不知道"这个文件叫什么、在哪个文件夹"。于是 Git 用 tree 来记录目录结构。

还是那个项目:

text 复制代码
shop/
  README.md
  src/
    app.js

Git 内部可以理解成:

text 复制代码
根 tree
  ├── README.md  →  README 的 blob
  └── src        →  src 目录的 tree
                    └── app.js → app.js 的 blob

所以一句话区分:

  • blob:文件"是什么内容";
  • tree:目录"有哪些文件和子目录,各自叫什么、指向哪个对象"。

tree 里记录的每一条,都包含:文件名、文件类型/权限、以及对应对象的哈希值。目录套目录,就是 tree 套 tree。


第 4 层:Commit ------ 一张"项目全景照片"

一句话理解:commit 是"某一刻整个被跟踪项目长什么样"的存档,它在逻辑上就是一张完整快照。

为什么叫"快照"而不是"改动记录"?因为 commit 指向的是那一刻的根 tree ,根 tree 又递归指向所有文件和子目录。无论文件改没改,顺着这棵 tree 都能还原出"全部文件在那一瞬间的内容"。所以一个 commit 本身就代表了项目当时的完整面貌------这就是"快照"的含义。

它不会把每个文件内容再抄一遍,而是指向项目的根 tree

text 复制代码
commit
  ↓
根 tree
  ├── README.md → blob
  └── src → tree
              ├── app.js  → blob
              └── login.js → blob

只要 Git 拿到这个 commit,就能顺着 tree 一层层找到所有文件,再从 blob 读出内容。所以:

commit 通过 tree + blob,间接表示"完整项目快照"。

一个 commit 对象里还保存了这些元信息:

text 复制代码
根 tree 的哈希
父 commit 的哈希      ← 指向上一个版本,串成历史
作者、提交者
提交时间
提交说明

执行 git commit -m "增加登录功能" 时,Git 会:算出新的 tree(第 4 层拼好的快照)→ 生成一个新 commit(带上父提交)→ 让当前分支指向它。

⚠️ 小坑提醒:commit 的哈希不是只由提交说明决定,而是由整个 commit 对象(tree、父提交、作者、时间、说明)一起算出来的。

快照 ≠ 复制整个项目

常说 Git"保存快照",容易让人以为每次提交都重抄一遍全部文件。其实不是:

text 复制代码
第 1 次提交:README→blob B1,app.js→blob B2
第 2 次提交:只改了 app.js
            README→blob B1(继续复用)
            app.js→blob B3(新内容)

逻辑上每次 commit 都是完整快照;物理上没改的文件复用旧 blob,不重复占空间。这正是 Git 省空间又高效的秘密。


第 5 层:这些对象都放在哪?------ .git 目录

前面四层的 blob、tree、commit,全都被收进一个隐藏目录:.git

bash 复制代码
git init

会在项目里生成:

text 复制代码
shop/
  .git/        ← Git 的核心数据库
  README.md
  src/app.js

.git 不是普通配置,而是一整个数据库,关键成员有:

text 复制代码
.git/
  objects/   存所有 blob / tree / commit(按哈希值命名)
  refs/      存"分支、标签"这些指针指向哪个 commit
  HEAD       记录"当前正在用哪个分支"
  index      暂存区(见第 8 层)
  config     仓库配置,比如 Gitee 地址

一句话:删掉 .git,项目文件还在,但所有历史、分支、提交记录全没了。 所以 .git 才是 Git 仓库本身,你看到的那些文件只是"某个版本的快照展开"。


第 6 层:分支 ------ 一个"会移动的书签"

一句话理解:分支的本质就是一个很小的文本文件,里面只写着一行------它所指向的那个 commit 的哈希值。

.git/refs/heads/ 目录下,每个分支对应一个同名文件:

text 复制代码
.git/refs/heads/main           → 文件内容是一行 40 位哈希,如 a1b2c3...
.git/refs/heads/feature/login  → 文件内容是另一个 commit 的哈希

所以"分支"并不是把项目复制一份,它只是:一个文件名(分支名)+ 文件内容(某个 commit 的哈希)

你每提交一次,Git 就把这个文件里的哈希"改写"成新 commit 的哈希------这就是分支"往前移动"的真相。多个分支能共享历史,也正因多个这样的文件可能指向同一条 commit 链上的不同节点。

常见的提交关系长这样:

text 复制代码
A --- B --- C        main
       \
        D --- E      feature/login
  • main 指向 C、feature/login 指向 E;
  • 两条分支共享 A、B 的历史
  • 之后各自独立往前提交。

所以"分支"的本质超级轻量------它只是一个指向 commit 的指针文件。


第 7 层:HEAD ------ 你现在"站在"哪个分支

一句话理解:HEAD 是"当前正在使用的分支"这个标记的标记。

最常见的关系:

text 复制代码
HEAD → main → commit C

当你执行:

bash 复制代码
git switch main

Git 就让 HEAD → main,然后依据 main 指向的 commit C,把 C 对应的快照(tree+blob)展开到你的工作区。于是你文件夹里看到的,就是 C 那版的样子。

切换分支的本质,就是把工作区文件换成另一个 commit 的快照

text 复制代码
当前分支 → 它指向的最新 commit → commit 对应的 tree/blob → 工作区文件

第 8 层:暂存区 index ------ 提交前的"购物清单"

一句话理解:暂存区是"下一次提交准备包含哪些文件版本"的候选清单。

text 复制代码
工作区改动
   ↓  git add
暂存区 index
   ↓  git commit
新的 commit

比如你改了三个文件,但只想提交前两个:

bash 复制代码
git add app.js login.js
git commit -m "完成登录逻辑"

那么 README.md 的改动仍留在工作区,不会进入这次 commit。git add 决定"买什么",git commit 才"结账"。

(对应第 5 层:暂存区就是 .git/index 这个文件。)


第 9 层:本地仓库 vs 远程仓库 ------ 两份一样的数据库

本地仓库

就是你电脑项目里的 .git。你可以完全离线地:

bash 复制代码
git add .
git commit -m "完成登录功能"

提交只存在于你这台电脑。

远程仓库

远程仓库是另一份独立的 Git 数据库,通常在 Gitee 服务器上,例如:

text 复制代码
https://gitee.com/your-name/shop.git

把它关联到本地:

bash 复制代码
git remote add origin <Gitee仓库地址>

这里 origin 只是这个远程地址的本地别名remote add 只是在你本地记下这个地址,既不上传也不下载代码------真正搬运要靠后面的 push/fetch。

一句话:本地仓库 = 你电脑里的 .git;远程仓库 = Gitee 上另一份 .git。两者结构完全一样,只是位置不同。


第 10 层:本地分支、远程分支、远程推送分支

要分清三样东西:

text 复制代码
① 本地分支(如 main)        → 在你电脑 .git/refs/heads/ 里的文本文件,指向本地最新 commit
② 远程分支(如 Gitee 的 main)→ 真正存在于 Gitee 服务器上的分支,想改它得 push
③ 远程推送分支(即 origin/main)→ 你电脑里的一条"记录",记着"上次从 origin 同步时,远程 main 指向哪个 commit"

关键区分:

  • 本地分支 :你 git switch / git commit 操作的,在本地;
  • 远程分支 :在 Gitee 上,得靠 git push 才能改;
  • 远程推送分支(origin/main)它不在 Gitee,而在你本地 。它是本地 .git 里的一个引用(命名带上了远程名 origin/),本质是"我上次听说远程 main 在哪"的缓存记录。

执行:

bash 复制代码
git fetch origin

Git 会下载远程的新提交并刷新这个远程推送分支,但不会动你的工作区文件 。所以 origin/main 是一个本地缓存的远程位置记录,不是远程本身。

记住这条铁律:fetch 只更新 origin/main 这种"本地记录",不会动你的工作区文件。


第 11 层:五种核心操作的底层本质

把前 10 层串起来,再看这些命令就通透了:

命令 底层在做什么
git clone <地址> 建本地仓库 → 记远程为 origin → 下载全部对象 → 建默认分支 → 检出文件
git fetch origin 下载远程新 commit,更新本地的 origin/main 等记录,不动工作区
git pull 通常 = fetch + merge:先更新记录,再把远端更新合并进当前分支
git push 把本地新 commit 上传到关联的 Gitee 分支,并更新真正的远程分支
git merge 分支X 分支X 指向的提交历史,合进"当前所在分支",生成新的项目快照

第 12 层:一条 Gitee 团队协作完整流程

把上面全都用上,一个典型协作长这样:

bash 复制代码
git clone <仓库地址>          # 第 9/11 层:拉下整份远程库
cd <项目目录>

git switch main
git pull                      # 同步远端最新

git switch -c feature/payment # 开一条本地新分支(第 6 层)

# 修改项目文件......
git add .                     # 第 8 层:挑进暂存区
git commit -m "增加支付功能"   # 第 4/6 层:生成 commit,分支指针前移

git push -u origin feature/payment  # 第 9/11 层:推到 Gitee

git switch main
git pull
git merge feature/payment     # 合回主线
git push

真实团队里,最后一步合并通常通过 Gitee 的"合并请求(Pull Request / MR)" 完成,方便做代码审查和自动测试,而不是直接在本地 merge 完就推。


结语:一句话记忆链

把整篇文章压成一条链,就这六句:

text 复制代码
文件内容  →  blob
目录结构  →  tree
项目快照  →  commit
最新指针  →  分支(本质是存着 commit 哈希的文本文件)
当前入口  →  HEAD
提交候选  →  暂存区 index

再补上协作视角:

text 复制代码
本地仓库  = 你电脑里的 .git(blob/tree/commit 都在这)
远程仓库  = Gitee 上的另一份 .git
本地分支  = 你本地的开发路线(.git/refs/heads/ 下的文本文件)
远程分支  = Gitee 上的开发路线
远程推送分支 = 你本地"记得的"远程分支位置(origin/main)

关键认知:Git 用 blob 存内容、tree 存结构、commit 存快照;分支只是贴在最新 commit 上的、存着哈希值的小文本文件;本地和远程是两份同构的数据库,靠 push/fetch 同步。理解了这一层,你会发现 Git 里再多的名词,都能挂回这条链上。

相关推荐
神奇小汤圆2 小时前
解放双手!SpringBoot 6种自动填充公共字的手段,这些代码真没必要手写!
后端
鱼人2 小时前
多态底层原理:向上转型、动态绑定,一文彻底搞懂
后端
神奇小汤圆2 小时前
吃透Redis缓存核心:淘汰策略、LRU/LFU区别与四大缓存问题全解
后端
无责任此方_修行中2 小时前
搓了一个国产大模型与 AI Agent 比价工具
前端·后端·ai编程
yunyi3 小时前
改个 SSH 端口,我把自己锁在了服务器外面
运维·后端
元界metalite3 小时前
MyBatis多数据源常见难题-事务开始了为何数据源还没选出来
后端·mybatis
orient3 小时前
ForkJoin 框架源码深度解析:工作窃取 + RecursiveTask 实战
后端
karry_k4 小时前
Skill 到底是什么?从提示词、脚本到多智能体工作流的完整指南
java·人工智能·后端