+++ title = "如何用电车难题理解 Git" slug = "git-with-trolley" tags = [ "编程", "哲学", "小品",] categories = "软件杂谈" date = 2026-02-10T23:53:53 draft = false toc = true toot = "https://akk.eltr.ac/objects/b35d9769-109a-4e4f-9d12-713eafe97038" url = "/essays/git-with-trolley/" +++ 你是一个疯狂的哲学家,你意识到反思、设想和解构已经无法满足你对人类道德的求知欲了,于是你决定开始实验。就这样,你找到了一些铁轨、一辆火车、一根拉杆和一些必要的机关,哦对,还有绳子,现在就差受害者了。 ## 挑选受害者 你可不想过于随意地进行这场伟大的实验,有幸参与实验的人也是需要层层筛选的。总而言之,你找到了一群愿意或不愿意的受试者,让我们把他们称作 A 组。尽管你在选取受试者时也是带有目的的,但在正式开始实验之前,理应再做一次筛选。于是,你把 A 组中的一部分人绑上了铁轨,让另一部分人直接离开了,我们把这一部分有幸被绑上铁轨的人称作 B 组。 读者可能猜到了,一开始进入这场思想实验的 A 组就是 Git 的工作目录(work tree),B 组自然代表从工作目录中挑选出来的布置区(staging area),那些被哲学家打发走的人就是被丢弃(discard)或者说复原(restore)的工作。当然,挑选到布置区的人们也并非是直接为伟大的哲学事业做出了贡献,哲学家还需要规划思考一下,应该让火车怎样碾过他们,最好是要绘制一个路线图,这一步相当于是承认(commit)了这些人有真正参与到这场实验的资格,当然,哲学家也因此犯下(commit)了杀人罪。 简单来说,哲学家在这里做了三次选择: 1. 在近乎无限的茫茫人海中,把受试者带到工作区域来; 2. 在工作区域中,挑选一部分人进入布置区,另一部分人留在工作区稍作等候,或者直接打发走; 3. 如果不需要对布置区里的人选进行调整了,就可以决定火车压过的路线了! 当哲学家命令火车向前行驶,压过铁轨(staging area)上被绑好的人们之后,我们可以说:他提交(commit)了他的工作。 ![](https://img.eltr.ac/geedeapro/essays/git-with-trolley/work-concepts.png) {.dark:invert} 显然,在这篇文章中,我们把对代码的改动抽象为「工作」,而在电车难题的比喻中,我们用人来代指「工作」。 嗯,让我们来看看我能用这个*不太道德的*比喻扯多久吧。 ## 无人生还 现在,哲学家已经把一些人绑在铁轨上了,距离电车压向他们已经不远了!从某种意义上来讲,他们是薛定谔的猫,几乎是已经死了,但又还没有死。无论怎么样,他们都可以被当作 Git 历史的一部分了。 如果你数学比较好的话,你可以把电车和这些人的距离理解为「无穷小」,而不是 0 或者负数。哲学家好像和上帝关系很好,或者如果你不信神的话,也可以说哲学家和疯狂科学家关系很好,他从隔壁爱因斯坦那里学会了和时间作对,于是成为了四维生物,能够在时间维度上行走。总之,哲学家可以在电车真的压向这些人之前做任何操作,甚至救下他们,但对这些凡人自己来说,他们已经必死无疑了。 ![](https://img.eltr.ac/geedeapro/essays/git-with-trolley/git-history.PNG) {.dark:invert} 这个时候,哲学家突然想再挑一些人上来。可是,就像我们说的,电车正在无限逼近,把人直接往铁轨后面叠加固然没问题,但属实缺乏一些工程美感(是的,哲学家也是思想的工程师!)。主要的原因是,哲学家自己其实也不太确定要不要真的把某些人纳入这场伟大的实验中,这可怎么办呢? 答案就是:分支! 我们假设,哲学家本来想要把奴隶主全都碾死,可转念一想,又觉得应该把所有的资本家也都放在铁轨上,但是他并不确定,可以确定的是,他想要奴隶主死,至于资本家,还要再斟酌斟酌。 于是他把奴隶主放在最主要的,车子一定会碾过的轨道上——我们把这条轨道称作主分支(master[^1])。至于那些资本家,先放在另一条轨道上,我们给这条分支随便起一个名字,比如 foo,原因是这些资本家挺蠢的(fool[^2]),但又有些实力,只能算四分之三的蠢吧。 ![](https://img.eltr.ac/geedeapro/essays/git-with-trolley/git-branch.png) {.dark:invert} 于是现在有了两条轨道,电车有两条行驶路径,哎呀呀,真是难选择啊。到底应该让车子往哪边开呢? 如果哲学家决定不让资本家献身了,那他可以把 `foo` 分支删掉,或者就放在那不管。反正,只要他不拉动拉杆,电车就只会碾过主分支上的奴隶主们。 如果哲学家下定决心,觉得资本家也该死,那么他有两个选择,第一个是—— ### *合并!(merge)* ![](https://img.eltr.ac/geedeapro/essays/git-with-trolley/git-merge.PNG) {.dark:invert} 需要注意的是,如果仅仅是把 `foo` 分支和主分支接轨,那是无法做到「无人生还」的。我们可以看到,轨道是在第 4 号奴隶主产生分支的,而在那之后,主分支上还相继出现了 5 号和 6 号奴隶主。如果让电车从分叉处驶入 `foo`,又在之后回到主分支,那分叉口之后才出现的 5 号和 6 号不就幸免于难了吗! 所以,我们要把 5 号和 6 号奴隶主移动到第二个分叉口,也就是电车回到主分支后会到达的地方。这样就能保证合并之后,不会有一个人被落下。 “可是,”你可能会说,“这样不是太费铁轨了吗?而且好复杂。”没关系,我们还有第二个选择—— ### *重整!(rebase)* ![](https://img.eltr.ac/geedeapro/essays/git-with-trolley/git-rebase.png) {.dark:invert} 对嘛,大家都躺成一排,这样就方便了。 ## 哲学家的时间旅行魔法 在此之前,我们一直使用数字对受试者进行编号,这很直观,可是真正的哲学家却不会这样做。实际上,哲学家使用一种凡人们读不懂的语言为每个人起名,这种语言由数字和字母组成,十分神秘,它长这个样子:`9a0f7b785e`。 这种编号是怎么构建出来的呢?很简单,只需要对比这位受试者相比上一位受试者有何不同,并从特征中提取摘要,再进行哈希运算即可得到。 不过,即使是哲学家也记不住那么多人名,因此,哲学家规定,在某个时刻下,最后一个会被电车碾压的人为人群之首,记为 `HEAD`。不难理解,`HEAD` 也就是哲学家最近挑选的、最新鲜的工作成果。不过,这其实还要更复杂一些。 我们知道,哲学家是四维生物,可以在时间上行走,他可以查看(checkout)任意一个时间点下轨道的状态,以一种凡人没法做到的方式回顾历史。对于哲学家来说,`HEAD` 是他当前查看的那一个工作,也是他开始工作的地方——如果哲学家要添加新鲜的人类样本,或者创建分支,那就是从 `HEAD` 开始的。 我们不是知道哲学家可以给铁轨创建分支吗?一般来说,`HEAD` 就是就是当前分支下最新的那一条工作。如果哲学家念出咒语 `git checkout foo`,它就会来到 `foo` 分支,准确来说是 `foo` 分支下最新的那条工作;要回到主分支,只需要念出咒语 `git checkout master`。由此可以发现,轨道的分支实际上就是指向当前分支最新改动的一个指针。 哲学家还会使用一种特殊的相对记名法,他使用 `HEAD^` 表示 `HEAD` 的前一条,使用 `HEAD~n` 表示 `HEAD` 前面的第 n 条。这样一来,时间旅行就很方便了,喊出咒语 `git checkout HEAD^` 就能查看当前头部的前一条,也就是把最近这一个人放上铁轨之前的状态。 我之前说过,哲学家和上帝或者说科学家的关系很好,他有手段改变历史。这种手段也不难实施,只需要谨慎即可,我现在就传授给你,你只需要喊出咒语 `git reset --hard HEAD^`,就能把时间拨回上一次改动之前。同一系列的咒语还有 `--soft` 和 `--mixed` 等等,但还要等到你更成熟之后,才能施展此法。 ## 严格筛选 哲学家对待他的作品有着近乎偏执的追求,他会像果农挑选樱桃一样挑选受试者。前文提到,哲学家可能还在纠结要不要碾死资本家,他可能发现,这些资本家其中的一些拖欠工资,而另一些则没有。显然,拖欠工资的资本家是一定要死的,但另一部分还需要再作考虑。 此时,哲学家显然不能把整个 `foo` 分支直接合并到主分支,他需要挑选其中的一些人,并把他们放在 `HEAD` 后面。这个过程就叫作 `cherry-pick`。 ![](https://img.eltr.ac/geedeapro/essays/git-with-trolley/git-cherry-pick.png) {.dark:invert} ## 现在,休息吧! 哲学家累了,挑选总是很累的。没关系,我们可以随时退出,之后再继续工作!不过,哲学家需要提醒你的是,千万不要把未经深思熟虑后选择的人合并到主分支,然后就回家睡觉了。一般来说,我们把主分支视作已经确定了的工作,那些没有确定的,就先放在另一个分支上吧,*反正也跑不了*。 一个原则是,一旦有新的想法,就要先创建新分支,然后在分支上添加新人,等工作完成并且确定之后再合并或重整。如你所见,合并和重整并不困难,只需要咒语 `git merge` 或者 `git rebase` 就可以了。如果你正在发展一个想法,但是突然需要回到主分支或者另一个分支进行别的工作,也可以用之前学会的 `git checkout` 咒语进行时间旅行,在时间中移动也是很简单的。 不简单的是改写历史,所以,千万不要把没确定的东西合并到主分支! 在你走之前,还有一些注意事项:如果读完此文发现自己既没有理解电车难题,也没有理解 Git,属于正常现象,前者请求助于道德哲学相关内容,后者可以参考 [Learn Git Branching](https://learngitbranching.js.org/) 这个网站。 好了,去睡吧。放心,哲学家是不会偷偷把睡着的人绑在铁轨上的。 [^1]: Pun intended. [^2]: 严肃地讲,这并不是 `foo` 这个词的词源。真正的词源可以参考 [RFC 3092](https://datatracker.ietf.org/doc/html/rfc3092)。