从最底层开始,把 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)时,实际做了两件事:
- 把"内容 + 一个很小的类型/长度头"压缩后存进对象库,这就是 blob 对象------它装的是内容,不是哈希;
- 用 SHA-1 对"blob 头 + 内容"算出哈希值,把这个哈希当作"文件名",把 blob 存放在
.git/objects/<前2位>/<后38位>里。
所以准确地说:blob 体内 = 文件内容;哈希值 = 这个 blob 在对象库里的地址/身份证号,并不是 blob 里面装的东西。
两点特性:
- blob 只认内容,不认名字和路径。 文件名、目录位置它一概不管。
- 内容相同就复用。 比如
a.txt和b.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 里再多的名词,都能挂回这条链上。