换一台机器,项目还能跑起来吗¶
你已经在自己的笔记本上把 AI 学习助手跑通了——HTTP adapter 能接请求,core 能
算出计划,Pydantic 能拦住坏数据。现在同事接手:他在另一台全新的 Linux 机器上
git clone 了仓库,打开终端,准备继续开发。
他需要从你这里获得什么?
显然不是你笔记本上的整个硬盘镜像。他需要的是能让他理解项目并重建运行环境 的那些东西——源代码、依赖声明、配置模板。至于你机器上跑出来的日志、你本地 的虚拟环境目录、你的数据库临时文件——这些是你这台机器的"快照",不是项目本身。
这个区分听起来简单,但当你面对一个真实项目目录时,每个文件到底属于"项目事实" 还是"机器快照",并不总是显而易见的。本章要解决的就是这个问题:一个可交付工程 应该如何划分 Git 跟踪的内容与本地或生成的 artifact。
1. 对方需要得到什么能力¶
让我们先不看任何具体文件。只问一个问题:你的协作者拿到仓库后,他的任务是什么?
他要做两件事:
- 理解——阅读源码和声明,知道项目做什么、怎么组织、依赖什么。
- 重建——在自己的机器上安装依赖、配置环境变量、运行服务。
注意第二件事说的是"重建",不是"复制"。他不需要你的 .venv/ 目录——那里面的
二进制文件是为你的操作系统和 CPU 架构编译的。他需要的是 pyproject.toml——
有了声明文件,他可以在自己的机器上用一条命令重建出等价的虚拟环境。
用一个类比:如果你做 ETL pipeline,交付给下游的是 pipeline 定义(SQL 文件、 DAG 配置),不是某次运行产生的中间 CSV。定义是可复现的事实;某次运行的输出 是特定时间点的快照。
暂停想一下:按照这个"理解 + 重建"的标准,你觉得以下哪些东西属于"项目事实"?
src/目录下的 Python 源码pyproject.toml.venv/目录- 上次运行产生的
app.log
如果你的回答是"源码和声明是事实,虚拟环境和日志是快照"——这就是接下来所有 分类的出发点。
2. 七个文件,各自属于谁¶
现在打开 AI 学习助手的项目根目录。经过前几章的开发,目录结构大致如下:
learning_planner/
├── src/
│ ├── contracts.py
│ ├── core.py
│ └── http_adapter.py
├── pyproject.toml
├── .venv/
│ ├── bin/
│ ├── lib/
│ └── pyvenv.cfg
├── __pycache__/
│ └── ...
├── .env
├── .env.example
└── app.log
七类文件摆在面前。我们一个一个过。
src/——源代码。这是项目的核心定义,协作者必须看到它才能理解项目做什么。
跟踪。
pyproject.toml——依赖声明和项目元数据。协作者用它重建环境
(pip install -e . 或 uv sync)。跟踪。
.venv/——本地虚拟环境。里面是针对你这台机器编译的二进制包。协作者的
机器可能是不同的操作系统或 Python 版本,这些二进制文件对他没用。而且他有
pyproject.toml,可以自己重建。不跟踪。
__pycache__/——Python 字节码缓存。Python 解释器自动生成,删掉后下次
运行会重新生成。纯粹的本地加速产物。不跟踪。
.env——环境变量文件。里面可能包含数据库密码、API 密钥等敏感值。这些是
你这台机器的本地配置,而且是 secret——一旦进入 Git 历史,任何有仓库访问权限
的人都能看到。不跟踪。
.env.example——环境变量模板。它只列出变量名和安全的占位符值
(如 DATABASE_URL=postgres://localhost/dev),不含真实 secret。协作者看到它
就知道需要配置哪些变量。跟踪。
app.log——运行日志。这是某次运行的输出,不是项目定义。删掉不影响任何人
理解或重建项目。不跟踪。
把结果整理成表:
| 文件 | 角色 | 跟踪? | 依据 |
|---|---|---|---|
src/ |
源码(项目定义) | 是 | 理解项目必需 |
pyproject.toml |
依赖声明 | 是 | 重建环境必需 |
.venv/ |
本地环境 | 否 | 机器相关,可从声明重建 |
__pycache__/ |
字节码缓存 | 否 | 自动生成,无审阅价值 |
.env |
本地 secret | 否 | 含敏感值,机器相关 |
.env.example |
安全模板 | 是 | 告知协作者需配置什么 |
app.log |
运行状态 | 否 | 某次运行的快照 |
三类"是"的共同点:它们是协作者理解或重建项目所必需的声明性输入。
四类"否"的共同点:它们要么是某台机器的本地状态,要么是可以从声明重建的产物, 要么包含不应公开的 secret。
到这里你已经能完成大多数日常文件的分类。但接下来有一个陷阱。
3. 加了 .gitignore 就安全了吗¶
你决定创建 .gitignore 来排除那些不该跟踪的文件:
看起来很干净。但有一个问题——假设你在项目早期不小心把 .env 提交了。三天后
你才意识到,于是把 .env 加入 .gitignore。
暂停预测一下:加入 .gitignore 之后,.env 会从 Git 的跟踪中消失吗?
让我们看看实际发生了什么:
$ git status
On branch main
Changes not staged for commit:
modified: .env
Untracked files:
.gitignore
.env 仍然出现在 "Changes not staged for commit" 中——它还在被跟踪。你新写
的 .gitignore 对它没有任何效果。
为什么?因为 .gitignore 的作用是告诉 Git对哪些尚未跟踪的路径保持忽略。
它描述的是"有意不跟踪"的意图。但 .env 已经在 Git 的索引里了——它的状态是
"tracked"。一个文件一旦进入 tracked 状态,.gitignore 不会改变这个事实。
用一个表来整理:
| 文件状态 | .gitignore 的效果 |
|---|---|
| 从未被跟踪(untracked) | 匹配规则后不再出现在 git status 的 untracked 列表中 |
| 已被跟踪(tracked) | 无效果——文件继续被跟踪,修改继续出现在 diff 中 |
这意味着:如果一个 secret 文件已经进入了 Git 历史,仅仅把它加入 .gitignore
不能解决问题。你需要额外的步骤把它从跟踪状态移除(这涉及 git rm --cached
等操作),然后还要考虑它是否已经存在于历史提交中。
本章不展开具体的清理命令和历史重写策略——那需要对 Git 内部模型有更深的理解。 这里只需要记住一个事实:ignore 规则和跟踪状态是两件独立的事。配置了 ignore 不等于完成了保护;你必须确认文件的实际 Git 状态。
这也解释了为什么预防优于补救:在项目第一次 git init 之后、第一次 commit 之前
就配置好 .gitignore,比事后补救要简单得多。
4. 生成的东西一定不该跟踪吗¶
核心文件分好了,ignore 和 tracked 的关系也清楚了。但在真实项目中,还有一些文件 不那么容易归类——它们都是"可以重新生成的",但跟踪策略却不一样。
看这五个候选:
tests/fixtures/sample_plan.json # 固定测试 fixture
uv.lock # 依赖锁文件
migrations/001_init.sql # 数据库 migration
docs/openapi.json # 从代码生成的 OpenAPI 规范
output/model_result_20260615.bin # 大型模型输出(50MB)
如果你的规则是"能重新生成的就不跟踪",那这五个全该排除。但暂停想一下—— 它们真的一样吗?
让我们用四个维度来逐个分析:
固定测试 fixture(sample_plan.json):
- 审阅价值:高——reviewer 需要看到测试期望什么输入输出。
- 重建成本:中——需要手动构造或从真实数据脱敏。
- 权威来源:仓库本身就是权威来源,没有外部系统可以重新生成它。
- 体积:小。
结论:跟踪。它是测试的声明性输入,和源码一样属于项目事实。
依赖锁文件(uv.lock):
- 审阅价值:中——diff 能看出哪些依赖版本变了。
- 重建成本:低——
uv lock可以重建,但结果可能因时间不同而不同(新版本发布)。 - 权威来源:锁文件本身就是"此刻确切版本"的权威声明。
- 体积:小到中。
结论:通常跟踪。锁文件保证不同机器、不同时间安装出完全相同的依赖版本。如果不
跟踪,协作者在不同时间 uv sync 可能得到不同的依赖树,导致"在我机器上能跑"
的问题。
数据库 migration(001_init.sql):
- 审阅价值:高——数据结构变更需要 code review。
- 重建成本:不可重建——它记录的是数据库结构的演变历史,删了就丢失了。
- 权威来源:仓库。
- 体积:小。
结论:跟踪。Migration 是数据结构的声明性演变记录。
生成的 OpenAPI 规范(docs/openapi.json):
- 审阅价值:中——可以看到 API 变化,但 reviewer 也可以直接看源码。
- 重建成本:极低——从代码自动生成,一条命令即可。
- 权威来源:源码是权威,openapi.json 是派生物。
- 体积:小。
结论:可以不跟踪——因为源码已经是权威来源,生成物随时可以从权威重建。但也有 团队选择跟踪它来方便 PR 中直接看 API diff。这是一个条件性选择,取决于团队 对"在 PR 中看到派生物 diff"的需求。
大型模型输出(output/model_result_20260615.bin,50MB):
- 审阅价值:低——二进制文件无法在 PR 中有意义地 diff。
- 重建成本:高(需要重新训练),但它不是项目定义的一部分。
- 权威来源:训练过程,不是仓库。
- 体积:大——会显著增加仓库克隆时间。
结论:不跟踪。它不是源码也不是声明,体积大且无法 diff,权威来源在训练环境 而非仓库。
整理一下这五项的分析结果:
| 候选 | 审阅价值 | 重建成本 | 权威来源 | 体积 | 跟踪? |
|---|---|---|---|---|---|
| 测试 fixture | 高 | 中 | 仓库 | 小 | 是 |
| 依赖锁文件 | 中 | 低但不确定性 | 锁文件本身 | 小 | 通常是 |
| Migration | 高 | 不可重建 | 仓库 | 小 | 是 |
| 生成 OpenAPI | 中 | 极低 | 源码(派生物) | 小 | 条件性 |
| 大型模型输出 | 低 | 高 | 训练环境 | 大 | 否 |
关键发现:"可以重新生成"不等于"不应该跟踪"。fixture 可以手动重建但仓库 是权威来源;锁文件可以重新生成但结果可能不同。真正决定跟踪策略的不是"能不能 重新生成",而是这个文件在交付、审阅和复现中扮演什么角色。
5. 三个问题决定一切¶
你已经完成了核心文件分类、理解了 ignore 与 tracked 的独立性、处理了几种边界 情况。现在来测试一下:如果出现一个新文件,你能不依赖任何参考材料做出判断吗?
两个新候选:
.mypy_cache/type_index.json——mypy 类型检查器生成的本地索引文件docs/db_schema.json——从 migration 文件自动生成的小型数据库 schema 快照 (2KB,一条命令可重新生成)
暂停回答:这两个文件应该跟踪还是忽略?
分析第一个:.mypy_cache/type_index.json
- 它是 mypy 在你这台机器上运行时生成的缓存,用来加速下次检查。
- 协作者的 mypy 会根据他的环境重新生成自己的缓存。
- 它没有审阅价值(没人在 PR 中看 mypy 缓存 diff)。
- 权威来源是源码本身——mypy 从源码派生出类型信息。
结论:不跟踪。它是本地工具缓存,和 __pycache__/ 同类。
分析第二个:docs/db_schema.json
- 它从 migration 文件自动生成,一条命令可以重建。
- 权威来源是 migration 文件(已跟踪),这个 json 是派生物。
- 但它体积很小(2KB),且能让 reviewer 在 PR 中快速看到 schema 变化。
- 如果不跟踪,reviewer 需要自己运行生成命令或者在脑中从 SQL 推导 schema。
结论:这是一个条件性选择。如果团队认为"在 PR 中直接看到 schema diff"有价值, 跟踪是合理的;如果团队认为"源码为准,派生物不入库",不跟踪也合理。关键是 做出有依据的选择,而不是机械地套用"生成物一律排除"。
现在让我们提取出贯穿整章的判断框架。面对任何一个文件,问三个问题:
问题一:这个文件在交付和复现中的角色是什么?
- 如果协作者理解项目或重建环境必须看到它——跟踪。
- 如果它纯粹是某台机器的本地状态——不跟踪。
问题二:它的权威来源在哪里?重建是否确定性?
- 如果仓库本身就是权威来源(源码、fixture、migration)——跟踪。
- 如果它是从已跟踪文件确定性派生的(可能跟踪也可能不跟踪,取决于审阅价值)。
- 如果结果不确定(锁文件在不同时间可能不同)——倾向跟踪以保证一致性。
问题三:跟踪它的实际代价是什么?
- 体积小、diff 有意义——代价低。
- 体积大、二进制无法 diff——代价高,需要额外理由才值得跟踪。
最后,别忘了本章第三节揭示的事实:.gitignore 不能把已经跟踪的文件变成
未跟踪的。即使你的三个问题得出"不该跟踪"的结论,如果文件已经在 Git 历史里,
ignore 规则本身无法完成清理。你需要确认文件的实际跟踪状态,必要时采取额外
步骤。
边界与常见误区¶
本章建立的是 artifact 分类和跟踪决策的判断框架。以下是它不意味着的东西:
-
不是完整的 secret 管理方案。
.env不跟踪只是最低要求;完整的 secret 策略涉及密钥轮换、加密存储和 CI 注入,不在本课范围内。 -
不是大型文件存储的解决方案。对于需要版本化的大型二进制文件(模型权重、 媒体资源),存在 Git LFS 等专门工具。本章不展开这些方案。
-
不替你的项目做最终决定。本章的分类结果是为了建立判断能力,不表示真实 P1 仓库已经配置好了
.gitignore。具体项目的最终模板需要你根据实际文件 结构和团队约定来确定。 -
不要把"生成物不跟踪"当成绝对规则。锁文件是生成物但通常跟踪; fixture 可以重建但仓库是权威来源。判断依据是角色和代价,不是"是否生成"。
本章小结¶
把一个项目交给另一台机器上的协作者,需要传递的是项目事实(源码、声明、 安全模板),而不是机器快照(本地环境、缓存、日志、secret 值)。
文件分类的底层逻辑是三个问题:
- 交付和复现中的角色——理解/重建必需,还是纯粹本地状态?
- 权威来源和重建确定性——仓库是权威,还是从已跟踪文件派生?
- 跟踪的实际代价——体积、diff 可读性、仓库膨胀。
.gitignore 描述的是"有意不跟踪"的路径,不会自动将已跟踪文件移出索引。
ignore 规则和文件的实际 Git 状态是两件独立的事。
边界项(fixture、锁文件、生成契约等)不能用"能不能重新生成"一刀切——审阅 价值、重建确定性、权威来源和体积四个维度共同决定跟踪策略。
现在你知道哪些文件应该进入仓库、哪些应该排除在外。但协作者 clone 之后,面对 这些被保留下来的源码和声明文件,他怎么知道从哪里开始看?模块的公开入口在哪、 依赖声明怎么读、项目的整体结构是什么?——这正是下一章要解决的问题。
练习¶
练习一:预测 git status 的输出¶
你在一个新项目中执行了以下操作:
$ git init
$ echo "hello" > README.md
$ git add README.md
$ git commit -m "init"
$ echo "secret=abc123" > .env
$ git add .env
$ git commit -m "add env"
$ echo ".env" >> .gitignore
$ git add .gitignore
$ git commit -m "add gitignore"
$ echo "secret=xyz789" > .env
现在执行 git status,请预测:
.env会出现在哪个区域(staged / not staged / untracked)?.gitignore对.env产生了什么效果?- 如果你现在执行
git diff,能看到.env的变更吗?
练习二:为新项目分类文件¶
一个 FastAPI 项目的根目录如下:
my_api/
├── src/
│ ├── main.py
│ └── models.py
├── tests/
│ └── fixtures/
│ └── expected_response.json
├── pyproject.toml
├── uv.lock
├── .python-version
├── .venv/
├── __pycache__/
├── .env
├── .env.example
├── docs/
│ └── openapi.json # 从 src/ 自动生成
└── coverage_report/
└── index.html # 测试覆盖率报告,可重新生成
请为每个文件/目录做出跟踪决策,并用本章的三个问题说明依据。对于"条件性"选择, 说明什么条件下选择跟踪、什么条件下选择不跟踪。
练习三:边界项的策略选择¶
你的团队正在讨论以下两个文件是否应该加入 .gitignore:
候选 A:tests/fixtures/large_dataset.csv(20MB,由测试脚本从生产数据
脱敏生成,生成过程需要访问生产数据库,耗时约 5 分钟)
候选 B:generated/api_client.py(从 OpenAPI 规范自动生成的 SDK 代码,
体积 15KB,生成命令 make generate-client 耗时 2 秒)
请回答:
- 用三个问题分别分析这两个候选。
- 给出你的跟踪建议,并说明在什么情况下你会改变建议。
- 如果候选 A 已经被跟踪了三个月,现在团队决定不再跟踪它,仅仅把路径加入
.gitignore够不够?为什么?
练习解析¶
练习一解析¶
1. .env 出现在哪个区域?
出现在 "Changes not staged for commit: modified: .env"。
原因:.env 在第二次 commit 时已经进入 tracked 状态。之后修改了它的内容
(从 secret=abc123 变为 secret=xyz789),Git 检测到 tracked 文件发生了
变更,所以它出现在 "not staged" 区域。
2. .gitignore 对 .env 产生了什么效果?
没有任何效果。.gitignore 只影响尚未被跟踪的文件。.env 在被加入
.gitignore 之前就已经是 tracked 状态了,ignore 规则不会改变已有的跟踪状态。
如果删除 .env 然后创建一个新的同名文件——即使路径相同,如果你先用
git rm --cached .env 将它从索引中移除,那之后新的 .env 才会被 .gitignore
覆盖。但在本题的操作序列中,没有执行过移除操作。
3. 能看到 .env 的变更吗?
能。git diff 会显示 .env 从 secret=abc123 变为 secret=xyz789 的差异。
因为 .env 仍然是 tracked 文件,Git 会正常追踪它的所有变更。
这恰恰是危险所在:你以为加了 .gitignore 就"保护"了 .env,但实际上每次
修改都会出现在 diff 中,如果不注意就会继续 commit 新的 secret 值到历史里。
练习二解析¶
| 文件/目录 | 跟踪? | 依据 |
|---|---|---|
src/ |
是 | 角色:项目定义,理解必需。权威来源:仓库。代价:小。 |
tests/fixtures/expected_response.json |
是 | 角色:测试的声明性输入,审阅必需。权威来源:仓库(手动构造)。代价:小。 |
pyproject.toml |
是 | 角色:依赖声明,重建必需。权威来源:仓库。代价:小。 |
uv.lock |
是 | 角色:确保一致性。权威来源:锁文件本身。代价:小到中。不跟踪的话不同时间安装可能得到不同版本。 |
.python-version |
是 | 角色:声明项目使用的 Python 版本,重建环境的参考。权威来源:仓库。代价:极小。 |
.venv/ |
否 | 角色:本地环境。权威来源:pyproject.toml(可重建)。代价:体积大,平台相关。 |
__pycache__/ |
否 | 角色:本地缓存。权威来源:源码(自动派生)。代价:无审阅价值。 |
.env |
否 | 角色:本地 secret 配置。含敏感值,机器相关。 |
.env.example |
是 | 角色:安全模板,告知协作者需要什么变量。权威来源:仓库。代价:极小。 |
docs/openapi.json |
条件性 | 角色:派生物,审阅有一定价值。权威来源:src/。代价:小。跟踪条件:团队希望在 PR 中直接看 API diff。不跟踪条件:团队认为源码为准,CI 可自动验证一致性。 |
coverage_report/ |
否 | 角色:测试运行的输出。权威来源:测试代码(可重建,pytest --cov)。代价:HTML 文件在 diff 中无意义。 |
练习三解析¶
候选 A 分析(large_dataset.csv,20MB):
- 问题一(角色):它是测试的输入数据。如果测试依赖它才能运行,那协作者 clone 后必须拥有它。
- 问题二(权威来源/重建确定性):从生产数据库脱敏生成——需要访问生产数据库。 协作者可能没有生产数据库访问权限,且生产数据可能随时间变化。重建不确定。
- 问题三(代价):20MB 比较大,会增加 clone 时间,但不算极端。
建议:倾向跟踪。理由是协作者可能无法重建(没有生产数据库权限),且测试 依赖它。但如果团队所有成员都有生产访问权限,且能接受 5 分钟重建时间,也可以 不跟踪而在 README 里说明重建方式。如果体积持续增长超过合理范围,可以考虑 Git LFS。
改变建议的条件:如果文件增长到 100MB+,或者团队有更好的 fixture 管理方案 (如独立的 fixture 仓库或对象存储),可以改为不跟踪。
候选 B 分析(generated/api_client.py,15KB):
- 问题一(角色):它是从 OpenAPI 规范派生的代码。协作者可以用一条命令在 2 秒 内重建。
- 问题二(权威来源/重建确定性):权威来源是 OpenAPI 规范(假设已跟踪), 生成结果确定性高(同一输入 → 同一输出)。
- 问题三(代价):体积小(15KB),但作为派生物,每次上游 spec 变化都会在 PR 中产生额外 diff,可能干扰 review。
建议:倾向不跟踪。理由是权威来源已跟踪、重建成本极低(2 秒)、结果确定。 可以在 Makefile 或 CI 中自动生成。
改变建议的条件:如果生成过程依赖外部工具且该工具版本不稳定(不同版本产生 不同输出),跟踪生成结果可以作为"快照"确保一致性。
3. 仅加入 .gitignore 够不够?
不够。候选 A 已经被跟踪了三个月,它存在于 Git 索引和历史中。把路径加入
.gitignore 只会让 Git 不再提示新的同路径 untracked 文件,但已
tracked 文件的状态不会改变。
你需要至少执行 git rm --cached tests/fixtures/large_dataset.csv 将它从索引
中移除(文件本身保留在磁盘上),然后 commit 这个变更。之后 .gitignore 才会
对这个路径生效。
而且,即使从索引中移除了,文件仍然存在于历史提交中。如果它包含敏感数据, 还需要历史重写——但本章不展开这个话题。
参考资料¶
- Git Documentation: gitignore——
.gitignore的匹配规则和作用范围 - Git Documentation: git-rm——
--cached选项的行为 - Git Documentation: git-status——tracked、untracked 和 ignored 文件的状态分类