Git 原理与非文本文件管理学习笔记
适合已经了解 Git 基本操作,希望进一步建立完整原理模型,并学习如何管理图片、音频、CAD、模型、数据集等非文本文件的读者。
目录
- 1. Git 的核心定义
- 2. Git 解决的根本问题
- 3. Git 的四类核心对象
- 4. 内容寻址与对象不可变
- 5. 提交图、分支与引用
- 6. HEAD、暂存区与工作区
- 7. 常用命令在底层做了什么
- 8. Merge、Rebase 与 Cherry-pick
- 9. 远程仓库与协作
- 10. Git 是否适合管理非文本文件
- 11. 非文本文件的四种管理策略
- 12. Git LFS 的原理与使用
- 13. 文件锁、元数据与可审查性
- 14. 推荐的仓库结构
- 15. 常见误区
- 16. 实践练习
- 17. 速查表
1. Git 的核心定义
Git 可以用下面这句话概括:
Git 本质上是一个内容寻址的不可变对象数据库,加上一组可移动的引用,以及工作区和暂存区这两个状态视图。
这句话包含了 Git 的几个最重要概念:
内容寻址
Git 根据对象内容计算对象 ID,并通过对象 ID 查找内容。对象不可变
对象一旦创建,就不会在原位置上被修改。内容发生变化时,Git 会创建新对象。引用可移动
分支、标签和远程跟踪引用本质上都是指向对象的名字。分支会随着提交向前移动。工作区和暂存区是状态视图
工作区表示当前磁盘上的文件;暂存区表示下一次提交准备保存的内容。
理解这套模型之后,许多 Git 命令就不再是需要死记的独立规则,而是对对象、引用和状态视图进行操作。
2. Git 解决的根本问题
Git 主要解决以下问题:
- 记录文件在不同时间点的状态;
- 比较两个版本之间的变化;
- 恢复错误修改;
- 在不破坏稳定版本的情况下尝试新方案;
- 让多个人分别修改,再共同验收和合并;
- 追踪修改者、修改时间和修改原因;
- 在多个仓库之间交换提交和对象。
Git 不只适用于程序代码,也可以管理:
- Markdown 文档;
- 配置文件;
- 项目说明;
- 实验记录;
- CAD 导出信息;
- 图片、音频和模型文件;
- 数据清单与校验信息。
但不同文件类型适合采用不同管理方式。
3. Git 的四类核心对象
Git 的对象数据库主要包含四类对象:
blob
tree
commit
tag3.1 Blob:保存文件内容
Blob 保存文件的字节内容。
它通常不直接保存:
- 文件名;
- 文件所在路径;
- 文件的创建时间;
- 完整的文件系统元数据。
因此,相同内容的两个文件可以共享同一个 blob。
git hash-object file.txt
git cat-file -p <blob-id>可以把 blob 理解为:
一段没有名字的文件内容。
3.2 Tree:保存目录结构
Tree 保存:
- 文件或目录名称;
- 文件模式;
- 指向 blob 或子 tree 的对象 ID。
例如:
root tree
├── README.md → blob A
└── src → tree B
├── main.py → blob C
└── util.py → blob D查看目录树:
git ls-tree HEAD
git cat-file -p HEAD^{tree}Tree 负责把“没有文件名的 blob”组织成真正的目录结构。
3.3 Commit:连接快照与历史
Commit 主要包含:
- 一个根 tree;
- 一个或多个父 commit;
- 作者;
- 提交者;
- 时间;
- 提交说明。
查看 commit:
git cat-file -p HEAD普通提交通常有一个父提交:
C3 → C2 → C1合并提交通常有两个父提交:
A2
/ \
A1─── M
\ /
B2Commit 保存的是一个项目快照及其历史关系,而不是简单保存“相对于上一次修改了哪些行”。
3.4 Tag:为对象提供稳定名称
轻量标签本质上是一个引用:
refs/tags/v1.0 → commit附注标签则会先指向一个 tag 对象:
refs/tags/v1.0 → tag object → commit附注标签可以包含:
- 标签说明;
- 创建者;
- 创建时间;
- 数字签名。
4. 内容寻址与对象不可变
4.1 内容寻址
Git 对对象的类型、长度和内容进行哈希计算,得到对象 ID。
简化理解:
对象 ID = hash(对象类型 + 对象长度 + 对象内容)因此:
- 内容相同,对象 ID 通常相同;
- 内容改变,对象 ID 会改变;
- 对象 ID 同时承担身份标识和完整性校验作用。
4.2 对象不可变
当文件内容被修改时,Git 不会直接改写原 blob,而是创建新 blob。
当目录内容发生变化时,Git会创建新的 tree。
当提交信息、父提交或项目快照变化时,Git会创建新的 commit。
因此 Git 的基本模式是:
旧对象保留
+
创建新对象
+
移动引用这解释了为什么许多“撤销”操作实际上并没有立即删除旧历史。
4.3 Git 逻辑上保存快照
Git 的逻辑模型是快照:
提交 A → 项目快照 A
提交 B → 项目快照 B
提交 C → 项目快照 C但未变化的对象可以被多个快照共享,因此不等于每次都完整复制整个项目。
在物理存储层,Git 还可能在 packfile 中采用差量压缩,以减少重复数据。
要区分:
- 逻辑模型:快照
- 物理存储:可以使用差量压缩
5. 提交图、分支与引用
5.1 分支不是项目副本
分支本质上是一个指向 commit 的可移动引用:
refs/heads/main → C3创建新分支时,Git 通常不会复制所有项目文件,只会创建一个新引用:
main → C3
feature → C3当在 feature 上提交后:
main → C3
feature → C45.2 引用可变,对象不可变
一次新提交通常会发生:
- 创建新的 blob、tree 和 commit;
- 将当前分支引用移动到新 commit;
- 更新 HEAD 所代表的当前位置。
旧 commit 不会被修改。
5.3 可达性
Git 判断对象是否仍然属于有效历史时,会从引用出发沿对象关系遍历。
常见引用根包括:
- 本地分支;
- 标签;
- HEAD;
- 远程跟踪分支;
- reflog 暂时记录的位置。
能从这些引用访问到的对象称为“可达对象”。
删除分支:
git branch -D feature通常只是删除引用,并不会立刻从磁盘中删除所有相关 commit。
5.4 Reflog
Reflog 记录本地引用如何移动:
git reflog
git reflog show main它可以帮助恢复:
- 错误 reset;
- 错误 rebase;
- 被删除的分支;
- detached HEAD 中创建的提交。
可以这样区分:
git log:查看提交图;git reflog:查看本地引用移动记录。
6. HEAD、暂存区与工作区
Git 日常操作可以用三个状态解释:
HEAD
上一次提交的快照
Index / Staging Area
下一次提交准备保存的快照
Working Tree
当前磁盘上的文件6.1 HEAD
正常情况下:
HEAD → refs/heads/main → commit查看:
cat .git/HEAD可能显示:
ref: refs/heads/mainDetached HEAD 时,HEAD 会直接指向某个 commit,而不是指向分支。
6.2 暂存区
暂存区不是简单的“文件等待列表”。
更准确地说:
暂存区是下一次提交的预备目录树。
查看暂存区内容:
git ls-files --stage执行:
git add file.txt大致相当于:
- 为当前文件内容创建或查找 blob;
- 更新暂存区中对应路径的对象 ID。
6.3 两种 diff
git diff比较:
工作区 ↔ 暂存区git diff --cached比较:
暂存区 ↔ HEAD因此,一个文件可以同时处于:
HEAD:版本 A
Index:版本 B
Working Tree:版本 C这也是部分暂存能够存在的原因。
7. 常用命令在底层做了什么
7.1 git add
git add file.txt作用:
- 读取工作区文件内容;
- 创建或复用 blob;
- 更新暂存区对应条目。
它不是“上传文件”,也不是“永久保存历史”。
7.2 git commit
git commit -m "Add feature"作用大致是:
- 把暂存区写成 tree;
- 创建 commit;
- 让 commit 指向当前 HEAD 作为父提交;
- 移动当前分支引用。
7.3 git restore
恢复工作区文件:
git restore file.txt取消暂存:
git restore --staged file.txt两者作用对象不同:
- 默认
restore修改工作区; restore --staged修改暂存区。
7.4 git reset
reset 可以依次影响三个层次:
| 模式 | 分支引用 | 暂存区 | 工作区 |
|---|---|---|---|
--soft | 修改 | 保留 | 保留 |
--mixed | 修改 | 修改 | 保留 |
--hard | 修改 | 修改 | 修改 |
示例:
git reset --soft HEAD~1
git reset --mixed HEAD~1
git reset --hard HEAD~1--hard 可能直接覆盖未提交修改,执行前必须确认是否需要备份。
7.5 git switch
git switch main
git switch -c feature它主要用于:
- 改变 HEAD 所在分支;
- 更新暂存区;
- 更新工作区,使其符合目标提交。
8. Merge、Rebase 与 Cherry-pick
假设初始历史:
A──B feature
/
O──P──Q main8.1 Merge
执行:
git switch main
git merge feature可能得到:
A──B
/ \
O──P──Q────MMerge 会:
- 寻找共同祖先;
- 比较双方变化;
- 执行三方合并;
- 必要时创建多父提交。
如果 main 没有额外前进,可能只发生 fast-forward,即移动引用而不创建合并提交。
8.2 Rebase
执行:
git rebase main feature可能得到:
O──P──Q──A'──B'Rebase 不是简单“移动原提交”,而是:
- 找出 feature 独有的修改;
- 在新的基点上重新应用;
- 创建新的 commit。
因此:
A' ≠ A
B' ≠ B即使文件内容相似,父提交不同也会使 commit ID 不同。
8.3 Cherry-pick
git cherry-pick <commit-id>Cherry-pick 会:
- 计算目标提交相对于其父提交的变化;
- 把变化应用到当前 HEAD;
- 创建一个新提交。
它复制的是修改效果,不是完整继承原提交身份和历史关系。
8.4 合并冲突
发生冲突时,暂存区可能保存三份内容:
stage 1:共同祖先
stage 2:ours
stage 3:theirs查看:
git ls-files -uGit 能指出无法自动合并的位置,但不能替团队判断哪种结果符合项目目标。
解决冲突应当依据:
- 项目需求;
- 接口约定;
- 测试结果;
- 设计标准;
- 团队共同验收结论。
9. 远程仓库与协作
9.1 远程仓库不是天然的“主仓库”
Git 是分布式系统。每个完整克隆通常都拥有自己的:
- 对象数据库;
- 本地分支;
- 标签;
- 提交历史。
GitHub、GitLab 或自建服务器只是协作中被共同使用的远程仓库。
9.2 远程跟踪引用
本地可能同时存在:
refs/heads/main
refs/remotes/origin/mainorigin/main 是本地记录的远程状态,不是远程服务器上的实时指针。
执行:
git fetch origin通常会:
- 下载本地缺少的对象;
- 更新远程跟踪引用。
9.3 Pull
常见情况下:
git pull = git fetch + git merge也可能被配置为:
git pull = git fetch + git rebase9.4 Push
Push 通常包含:
- 找出远程缺失的对象;
- 打包和传输对象;
- 请求远程更新引用;
- 远程检查引用移动是否合法。
Non-fast-forward 被拒绝,通常意味着直接移动远程引用可能使已有历史失去正常引用。
--force-with-lease 会在强制更新前检查远程引用是否仍处于预期位置,因此通常比 --force 更安全。
10. Git 是否适合管理非文本文件
Git 能保存任意字节内容,因此从技术上可以管理:
- PNG、JPEG;
- PDF;
- WAV、MP3;
- Blender 文件;
- CAD 文件;
- 模型权重;
- ZIP 压缩包;
- 固件二进制。
但“能够保存”不等于“适合直接管理”。
判断时应考虑:
- 文件有多大;
- 修改是否频繁;
- Git 是否能展示有意义的差异;
- 多人修改时是否能合并;
- 历史版本是否必须长期保留;
- 文件是否可以通过脚本重新生成。
10.1 普通 Git 的问题
仓库膨胀
大型二进制文件每次变化都会产生新对象。
即使工作区只保留最新版,旧版本仍可能存在于历史中,新成员克隆时仍需处理这些历史数据。
差异不可读
Git 对文本文件可以显示逐行差异,但对多数二进制文件只能显示:
Binary files differ难以合并
两个人同时修改同一个 Blender、PSD 或 CAD 单体文件时,Git 通常无法自动理解各自修改的部件。
11. 非文本文件的四种管理策略
11.1 普通 Git
适合:
- 小型图片;
- 小型 PDF;
- 固件文件;
- 很少变化的参考资源;
- 必须随项目一起保存的小文件。
经验原则:
文件较小
+
修改不频繁
+
数量有限
=
可以直接使用普通 Git11.2 Git LFS
适合:
- 大型图片;
- 音频和视频;
- CAD 与 Blender 文件;
- PSD;
- 模型权重;
- 必须随版本保存的大型二进制资源。
Git 仓库保存指针文件,真实内容存储在 LFS 服务中。
11.3 外部对象或数据存储
适合:
- 大型数据集;
- 原始音频库;
- 批量视频;
- 大量训练 checkpoint;
- 渲染帧序列;
- 多 GB 或数百 GB 的资产集合。
Git 中只保留:
- manifest;
- 文件哈希;
- 数据版本号;
- 下载脚本;
- 生成脚本;
- 数据来源说明。
11.4 .gitignore
适合:
- 缓存;
- 临时文件;
- 构建产物;
- 日志;
- 虚拟环境;
- 可重新生成的中间结果。
示例:
__pycache__/
*.pyc
.venv/
cache/
runs/
checkpoints/
build/
dist/
*.log
*.tmp12. Git LFS 的原理与使用
12.1 原理
普通 Git 中不再直接保存大文件,而是保存一个小型指针:
version https://git-lfs.github.com/spec/v1
oid sha256:<object-id>
size <bytes>真实大文件由 LFS 存储服务管理。
工作区中仍然会显示正常文件,因此使用体验与普通 Git 接近。
12.2 初始化
git lfs install12.3 跟踪文件类型
git lfs track "*.blend"
git lfs track "*.f3d"
git lfs track "*.psd"
git lfs track "*.wav"
git lfs track "*.pt"这些规则会写入 .gitattributes:
*.blend filter=lfs diff=lfs merge=lfs -text
*.f3d filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
*.pt filter=lfs diff=lfs merge=lfs -text必须提交 .gitattributes:
git add .gitattributes
git commit -m "Configure Git LFS"12.4 检查 LFS 文件
git lfs ls-files
git lfs status12.5 迁移已有历史
仅执行 git lfs track 不会自动修改旧提交。
迁移历史可以使用:
git lfs migrate import \
--include="*.blend,*.f3d,*.wav,*.pt" \
--everything该操作会重写历史,因此会改变 commit ID。
操作前应:
- 保证工作区干净;
- 备份仓库;
- 停止团队同时提交;
- 记录分支和标签;
- 通知协作者重新同步;
- 谨慎执行强制推送。
13. 文件锁、元数据与可审查性
13.1 文件锁
对于无法自动合并的单体二进制文件,可以使用 LFS 锁:
git lfs track --lockable "*.blend"
git lfs lock models/robot.blend
git lfs unlock models/robot.blend典型流程:
领取编辑权
→ 修改
→ 导出预览
→ 提交
→ 团队验收
→ 释放编辑权文件锁解决的是多人同时编辑问题,不是存储体积问题。
13.2 为二进制文件提供文本元数据
建议为重要二进制资产配套保存可比较文本:
models/
├── robot.blend
├── robot.preview.png
├── robot.metadata.yaml
└── robot_bom.csv示例:
version: 1.4
author: example
dimensions_mm:
length: 530
width: 680
height: 740
changes:
- increased bracket thickness from 3 mm to 4 mm
- moved motor mount by 8 mm
validation:
interference_check: passed
mass_kg: 12.4这样团队可以审查:
- 改了什么;
- 为什么修改;
- 是否满足尺寸标准;
- 是否通过干涉检查;
- 对应哪个二进制版本。
13.3 导出可审查格式
对于 CAD、Blender、设计和数据项目,可考虑额外保存:
- 预览 PNG;
- PDF 工程图;
- BOM.csv;
- STEP;
- 参数 JSON/YAML;
- 质量属性;
- 实验指标;
- checksum。
并非所有导出文件都必须提交,应保留真正帮助复核和复现的结果。
14. 推荐的仓库结构
以下结构适合同时包含代码、机械设计、数据和训练结果的项目:
project/
├── src/ # 源码,普通 Git
├── tests/ # 测试,普通 Git
├── configs/ # 配置,普通 Git
├── docs/ # 文档,普通 Git
├── manifests/ # 数据清单,普通 Git
├── reports/ # 最终报告和指标
├── assets/
│ ├── previews/ # 小型预览图,普通 Git
│ └── source/ # 大型设计源文件,Git LFS
├── models/
│ ├── metadata/ # 模型说明,普通 Git
│ └── released/ # 少量正式权重,Git LFS
├── scripts/
│ ├── download_data.py
│ └── build_assets.py
├── .gitattributes
├── .gitignore
└── README.md不进入 Git 的内容可以放在:
data/
cache/
runs/
checkpoints/
render_frames/
build/并通过 manifest、脚本和校验文件保持可复现性。
15. 常见误区
误区 1:Git 保存的是每次提交之间的 diff
更准确的说法:
Git 的逻辑对象模型保存快照,diff 是比较快照后计算出来的。
误区 2:分支是项目完整副本
更准确的说法:
分支只是一个指向 commit 的可移动引用。
误区 3:git add 是保存文件
更准确的说法:
git add把当前内容写入对象数据库,并更新下一次提交的预备快照。
误区 4:Commit 等于上传
更准确的说法:
- commit:创建本地历史;
- push:把对象和引用变化发送到远程。
误区 5:删除分支等于删除提交
更准确的说法:
删除分支通常只删除引用。提交可能仍可通过其他引用或 reflog 找回。
误区 6:Git LFS 能解决所有二进制协作问题
Git LFS 主要解决:
- 大文件存储;
- 仓库对象膨胀;
- 大文件传输。
它不能自动解决:
- CAD 文件语义合并;
- Blender 场景冲突;
- PSD 图层冲突;
- 团队验收标准不清。
不可合并文件通常还需要文件锁、任务分工和验收流程。
误区 7:所有生成文件都应该忽略
有些生成文件具有长期价值,例如:
- 最终评估指标;
- 正式发布模型;
- 基线报告;
- 关键图表;
- 对外发布 PDF。
判断标准应是:
该文件是否是正式成果、复现实验所需证据,或发布版本的一部分。
16. 实践练习
练习 1:观察对象结构
git init git-internals-demo
cd git-internals-demo
echo "hello" > hello.txt
git add hello.txt
git commit -m "Create hello file"
git cat-file -p HEAD
git cat-file -p HEAD^{tree}
git ls-tree HEAD目标:
- 找到 commit 指向的 tree;
- 找到 tree 指向的 blob;
- 理解文件名属于 tree,而不是 blob。
练习 2:观察三个状态
修改文件后依次执行:
git status
git diff
git add hello.txt
git diff
git diff --cached目标:
- 理解工作区、暂存区和 HEAD;
- 理解两种 diff 的比较对象。
练习 3:观察分支引用
git switch -c feature
echo "feature" >> hello.txt
git add hello.txt
git commit -m "Add feature"
git show-ref
git log --oneline --graph --decorate --all目标:
- 观察 main 和 feature 分别指向哪个 commit;
- 理解创建分支不会复制整个项目。
练习 4:观察 Rebase
创建分叉历史后:
git log --oneline --graph --decorate --all
git rebase main feature
git log --oneline --graph --decorate --all目标:
- 比较 rebase 前后的 commit ID;
- 理解 rebase 会创建新 commit。
练习 5:配置 Git LFS
git lfs install
git lfs track "*.blend"
git add .gitattributes
git commit -m "Track Blender files with Git LFS"目标:
- 查看
.gitattributes; - 添加一个测试文件;
- 使用
git lfs ls-files检查。
练习 6:设计仓库管理策略
为一个包含以下内容的项目分类:
Python 源码
Markdown 文档
20 MB PDF
300 MB Blender 文件
50 GB 音频数据
训练缓存
正式发布模型
实验日志
最终评估报告参考答案:
| 内容 | 建议 |
|---|---|
| Python 源码 | 普通 Git |
| Markdown 文档 | 普通 Git |
| 20 MB PDF | 根据修改频率选择普通 Git 或 LFS |
| 300 MB Blender 文件 | Git LFS + 文件锁 |
| 50 GB 音频数据 | 外部存储 + manifest |
| 训练缓存 | .gitignore |
| 正式发布模型 | Git LFS 或发布制品存储 |
| 实验日志 | 原始大量日志忽略,摘要保留 |
| 最终评估报告 | 普通 Git |
17. 速查表
Git 核心模型
blob:文件内容
tree:目录结构
commit:快照 + 父提交 + 元数据
tag:稳定标记
对象:通常不可变
引用:可以移动三个状态
HEAD:上一次提交
Index:下一次提交
Working Tree:当前文件常见比较
git diff # 工作区 vs 暂存区
git diff --cached # 暂存区 vs HEAD
git diff A B # 提交 A vs 提交 B常见对象查看命令
git cat-file -p HEAD
git cat-file -p HEAD^{tree}
git ls-tree HEAD
git ls-files --stage
git show-ref
git reflog非文本文件选择
小且少变
→ 普通 Git
大且必须随版本保存
→ Git LFS
不可合并且多人编辑
→ Git LFS + 文件锁
大型数据集与批量产物
→ 外部存储 + manifest + checksum
可重新生成
→ .gitignore总结
Git 的核心不是某一组命令,而是一套统一的数据模型:
不可变对象
+
可移动引用
+
工作区
+
暂存区日常的提交、分支、合并、恢复和远程同步,都可以还原为对这几类对象和状态的操作。
对于非文本文件,不应简单判断“Git 能不能保存”,而应根据文件大小、修改频率、可合并性和复现需求选择:
- 普通 Git;
- Git LFS;
- 文件锁;
- 外部存储;
.gitignore;- 文本 manifest 与校验信息。
合理的目标不是把所有文件都塞进 Git,而是让项目的历史清晰、协作可控、成果可验收、过程可复现。