第3讲 · 软件仓库管理

课程 南京大学《生成式软件工程》· 第三讲(版本管理 / 软件仓库管理)
视频 软件仓库管理(时长约 100 分钟)
讲义与幻灯片 由课程主页 jyywiki.cn 发布,讲义见讲次笔记,幻灯片见 course/slides_text.md
性质 本书由课堂实录(口语)重构为书面教程;凡讲师未展开、由编者补充的内容,均以 编者说明 / 编者拓展 明确标出。
许可 课程讲义、幻灯片与代码版权归作者(蒋炎岩)所有,依 CC BY-NC 4.0 许可发布。

对应讲次:第3讲(南京大学 生成式软件工程)

制作模型:deepseek-flash(vb compose,据 run.log)


一、开场:GPT-6 之后,我们还要学什么

(参考时间: 00:00:38)

快讯:GPT-6 发布了

📷 快讯:GPT-6 发布了
回到原视频 (00:00:38)

前两讲讨论的是怎么把 AI 整合进生活流程:生活中会遇到无穷多「不顺心」,只要把真实意图讲清楚,AI 就能替我们挡掉这些琐事,甚至把过程中的经验蒸馏成可复用的技能(skill),让下一次更平顺。这一讲开场,先更新一下模型进展的现场——GPT-6 已经发布。

讲师的现场使用感受是:惊喜确实有,但编程能力的提升有限,尚未学会「好品味(taste)」。有经验的软件工程师凭品味可以指引 AI 做出更好的东西,而今天的 AI 更多是「完成任务」;于是仍然需要不断批评、反馈。用讲师的原话说:

「我经常还要骂它,所以我觉得我可能还能再苟活个几个月。」(参考时间: 00:01:07)

不过在更复杂的任务上,模型更快、更省 token 了;讲师还专门 measure 过这一点。

但设计上还是要骂

📷 但设计上还是要骂
回到原视频 (00:01:06)

为什么我们总觉得「模型做得不好」

(参考时间: 00:01:25)

关键在于,我们面对的是一大片非常开放的 intent–spec–impl 空间。同样一个任务交给人,哪怕是顶级产品经理,各自脑子里想的都不一样;AI 无法与人脑直接对齐,它一定会按自己的方式漂移。这也延续了前两讲的主题:从意图(Intent),到规格(Spec),再到实现(Impl),中间存在极其开放的选择空间。你说「做一个好用的界面」,往往要看到第一版,才说得清自己真正介意的是什么——实现反过来在帮我们确定需求。

另一个新现象是:AI 写出的代码,人类越来越难读了。讲师观察到从 GPT-5、5.6 到 6,模型的思维链越来越紧凑,输出的代码也越来越紧凑,人几乎无法 review。过去大家还会用提示词要求「写出符合人类工程规范的代码」,现在连这一层也慢慢放弃——反正代码能跑对就行。

编者拓展|开放问题为什么能逐步收敛 幻灯片引用了 Simon 在 1973 年的论断:结构良好(well-structured)与结构不良(ill-structured)问题之间的边界是流动的(The Structure of Ill Structured Problems)。这与本讲的乐观基调一致:每一次反馈,都在把「我想要」翻译成更明确的约束。今天还需要骂,并不等于这是永久的能力边界。

「理应当做到」的事就是能做到

(参考时间: 00:02:32)

「理应当做到」的事就是能做到

📷 「理应当做到」的事就是能做到
回到原视频 (00:02:32)

GPT-6 最大的惊喜,是空间感知与理解能力的跃升。讲师由此谈了他对 OpenAI AGI 路线图的理解:一切对齐到数学逻辑推理。

编者说明:这套「数学优先」的路线判断,是讲师从使用经验出发的个人理解,并非 OpenAI 的官方表述;把它当作一种解释框架即可。

从鲁班锁到机械臂

(参考时间: 00:04:16)

讲师认为,空间感知不是「绝对学不会」的能力。让你面对一个复杂的鲁班锁,问「按什么方向能抽出第一块」;或者做智力题里那种立方体投影、骰子展开图对应的判断,人靠的是脑子里的空间想象——只要从简单任务开始做强化学习,模型也能学会。

这里有一段思想的起源:看到 GPT-6 那个空间能力的例子时(当时还没发布),讲师「心疼自己的 PhD student」——他们已经做了小半年的 3D 打印工作。在 GPT-5.6 空间感知很差的情况下,师生靠直接对话也能生成一个像样的、符合规范的 3D 打印模型。他们有一个比较 killer 的应用:做那种光栅玩具(从左边看和右边看是两张不同图案),本来需要一棵很长的搜索树来做「光栅纸」;结果昨天 GPT-6 one-shot 直接搞定。现在团队正在想怎么解决这个问题。

编者拓展|光栅立体画(lenticular / 柱透镜光栅) 这类玩具用一层柱透镜光栅把两张(或多张)交错切割的图样分别折射到不同视角:左眼看 A、右眼看 B。制作难点在于「交织(interlacing)」:需要按光栅的透镜间距,把每张图按列切片后精确错位合成,列对齐差一个像素就会串图。所以讲师说过去要靠「很长的搜索树」来试参数;而一个具备空间与数学能力的模型,理论上可以一步推出间距与相位。

既然有了空间感知与数学能力,模型在具身(embodied)赛道上就能做得很好。第二天就有人在社交平台上发帖:直接用 GPT-6 控制机械臂完成抓取、叠衣服等复杂动作。注意 GPT-6 本身是语言模型,虽然带视觉能力,但只是「一帧一帧地问下一步做什么」,尚未针对这种场景做专门的提示词工程,就已经达到相当好、甚至 state-of-the-art 的水平了。模型一定会继续进步。

汽车类比:我们为什么还要学习

(参考时间: 00:06:42)

Anyway... 我们还是要学习的

📷 Anyway... 我们还是要学习的
回到原视频 (00:06:42)

既然模型进步这么快,我们还要学什么?有人问讲师:「AI 时代还要不要学数据库、学操作系统?」讲师打过一个比方:

AI 就有点像这种「更强的东西」:它替代的不只是人的脚力,而是人的智力。那我们还学不学?讲师的当前答案是要学——因为学了正确的概念,你才能更好地解锁 AI 的创造力;但「以后该怎么学」,他自己也暂时不知道,欢迎同学来探讨。

编者拓展|专家的知识是围绕核心概念组织的 幻灯片引用了《How People Learn (2000)》的结论:专家的知识围绕核心概念组织,而这种组织方式帮助人在新情境中迁移、运用知识。这正是「AI 时代仍要学基础」的认知科学依据——你需要的不是记住操作步骤,而是建立可外推的概念结构。


二、一个目录,为什么还不算管好了项目

(参考时间: 00:09:45)

什么是一个软件工程项目?

📷 什么是一个软件工程项目?
回到原视频 (00:09:45)

今天我们进入真正的软件工程任务:从零开始构建一个项目。那么第一步做什么?如果是做作业,你会从课程网站或老师那里下载一个东西;但如果是真的想做一个项目,第一件事一定是先建一个目录,给它起个名字(比如「我的最酷的 game」),然后开始 web coding。

一个目录,一组文件,一个可交付的东西

(参考时间: 00:10:26)

回想你是怎么学会「该做什么」的:看到老师打开一个工程,里面有若干文件,你在不经意间记住了一些东西;同学或老师推荐某个项目,你拿到一个 GitHub 链接,打开那个对人类很友好的网页,看到一些文件——于是你知道一个目录里该有一些文件。这些文件共同构成一个可交付的东西。

graph TD
    A["项目目录"] --> B["需求说明 / 接口文档 / 源代码 / 构建脚本"]
    B --> C["构建 build / 运行"]
    C --> D["可交付物:本地网页、可执行文件……"]

一个著名的笑话是:有人发帖说「GPT-6 太牛了,我又做了多少个 killer 级产品」,结果它们全都是 http://localhost:5173 上的页面——世界上的其他人打不开。因为 localhost 只是你本机。

编者说明:这引出 intent 与 spec 的重要性。如果你在意图文档里说明「这个东西要被全世界的人看到」,模型就会去考虑部署;如果你不说,模型默认把你当成开发者,先起一个本地 development server 调试,等 ready 了再推到远端。

在计算机里,「软件」无处不在:不只是你眼前的屏幕,连教室里这张可升降的桌子也得靠软件才能升起、降下(此刻桌面高度 106 厘米)。老先生爱问「什么是软件」,标准定义是「代码、文档等一切与软件相关的东西的集合」。而讲师更愿意说:软件是人类世界的需求在信息世界里的一个投影——这个投影今天呈现为一个目录,目录里有各种各样的东西。

古早时期:That's All

(参考时间: 00:13:41)

古早时期:That's All

📷 古早时期:That's All
回到原视频 (00:13:41)

目录里有 hello.c,编译出一个 hello,你其实就已经在做软件了。只是这不符合现代软件工程的 best practice。十年、十几年前,大家真的就是这样做的。讲师讲了自己第一个博士生的故事:

很长一段时间,大家都是这样管理代码的。讲师自己上学时,交作业的方式是打一个压缩包发邮件;现在的同学改成网页上传,交作业是方便了,但「单靠目录里存着文件」的项目管理方式,并没有发生根本变化。

今天:Git

(参考时间: 00:15:14)

今天:Git

📷 今天:Git
回到原视频 (00:15:14)

今天,软件工程有了标准答案:用 Git。

git clone https://.../homework.git   # 获取作业
git pull                              # 接收老师的更新
make submit                           # 打包并提交(由课程 Makefile 定义)

编者说明:make submit 是课程项目约定的入口,由项目里的 Makefile 定义,不是 Git 自带的命令。Git 负责版本,项目规则负责构建、验证与交付。这套「打包—上传」的流程有点像现代 CI/CD,本课后面会专门讲一次。

讲师补充:过去发代码、发作业都靠邮件(讲师甚至回忆自己「发邮件管理项目」的年代),而今天是 AI 时代,有十倍、百倍的生产力提升——他有时用 Codex 写完一封投诉信后,选择直接信任它,看都不看一眼,让它结束时顺手把浏览器关掉。

编者拓展|RCS、CVS、SVN 与 Git 的谱系 版本管理并非一蹴而就:更早的 RCS 只管单个文件的历史;CVS、SVN 则把历史放到一台服务器上,大家取出、修改、再交回;Git 让开发者本地就拥有完整历史。脉络的细节见 Git 简史。


三、学习变容易以后,留意变得更重要

(参考时间: 00:17:00)

学习变容易以后,学习意识更重要

📷 学习变容易以后,学习意识更重要
回到原视频 (00:17:00)

讲师几乎每节课都要讲这件事:这门课讲的知识都不重要,重要的是你的学习意识。

现在到处都能看到 GitHub 链接,每一个真实项目都可能成为学习素材。问题在于——看到很多项目,不会自动把它们的设计变成你的知识。你可能打开过几十个仓库,却没注意过:有的项目有某个文件、有的没有;有的文档这样写、有的那样写。在大量信号不断进入的过程中,你本可以判断出「Git 应该怎么用」。

很多同学带着一种中学心态:老师说做什么就做什么,绝不多碰一点点,因为那最省时间。但 AI 其实也被「调」成了这样——老师说把作业拉下来,它就只拉作业。讲师劝大家愿意多花一点点时间点进去看一看:比如满世界的 GitHub 项目都是巨长无比的 README,用各种 Unicode 字符拼出目录结构框图,一股「AI slop(AI 生成的廉价内容)」味;而某个维护了几十年的老项目,README 清新舒服,很短,看完之后你想去的地方就靠三个链接,不用读那么长的 slop。于是你就学会了文档该怎么写。

编者拓展|从路牌对比学 UI 设计 讲师举了一个很有画面感的例子:在香港找路时,每当出现不确定性(比如前方有三个岔口),正前方一定有一块路牌告诉你三个口分别通向哪里;而回到南京,有时要在地铁站里环顾四周、走很远才找到一张地图。 他从这种对比里学到的 lesson 是:如果我要做 UI 设计,就应当站在「人站在这个页面上会想什么」的角度,让 A、B、C 三类不同需求的人,都能用最少的方式得到各自要的信息。这就是「上课耽误学习」的另一面——课外的信息量与信息密度已经远超任何一个课堂,只是传统教学没教过我们怎么用好它。


四、从「我想撤销」推导出快照

(参考时间: 00:25:07)

Git:听说过,还是用过?

📷 Git:听说过,还是用过?
回到原视频 (00:25:07)

上周保研面试的现场

讲师上周末参加了推免(保研)面试,问了几个问题:

讲师说,这不只是「少背了几个术语」。第一,是真没用过;第二,是没有留意。 就算平时一直用 AI 帮你解决 merge 冲突,你也很容易遇到冲突——尤其是让 Agent 在不同分支上开发时。你只需要一点注意力:在一两次冲突里停下来看一眼,就会看到好多个等号、好多尖括号、一些 incoming 标记;至少能说出「两边冲突的代码会同时显示在一起」。

从需求出发,自己推导出 Git

(参考时间: 00:29:14)

Git 之前,版本怎么管?

📷 Git 之前,版本怎么管?
回到原视频 (00:29:14)

最朴素的做法是:复制整个目录,分别叫作「作业-v1」「作业-v2」「作业-最终版」。这能找回旧文件——新手阶段手滑、改错很正常,你想回去看看上一版是怎么写的,这就是一个需求。但版本一多,「哪份从哪份改来、谁改了什么」就要靠人记。

从需求出发,你自己就能发明一个简化版的 Git。 与其叫「作业一、作业二、作业三」,不如只保留一个作业目录,再提供一个命令行工具:list 一下版本、把当前版本「回到」过去的某个版本。用着用着,你又会自然发明出分支、合并、冲突——比如你要和室友同步,两人一起改了同一个文件,这时就得坐下来想「一起改同一个文件该怎么办」。

graph TD
    N["我要回到上一个版本"] --> V["需要保存目录的多个版本"]
    V --> T["一个目录 + 一个能切换版本的命令行工具"]
    T --> B["和室友同时改同一文件 → 需要分支与合并"]
    B --> C["两人改了同一段 → 需要冲突标记与解决"]

讲师说,这也体现了人类的局限:这些概念其实都能顺理成章地推出来,但大家没有去留意。

SVN 与 Git:存「变化」还是存「快照」

(参考时间: 00:31:30)

在 Git 之前,大家用过 SVN(check in / check out)。两者的设计不同:

所以 Git「基本上走对了路」。它的作者正是 Linux 的作者:

Git 从哪里来?

📷 Git 从哪里来?
回到原视频 (00:32:39)

我们要的是项目级「回溯时间」

(参考时间: 00:33:56)

我们要的是项目级「回溯时间」

📷 我们要的是项目级「回溯时间」
回到原视频 (00:33:56)

人和 Agent 都会犯错误:设计会被推翻,对的代码会被改错,手滑还会把文件删掉。所以我们需要一种项目级的「回溯时间」:只要那个状态已经保存在 Git 历史里,就能捞回来。

最重要的概念是:Git 管理项目的快照。 一次提交(commit)记下纳入版本管理的文件在当时的内容、以及它们在目录里的位置。

为什么要以「项目」为单位? 因为源代码、接口和配置经常要一起变化。只恢复旧代码、却留下新配置,可能拼出一个历史上从未正常运行过的版本。

「存过盘」不等于「存进历史」:你在编辑器里存盘,和 Git 里提交,保存的是不同层次的状态;你希望将来能回到的状态,需要提交保存。

从已有经验,长出 Git 的概念

(参考时间: 00:34:07)

从已有经验,长出 Git 的概念

📷 从已有经验,长出 Git 的概念
回到原视频 (00:34:07)

幻灯片把学习方式概括为「外推」:「现在的你」是一个知识网络,学习就是自然地把它向外推一点。

graph TD
    E1["你知道改错了想撤销"] --> G1["项目也需要能回去的旧状态"]
    E2["你知道几个文件要配套"] --> G2["应当记住项目在同一时刻的样子"]
    E3["你知道多人会同时修改"] --> G3["需要知道各自从哪里出发,才能汇合改动"]

你可以试着自己预测:删除一个已经提交过的文件,再提交一次,旧版本里还能找到它吗? 能——新快照里没有它,旧快照仍然记着它。

编者说明:这正是「快照」与「差异」两种世界观的分野。Git 提交记录的是一棵目录树在该时刻的完整样子,父提交记录它从哪里来,于是许多状态串成历史;旧快照里删除的文件依然存在。存储层面,Git 用对象保存内容与目录结构,未变化的部分可以复用——逻辑上是完整快照,物理上共享数据(详见下文)。参考 Git 对象。


五、从 init、add、commit 到分支与合并

(参考时间: 00:34:57)

从空白开始

📷 从空白开始
回到原视频 (00:34:57)

现在从空白开始。在终端里执行 git init:

$ git init
Initialized empty Git repository in /path/to/project/.git/

它会告诉你一些「奇怪的信息」。这时你有两种选择:让 Agent 帮你解释你不懂的地方,或者直接忽略。讲师说,显然是前者更高效。

终端中执行 git init 后产生隐藏的 .git 目录

📷 终端中执行 git init 后产生隐藏的 .git 目录
回到原视频 (00:35:43)

执行完之后,你的项目目录里就多了一个隐藏的 .git 目录(默认不显示,所以你看不到它)。这就是全部——你的项目从此成了一个被 Git 版本管理的仓库(repository,简称 repo)。你还可以用 git clone 从远端建立本地副本,或把本地代码推上去。今天 GitHub、GitLab 可以托管远端仓库,但在本地记录版本并不要求先连接这些平台。

三个区域:工作区、暂存区、提交历史

graph LR
    W["工作区<br/>你正在编辑的文件"] -- git add --> S["暂存区 (Staging Area / Index)<br/>下一次提交准备采用的内容"]
    S -- git commit --> H["提交历史<br/>已记录下来的快照"]
    W -- git diff --> S
    S -- git diff --cached --> H

git add hello.c 会把此刻 hello.c 的内容放入暂存区;普通的 git commit 再据此创建提交。用一个你值得亲手试一遍的问题来检验理解:

先修改 hello.c,执行 git add hello.c,然后又修改它,最后执行 git commit——提交的是哪个版本?

答案是 add 时暂存的版本。后续的修改仍在工作区,要再次 add 才会进入这次提交。可以用 git diff 观察尚未暂存的修改,用 git diff --cached 观察暂存区相对当前提交的变化。暂存区让你能组织一次提交的内容,而不是被迫把此刻所有编辑一并记录下来。 参考 git add 文档。

编者说明:「会运行一个命令」和「知道它改变了什么」是两回事。上面这个实验就是训练后者的最小入口。

分支与合并

(参考时间: 00:31:30 起为背景,命令细节见全程演示)

分支让项目能从同一个起点沿不同方向推进。你把分支名理解为指向某个提交的可移动引用;在通常的工作方式下,HEAD 表示当前所在的分支。你在该分支上创建新提交,分支引用就向前移动;另一个分支仍然停在原处,或沿自己的路线前进。

graph TD
    C0["共同祖先"] --> A1["你在改界面"]
    C0 --> B1["同学在修解析器"]
    A1 --> A2["提交 A2"]
    B1 --> B2["提交 B2"]
    A2 --> M["合并提交(两个父提交)"]
    B2 --> M

假设你和同学从同一版本开始,一个改界面、一个修解析器,git merge 会结合共同祖先与双方变化,尝试得到合并结果:

当双方对同一段文本做了无法自动协调的修改,Git 通常会在工作区文件里写入 <<<<<<<、=======、>>>>>>> 等标记,展示需要人工决定的内容,同时在 git status 中报告 both modified 之类的状态:

<<<<<<< HEAD
int main(void) {   // 你这一侧
=======
int main() {       // 对方那一侧
>>>>>>> feature-b
合并冲突后工作区文件里出现的冲突标记

📷 合并冲突后工作区文件里出现的冲突标记
回到原视频 (01:16:57)

解决冲突,就是阅读上下文,把文件编辑成最终应有的内容,移除标记,再用 git add 标记已解决,并完成合并。二进制文件、删除与修改冲突等情况不一定呈现同样的文本标记,所以关键是同时检查状态和文件。参考 分支与合并。

编者说明:冲突标记之所以「把双方内容和标签一起写进文件」,是 Git 设计者从 first principle 出发的选择——一旦发生冲突,就把解决问题所需的全部信息放在「你此刻必然会看到的地方」。讲师说,这和他举的香港路牌是同一个原则。若用 IDE,冲突区域上方还会出现「接受本地/接受对方」之类的按钮,你可以在编辑器里直接决定谁进谁出,然后创建一个提交把它 resolve 掉。

rebase

git rebase 则把一串提交所代表的改动,依次重放到新的起点上。例如你基于旧主线做了两次修改,现在主线已经前进,可以把这两次修改重新应用到新主线之后。重放得到的提交通常有新的标识,过程中也可能冲突。它适合在约定允许的范围内整理工作;如果别人已经依赖原来的提交,你再改写它们,就会增加协作成本。参考 Rebase。

理解这些操作后,再回头看命令,就不再只是「记住输入什么」。你可以在操作前预测:工作区会变吗?下一次提交包含什么?哪个引用会移动?历史里会增加什么关系? 这才是可以继续外推的知识。

Git 底层:一个会持久化的数据结构

(参考时间: 01:03:44)

讲解师在操作系统课上已经讲过 Git 底层,这里只讲一个「头」:Git 是一个持久化的数据结构,持久化的是目录树。

设想你有一棵很大的项目树(根目录 hi 下有 A.txt、B.txt,以及子目录 D……)。现在要创建 V2,你不需要把每个文件都复制一份:

graph TD
    subgraph V1
      R1["根目录树"] --> A1["A.txt (blob)"]
      R1 --> B1["B.txt 旧内容 (blob)"]
      R1 --> D1["D 子树 (tree)"]
    end
    subgraph V2
      R2["新根目录树"] --> A2["指向同一个 A.txt 对象"]
      R2 --> B2["B.txt 新内容 (blob)"]
      R2 --> D2["指向同一个 D 子树"]
    end

如果你只改了 B.txt,那么从被改文件到根目录的路径必须复制(否则根目录还会指向旧的 B.txt),但 A.txt、D 这些未变化的部分,可以用指针指过去。即使项目里有 100 万个文件,目录本身也不会很深(没人会建 1000 层深的目录),每一级索引都在指数级地「把类分完」,所以你只需创建一个非常小的增量,就得到了一份版本快照。

这也解释了 Git 底层 .git 目录里那些带 hash 索引的 objects——它们就是普通文件,是各种节点:目录的节点、文件的节点,甚至有 commit 的节点。

三大类型(commit object、tree object、blob object)构成一个可以不断向后生长的数据结构。从 first principle 出发,一个大学生想要设计出这样的东西,是完全有可能的。


六、哪怕只有 hello.c,也需要工程判断

(参考时间: 00:38:29)

获取下一级知识

📷 获取下一级知识
回到原视频 (00:38:29)

教程把几个命令教完,似乎就可以开工了。但软件和系统里还有大量约定俗成的 best practice——它们经常来自一次次踩坑,或者开发者对旧设计忍无可忍后的改进。讲师说得很直接:「绝大部分人都倒在了『获取下一级知识』的路上。」

多问一句:为什么、该不该?

📷 多问一句:为什么、该不该?
回到原视频 (00:43:30)

以最朴素的 hello.c 为例:目录里同时有 hello.c 和编译得到的 hello,要不要一起提交?

「我的目录里有什么」和「项目需要共同保存什么」,是两个问题。

让「下一步判断」长出解释

你可以把自己的学习需要写进 AGENTS.md。幻灯片给出了一条学习约定:

在做任何 git 操作之前,都逐个文件检查变化,并为我(新手)解释
是否符合 best practice,为什么,以及相关的概念。

这条约定的价值,在于让具体变化成为学习入口。面对一个文件,先判断它承担什么作用,再决定是否提交:

graph TD
    F["某个文件"] --> Q1{"它是项目的输入,还是构建/运行的产物?"}
    Q1 -- 输入 --> K["保存(如 hello.c)"]
    Q1 -- 产物 --> Q2{"别人拿到仓库,能否重建它?"}
    Q2 -- 能 --> I["不提交,写进 .gitignore"]
    Q2 -- 不能 --> Q3["说明构建方法,必要时才提交生成物"]

进组以后的高血压时刻

(参考时间: 00:55:16)

进组以后的高血压时刻

📷 进组以后的高血压时刻
回到原视频 (00:55:16)

讲师说,在 LLM 之前,新同学进组通常会有一段「高血压时刻」——开完 Git 仓库,进去一看就血压上来。他把反复提醒新生的几类问题放在一起:

关于 .DS_Store 有一段插曲:在苹果系统上用 Finder 打开一个目录,它会自动留下索引(记录了哪些子目录被展开),放在 .DS_Store 里。这个文件以点开头,默认隐藏,ls 时你看不到它;于是一个 git add . 就把它悄悄追踪进去了。git add . 本身是合理的(因为 .github/workflow 等点开头的约定文件也应当被纳入),但对这个会频繁变化的临时文件就有点糟糕。

在 macOS 上 git add . 后 .DS_Store 被误追踪

📷 在 macOS 上 git add . 后 .DS_Store 被误追踪
回到原视频 (00:57:12)

重要的是——这些不是按文件名机械执行的禁令,规则是活的:

需要解释的是:这个文件为何构成共同维护的项目状态,以及怎样保证它与其他文件一致。

.gitignore 可以把这些约定落实为忽略规则,但它不会让已经被跟踪的文件自动退出版本管理,更不会抹掉历史(参考 gitignore 文档)。这也解释了为什么误提交密钥后,仅仅删除当前文件不够:旧提交可能仍保留它,别人也可能已经取得副本。版本管理帮我们记住过去;不该公开的东西一旦进入过去,就不能假装它从未发生。

编者说明:这几类问题背后其实有一个统一的判据——这个文件是否表达项目意图、是否参与项目构建、是否可移植地重建。把它想清楚,比背「哪些文件名不能提交」更管用。

讲师还提到,他自己做软件工程研究时,一个非常标准的研究方向就是:检测一个 Git 项目满不满足这些 best practice,不满足就给出 warning(例如针对 .DS_Store 这类问题写一条规则),最后做成一个小工具,在 git commit 之前告诉你 OK 不 OK。但所有这些东西都被大模型杀死了——因为大模型可以如此耐心地给你检查,还附赠一个 emoji 缓和情绪。

编者拓展|「导师的高血压」与 AI 的解放作用 讲师讲了导师批评学生的场景:作为导师要装作 nice,学生听到批评也会有压力。而 AI 作为一个「你无法控制的力量」,可以不带情绪地反复解释 best practice,把学生从这种心理压力中解放出来。讲师特别强调,很多时候模型表达得比他现场讲得更好——知识内化在他脑子里,但课堂上要同时专注讲授,「思考的那部分容量就不够了」,讲出来的往往不是最好的版本(他自称是「省电模式」)。他建议:回头把「为什么要这样写 commit message」之类的问题,去和 ChatGPT 这类「知道世界上所有东西」的助手聊清楚。


七、大写还是小写,也值得追问

(参考时间: 00:59:51)

大写还是小写?

📷 大写还是小写?
回到原视频 (00:59:51)

在做一个更严肃的项目时,讲师需要在 pre-commit hook 上做若干检查(他可能有几十条 checking rules),于是要给这些规则命名——这就出现了一个两难的小设计选择:

[TYPO-CHECK] Docs should be typo-free
[typo-check] Docs should be typo-free

这是同一条文档检查规则的两种写法。讲师的直觉是「大写似乎更好,但不是极强」,一时又说不清原因。这样的犹豫很有价值:它提示我们,直觉背后可能有可以讨论的设计理由。

这里,我会选 TYPO-CHECK

📷 这里,我会选 TYPO-CHECK
回到原视频 (01:02:42)

方括号里的名字是固定的规则标识,后面则是解释规则的自然语言。大写让两者更容易在视觉上区分;讨论「违反了 TYPO-CHECK」时,它也更像一个明确的引用。这样,读者可以迅速区分「我们正在引用哪条规则」和「这条规则说了什么」。

编者拓展|约定的字形承载特殊含义(RFC 8174) 讲师让 AI(ChatGPT)帮他分析「为什么会觉得全大写更强」,AI 引用了 RFC 8174。该文档明确:MUST、SHOULD 等关键词仅在大写时才承载约定的特殊含义,以避免与普通行文混淆。这是一个很好的类比——但它没有规定 check-rule 必须大写。如果整个项目已经使用小写标识,或处理规则的工具要求小写,保持一致可能比局部的醒目更重要。我们得到的不是「大写永远优于小写」,而是理解了这个选择涉及角色区分、可引用性与一致性。

对一个微小细节多问一句「为什么」,也是在训练设计能力。


八、让工具把真实细节展开

(参考时间: 01:08:05)

告别古法学习

📷 告别古法学习
回到原视频 (01:08:05)

课堂上有一句半开玩笑的「计算机科学定律」:你想要的,就一定有人做。 遇到难以理解的概念,可以先寻找有没有工具把它直观地展示出来——人类视觉系统是非常高效的。现在还多了一种可能:把自己的学习需要写清楚,让 Agent 制作一个工具。

讲师先展示了「古法编程的巅峰之作」之一:一个以可视化方式展示 Git 如何工作的网页工具 Learn Git Branching。它模拟了各种命令(git status、git commit),右下角会随命令动画式地长出新的版本,点某个版本还能 git checkout 看到变化——它教你 Git 是怎么工作的。

编者说明:Learn Git Branching 是模拟出来的,它内置了一个 Git 模型。

把真实的 Git Repo 展开给你看

(参考时间: 01:10:31)

把真实的 Git Repo 展开给你看

📷 把真实的 Git Repo 展开给你看
回到原视频 (01:10:31)

今天,我们可以直接在 live 的、真实的 Git 仓库上看到它发生了什么。讲师现场 web coding 了一个工具,给模型的提示词大致如下:

实现一个 gitv 命令行工具(Node.js),把一个真实的 Git repo(可以使用 git 命令)路径里的主要概念彻底、完整地可视化。展示任何对象之前,都从文件系统里的真实文件、原始 bytes 出发,展示一步步如何解析、解析成了什么。通过空间合理的方式排布各类对象,并支持拖动;明快、极简的风格,不要多余的 tooltip。大小文件的可视化都要有不错的视觉效果,对于有具体内容的文件,有适当的大小和视觉风格显示内容、diff 等。启动一个 HTTP server,持续监听目录中 Git 的变化,并在发生变化后用 CSS 动画效果展示 repo 的变化。浅色背景适合课堂展示。

gitv 工具实时可视化真实的 Git 仓库:左侧对象图与右侧文件状态联动

📷 gitv 工具实时可视化真实的 Git 仓库:左侧对象图与右侧文件状态联动
回到原视频 (01:10:11)

演示中的关键观察:

分支就是一个文件:refs/heads/A 这样一个指针,HEAD 则是「指向指针的指针」。在磁盘上它就是一段字节,指向一个 commit object,commit 再指向一个 tree object。讲师现场让工具创建了两个分支,左右实时同步,动画展示引用如何移动。

gitv 中创建分支 refs/heads/A 与 B,引用指针实时动画

📷 gitv 中创建分支 refs/heads/A 与 B,引用指针实时动画
回到原视频 (01:15:01)

然后他让工具展示 merge conflict 状态:两个开发者都重构了同一段代码,把 A 拉到本地就会进入冲突——工作区文件里出现尖括号标记;页面上还能看到 Git 悄悄在 index 里留了三个版本:base、ours、theirs。解决冲突后,临时文件消失,Git 创建了一个有两个父提交的 merge commit,指向关系被正确保留(tree 选的是解决后的版本)。

gitv 展示 merge 冲突:index 中保留 base/ours/theirs 三份版本

📷 gitv 展示 merge 冲突:index 中保留 base/ours/theirs 三份版本
回到原视频 (01:17:16)

冲突解决后生成的 merge commit:两个父提交、指向关系被正确保留

📷 冲突解决后生成的 merge commit:两个父提交、指向关系被正确保留
回到原视频 (01:17:46)

讲师说,一次性直出的视觉效果不完美(毕竟 one-shot,没怎么调),但跟它对话几轮,就能得到近乎完美的 Git 可视化。

编者说明:课堂上把这个直接生成的工具与 Learn Git Branching 作了比较,也拿生成的教学编程环境与 61A Code(SICP / CS 61A 的官方 Python 在线 IDE)对照。讲师提醒:这样的比较应该落在细节上——能否看清关键状态、交互是否顺畅、边界情况是否处理得当、能不能真的帮到使用者。一次课堂生成结果体现的是这次任务的表现,不能直接推成模型之间的普遍排名。

从「看懂」到「学懂」

愿意学懂,成了真正有价值的品质

📷 愿意学懂,成了真正有价值的品质
回到原视频 (01:24:43)

讲师补充了可视化研究的一个启发(幻灯片引用 Hundhausen 等,2002):学习者怎样使用可视化,比可视化本身展示了什么,更影响学习效果。 所以一个适合学习的实验是:

  1. 先预测执行 git add 后会发生什么,再观察;
  2. 接着修改文件但不 add,看看哪些内容保持原样;
  3. 提交以后,再找出新提交、目录树和分支引用。

视觉工具降低了观察成本,预测与核对则让观察变成理解。

「愿意学懂」成了真正有价值的品质

(参考时间: 01:24:43)

讲师对「只读过公众号介绍、却从未真正打开过好东西」的学习方式很不满意:

好消息是:真正学懂和成绩是正相关的,愿意学懂成了真正有价值的品质。 因为一旦有人学会了「把细节做好」,招人的单位就会用这个标准来衡量你。他能想象两种毕业生的差距:一个面对「git merge 之后会发生什么」毫无概念;另一个在每门专业课、每个算法、每个细节上,都在 AI 指导下巩固、消化、从零开始构建这些概念。

讲师还现场展示了另一个 web coding 的项目——一个给 3–4 年级小朋友教 Python 的可视化环境:

为小朋友定制的 Python 可视化教学环境:逐帧调试与动画播放

📷 为小朋友定制的 Python 可视化教学环境:逐帧调试与动画播放
回到原视频 (01:21:04)

讲师说,这个项目整个框架他大概用了两天的碎片时间就完成了(包括视觉调整),还试了便宜的国产模型也「完全可用」的状态。「做这些事情已经没有任何成本。」正因如此,他感叹:

「我马上就要淘汰了……我自己学习的时间非常有限,我很想学很多东西,但没有你们那么多时间。」(参考时间: 01:26:18)

编者拓展|把「可视化」变成「学习」的机制 讲师所引的「自我解释(self-explanation)」研究(Chi 等,1989)指出:把解题步骤与背后的原理联系起来,是有效学习的关键。这就是为什么 gitv 这类工具要「从真实 bytes 出发一步步解析」——它不是给你一个漂亮的结论,而是让你看见推理的每一步,从而完成预测→核对→理解的闭环。


九、提交要留下可以理解的变化

(参考时间: 01:26:34)

更多的 Best Practices

📷 更多的 Best Practices
回到原视频 (01:26:34)

日常问题:你会写 commit message 吗? update 当然能当提交说明,但几个月后翻历史,它几乎帮不了任何人。

前面演示中,AI 生成的 commit 基本遵循了「一行标题 + 详细解释 + message body」的结构,但你会发现它有时开始中英混杂——这也是 AI slop 的一种表现,不过它至少保留了「有 scope(范围)、说明做了什么、再总结」的骨架。这个格式其实是有标准的,叫 Conventional Commits:

fix(parser): accept repeated spaces between arguments

标题之后,还可以解释理由,例如:

旧解析器把连续空格当成空参数,导致复制来的命令失败;现在忽略参数之间的多余空格,但保留引号内部的空格。

代码 diff 展示「怎样改」,提交说明补上「为什么改、想保留什么」。 将来有人想删除这段逻辑,就能找到判断依据,而不必重新猜一遍。fix: 只能说明「修了 bug」;触发条件、设计理由和 issue 链接,才能帮助后来的人判断「这段代码能不能删」。

编者说明:commit message 的「写一行还是写多行」「用祈使句还是陈述句」「中文还是英文」,本身也属于约定俗成的 best practice。讲师提醒:关键不是某一种写法,而是形成稳定、可理解的表达——一个项目内部保持一致,比追求「唯一正确」更重要。他顺带吐槽过文档排版:Markdown 里中英文之间要留一个空格,如果不留,作为读者的他会觉得「浑身难受」;但真正让他无法接受的是同一份文档里有的留、有的不留——不一致本身就传递了「没有规范」的信号。

Best Practices:让错误可恢复

(参考时间: 01:30:01)

Best Practices:让错误可恢复

📷 Best Practices:让错误可恢复
回到原视频 (01:29:55)

Git Best Practices 收集了许多实践经验。讲师把它们归纳为几个工程问题:怎样留退路、怎样让别人读懂变化、怎样让协作可预期、怎样让约定得到执行。

一、给自己留退路。

Best Practices:让协作可预期

(参考时间: 01:31:22)

Best Practices:让协作可预期

📷 Best Practices:让协作可预期
回到原视频 (01:31:22)

二、把探索整理成别人能接手的变化。

三、稳定共同依赖的东西。

四、让约定有执行机制。

这些机制并不替代思考。你自己的修改测试通过了,合入更新后的主线还需要检查组合结果;一段提交说明符合格式,也不等于它解释清楚了行为。规则的价值,最终要落实到恢复、理解和协调的成本上。

编者拓展|hooks 到底是什么 「钩子(hook)」是在某个事件发生时自动触发的程序。它可以是编程语言里对函数调用的挂钩,也可以是你给 Agent 加的钩子——比如讲师自己配了一个 hook:当 Agent 完成、需要他输入时「叮」一声提醒他。你可以直接对 Agent 说「帮我配置一个全局 hook,在需要输入时叮一声」,Claude Code、Codex 等工具都支持。而 pre-commit hook 则是在提交之前运行的检查脚本,正好用来执行前面那几十条 checking rules。

一个有意思的边角:空提交(empty commit)

(参考时间: 00:41:22)

讲师还演示了一个「not best practice」的操作:在不改动任何代码的情况下再提交一次(即让 V2 和 V1 完全相同)。

这在偶尔的情况下可以允许,但显然不能创建大量这样的空提交。

编者说明:空提交(git commit --allow-empty)的存在本身就说明——Git 的「提交」不只是内容快照,它还是历史里的一个事件锚点。理解了这一点,你就不会再把它当成纯粹的「存盘」。


十、Git 之外:用 gh 与 glab 接起协作流程

(参考时间: 01:31:30)

如果你有「惊人的注意力」,应该观察过:托管在 GitHub/GitLab 上的项目,AI Agent 会试图调用一些 gh 或 glab 的命令,结果报 command not found,于是它回退到「古法编程」,又开始写 git 命令。

原因在于:Git 管理的是代码快照,但 GitHub 上还有 issue——你可以发一段文本、对代码做评论,这些并不随 Git 的提交而持久化。为了管理它们,就有必要有一个命令行工具:

Git 之外:gh 与 glab

📷 Git 之外:gh 与 glab
回到原视频 (01:31:40)

graph LR
    I["读取 issue"] --> M["本地修改 / 测试"]
    M --> C["git commit / push"]
    C --> P["创建 PR / MR"]
    P --> R["查看 CI 检查与审查反馈"]
    R --> M
    R --> Z["合入主线"]

这样,人和 Agent 都能通过命令行把版本操作与平台上的协作过程连接起来。

编者说明:这里分别是 GitHub 官方的 gh 与 GitLab 官方的 glab。它们操作的是平台协作对象(issue、PR/MR、CI 状态),与 Git 操作的版本对象(快照、分支、提交)互补。讲师还展示了他的某个项目里「大量由 AI 生成、他很多时候都不太管」的提交记录,用来说明:当平台协作也接进命令行之后,Agent 参与开发的完整链路就被打通了。


十一、用原子变更推进项目

(参考时间: 01:33:33)

讲了这么多 Git,但这门课讲的其实还是软件工程。最后这一小段总结,是今天更重要的部分:

项目是由原子变更推进的快照,可以分支,可以合并。

回到软件工程

📷 回到软件工程
回到原视频 (01:33:33)

原子变更(atomic change)在这里指围绕一个明确意图组织的完整修改,它应当能够被解释、验证,并在需要时单独撤销。它不一定只改一个文件,也不一定只有几行。

graph TD
    A["一个明确意图"] --> B["一次原子变更(commit)"]
    B --> C{"结果如何?"}
    C -- 成功 --> D["合并进主线"]
    C -- 失败 --> E["安全地丢弃该分支"]
    B --> F["出现问题:bisect 定位 / blame 追责 / revert 撤销"]

为什么强调这一点?因为人很难处理「一团混沌」。 一次改了接口、重写了实现、更新了依赖、调整了格式,最后测试失败,应该从哪里找起?如果这些变化有清楚的边界,就可以分别理解、审查和处理冲突。

历史还可以成为调试工具:

它们好不好用,与历史中的变化是否完整、可验证密切相关。

编者拓展|bisect 就是二分查找 你可以把 Git 的提交历史想象成一个很长的数组:从某个时刻开始,后面的提交都「坏了」,之前的一直是好的——所有前缀都是 0、后缀都是 1。git bisect 要做的就是「找到第一个 1」。只要你能提供一个能检查出该 bug 的测试用例(任何能判断好坏的脚本),就不必自己从头翻,Git 会帮你对整段历史做二分。另外,git blame 能告诉你每一行代码是在哪一次提交、由谁改的,出错时就容易追到源头。这两个功能,都是「原子变更 + 完整可验证的历史」带来的红利。

线性历史(linear history):有些项目要求把集成后的提交组织成较直接的一条主线。这与 Git 默认「一个合并提交可以有两个父提交」的结构不同——但在今天,反正 AI 做 resolve、rebase 都很容易,很多团队强行要求提交历史是线性的。好处是:

不过,线性只是组织形式。 每一步都意义不明、不能运行,排成一条直线也不会自动变成好历史。

在模型能力还有限时,AI 参与开发同样需要避免「一团混沌」:一次交给它一个有边界的目标,保留可以检验的状态,发现偏差后及时反馈。代码生成速度提高了,并没有自动消除理解和集成的难度。


十二、Agent 能力之内的项目,怎样协作

(参考时间: 01:36:47)

Agent 能力之内的项目

📷 Agent 能力之内的项目
回到原视频 (01:36:47)

如果项目已经落在 Agent 能力之内(例如讲师提到的 Pico Playground),人的工作可以怎样变化?讲师给出的实践分两阶段:

架构设计:单线程,反复讨论

编者说明:讲师以自己那个「教学 Playground」为例说明了「架构把关」的含义——他把绝大部分接口定义成纯函数(pure function),全局互相影响的共享状态(光标位置、菜单条等)都注册到一个全局的地方,而那个核心的共享状态「也就只有十几行」。剩下的所有东西都是黑盒子,他甚至要求 Agent 在设计可视化时禁止阅读任何库函数的实现,只看接口——因此它的 token 消耗非常省,几万个 token 就能做出一个 playground。「一旦架构设计完成」,才进入实现阶段。

实现:有限并发,立即反馈

graph TD
    A["架构阶段:单线程<br/>与强模型反复讨论,收敛设计"] --> B["把设计写成文档 / 规则 / 示例"]
    B --> C["实现阶段:≤3 个终端并行"]
    C --> D["人脑并发控制:安排不冲突的任务"]
    D --> E["立即检查、视觉验收、反馈"]
    E --> C

架构讨论收敛共同前提,有限并发推进独立工作,快速反馈纠正理解偏差——三者需要配合起来。


十三、如果代码产能再提高一千倍

(参考时间: 01:40:00)

当 Agent 快了 1000 倍

📷 当 Agent 快了 1000 倍
回到原视频 (01:40:00)

最后,讲师留下一个思想实验(他明确说下节课再展开):假设 Agent 生产代码的速度提高一千倍,会发生什么?

讲师给下节课埋下的问题是:从零开始为 Agent 设计一个版本控制系统,应该长什么样? 关键词是「快照、变更,以及一个 change algebra(变更代数)」——系统要回答:哪些变更能组合、哪些必须排队、哪些需要重新验证。


附录:课堂未展示的幻灯片

编者说明:以下几页幻灯片出现在课程幻灯片文件中,但本次课堂实录未展开讲授(多为讲解「原子变更」之后的延伸、以及更进一步的并发/审计思考)。为遵守「每一页幻灯片在书中恰好出现一次」的约定,统一收录于此,并依据课程讲义补注其要点,不代表本次课堂讲授的原话。

Slide 27:SWE-Bench——接手已有项目的一次修改

Slide 28:一行修复,背后是一整套工程工作

Slide 33:快照、变更与并发控制

$$ \Delta_B(\Delta_A(S)) = \Delta_A(\Delta_B(S)) $$

但**组合结果仍需满足项目的不变量(invariants)**。

Slide 34:合并成功之后,还要问什么?

编者拓展|什么是 write skew 在可串行化(serializable)隔离级别之下,数据库会保证「并发执行的结果等价于某个串行顺序」。而 write skew 恰恰是快照隔离(snapshot isolation)下仍会发生的一类异常:两个事务各自读取了同一份旧快照、分别写入不相交的数据,文本(数据项)层面没有任何「写—写冲突」,但它们的组合违反了那条跨越多个数据项的约束(如「至少一人在岗」)。修复方向有二:把约束显式建模并做冲突检测,或采用更强的并发控制(如串行化/可串行化快照)。

Slide 35:Agent 时代,项目管理会变成什么?

编者说明:第 33–35 页共同勾勒了本讲最后的思想实验——「面向 Agent 的版本控制」。它把今天学到的快照、分支、原子变更与数据库的并发控制(MVCC、write skew)连接起来,正是「学习正确的概念,才能向外推演」的注脚。


附:参考与延伸阅读

以下讲义原文链接来自课程官方讲义(注明了出处文件);编者补充是撰写本书时另加的,不与讲义混淆。

讲义原文链接

编者补充


附录:课堂未展示的幻灯片

第 9 页幻灯片 · 留意:让每一次阅读都成为学习

🖼️ 第 9 页幻灯片 · 留意:让每一次阅读都成为学习

第 27 页幻灯片 · SWE-Bench:接手已有项目的一次修改

🖼️ 第 27 页幻灯片 · SWE-Bench:接手已有项目的一次修改

第 28 页幻灯片 · 一行修复,背后是一整套工程工作

🖼️ 第 28 页幻灯片 · 一行修复,背后是一整套工程工作

第 33 页幻灯片 · 快照、变更与并发控制

🖼️ 第 33 页幻灯片 · 快照、变更与并发控制

第 34 页幻灯片 · 合并成功之后,还要问什么?

🖼️ 第 34 页幻灯片 · 合并成功之后,还要问什么?

第 35 页幻灯片 · Agent 时代:项目管理会变成什么?

🖼️ 第 35 页幻灯片 · Agent 时代:项目管理会变成什么?