2026-08-25

用 Git 开发一个多章节技术教程:从章节起点到验证与教学里程碑

以第三章为贯穿案例,使用 Git 实现章节继承、双轨开发、干净教学起点、里程碑同步与 patch 交付。

本课程由多个顺序开发的章节组成。后一章继承上一章的完成状态,同时保留独立、可构建的工程目录。Git 不只是保存最终代码,还负责固定章节起点、隔离先行验证与教学实施、标记读者里程碑、生成差异补丁,并把完成状态可靠地交给下一章。

本附录以第三章 integrating_simulink_codegen 为贯穿案例,展示以下完整循环:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
ch02 完成态
    ↓ 记录为 main 上的确定 commit
ch03 治理基线 B
    ├─ codex/ch03-verify:Agent 先行验证技术路线
    └─ course/ch03-milestones:人工与 Agent 重建教学路线
           m0 → m1 → … → mN
codex/ch03-teaching 与 tutorial/section3/m0 指向干净起点 m0
verify 与 mN 的受控内容收敛
main 快进到 mN,成为 ch04 的唯一正式起点

这里同时存在两种真实性:verify 记录技术路线如何被证明,milestones 记录课程如何被逐步实施。两条历史不必相同,但最终受版本控制的内容必须一致。

1. Git 管理的不是文件夹副本,而是项目状态

1.1 Commit:不可变的项目快照

一个 commit 记录整个受版本控制目录树、父 commit 以及提交说明等元数据。Commit 创建后不会原地改变;后续修改会形成新 commit。

因此,“从 ch02 终点开始”必须对应一个确定的 commit,而不能只依赖某台机器当前目录的外观。

人类排错/高级操作:确认当前提交身份

1
2
3
git status
git log --oneline --decorate --graph -n 20
git rev-parse HEAD

HEAD 表示当前检出的 commit。git rev-parse HEAD 输出的哈希值是精确的版本身份,可用于课程版本地图、验证记录和问题复现。

1.2 Branch:会向前移动的 commit 名称

Branch 不保存另一份物理源码,只是指向某个 commit 的引用。分支产生新提交后,引用向前移动。

1
2
3
B ── v1 ── v2    ← codex/ch03-verify
 \
  m0 ── m1       ← course/ch03-milestones

两个分支共享祖先 B,随后保留不同形成过程。切换分支时,Git 按目标 commit 更新同一个工作树,而不是进入另一份仓库。

1.3 HEAD、暂存区与工作树

Git 日常操作同时涉及三层状态:

1
2
HEAD                    Index / Staging Area       Working Tree
最近一次提交的快照      下一次准备提交的快照       磁盘上正在编辑的文件

人类日常操作:提交前检查三层状态

1
2
3
4
git status --short
git diff --name-only
git diff --cached --name-only
git diff --cached

Pre-commit hook 检查暂存区,因为它需要回答“下一次提交将包含什么”,而不是只看磁盘上有哪些修改。

1.4 Worktree:实际编辑文件的检出目录

仓库默认包含一个主工作树。git worktree add 可以建立 linked worktree,让多个分支同时出现在不同目录;但同一个本地分支通常不能同时检出到两个工作树。

本课程按顺序开发章节,只使用以下根工作树:

1
C:\Users\blancas\Documents\Teach\pmsm_zero_to_one

Verify、milestones 和 main 在同一目录中按阶段切换。这样 Codex、VS Code、终端和 CMake 始终面对同一个工作目录。

人类排错/高级操作:检查分支是否被额外 worktree 占用

1
git worktree list

出现“branch is already used by worktree”时,应先定位对应工作树和未提交内容。不得把 linked worktree 放入 build/,也不得在未知状态下直接删除其目录。

1.5 Sparse checkout:减少可见内容,不改变历史

Sparse checkout 只控制工作树展开哪些路径。未展开的章节仍存在于 Git commit 和 refs 中,并未被删除。

人类排错/高级操作:只展开第三章与治理目录

1
2
git sparse-checkout set chapters/ch03 docs .githooks
git sparse-checkout list

Cone 模式会同时保留仓库根目录文件。Sparse checkout 用于减少视觉干扰,不是权限系统;章节保护还需要分支约定、修改范围检查和 pre-commit hook。

2. 阶段一:把 ch02 完成态固定为 ch03 的来源

第三章起点来自 main 上已经验收的 ch02 完成态。开始之前,根工作树必须没有未提交内容;否则分支切换可能失败,或者把不属于治理基线的文件混入 B

人类日常操作:确认 main 和工作区状态

1
2
3
git switch main
git status --short
git log -1 --oneline --decorate

成功判据:git status --short 没有输出,最近提交是已经验收的 ch02 完成态。

如果工作区不干净,应先判断改动归属。属于有效工作的内容应在原分支提交;临时内容可以建立明确命名的 stash。不得为方便切换而执行 git reset --hard

人类排错/高级操作:临时保存尚不能提交的修改

1
2
git stash push --include-untracked --message "pause before ch03 baseline"
git stash list

Stash 是临时状态,不是课程里程碑。恢复后仍需整理为正常提交。

2.1 建立治理基线 B

治理基线只登记新章节路径、分支职责、写入范围和课程状态,不包含 ch03 最终技术实现。

完成治理文件修改后,在 main 创建基线提交:

人类日常操作:提交 ch03 治理基线

1
2
3
4
git add AGENTS.md README.md docs/course-version-map.md docs/worktree-policy.md .githooks/pre-commit
git diff --cached --name-status
git commit -m "course(ch03): establish chapter governance baseline"
git rev-parse HEAD

最后一个命令输出的 commit 即图中的 B。本课程不要求额外建立名为 B 的长期分支;B 是被版本地图记录的确定 commit。

状态变化如下:

1
ch02 complete ── B ← main

3. 阶段二:建立 verify 并搬运上一章终点

3.1 从 B 创建 verify

Agent/CI 验证:建立第三章先行验证分支

1
2
3
git switch -c codex/ch03-verify main
git branch --show-current
git merge-base --is-ancestor main codex/ch03-verify

成功判据:当前分支为 codex/ch03-verify,最后一条命令退出码为 0

3.2 使用 Git 导出确定的 ch02 工程树

直接从当前可见目录复制 ch02,可能把构建输出、旧日志或未提交修改一起带入新章。git archive 只导出指定 commit 中已经受版本控制的目录树,更适合建立可审计起点。

以下示例从 main 指向的基线 commit 中读取 ch02 工程,不要求 sparse checkout 长期展开 ch02。

Agent/CI 验证:导出 ch02 完成态并建立 ch03 工程目录

1
2
3
4
5
6
7
$Baseline = git rev-parse main
$Archive = Join-Path $env:TEMP 'pmsm-ch02-endpoint.tar'
$Target = 'chapters/ch03/code/integrating_simulink_codegen'

git archive --format=tar --output=$Archive "${Baseline}:chapters/ch02/code/three_phase_inverter"
New-Item -ItemType Directory -Force -Path $Target | Out-Null
tar -xf $Archive -C $Target

这一步的 Git 输入是 commit B 中的 ch02 子树,输出是 ch03 目录中的普通文件。Git 不保存“复制关系”;下一次 commit 只会记录新目录树。因为 ch02 原目录仍然存在,git diff 通常把这些文件显示为 ch03 新增文件,而不是 rename。

Agent/CI 验证:确认来源树和新目录状态

1
2
3
git ls-tree -r --name-only $Baseline chapters/ch02/code/three_phase_inverter
tar -tf $Archive | Select-Object -First 20
git status --short chapters/ch03

第一条命令列出 commit 中的真实来源,第二条命令检查归档内容,第三条命令显示新目录尚未提交的状态。

3.3 完成当前章工程身份重命名

独立化至少覆盖:

重命名后,ch03 不得在 Configure、Build、测试或生成过程中读取 ch02 目录。

人类日常操作:检查并提交 verify 起点

1
2
3
4
5
git status --short chapters/ch03
git add chapters/ch03
git diff --cached --name-status
git diff --cached --check
git commit -m "verify(ch03): establish independent ch02 completion snapshot"

成功判据:提交只包含 chapters/ch03/**,新工程能够独立 Configure 和 Build,旧机器日志与构建输出没有进入暂存区。

4. 阶段三:在 verify 中证明完整技术路线

Verify 用于尽早消除技术不确定性。Agent 可以在这里执行环境发现、自动化构建、接口验证、测试和证据采集,也可以保留必要的失败与修正提交。

Agent/CI 验证:观察 verify 独有历史

1
2
git log --oneline --decorate main..codex/ch03-verify
git diff --stat main..codex/ch03-verify

第一条命令显示 verify 在 B 之后增加了哪些 commit;第二条命令显示 verify 完成态相对基线改变了哪些文件。

Verify 提交应按真实技术问题划分,例如:

1
2
3
4
5
verify(ch03): establish independent ch02 completion snapshot
verify(ch03): import controlled Simulink generated snapshot
verify(ch03): add course-to-ExtU-ExtY adapter
verify(ch03): add host regression entry
verify(ch03): record build and board validation evidence

自动化成功只证明技术入口成立,不代表人工已经理解或完成 GUI 操作。因此 verify 走通后,不能把这些提交重命名为教学里程碑。

5. 阶段四:从 B 重新建立 milestones

5.1 从 main 而不是 verify 创建教学分支

Verify 完成并提交后,工作区必须恢复干净状态。

人类日常操作:确认切换条件

1
2
git status --short
git log -1 --oneline --decorate codex/ch03-verify

成功判据:第一条命令没有输出,第二条命令对应已经走通的 verify 完成态。

随后从仍指向 Bmain 创建 milestones:

人类日常操作:创建教学里程碑分支

1
2
3
git switch -c course/ch03-milestones main
git merge-base --is-ancestor main course/ch03-milestones
git log -1 --oneline --decorate

人类排错/高级操作:不得从 verify 创建 milestones

1
git switch -c course/ch03-milestones codex/ch03-verify

从 verify 创建 milestones 会继承 Agent 的探索历史,无法证明教学步骤确实从治理基线重新实施。

5.2 重新建立并提交 m0

在 milestones 分支重复“从 ch02 commit 导出、建立 ch03 独立工程、完成技术身份重命名”的操作。重复实施不是浪费:它证明章节起点可以从 Git 中确定性重建。

m0 必须满足:

人类日常操作:提交并固定 m0

1
2
3
4
git add chapters/ch03
git diff --cached --name-status
git commit -m "course(ch03): establish clean reader starting point"
git tag -a tutorial/section3/m0 -m "Chapter 3 clean reader starting point"

Tag 通常不会随新提交移动,适合固定读者里程碑;branch 会继续向前移动,适合持续开发。

5.3 让 teaching 分支精确指向 m0

教学入口不需要检出到第二个 worktree。可以直接建立一个未检出的 branch 引用:

人类日常操作:建立教学入口

1
2
3
git branch codex/ch03-teaching tutorial/section3/m0
git rev-parse codex/ch03-teaching
git rev-parse 'tutorial/section3/m0^{commit}'

两个 rev-parse 输出必须相同。若 teaching 分支已经存在,应先检查它的当前指向;不得用 git branch -f 掩盖未知状态。

6. 阶段五:人工与 Agent 协同形成 m1 到 mN

每个 milestone 都是一个可理解、可验证、可暂停的教学节点。每个节点同时保留人工入口和 Agent/CI 入口,两者使用同一份源码和配置。

人类日常操作:形成一个教学里程碑

1
2
3
4
5
6
7
git status --short
git diff --name-only
git add chapters/ch03
git diff --cached --name-status
git diff --cached --check
git commit -m "course(ch03): complete milestone m1"
git tag -a tutorial/section3/m1 -m "Chapter 3 milestone m1"

一个 milestone 的文档至少回答:

  1. 本步建立什么能力;
  2. 人工从哪个 GUI 或短命令入口开始;
  3. Agent/CI 从哪个确定性入口验证;
  4. 会修改和生成什么;
  5. 成功判据和产物位置;
  6. 在哪里暂停并检查结果。

Milestones 由人工与 Agent 协同形成,可能发现 verify 中没有暴露的接口、命名、GUI 顺序或验证问题。此时 milestone 内容暂时领先 verify 是正常状态,但这种分叉必须在本章验收前收敛。

7. 阶段六:把 milestone 改进同步回 verify

Git 提供多种同步手段。选择依据是改进如何形成,而不是追求统一命令。

7.1 Cherry-pick:同步边界清楚的独立提交

当某个 milestone commit 只包含一项可独立复用的修正时,可以把它重放到 verify:

Agent/CI 验证:检查并同步独立改进

1
2
3
4
git log --oneline codex/ch03-verify..course/ch03-milestones
git switch codex/ch03-verify
$MilestoneFix = '把上一步选定的完整 commit hash 填写在这里'
git cherry-pick $MilestoneFix

Cherry-pick 会创建新的 commit;新 commit 与原 commit 内容可能相同,但哈希通常不同,因为父提交和元数据不同。

7.2 收敛 patch:同步两棵完成态目录树的差异

当 milestone 改进跨越多个教学提交,而 verify 需要一个集中同步提交时,可以生成从 verify 到 milestones 的 patch。

Agent/CI 验证:生成内容收敛 patch

1
2
3
$SyncPatch = Join-Path $env:TEMP 'ch03-verify-to-milestones.patch'
git diff --binary --output=$SyncPatch codex/ch03-verify course/ch03-milestones
git apply --stat $SyncPatch

git diff A B 描述把状态 A 变成状态 B 所需的修改,因此这里的方向不能颠倒。

Agent/CI 验证:预检、应用并提交同步

1
2
3
4
5
6
git switch codex/ch03-verify
git status --short
git apply --check $SyncPatch
git apply --index $SyncPatch
git diff --cached --name-status
git commit -m "verify(ch03): synchronize validated milestone improvements"

git apply --check 只验证 patch 能否干净应用,不修改文件;检查通过后再使用 --index 同时更新工作树和暂存区。

7.3 为什么不直接合并 milestones

git merge course/ch03-milestones 会把完整教学历史并入 verify,表达“两条历史在这里汇合”。但本课程需要保留 verify 的探索叙事和 milestones 的教学叙事,因此通常采用 cherry-pick、收敛 patch 或按 verify 粒度重新实施。

Merge 不是技术上错误,而是多数情况下不符合这里需要表达的历史语义。

7.4 验证“内容相同、历史不同”

Agent/CI 验证:比较完成态内容和 commit 身份

1
2
3
git rev-parse codex/ch03-verify
git rev-parse course/ch03-milestones
git diff --exit-code codex/ch03-verify course/ch03-milestones

前两个哈希可以不同;最后一条命令必须以退出码 0 结束。该结果表示两个 ref 的受版本控制目录树一致,并不表示历史相同。

8. 阶段七:生成相邻教学里程碑 patch

git diff A B 可以保存为 patch,描述两个教学状态之间的文件变化。Patch 用于准确参考、排错和自动化重放,不替代人工操作。

Agent/CI 验证:生成 m0 到 m1 的课程 patch

1
2
3
4
5
6
$PatchDir = Join-Path $env:TEMP 'pmsm-course-patches/ch03'
$PatchFile = Join-Path $PatchDir 'm0-to-m1.patch'
New-Item -ItemType Directory -Force -Path $PatchDir | Out-Null

git diff --binary --output=$PatchFile tutorial/section3/m0 tutorial/section3/m1
git apply --stat $PatchFile

二进制差异通过 --binary 保留。使用 --output 而不是 PowerShell 文本重定向,可避免旧版 PowerShell 编码改变 patch 内容。

每个 patch 的说明至少记录:

git diff patch 主要配合 git apply,描述文件内容变化;git format-patch 生成的邮件格式 patch 主要配合 git am,还携带作者和提交说明。相邻教学状态强调“发生了哪些内容变化”,因此采用 git diff patch。

人类排错/高级操作:在干净起点预检课程 patch

1
2
3
git switch --detach tutorial/section3/m0
git apply --check $PatchFile
git switch course/ch03-milestones

Detached HEAD 适合临时检查固定 commit,但不适合持续开发。测试结束后应切回明确分支。

9. 阶段八:验收本章并推进 main

最终课程历史采用 milestones 轨道,因为它记录人工与 Agent 实际形成的教学顺序。Verify 提供独立的技术证明,不直接合入 main

推进前必须满足:

Agent/CI 验证:最终引用与内容检查

1
2
3
4
5
git status --short
git diff --exit-code codex/ch03-verify course/ch03-milestones
git rev-parse codex/ch03-teaching
git rev-parse 'tutorial/section3/m0^{commit}'
git log --oneline --decorate --graph --all -n 40

人类日常操作:将 main 快进到教学完成态

1
2
3
git switch main
git merge --ff-only course/ch03-milestones
git log -1 --oneline --decorate

Fast-forward 不创建额外 merge commit,只把 main 指针沿现有 milestones 历史向前移动。--ff-only 会在历史意外分叉时停止,避免自动制造未经设计的合并。

1
2
3
4
5
6
B ── m0 ── m1 ── … ── mN ← course/ch03-milestones, main
      └─ tutorial/section3/m0, codex/ch03-teaching

B ── v1 ── v2 ── … ── vN ← codex/ch03-verify
                         内容与 mN 相同,历史不同

此时 main 是 ch03 的累计完成态,也是 ch04 唯一正式起点。

10. 用 Hook 和检查命令保护章节边界

当前分支决定唯一技术写入根:

1
2
3
codex/ch03-* 或 course/ch03-*
允许技术写入 chapters/ch03/**

仓库把 hook 版本化到 .githooks/,再通过以下配置启用:

人类日常操作:启用并检查仓库 hook

1
2
git config core.hooksPath .githooks
git config --get core.hooksPath

Pre-commit hook 从当前分支解析 chXX,再检查暂存路径。新增章节时复用同一套逻辑,不为每章复制脚本。

Agent/CI 验证:提交前范围检查

1
2
3
git branch --show-current
git diff --cached --name-only
git diff --cached --check

本地 hook 是快速反馈入口,不能成为唯一防线。CI 应执行同等的暂存路径或 commit 差异范围检查。

11. 常见 Git 问题与安全处理

分支切换被未提交修改阻止

先运行 git status --short,确定改动属于有效工作、临时实验还是构建输出。有效工作应提交到正确分支;临时实验可使用明确命名的 stash;构建输出应进入忽略目录。

文件被错误加入暂存区

人类排错/高级操作:只取消暂存,不丢弃工作区内容

1
2
git restore --staged chapters/ch03/code/integrating_simulink_codegen/CMakeLists.txt
git status --short

这不会删除磁盘上的修改,只把文件移出下一次提交快照。

分支已被另一个 worktree 占用

运行 git worktree list,检查实际目录、分支和未提交内容。只有在确认 linked worktree 已不再需要且工作区干净后,才设计移除步骤。本课程的正常开发流程不创建 linked worktree。

Patch 无法应用

先确认当前 commit 是否为 patch 声明的来源里程碑,再运行 git apply --check。Patch 应用失败通常表示起点不一致、相同区域已经修改,或者生成方向颠倒。

Verify 与 milestones 比较仍有差异

Agent/CI 验证:逐层缩小差异范围

1
2
3
git diff --name-status codex/ch03-verify course/ch03-milestones
git diff --stat codex/ch03-verify course/ch03-milestones
git diff codex/ch03-verify course/ch03-milestones -- chapters/ch03

差异必须被同步或明确排除后才能推进 main。机器日志和构建输出应位于忽略目录,不应参与受控文件树比较。

12. 每章都重复同一个 Git 闭环

章节编号和工程内容会变化,Git 闭环保持不变:

  1. main 固定上一章已验收完成态;
  2. main 创建当前章治理基线 B
  3. B 建立 verify,并用 Git 导出上一章受控工程树;
  4. Verify 先行证明完整技术路线;
  5. B 重新建立 milestones 和干净 m0
  6. 人工与 Agent 协同形成 m1mN
  7. Milestone 改进同步回 verify;
  8. Git 验证两条轨道内容一致;
  9. 生成相邻里程碑 patch;
  10. main 以 fast-forward 前进到 mN

这套流程让 Git 同时承担四种职责:版本数据库、章节边界、教学状态机和复现工具。最终结果不仅是一个可以构建的工程,也是一条可以审计、学习、验证和继续继承的课程历史。