开发工具链的第三环:版本控制。
从“为什么需要版本控制”到“Git核心概念和工作流”,用生活类比和具体命令演示。
覆盖常用场景:提交、分支、合并、冲突解决、远程协作。
接着“完整的开发工具链”继续走。前两环分别是编辑器(写源码)和编译器/解释器(转译代码)。现在进入第三环:版本控制系统(Version Control System,VCS),最典型的就是 Git。
这一环的核心任务是:记录代码每一次变更的历史,支持多人协作无冲突,并且能在任何时候回退到任意历史版本。
很多人把 Git 仅仅当作“代码备份工具”,那就太小看它了。它本质上是一个精心设计的、分布式的、基于内容寻址的文件系统,附带了一个强大的分支与合并模型。
下面用最通俗的实例,从小白“小李”独自开发一个简单功能开始,再到他和同事“小美”协作,逐步揭开 Git 的核心能力。
一、为什么需要版本控制?一个血泪故事
假设小李没有用任何版本控制,他在电脑上开发一个 bmi.js 文件。
- 第一天:写好了基础功能,文件命名为 bmi_v1.js。
- 第二天:加了新功能,另存为 bmi_v2.js。
- 第三天:发现 v2 有 bug,想回到 v1,但 v1 里少了第二天新加的一个好功能,于是手动从两个文件里复制粘贴……
- 第四天:同事小美发来一个 bmi_final_final_v3_real.js,说这是她改的,小李需要手动合并自己和她的修改,漏了一行,程序崩了。
这种 “文件夹命名式版本管理” 的痛点:
Git 解决了所有这些痛点。
二、Git 核心概念:用“时光机 + 平行宇宙”来理解
| 仓库 (Repository) | 你的项目文件夹,但被 Git“附魔”了。里面有一个隐藏的 .git 目录,记录了所有变更历史。 |
| 提交 (Commit) | 给当前所有文件拍一张“快照”,并附上一段说明(比如“修复了 BMI 计算精度 bug”)。每个快照有一个唯一的 ID(如 a3f2b9c)。 |
| 分支 (Branch) | 一个独立的“平行宇宙”。你可以在主宇宙(main 分支)稳定推进,同时在实验分支里大胆尝试新特性,互不干扰。 |
| 暂存区 (Staging Area) | 一个“购物车”。你先挑选要提交的文件变更(add),然后一次性提交(commit)。 |
| 工作区 (Working Directory) | 你正在编辑的真实文件。 |
| HEAD | 一个指针,指向当前所在的提交(也就是你正在哪个快照上)。 |
| 远程仓库 (Remote) | 放在 GitHub、GitLab 上的公共仓库,相当于团队的“共享云盘”。 |
三、实例:小李用 Git 管理 BMI 计算器项目
1. 初始化仓库并做第一次提交
小李新建一个文件夹 bmi-calculator,在里面创建 bmi.js:
function calculateBMI(weight, height) {
return weight / (height * height);
}
console.log(calculateBMI(70, 1.75));
然后打开终端,执行:
git init # 初始化一个空仓库
git add bmi.js # 把文件加入暂存区(购物车)
git commit -m "初始版本:实现基础BMI计算" # 提交,生成第一个快照
背后发生了什么:
- .git 文件夹里创建了 objects 目录,把 bmi.js 的内容压缩并存储为一个 blob 对象。
- 创建一个 tree 对象,记录文件名和对应的 blob。
- 创建一个 commit 对象,指向 tree,并记录作者、时间、父提交(第一个提交没有父提交)。
- HEAD 指向这个 commit。
小李现在有了一个“时光原点”。
2. 修改代码并查看差异
小李想增加精度控制,改为返回一位小数。修改 bmi.js:
function calculateBMI(weight, height) {
let bmi = weight / (height * height);
return bmi.toFixed(1);
}
console.log(calculateBMI(70, 1.75));
运行 git status,看到:
Changes not staged for commit:
modified: bmi.js
运行 git diff,可以看到具体的行变更:
– return weight / (height * height);
+ let bmi = weight / (height * height);
+ return bmi.toFixed(1);
价值:清晰知道改了什么,不用肉眼对比两个文件。
3. 提交第二次变更
git add bmi.js
git commit -m "返回BMI值时保留一位小数"
现在 Git 历史里有两次提交。运行 git log –oneline:
a3f2b9c (HEAD -> main) 返回BMI值时保留一位小数
d4e5f6a 初始版本:实现基础BMI计算
小李随时可以回退:git checkout d4e5f6a,文件就会恢复到第一个版本。看完再 git checkout main 回来。
四、分支与合并:平行宇宙的威力
小李想尝试一个新的算法(用身高英寸为单位),但又不想破坏主分支的稳定代码。
1. 创建并切换分支
git checkout -b experiment-inches
这相当于创建了一个叫 experiment-inches 的新分支,并切换过去。此时两个分支的内容完全一样。
2. 在实验分支上修改
他修改 bmi.js,改用英寸和磅:
function calculateBMI(weightLbs, heightInches) {
let bmi = (weightLbs / (heightInches * heightInches)) * 703;
return bmi.toFixed(1);
}
console.log(calculateBMI(154, 69));
然后提交:
git add bmi.js
git commit -m "实验:支持英制单位"
3. 切换回主分支
git checkout main
此时 bmi.js 又变回原来的公制版本。两个分支完全独立。
4. 合并实验分支到主分支
小李觉得英制功能不错,想合并进来。
git merge experiment-inches
Git 会尝试自动合并。因为两个分支修改的是同一个文件的不同位置(主分支还是 weight, height 参数,实验分支完全改了函数签名),Git 会提示冲突:
Auto-merging bmi.js
CONFLICT (content): Merge conflict in bmi.js
Automatic merge failed; fix conflicts and then commit the result.
小李打开 bmi.js,看到冲突标记:
<<<<<<< HEAD
function calculateBMI(weight, height) {
let bmi = weight / (height * height);
return bmi.toFixed(1);
}
=======
function calculateBMI(weightLbs, heightInches) {
let bmi = (weightLbs / (heightInches * heightInches)) * 703;
return bmi.toFixed(1);
}
>>>>>>> experiment–inches
他决定让函数同时支持两种单位,通过参数判断:
function calculateBMI(weight, height, unit = 'metric') {
if (unit === 'metric') {
let bmi = weight / (height * height);
return bmi.toFixed(1);
} else {
let bmi = (weight / (height * height)) * 703;
return bmi.toFixed(1);
}
}
删除冲突标记,保存。然后:
git add bmi.js
git commit -m "合并英制支持,同时保留公制"
冲突解决是 Git 协作中最核心的技能,但 Git 只标记冲突,不替你决定,把选择权留给开发者。
五、远程协作:Git 与 GitHub
小李现在想和小美一起开发。他需要在 GitHub 上创建一个远程仓库,并把自己的本地仓库推送上去。
1. 添加远程仓库
git remote add origin https://github.com/lixiao/bmi-calculator.git
2. 推送本地分支
git push -u origin main
现在 GitHub 上有了代码。
3. 小美克隆仓库
git clone https://github.com/lixiao/bmi-calculator.git
cd bmi-calculator
小美在本地也得到完整的历史记录(包括所有分支)。
4. 小美修改并推送
小美修复了一个 bug(当身高为0时应该报错)。她修改代码后:
git add bmi.js
git commit -m "修复身高为零时的除零错误"
git push origin main
5. 小李拉取最新变更
小李在本地继续工作前,先拉取小美的修改:
git pull origin main
git pull 实际上是 git fetch(下载远程新提交)+ git merge(合并到本地分支)。如果没有冲突,自动完成。
6. 处理推送冲突
如果小李和小美同时修改了同一处代码,小李推送时会收到 rejection:
! [rejected] main -> main (fetch first)
error: failed to push some refs
小李必须先 git pull,解决冲突,再 git push。这正是分布式协作的常态。
六、高级但常用的 Git 能力
| git stash | 临时藏起当前未提交的修改,切换到其他分支干活 | 正在改功能,突然需要修紧急 bug,git stash,修完 bug 后 git stash pop 恢复 |
| git reset –hard | 彻底丢弃本地所有未提交的修改 | 改乱了,想回到上一次提交的干净状态 |
| git reflog | 查看所有 HEAD 移动记录,找回丢失的提交 | 误 reset 删除了一个提交,用 reflog 找到它的 ID 然后 reset 回去 |
| git cherry-pick | 把某个其他分支的一个提交复制到当前分支 | 实验分支中只有一个提交是有用的,不想合并整个分支,只挑这一个 |
| git bisect | 二分查找定位引入 bug 的提交 | 发现最近 100 个提交中某个版本引入了 bug,用 bisect 自动二分,快速定位 |
七、Git 的核心优点总结
八、一句话理解版本控制在工具链中的位置
编辑器让你创造内容,编译器把内容变成可执行物,而版本控制让你敢大胆修改——因为你知道,每一次变更都被记录,每一个错误都可以撤销,每一个协作都能有序合并。
Git 不是简单的“Ctrl+S 历史版本”,而是一个为非线性协作设计的分布式变更管理系统。掌握了它,你才真正从“单人写代码”进化到“团队工程化”。
下一步,等代码被版本控制管理好之后,就会进入依赖管理(如 npm、pip)和持续集成环节。
下次继续。





