fedorthinks

速成课 · No. 17

Git 记录你项目的每一个版本,让整个团队改动同一批文件而不会互相覆盖。围绕它的恐惧几乎全部来自陌生的词——commit、branch、rebase——而不是真正的难度。学会那寥寥几个术语,Git 就从一个让人发怵的东西,变成你拥有过的最让人安心的安全网。

21 个概念 · 每个都配图示 · 结尾附速查表

§ 01

在命令之前,先看问题。一旦你感受到版本控制消除的那种痛,Git 的每一个特性读起来就都是显而易见的答案,而不是莫名其妙的术语。

地图:六组共 21 个概念

一张地铁线路图:先看到所有线路的全貌,再一站一站地坐过去。

先讲基础——什么是版本控制、Git 和 repository。然后每组一章:commit(快照是怎么保存的)、branch(并行的工作线)、merge 与 conflict、remote 与团队协作,以及 history 与安全的撤销。最后一章讲好习惯、关系图和速查表。

BasicsVersion controlGit · snapshotsRepositoryThe commitCommitStaging areaCommit messageHEADBranchesBranchmainSwitch / checkoutMergingMergeMerge conflictResolveRemotesLocal vs remoteClone · push · pull · fetchPull requestHistoryLogRevert vs resetRebaseGood habitsSmall commitsFeature branches
六组概念——第 1–6 章依次对应。第 7 章是把一切串起来的习惯。

怎么读每个概念

一本护照:每个人的栏目都一样,所以很容易比较。

从这里往后,每个概念都有相同的几部分:它的英文名称(记住它——这是你查文档和 Git 报错时要搜的词)、一幅来自日常生活的画面、一张图示,以及一张卡片:是什么、怎么运作、一个例子(常带命令)、注意点,以及它与其他概念的关联。结尾是关系图和速查表。

Version Control — 用它代替塞满 _final_v2_FINAL 文件的文件夹

一张桌子被 report.doc、report_v2.doc、report_final.doc、report_final_FINAL.doc 淹没——没人确定哪个是最新的,而周二那个好版本永远丢了。

人人都经历过手动留版本的方式:复制文件,加个后缀,碰运气。它瞬间就崩了——你分不清哪个最新,找不回被覆盖的版本,两个人一起编辑就意味着其中一个的工作会丢。版本控制正是为终结这一切而生的工具:一个项目,它的全部历史被安全保存,每一个版本都可以找回。

Without version controlreport.docreport_v2.docreport_final.docreport_final_FINAL.docwhich one is current?With version controlreport.docone file + its historyv1v2v3v4every version restorabletime
左——一堆副本,不知道哪个才是真的。右——一个文件加一份完整历史:任何版本都能回来。
ConceptVersion Control· 版本控制,VCS
问题
带后缀的副本:不知道哪个最新,旧的没了,两个人互相覆盖。
思路
一个项目 + 完整的修改历史,任何版本都能恢复。
例子

不再有 report_final_FINAL.doc——只有一个 report.doc,加上它每个版本的记录。

注意
只有把修改存进系统里才管用,而不是存在系统之外。
关联
Git 是最流行的版本控制系统。它把历史保存在 Repository 里。

Git — 一条快照时间线

一台相机,每次你说「保存这个」就把你整个项目拍下来——于是你可以翻回去,看到过去任意时刻一切的模样。

Git 是几乎人人都在用的版本控制系统。它把你的项目记录成一连串 snapshot——每一个都是你选择保存的那一刻所有文件的完整画面。结果是一条你可以穿行的时间线:看到什么变了、何时变的、由谁改的,并回到任意过去的状态。它是一个永远不会忘记任何一次保存的保存按钮。

Snapshot 1index.htmlSnapshot 2index.htmlapp.jsSnapshot 3index.htmlapp.jsstyle.cssSnapshot 4index.htmlapp.jsstyle.csstimeeach save = a full picture of all files · changed ones highlighted
每次保存都是所有文件的一份完整快照。高亮的是自上次保存以来改动过的文件。
ToolGit· 版本控制系统,快照
是什么
使用最广泛的版本控制系统。
怎么运作
把项目存成一条时间线,上面是你选定时刻所有文件的 snapshot。
例子
快照 3 加入了 style.css;一个月后,你可以精确回到快照 2。
注意
吓人的是词,而不是难度:术语只是需要学一学。
关联
每个快照就是一个 Commit;时间线存放在 Repository 里。

Repository — 项目加上它的全部历史

一本书把它曾经的每一版草稿都装订在书后——你手里既有当前的文字,也有它如何变成这样的完整记录。

repository(或 repo)就是被 Git 跟踪之后的项目文件夹:当前的文件加上每一次快照的隐藏历史(它存在一个隐藏的 .git 文件夹里)。clone 一个 repo,你得到的不只是今天的代码,而是完整的历史。它是你工作的基本单位——repo 就是「Git 记得的一个项目」,本课其余的一切都是你在某个 repo 里做的事。

Repository (repo) = project folder + its whole historyWorking files (today)index.htmlapp.jswhat you see and edit.git/ (hidden history)c1c2c3c4c5every snapshot ever madegit clone copies both — today’s files and the whole history
一个 repository = 你看到的工作文件 + 一个藏着每份快照的隐藏 .git 文件夹。
ConceptRepository· repo,仓库
是什么
一个被 Git 盯着的项目文件夹:文件 + 全部历史。
怎么运作
历史存在隐藏的 .git 文件夹里;工作文件就是你现在看到的样子。
例子

git init 把一个文件夹变成 repo;git clone 把别人的 repo 整个复制下来,历史也在内。

注意
删掉 .git,只剩文件——整个历史就没了。
关联
由 Commits 组成;分为 local(你的)和 remote(在服务器上)。

版本控制用一个项目和一份完整、可恢复的历史,取代了那个塞满 _final_FINAL 副本的文件夹。Git 把这份历史存成一条快照时间线。

§ 02

Git 里最重要的一个词就是 commit——时间线上一份保存下来的快照。弄懂一次 commit 是怎么产生的,你就弄懂了整个系统的心跳。

Commit — 一份保存下来的快照

你整个项目在选定瞬间的一张照片,订着一张说明那一刻是什么的便条——永久钉在时间线上。

commit 是项目历史中的一份快照:文件的一个被记录下来的状态,带着唯一的 ID(像 e81f53 这样的 hash)和一条说明。每个 commit 都指回它前面那个——它的 parent——形成一条链,这条链就是你的历史。你不会不停地 commit,而是在有意义的节点——「加了登录」「修了日期 bug」——这样时间线读起来就是一连串有意为之的步骤,每一步都是你能回去的地方。

commit = snapshot + unique id + message + link to parent9f2e1ainit project4c7d0badd logine81f53fix date bugb06a9cadd exporteach arrow = “my parent is…” — every commit points back to the one beforethe chain of parents IS the history
四个 commit:每个都有 ID 和 message,每个都指回前一个。这条链就是历史。
ConceptCommit· 快照
是什么
项目的一份保存下来的快照,带有 ID、作者、时间和 message。
怎么运作
每个 commit 指向它的 parent;parent 链就是历史。
例子
git commit -m "fix date bug" 创建 commit e81f53。
注意
commit 只保存你放进 staging area 的内容,而不是你做过的每一处修改。
关联

在 Staging Area 里组装,由 Commit Message 标注,最新的一个由 HEAD 标记。

Staging Area — 封箱之前先把箱子装好

桌上的一个快递箱:你把这次要寄的东西恰好放进去,其他的留在一边,然后才封上胶带寄出。

Git 不会自动给你改过的所有东西都拍快照。你要先挑出哪些改动进入下一个 commit,方法是把它们 stage(git add)——放进 staging area,也就是你正在装的那只箱子。然后 git commit 把恰好那只箱子封起来。这两步——先把该放一起的 stage 好,再 commit——正是 Git 历史能保持干净的原因:每份快照里装什么由你决定,而不是把所有改动一股脑倒进去。

Working directorylogin.jslogin.cssnotes.txt3 changed filesStaging arealogin.jslogin.cssthe packed boxRepositorycommitadd loginlogin.js + login.csssealed snapshotgit addgit commitnotes.txt stays out
改了三个文件,但只有关于登录的两个进了箱子(staging area)。commit 恰好封起这两个;notes.txt 等下一次。
ConceptStaging Area· 暂存区,the index
是什么
位于你的修改和 commit 之间的一只「箱子」。
怎么运作
git add 把改动放进箱子,git commit 把它封成一份快照。
例子

改了 login.js、login.css 和 notes.txt——只 stage 前两个,commit「add login」。

注意
忘了 git add,这个改动就不会进入 commit。
关联
从你的工作文件装入,倒进一个 Commit。是小 commit 的基础。

Commit Message — 写给未来的便条

在那只封好的箱子上贴标签,写明里面是什么——这样几个月后有人扫一眼货架,不用拆开就知道每个箱子里有什么。

每个 commit 都带着一条 commit message,说明改了什么、为什么改。它感觉像杂活,但日后是纯粹的黄金:当你在找搞坏了东西的那次改动,或想弄懂某一行为什么存在时,这些 message 就是项目的故事。一条好 message(「fix timezone bug in invoice export」)能把历史从一堵晦涩 ID 组成的墙,变成一份可读的日志。写给六个月后读它的人——通常就是你自己。

Unreadable logReadable logfixchangeswipasdffix timezone bug in invoice exportadd CSV download to reportsremove unused login codebump date library to v3a wall of mysterythe story of the project
左——什么也说明不了的日志。右——读起来像项目故事的日志。
PracticeCommit Message· commit 上的说明
是什么
commit 上的一条简短说明:改了什么、为什么改。
怎么运作
用 git commit -m "…" 写下;之后在 log 里显示在每份快照旁边。
例子
差:「fix」「wip」。好:「fix timezone bug in invoice export」。
注意
现在省下五秒,六个月后要花上几个小时去挖。
关联
给 Commit 贴标签;让 Log 变得可读。

HEAD — 你的「你在这里」标记

时间线地图上的「你在这里」圆点——标出你当前站在哪一份快照上。

HEAD 是 Git 指向你此刻在历史中所处位置的指针——通常是当前 branch 上最新的 commit。你做一个新 commit 时,它就加在 HEAD 之后,然后 HEAD 前移到它上面。你很少直接去碰 HEAD,但这个词到处都会出现,知道它只是表示「我在时间线上的当前位置」,你读到的大部分内容就不再神秘了。

c1c2c3c4newmainHEADyou are herenext commitolderHEAD moves forward with every commit
HEAD 指向 main 分支,main 指向最新的 commit。下一个 commit 落在它后面,HEAD 随之前移。
TermHEAD· 「你在这里」指针
是什么
指向你所在 commit 的指针(通常通过 branch)。
怎么运作
新 commit 紧接在 HEAD 之后,HEAD 移到它上面。
例子

HEAD → main → c4:你在 main 上,位于 commit c4。commit 一下——现在你就在新的那个上了。

注意
这个词在 Git 的提示里看着吓人,其实只是「当前位置」。
关联
随每个 Commit 移动;Switch 把它移到另一个 branch。

commit 是一份保存下来的快照。你 stage 恰好该放一起的改动,用一条 message 把它们封好——而 HEAD 标出你站在哪里。

§ 03

branch 是 Git 不再只是一个花哨的保存按钮、而成为让许多人——或许多想法——同时推进又互不踩脚的方式的地方。

Branch — 一条并行的工作线

把手稿复印一份,你可以在自己的那份上大胆涂改重写,而原稿保持干净,别人还能继续在上面工作。

branch 是一条独立的开发线:你自己的那份时间线副本,你可以在上面 commit,而不影响任何人的工作。你为开发一个功能或尝试一个想法创建 branch,在上面随意 commit,主版本在你准备好之前保持原样。branch 让一个团队能在同一个 repo 里同时做十件事而不乱:每个人在自己的线上。

m1m2m3m4f1f2f3mainfeature/loginbranch starts herecommits on the branch don’t touch main
feature/login 分支从 main 分出。它的 commit f1–f3 单独存在,main 看不到它们。
ConceptBranch· 工作线,分支
是什么
一个 repo 内部一条独立的 commit 线。
怎么运作

branch 只是贴在某条线最新 commit 上的一个可移动标签;新 commit 会让它前移。

例子
git switch -c feature/login——为登录表单创建一个 branch 并切换过去。
注意
一个活得太久的 branch 会偏离 main,合并时很痛苦。
关联

从 main 分出,通过 Merge 或 Rebase 回来,在 Pull Request 里被讨论。

main — 被信任的那条线

一本大家都信任的书的官方上架版——草稿在别处写,只有完成并获批的作品才会装订进去。

按惯例,主 branch 叫 main(老一些的 repo 可能叫 master)。它保存项目稳定、可用的版本——你会拿去发布的那个。新工作在其他 branch 上进行,只有在准备好并经过评审后才并入 main。把 main 当作被信任的那条线,而绝不是草稿纸,是大多数健康团队工作流背后那条简单的纪律。

m1m2m3m4Mm6Mm8a1a2b1feature/searchfix/datemain — always worksdrafts happen elsewhere,only reviewed work joins main
main 一条直线向前。feature/search 和 fix/date 分支从它分出,只有准备好时才通过 merge (M) 回来。
Termmain· 主分支,原名 master
是什么
主 branch,保存可发布的、可用的版本。
怎么运作
没人直接往里写;工作在评审后通过 merge 从其他 branch 进来。
例子
feature/search 和 fix/date 分支在准备好后合并进 main。
注意
直接 commit 到 main,有可能搞坏大家都依赖的东西。
关联

通过 Merge 和 Pull Request 接收工作;每个 Branch 都从它出发。

Switch (checkout) — 切换 branch 会改变你的文件

换一份摊在桌上的草稿——还是同一张桌子,但瞬间显示出完全不同的一个版本的工作。

当你 switch 到某个 branch(git switch,以前是 git checkout),Git 会把你文件夹里的文件改成与该 branch 的快照一致。切到你的功能 branch,你的修改就出现了;切回 main,它们就从视野里消失(被安全保存,没有丢)。Git 里的 branch 又便宜又即时,所以每个功能或修复建一个是很正常的:不是什么沉重的动作,而是日常的一步。

on mainindex.htmlapp.jsgit switchfeature/loginon feature/loginindex.htmlapp.jslogin.jsyour branch edits appearsame folder · different snapshot · nothing is lost
同一个文件夹:在 main 上有两个文件;执行 git switch feature/login 之后——一个改过的 app.js 和一个新的 login.js。
ToolSwitch / Checkout· 切换分支
是什么
把你移到另一个 branch 上的命令。
怎么运作

Git 把你文件夹里的文件改写成那个 branch 的快照,并把 HEAD 移到那里。

例子

git switch feature/login——login.js 出现;git switch main——它被隐藏,但很安全。

注意
未 commit 的修改可能会阻止切换——先把它们 commit 掉。
关联
在各个 Branch 之间移动 HEAD。

branch 是一条并行的工作线,你可以随意 commit 而不打扰任何人。main 是被信任的版本;新工作待在自己的 branch 上,直到准备好。

§ 04

分到各个 branch 上的工作,迟早要重新汇合。merge 是 Git 把各条工作线重新合在一起的方式,而 conflict 是它唯一需要你来做决定的时刻。

Merge — 把两个 branch 重新合在一起

把你那份单独草稿上的修改折回官方手稿,于是这本书现在既含原文,也含你的新章节。

merge 把一个 branch 的 commit 合并进另一个——通常是把完成的功能 branch 折回 main。Git 查看两边各改了什么,把它们编织成一条同时包含两份工作的时间线,常常通过一个 merge commit——一个有两个 parent 的 commit。大多数时候这是自动且毫不费力的:两个 branch 改的是不同的文件或同一文件的不同部分,Git 只是把它们缝在一起。

m1m2m3f1f2Mmainfeaturemerge commit — has two parentsmain
功能 branch(f1、f2)回到 main。merge commit M 把两条线连在一起——它有两个 parent。
ConceptMerge· 合并分支
是什么
把一个 branch 的工作合并进另一个。
怎么运作
Git 比较两边的改动,创建一个带两个 parent 的 merge commit。
例子
git switch main → git merge feature/login——登录表单现在进了 main。
注意
如果两边改了同一行,就会出现 conflict,由你来决定。
关联

另一种做法是 Rebase。可能引发 Merge Conflict。在团队里它要经过 Pull Request。

Merge Conflict — 对同一行的两次修改

两个编辑各自把同一句话改成了不同的样子——没有机器能知道你想要哪个,所以它停下来问你。

当两个 branch 以不相容的方式改了同一个文件的同一处,就会发生 merge conflict。Git 猜不出哪个版本才对,所以它暂停,标出那个位置,并把两个版本并排给你看。这不是错误,也不是灾难——这是 Git 正确地拒绝悄悄丢掉某人的工作。conflict 第一次碰到时让人害怕,恰恰因为利害(别丢了工作)很清楚。

main changed line 3Hello, team!feature changed line 3Hi, world!CONFLICTgreeting.txt · line 3Git won’t guess which one is right — it stops and asks you
在 main 上第 3 行变成了“Hello, team!”,在 feature 上——“Hi, world!”。Git 无从知道哪个对,所以停了下来。
TermMerge Conflict· conflict,合并冲突
是什么
两个 branch 对文件中的同一处改成了不同的内容。
怎么运作
Git 中止这次 merge,并在文件里标出有争议的行。
例子
greeting.txt 的第 3 行:“Hello, team!” 对 “Hi, world!”。
注意
这不是坏了——这是 Git 拒绝悄悄丢掉某人的工作。
关联

出现在 Merge 或 Rebase 期间;由 Conflict Resolution 解决;有短命 branch 时更少见。

Conflict Resolution — 解决 conflict 只是做个选择

读完两句被改写的话,然后写出你真正想要的最终版本——留下一个、另一个,或者两者混合——然后继续往前走。

你通过编辑被标记的文件来解决 conflict,改成你想要的版本——留下你的、留下他们的,或把两者合并——删掉 <<<<<<<、=======、>>>>>>> 这些标记,然后 commit 结果。这就是全部的秘密:conflict 是 Git 把一个只有人能做的决定交给你,而解决就是做出这个决定。当 branch 短命、你又常常 merge 时,conflict 会变得更小也更少,因为两边永远不会漂得太远。

greeting.txt (marked)<<<<<<< HEADHello, team!=======Hi, world!>>>>>>> featureeditgreeting.txt (resolved)Hi, team!ours, theirs or a mix —markers removedthen: git add + git commit → the merge is done
左——带 conflict 标记的文件:两个版本夹在标记之间。右——人做出的选择。然后 git add 和 git commit。
PracticeConflict Resolution· 解决冲突
是什么
手动选出有争议处的最终版本。
怎么运作

编辑文件,删掉 <<<<<<< / ======= / >>>>>>> 标记,然后 git add 并 git commit。

例子
“Hello, team!” 和 “Hi, world!” 变成了 “Hi, team!”。
注意
要选得好,就得理解两个版本——不确定就问作者。
关联
在 Merge Conflict 之后完成 Merge。

merge 把两个 branch 编织成一条时间线。conflict 只是对同一行的两次修改——Git 拒绝去猜,请你来选。

§ 05

到目前为止,一切都在你的机器上。要和别人合作——或备份你的工作——就意味着把你本地的 repo 连到云端的共享 repo。

Local 与 Remote — 两份副本,origin 是共享的那份

你在家里的个人工作版手稿,以及办公室里整个团队保持同步的共享主稿。

Git 是分布式的:你机器上有一份完整的 repo 副本(local),通常在服务器上还有一份共享副本(remote)——在 GitHub 或 GitLab 这类服务上。你所有的工作都在本地进行——很快,甚至可以离线——然后与 remote 同步,把它分享出去并拉入别人的工作。默认的 remote 按惯例叫 origin。两份副本,刻意保持同步。

origin (remote)GitHub / GitLab · sharedYour laptopfull local copyTeammate Afull local copyTeammate Bfull local copydistributed: everyone has the whole repo, origin keeps the team in sync
团队里每个人都有一份完整的 repo 副本。GitHub 或 GitLab 上共享的 origin 副本让所有人保持同步。
ConceptLocal / Remote· 本地与远程 repo,origin
是什么

local 是你自己那份完整的 repo 副本;remote 是服务器上的共享副本。主要的 remote 叫 origin。

怎么运作
在本地工作(甚至离线),然后与 remote 同步。
例子
GitHub 上的 repo 是 origin;你和队友各自在笔记本上有一份副本。
注意
你没有 push 的工作只存在你的笔记本上——笔记本坏了它也就没了。
关联

副本通过 Clone / Push / Pull / Fetch 同步;Pull Request 存在于 remote 上。

Clone、Push、Pull、Fetch — 四个同步动作

把办公室的主稿复印带回家(clone),把你完成的页面送回去(push),再收取其他人的新页面(pull)。

四个词在 local 和 remote 之间搬运工作。Clone 做出一份 remote repo 的第一个本地副本。Push 把你的 commit 送上 remote,让别人能看到。Pull 把别人的新 commit 拉进你的副本并合并。Fetch 下载新内容但暂不合并,让你先看看。掌握这四个,你就能协作:它们就是保持同步的全部词汇。

Local repoyour machineRemoteoriginclone — first full copypush — send my commits uppull — bring theirs down + mergefetch — download, don’t merge yetfour verbs keep the two copies in step
Clone——第一份副本。Push——把你的送上去。Pull——把他们的拉下来并合并。Fetch——只下载,不合并。
ToolClone / Push / Pull / Fetch· 四个同步命令
Clone
git clone <url>——你第一份完整的 remote repo 本地副本。
Push
git push——把你的 commit 送到 remote。
Pull
git pull——下载别人的 commit 并合并进来(fetch + merge)。
Fetch
git fetch——下载新内容但暂不合并:先看看。
注意

不先 pull 就 push 常会被拒绝:先把别人的改动拉进来,再送出你的。

关联

在 Local 与 Remote 之间搬运 Commits;pull 内部会做一次 Merge。

Pull Request — 团队如何评审工作

把你的新章节交给编辑,他们阅读、评论、批准之后,才会装订进官方的书里。

在 GitHub 这类平台上,你通常不会直接 merge 进 main。你 push 自己的 branch,然后开一个 pull request(PR;在 GitLab 上叫 merge request)——一份合并你工作的提议,队友会在其中评审、留评论,并在它并入 main 之前批准。这是第二双眼睛发现问题、团队确认某个改动已准备好的检查站。团队里的 Git 大多数就发生在 PR 里。

featureyour branchpushPull requestdiff + discussionReviewcomments · approveokmainmergedchanges requested → fix and push againa second pair of eyes before anything joins main
push 分支,开 PR,队友评审。要求修改——修好再 push。批准——merge 进 main。
PracticePull Request· PR,merge request
是什么
把一个 branch 合并进 main 的提议,附带讨论和评审。
怎么运作
push branch → 开 PR → 评论与修改 → 批准 → merge。
例子
PR「add login form」:队友请你给一个函数改名,你 push 了修改,PR 获批。
注意
没人会真去读一个巨大的 PR——保持它们小。
关联

存在于 Remote 上,承载一个 Branch,以 Merge 进 main 收尾。

local 是你的副本;remote(origin)是共享的那份。clone、push、pull 和 fetch 让它们保持同步——而 pull request 是团队在合并前评审的地方。

§ 06

Git 最深的安慰在于:几乎没有什么是真正丢掉的。知道如何读 history、如何安全地退回去,会让 Git 从吓人变成让人安心。

Log — 项目的故事

翻阅一叠带日期、有署名的每一版草稿——准确看到每一步改了什么、是谁改的。

log(git log)是 commit 的列表,按顺序排列的你项目的历史,每条都带着它的 message、作者和时间。它回答「发生了什么,什么时候?」,是弄懂任何代码库或追查任何 bug 的起点。出问题时,log 让你找到引入问题的那个确切的 commit,因为每个改动都被记录了。一份你能读懂的历史,就是一份你能调试的历史。

git log — newest firstcommitauthordatemessageb06a9cAnaJun 12add CSV exporte81f53MaxJun 11change date parsing4c7d0bAnaJun 10add login9f2e1aMaxJun 09init projectbug starts hereevery change recorded → you can find the exact commit that broke things
git log:commit 从新到旧排列,带有作者、日期和 message。日期坏了?去找那个改了日期解析的 commit。
ToolLog· commit 历史,git log
是什么
每个 commit 的列表:ID、作者、时间、message。
怎么运作
git log 从最新的 commit 到最旧的依次显示历史。
例子
日期在 6 月 11 日坏了——log 显示 commit e81f53「change date parsing」。
注意
log 有多大用处,取决于里面的 commit message 写得怎么样。
关联
读取 Commits 组成的链;它的价值来自 Commit Message。

Revert 与 Reset — 两种撤销:诚实的和改写的

撤销一个错误的两种方式:在记录里加一条标注清楚的更正(revert),或者悄悄撕掉最后一页,仿佛什么都没发生过(reset)。

Git 给了你安全退回去的办法。Revert 创建一个新 commit 来撤销之前的某个 commit——错误留在历史里,它的撤销记录叠在上面,这既诚实,对共享的工作也安全。Reset 把你移回到较早的一个 commit,改写近期历史,就好像那些 commit 从没发生过——强大,但对别人已经拿到的工作很危险。经验法则:公开的东西用 revert,只有私人的、本地的工作才用 reset。

git revertABCC⁻¹mistakenew commit undoes Csafe for sharedworkgit resetABCHEADgone from historyprivate, localwork onlyrevert adds an honest fix · reset rewrites the past
上,revert:错误的 C 还在,一个新 commit 把它撤销。下,reset:HEAD 跳回 B,C 从历史中消失。
ToolRevert vs Reset· 用 commit 撤销 / 回退历史
Revert
git revert C——一个撤销 C 的新 commit。历史保持完整。
Reset
git reset B——把 branch 移回 B,就像 C 从没发生过。
例子

在 main 上用一个 commit 搞坏了生产环境——revert。在本地搞砸了、从没 push 过——reset。

注意
对队友已经拉走的内容做 reset,会让整个团队失去同步。
关联
作用于 Commits 和 HEAD;reset 和 Rebase 一样会改写历史。

Rebase — 重放你的工作;遵守黄金法则

不是把你那份旁支草稿带着明显接缝订到手稿上(merge),而是把你的修改干净地重写在最新版本之上,就像你一开始就是从它出发的(rebase)。

Rebase 是 merge 的替代做法:它不是用一个 merge commit 把两条线连起来,而是把你的 commit 重放到另一个 branch 之上,得到一条整洁的直线历史。在分享之前整理你自己的 branch,它非常好用。但它会改写 commit(会创建它们的新副本),所以有一条黄金法则:永远不要对别人已经拉走的历史做 rebase。改写共享的历史,会让所有人失去同步。私人的工作用 rebase;合并共享的工作用 merge。

BeforeABCXYmainfeatureAfter git rebase mainABCX′Y′new copies, replayedone straight line, no merge commitgolden rule: never rebase history others already pulled
之前:X 和 Y 从 B 分出。git rebase main 之后:它们的副本 X′ 和 Y′ 落在 C 之后——一条直线,没有 merge commit。
ToolRebase· 重放 commit,变基
是什么
把你的 commit 重放到另一个 branch 之上。
怎么运作
Git 取出你的 commit,在新的基点上重新创建它们的副本。
例子

在 feature 上运行 git rebase main——你的 X 和 Y 现在落在 main 上新的 C 之后。

注意
黄金法则:永远不要对别人已经拉走的历史做 rebase。
关联

Merge 的替代做法;可能引发 Merge Conflict;和 Reset 一样会改写历史。

log 是可读的故事;revert 和 reset 是你退回去的办法。rebase 能得到整洁的历史——但永远不要改写别人已经拉走的历史。

§ 07

Git 有很多命令,但好的实践只是一份简短的习惯清单。照着做,Git 就始终是一张让人安心的安全网,而不是偶尔发生的危机。

Small Commits — 小而专注的 commit

一本相册,每张照片捕捉一个清晰的瞬间并配着说明——比一张把所有东西一次拍糊的照片有用得多。

小步且频繁地 commit(小的、原子的 commit),让每个 commit 都是一个连贯的改动,并带一条解释它的 message。细小又标注清楚的 commit,让历史可读,bug 易于追踪,撤销也精准:你可以 revert 一个精确的改动,而不丢掉其他任何东西。一个把一周混杂的工作一股脑倒进去的巨型 commit 则正相反——无法评审,无法理清,也无法部分撤销。这条纪律只花几秒,却在项目的整个生命周期里持续回报。

1 giant commitlogin + CSS + bugfix + refactorA week dumped at oncecan’t review, untangle or undo a partsplitSmall, focused commitsadd login formstyle login pagefix date bugrename helperseasy to review · revert just one
一周一个巨大的 commit,对比四个小 commit:每个只关于一件事,任何一个都能单独撤销。
PracticeSmall Commits· 小的原子 commit
问题
一个巨型 commit 无法评审、无法理清,也无法部分撤销。
思路
一个 commit = 一个连贯的改动 + 一条清晰的 message。
例子

不要「week of work」——而是「add login form」「style login page」「fix date bug」。

代价
花几秒钟去 git add 对的文件并写好 message。
关联

建立在 Staging Area 和 Commit Message 之上;让 Revert 和 Log 变得有用。

Feature Branch Workflow — 每个功能一个 branch,常常合并

每个新章节在自己的副本上起草,趁它还小就折回去——这样没有哪份草稿会偏离全书太远,以至于重新合并变成一场硬仗。

每一项工作都在它自己的 branch(功能 branch)上做,保持它短命,并常常合并回去。长命的 branch 会偏离 main 很远,让 merge 变成一堆痛苦的 conflict;小 branch 频繁合并则始终轻松。再配上先 pull 再 push——先把别人的改动拉进来——你就是在持续集成,而不是到最后才撞车。这个节奏是顺畅的团队 Git 的核心。

Short-lived branches, merged often → tiny conflictsfffOne long-lived branch → drift → a big pile of conflicts!
上——很快就回来的短 branch:conflict 始终很小。下——一条漂得很远的长 branch:最后是一大堆 conflict。
PracticeFeature Branch Workflow· 每个功能一个 branch
问题
所有人都往 main 写——互相搞坏;一条长 branch——最后一座 conflict 的大山。
思路
一个任务——一个短 branch,通过 PR 在几天而不是几周内合并。
例子

fix/date:pull、建 branch、两个 commit、push、PR、评审、merge——一天内搞定。

代价
你得把工作切成小块,并且常常 pull main。
关联

把 Branch、Pull、Push、Pull Request 和 Merge 串在一起。

这些概念如何连在一起

一条流水线:从文件里的一处修改到被信任的版本——一条路,沿途有清晰的站点。

整条路是这样的。你的工作文件里的修改,通过 git add 进入 staging area,再通过 git commit 变成一个 commit,你在 log 里读到它。commit 在 branch 上累积;branch 通过 merge 或 rebase 回来,有时会遇到 conflict。在团队里,你把 branch push 到 remote,开一个 pull request,评审之后 merge 进 main。如果出了问题——revert 或 reset。

Working dirStaging areaCommit+ messageLogreads historyaddcommitBranchHEAD · mainMerge / RebaseConflictyou resolveRemote · originPull requestreviewmaintrusted lineRevert / Resetundopush
本课概念之间的主要联系:从一处修改到 main,再回来。
你的日常 Git 习惯
  • 开工前先拉取最新,这样你是在当前的工作之上构建。
  • 在 branch 上工作,绝不直接在脆弱的 main 上。
  • 边做边写小 commit 和清晰的 message。
  • 常常 merge,趁 branch 还短,让 conflict 保持很小。
  • Push 你的工作,并开一个 pull request 请人评审。
  • 共享的工作用 revert 撤销;永远不要对别人已有的历史做 rebase。
你把 Git 用好了的迹象
  • 你的历史读起来像一个由带标签的小步骤组成的清晰故事。
  • 新工作在 branch 上;main 始终能用。
  • 你先 pull 再 push,并趁 branch 还小就 merge。
  • 共享的工作你用 revert,绝不改写共享的 history。
  • Git 给你的感觉像一张安全网——你放心地实验,因为知道总能回得来。

好的 Git 就是几个习惯:先 pull、把工作放在 branch 上、写小 commit 和清晰的 message、常常 merge、merge 前先评审。命令很多;纪律很短。

速查表:全部 21 个概念

基础

Version ControlVCS

一个项目 + 完整历史;任何版本都能回来。

Git快照时间线

最流行的版本控制系统;把项目存成快照。

Repositoryrepo

项目文件夹 + 藏在 .git 里的历史。

commit

Commit快照

一份保存下来的快照,带 ID 和指向其 parent 的链接。

Staging Area暂存区

commit 之前的箱子:git add 装箱,git commit 封箱。

Commit Message便条

写给未来的便条:改了什么、为什么改。

HEAD你在这里

指向当前 commit 的指针;随每个 commit 前移。

branch

Branch工作线

一条不碰别人工作的并行 commit 线。

main主分支(master)

被信任的、始终可用的版本。

Switch / Checkout切换分支

切到一个 branch:文件夹里的文件变成它的快照。

merge 与 conflict

Merge合并

把 branch 合到一起;常通过一个有两个 parent 的 commit。

Merge Conflict冲突

两个 branch 把同一行改成了不同内容;Git 请你来决定。

Conflict Resolution解决冲突

选出最终版本,删掉标记,commit。

remote 与团队

Local / Remoteorigin

你那份完整的副本和服务器上的共享副本。

Clone / Push / Pull / Fetch同步

第一份副本 · 送上你的 · 拉下他们的并合并 · 只下载。

Pull RequestPR,评审

评审后把 branch 合并进 main 的提议。

撤销与 history

Log历史

commit 的列表:谁、何时、做了什么、为什么。

Revert vs Reset撤销

Revert——一个新的撤销 commit(用于共享的工作)。Reset——改写(只用于你自己的)。

Rebase重放

把 commit 重放到另一个 branch 之上。绝不用于共享的历史。

好习惯

Small Commits原子 commit

一个改动——一个带清晰 message 的 commit。

Feature Branch Workflow每个功能一个 branch

每个任务一个短 branch,通过 PR 常常合并。

速成课结束 · 7 章 · 21 个概念

接下来是练习:拿任何一个小项目,运行 git init,随着你改动做几次 commit——然后创建一个 branch,做一处修改,再把它合并回来。故意制造一个 conflict:在两个 branch 上改同一行,然后把它解决掉。只要你在一个无关紧要的东西上把每件事都做过一遍,恐惧就会蒸发。但请把一个想法放在所有想法之上:Git 是一个永远不会忘记的保存按钮,也是让许多人工作而不互相覆盖的方式。每一个吓人的词,都只是一个平静、可恢复的步骤的名字——而现在,你已经掌握了它们。