Skip to content

Git 原理与非文本文件管理学习笔记 ​

适合已经了解 Git 基本操作,希望进一步建立完整原理模型,并学习如何管理图片、音频、CAD、模型、数据集等非文本文件的读者。


目录 ​


1. Git 的核心定义 ​

Git 可以用下面这句话概括:

Git 本质上是一个内容寻址的不可变对象数据库,加上一组可移动的引用,以及工作区和暂存区这两个状态视图。

这句话包含了 Git 的几个最重要概念:

  1. 内容寻址
    Git 根据对象内容计算对象 ID,并通过对象 ID 查找内容。

  2. 对象不可变
    对象一旦创建,就不会在原位置上被修改。内容发生变化时,Git 会创建新对象。

  3. 引用可移动
    分支、标签和远程跟踪引用本质上都是指向对象的名字。分支会随着提交向前移动。

  4. 工作区和暂存区是状态视图
    工作区表示当前磁盘上的文件;暂存区表示下一次提交准备保存的内容。

理解这套模型之后,许多 Git 命令就不再是需要死记的独立规则,而是对对象、引用和状态视图进行操作。


2. Git 解决的根本问题 ​

Git 主要解决以下问题:

  • 记录文件在不同时间点的状态;
  • 比较两个版本之间的变化;
  • 恢复错误修改;
  • 在不破坏稳定版本的情况下尝试新方案;
  • 让多个人分别修改,再共同验收和合并;
  • 追踪修改者、修改时间和修改原因;
  • 在多个仓库之间交换提交和对象。

Git 不只适用于程序代码,也可以管理:

  • Markdown 文档;
  • 配置文件;
  • 项目说明;
  • 实验记录;
  • CAD 导出信息;
  • 图片、音频和模型文件;
  • 数据清单与校验信息。

但不同文件类型适合采用不同管理方式。


3. Git 的四类核心对象 ​

Git 的对象数据库主要包含四类对象:

text
blob
tree
commit
tag

3.1 Blob:保存文件内容 ​

Blob 保存文件的字节内容。

它通常不直接保存:

  • 文件名;
  • 文件所在路径;
  • 文件的创建时间;
  • 完整的文件系统元数据。

因此,相同内容的两个文件可以共享同一个 blob。

bash
git hash-object file.txt
git cat-file -p <blob-id>

可以把 blob 理解为:

一段没有名字的文件内容。


3.2 Tree:保存目录结构 ​

Tree 保存:

  • 文件或目录名称;
  • 文件模式;
  • 指向 blob 或子 tree 的对象 ID。

例如:

text
root tree
├── README.md → blob A
└── src → tree B
    ├── main.py → blob C
    └── util.py → blob D

查看目录树:

bash
git ls-tree HEAD
git cat-file -p HEAD^{tree}

Tree 负责把“没有文件名的 blob”组织成真正的目录结构。


3.3 Commit:连接快照与历史 ​

Commit 主要包含:

  • 一个根 tree;
  • 一个或多个父 commit;
  • 作者;
  • 提交者;
  • 时间;
  • 提交说明。

查看 commit:

bash
git cat-file -p HEAD

普通提交通常有一个父提交:

text
C3 → C2 → C1

合并提交通常有两个父提交:

text
      A2
     /  \
A1───    M
     \  /
      B2

Commit 保存的是一个项目快照及其历史关系,而不是简单保存“相对于上一次修改了哪些行”。


3.4 Tag:为对象提供稳定名称 ​

轻量标签本质上是一个引用:

text
refs/tags/v1.0 → commit

附注标签则会先指向一个 tag 对象:

text
refs/tags/v1.0 → tag object → commit

附注标签可以包含:

  • 标签说明;
  • 创建者;
  • 创建时间;
  • 数字签名。

4. 内容寻址与对象不可变 ​

4.1 内容寻址 ​

Git 对对象的类型、长度和内容进行哈希计算,得到对象 ID。

简化理解:

text
对象 ID = hash(对象类型 + 对象长度 + 对象内容)

因此:

  • 内容相同,对象 ID 通常相同;
  • 内容改变,对象 ID 会改变;
  • 对象 ID 同时承担身份标识和完整性校验作用。

4.2 对象不可变 ​

当文件内容被修改时,Git 不会直接改写原 blob,而是创建新 blob。

当目录内容发生变化时,Git会创建新的 tree。

当提交信息、父提交或项目快照变化时,Git会创建新的 commit。

因此 Git 的基本模式是:

text
旧对象保留
    +
创建新对象
    +
移动引用

这解释了为什么许多“撤销”操作实际上并没有立即删除旧历史。


4.3 Git 逻辑上保存快照 ​

Git 的逻辑模型是快照:

text
提交 A → 项目快照 A
提交 B → 项目快照 B
提交 C → 项目快照 C

但未变化的对象可以被多个快照共享,因此不等于每次都完整复制整个项目。

在物理存储层,Git 还可能在 packfile 中采用差量压缩,以减少重复数据。

要区分:

  • 逻辑模型:快照
  • 物理存储:可以使用差量压缩

5. 提交图、分支与引用 ​

5.1 分支不是项目副本 ​

分支本质上是一个指向 commit 的可移动引用:

text
refs/heads/main → C3

创建新分支时,Git 通常不会复制所有项目文件,只会创建一个新引用:

text
main    → C3
feature → C3

当在 feature 上提交后:

text
main    → C3
feature → C4

5.2 引用可变,对象不可变 ​

一次新提交通常会发生:

  1. 创建新的 blob、tree 和 commit;
  2. 将当前分支引用移动到新 commit;
  3. 更新 HEAD 所代表的当前位置。

旧 commit 不会被修改。


5.3 可达性 ​

Git 判断对象是否仍然属于有效历史时,会从引用出发沿对象关系遍历。

常见引用根包括:

  • 本地分支;
  • 标签;
  • HEAD;
  • 远程跟踪分支;
  • reflog 暂时记录的位置。

能从这些引用访问到的对象称为“可达对象”。

删除分支:

bash
git branch -D feature

通常只是删除引用,并不会立刻从磁盘中删除所有相关 commit。


5.4 Reflog ​

Reflog 记录本地引用如何移动:

bash
git reflog
git reflog show main

它可以帮助恢复:

  • 错误 reset;
  • 错误 rebase;
  • 被删除的分支;
  • detached HEAD 中创建的提交。

可以这样区分:

  • git log:查看提交图;
  • git reflog:查看本地引用移动记录。

6. HEAD、暂存区与工作区 ​

Git 日常操作可以用三个状态解释:

text
HEAD
  上一次提交的快照

Index / Staging Area
  下一次提交准备保存的快照

Working Tree
  当前磁盘上的文件

6.1 HEAD ​

正常情况下:

text
HEAD → refs/heads/main → commit

查看:

bash
cat .git/HEAD

可能显示:

text
ref: refs/heads/main

Detached HEAD 时,HEAD 会直接指向某个 commit,而不是指向分支。


6.2 暂存区 ​

暂存区不是简单的“文件等待列表”。

更准确地说:

暂存区是下一次提交的预备目录树。

查看暂存区内容:

bash
git ls-files --stage

执行:

bash
git add file.txt

大致相当于:

  1. 为当前文件内容创建或查找 blob;
  2. 更新暂存区中对应路径的对象 ID。

6.3 两种 diff ​

bash
git diff

比较:

text
工作区 ↔ 暂存区
bash
git diff --cached

比较:

text
暂存区 ↔ HEAD

因此,一个文件可以同时处于:

text
HEAD:版本 A
Index:版本 B
Working Tree:版本 C

这也是部分暂存能够存在的原因。


7. 常用命令在底层做了什么 ​

7.1 git add ​

bash
git add file.txt

作用:

  • 读取工作区文件内容;
  • 创建或复用 blob;
  • 更新暂存区对应条目。

它不是“上传文件”,也不是“永久保存历史”。


7.2 git commit ​

bash
git commit -m "Add feature"

作用大致是:

  1. 把暂存区写成 tree;
  2. 创建 commit;
  3. 让 commit 指向当前 HEAD 作为父提交;
  4. 移动当前分支引用。

7.3 git restore ​

恢复工作区文件:

bash
git restore file.txt

取消暂存:

bash
git restore --staged file.txt

两者作用对象不同:

  • 默认 restore 修改工作区;
  • restore --staged 修改暂存区。

7.4 git reset ​

reset 可以依次影响三个层次:

模式分支引用暂存区工作区
--soft修改保留保留
--mixed修改修改保留
--hard修改修改修改

示例:

bash
git reset --soft HEAD~1
git reset --mixed HEAD~1
git reset --hard HEAD~1

--hard 可能直接覆盖未提交修改,执行前必须确认是否需要备份。


7.5 git switch ​

bash
git switch main
git switch -c feature

它主要用于:

  • 改变 HEAD 所在分支;
  • 更新暂存区;
  • 更新工作区,使其符合目标提交。

8. Merge、Rebase 与 Cherry-pick ​

假设初始历史:

text
      A──B  feature
     /
O──P──Q    main

8.1 Merge ​

执行:

bash
git switch main
git merge feature

可能得到:

text
      A──B
     /    \
O──P──Q────M

Merge 会:

  • 寻找共同祖先;
  • 比较双方变化;
  • 执行三方合并;
  • 必要时创建多父提交。

如果 main 没有额外前进,可能只发生 fast-forward,即移动引用而不创建合并提交。


8.2 Rebase ​

执行:

bash
git rebase main feature

可能得到:

text
O──P──Q──A'──B'

Rebase 不是简单“移动原提交”,而是:

  1. 找出 feature 独有的修改;
  2. 在新的基点上重新应用;
  3. 创建新的 commit。

因此:

text
A' ≠ A
B' ≠ B

即使文件内容相似,父提交不同也会使 commit ID 不同。


8.3 Cherry-pick ​

bash
git cherry-pick <commit-id>

Cherry-pick 会:

  1. 计算目标提交相对于其父提交的变化;
  2. 把变化应用到当前 HEAD;
  3. 创建一个新提交。

它复制的是修改效果,不是完整继承原提交身份和历史关系。


8.4 合并冲突 ​

发生冲突时,暂存区可能保存三份内容:

text
stage 1:共同祖先
stage 2:ours
stage 3:theirs

查看:

bash
git ls-files -u

Git 能指出无法自动合并的位置,但不能替团队判断哪种结果符合项目目标。

解决冲突应当依据:

  • 项目需求;
  • 接口约定;
  • 测试结果;
  • 设计标准;
  • 团队共同验收结论。

9. 远程仓库与协作 ​

9.1 远程仓库不是天然的“主仓库” ​

Git 是分布式系统。每个完整克隆通常都拥有自己的:

  • 对象数据库;
  • 本地分支;
  • 标签;
  • 提交历史。

GitHub、GitLab 或自建服务器只是协作中被共同使用的远程仓库。


9.2 远程跟踪引用 ​

本地可能同时存在:

text
refs/heads/main
refs/remotes/origin/main

origin/main 是本地记录的远程状态,不是远程服务器上的实时指针。

执行:

bash
git fetch origin

通常会:

  1. 下载本地缺少的对象;
  2. 更新远程跟踪引用。

9.3 Pull ​

常见情况下:

text
git pull = git fetch + git merge

也可能被配置为:

text
git pull = git fetch + git rebase

9.4 Push ​

Push 通常包含:

  1. 找出远程缺失的对象;
  2. 打包和传输对象;
  3. 请求远程更新引用;
  4. 远程检查引用移动是否合法。

Non-fast-forward 被拒绝,通常意味着直接移动远程引用可能使已有历史失去正常引用。

--force-with-lease 会在强制更新前检查远程引用是否仍处于预期位置,因此通常比 --force 更安全。


10. Git 是否适合管理非文本文件 ​

Git 能保存任意字节内容,因此从技术上可以管理:

  • PNG、JPEG;
  • PDF;
  • WAV、MP3;
  • Blender 文件;
  • CAD 文件;
  • 模型权重;
  • ZIP 压缩包;
  • 固件二进制。

但“能够保存”不等于“适合直接管理”。

判断时应考虑:

  1. 文件有多大;
  2. 修改是否频繁;
  3. Git 是否能展示有意义的差异;
  4. 多人修改时是否能合并;
  5. 历史版本是否必须长期保留;
  6. 文件是否可以通过脚本重新生成。

10.1 普通 Git 的问题 ​

仓库膨胀 ​

大型二进制文件每次变化都会产生新对象。

即使工作区只保留最新版,旧版本仍可能存在于历史中,新成员克隆时仍需处理这些历史数据。

差异不可读 ​

Git 对文本文件可以显示逐行差异,但对多数二进制文件只能显示:

text
Binary files differ

难以合并 ​

两个人同时修改同一个 Blender、PSD 或 CAD 单体文件时,Git 通常无法自动理解各自修改的部件。


11. 非文本文件的四种管理策略 ​

11.1 普通 Git ​

适合:

  • 小型图片;
  • 小型 PDF;
  • 固件文件;
  • 很少变化的参考资源;
  • 必须随项目一起保存的小文件。

经验原则:

text
文件较小
+
修改不频繁
+
数量有限
=
可以直接使用普通 Git

11.2 Git LFS ​

适合:

  • 大型图片;
  • 音频和视频;
  • CAD 与 Blender 文件;
  • PSD;
  • 模型权重;
  • 必须随版本保存的大型二进制资源。

Git 仓库保存指针文件,真实内容存储在 LFS 服务中。


11.3 外部对象或数据存储 ​

适合:

  • 大型数据集;
  • 原始音频库;
  • 批量视频;
  • 大量训练 checkpoint;
  • 渲染帧序列;
  • 多 GB 或数百 GB 的资产集合。

Git 中只保留:

  • manifest;
  • 文件哈希;
  • 数据版本号;
  • 下载脚本;
  • 生成脚本;
  • 数据来源说明。

11.4 .gitignore ​

适合:

  • 缓存;
  • 临时文件;
  • 构建产物;
  • 日志;
  • 虚拟环境;
  • 可重新生成的中间结果。

示例:

gitignore
__pycache__/
*.pyc
.venv/

cache/
runs/
checkpoints/
build/
dist/

*.log
*.tmp

12. Git LFS 的原理与使用 ​

12.1 原理 ​

普通 Git 中不再直接保存大文件,而是保存一个小型指针:

text
version https://git-lfs.github.com/spec/v1
oid sha256:<object-id>
size <bytes>

真实大文件由 LFS 存储服务管理。

工作区中仍然会显示正常文件,因此使用体验与普通 Git 接近。


12.2 初始化 ​

bash
git lfs install

12.3 跟踪文件类型 ​

bash
git lfs track "*.blend"
git lfs track "*.f3d"
git lfs track "*.psd"
git lfs track "*.wav"
git lfs track "*.pt"

这些规则会写入 .gitattributes:

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:

bash
git add .gitattributes
git commit -m "Configure Git LFS"

12.4 检查 LFS 文件 ​

bash
git lfs ls-files
git lfs status

12.5 迁移已有历史 ​

仅执行 git lfs track 不会自动修改旧提交。

迁移历史可以使用:

bash
git lfs migrate import \
  --include="*.blend,*.f3d,*.wav,*.pt" \
  --everything

该操作会重写历史,因此会改变 commit ID。

操作前应:

  • 保证工作区干净;
  • 备份仓库;
  • 停止团队同时提交;
  • 记录分支和标签;
  • 通知协作者重新同步;
  • 谨慎执行强制推送。

13. 文件锁、元数据与可审查性 ​

13.1 文件锁 ​

对于无法自动合并的单体二进制文件,可以使用 LFS 锁:

bash
git lfs track --lockable "*.blend"
git lfs lock models/robot.blend
git lfs unlock models/robot.blend

典型流程:

text
领取编辑权
→ 修改
→ 导出预览
→ 提交
→ 团队验收
→ 释放编辑权

文件锁解决的是多人同时编辑问题,不是存储体积问题。


13.2 为二进制文件提供文本元数据 ​

建议为重要二进制资产配套保存可比较文本:

text
models/
├── robot.blend
├── robot.preview.png
├── robot.metadata.yaml
└── robot_bom.csv

示例:

yaml
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. 推荐的仓库结构 ​

以下结构适合同时包含代码、机械设计、数据和训练结果的项目:

text
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 的内容可以放在:

text
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:观察对象结构 ​

bash
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:观察三个状态 ​

修改文件后依次执行:

bash
git status
git diff
git add hello.txt
git diff
git diff --cached

目标:

  • 理解工作区、暂存区和 HEAD;
  • 理解两种 diff 的比较对象。

练习 3:观察分支引用 ​

bash
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 ​

创建分叉历史后:

bash
git log --oneline --graph --decorate --all
git rebase main feature
git log --oneline --graph --decorate --all

目标:

  • 比较 rebase 前后的 commit ID;
  • 理解 rebase 会创建新 commit。

练习 5:配置 Git LFS ​

bash
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:设计仓库管理策略 ​

为一个包含以下内容的项目分类:

text
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 核心模型 ​

text
blob:文件内容
tree:目录结构
commit:快照 + 父提交 + 元数据
tag:稳定标记

对象:通常不可变
引用:可以移动

三个状态 ​

text
HEAD:上一次提交
Index:下一次提交
Working Tree:当前文件

常见比较 ​

bash
git diff           # 工作区 vs 暂存区
git diff --cached  # 暂存区 vs HEAD
git diff A B       # 提交 A vs 提交 B

常见对象查看命令 ​

bash
git cat-file -p HEAD
git cat-file -p HEAD^{tree}
git ls-tree HEAD
git ls-files --stage
git show-ref
git reflog

非文本文件选择 ​

text
小且少变
→ 普通 Git

大且必须随版本保存
→ Git LFS

不可合并且多人编辑
→ Git LFS + 文件锁

大型数据集与批量产物
→ 外部存储 + manifest + checksum

可重新生成
→ .gitignore

总结 ​

Git 的核心不是某一组命令,而是一套统一的数据模型:

text
不可变对象
+
可移动引用
+
工作区
+
暂存区

日常的提交、分支、合并、恢复和远程同步,都可以还原为对这几类对象和状态的操作。

对于非文本文件,不应简单判断“Git 能不能保存”,而应根据文件大小、修改频率、可合并性和复现需求选择:

  • 普通 Git;
  • Git LFS;
  • 文件锁;
  • 外部存储;
  • .gitignore;
  • 文本 manifest 与校验信息。

合理的目标不是把所有文件都塞进 Git,而是让项目的历史清晰、协作可控、成果可验收、过程可复现。

内容以研究记录和项目文档为主