开源项目Git贡献全流程拆解(3)Git核心概念精讲:仓库、提交、分支与工作流
很多人学Git,看了一堆教程,背了一堆命令,真到开源项目里提交代码还是发怵。尤其是从“自己建个仓库随便玩玩”过渡到“给别人的项目贡献代码”,中间那道坎一直过不去。问题出在哪?我观察下来,绝大多数人是卡在了概念层面——不是因为Git命令难记,而是因为不知道每条命令背后到底在操作什么。
这篇是开源项目Git贡献全流程拆解的第三篇,重点把仓库、提交、分支、工作流这四个核心概念掰开揉碎讲清楚。这四个概念是后续一切操作的地基:你在开源项目里做的每一个操作,本质上都是在跟这四个东西打交道。理解了它们,后面讲fork、PR、冲突解决、代码审查,你才能一马平川。
1. 仓库:不是你看到的文件夹,而是三层区域的协作
先纠正一个特别常见的误区:很多人以为仓库就是那个能看到的项目文件夹。其实不对。git init之后,项目文件夹里多了一个隐藏的.git目录,那个.git目录才是真正的仓库本体,你肉眼看到的那些源代码文件,只是仓库在工作区里的一层“投影”。
1.1 仓库的三大区域:工作区、暂存区、版本库
理解仓库,关键是理解三个区域的分工。
-
工作区(Working Directory):就是你在编辑器里打开、修改的那些文件。你改代码、删文件、新建文件,操作的都是工作区。这是你直接接触的区域,也是唯一一个你能随心所欲改来改去而不会把仓库搞坏的区域。
-
暂存区(Index / Staging Area):Git里最容易被忽略、但恰恰是最精妙的设计。可以把它理解成一个“候车区”:你告诉Git“这三个文件我改完了,准备提交”,Git就把它们的快照(其实是文件内容)放入暂存区。为什么需要暂存区?因为一次提交应该是一个完整的逻辑单元,比如修复一个bug可能涉及前端1个文件、后端2个文件、测试1个文件,你希望这4个文件作为一个整体提交。但问题是你改代码不是一次性改完的,可能前端先改完,后端还没动。这时候你先把改完的前端文件
git add到暂存区,等4个文件都改完、都add进去,再一次git commit,就形成了一个完整且干净的提交。没有暂存区的话,你要么把所有改动一股脑提交(夹杂无关文件),要么用一堆git add -p交互式操作去挑选内容,体验会很割裂。 -
版本库(Repository / .git目录):这才是仓库的核心。它保存着你所有的提交记录、所有历史版本、所有分支指针。工作区和暂存区可以被随意清空、重建,版本库里的对象一旦提交,就构成了项目的完整历史。
1.2 .git目录里到底存了什么
打开.git目录,你会看到一堆文件和文件夹,第一次看会觉得很唬人,但核心就几个:
objects/:Git对象数据库。提交、文件快照(blob对象)、目录树(tree对象)都以压缩后的二进制形式存在这里。这是Git真正存数据的地方。refs/:引用目录。里面存的是分支指针、标签指针。打开refs/heads/master,里面就是一个SHA-1哈希值,指向某一次提交。HEAD:一个文件,里面存的是当前分支的引用,比如ref: refs/heads/master。它告诉你“你现在在哪个分支上”。index:暂存区的内容就是存在这个文件里。config:仓库级别的配置文件。
注意:这些你不需要全部记住,但知道它存在、知道它大致存什么,能帮你在遇到“为什么我文件删了但仓库还那么大”“为什么Git知道我在哪个分支”这类问题时,不慌。
1.3 从“新建一个开源项目仓库”看三层区域协作
假设你在本地初始化一个仓库:
bash复制git init my-project
cd my-project
echo "# My Project" > README.md
git status
这个时候git status会告诉你README.md是“Untracked files”(未跟踪文件),意思是Git看到了这个文件,但还没决定要管它。你执行:
bash复制git add README.md
git status
文件状态变成“Changes to be committed”(已暂存),说明文件已经进入暂存区。接着:
bash复制git commit -m "Initial commit"
Git会把暂存区的内容打包成一个提交对象,写入objects/,然后更新refs/heads/master指向这个新提交,同时清空暂存区。
这三步操作,就是三层区域协作最典型、最完整的循环。几乎所有Git操作,都可以映射到这三层区域的某个动作上。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 提交:从改动到历史记录的完整路径
提交(commit)是Git里最核心、也最容易被低估的概念。很多人把git commit当成“保存文件”的加强版——点一下,代码就存上去了。如果只是这种理解,那你后面读历史、写提交信息、处理冲突时都会很痛苦。
2.1 提交对象的内部结构:一次提交到底存了什么
在你执行git commit的那一刻,Git做了几件事:
- 把暂存区里的文件内容生成blob对象,存在
objects/里。 - 根据目录结构生成tree对象(记录了哪个文件在哪个目录、权限是什么、对应哪个blob)。
- 创建一个commit对象,这个对象里包含:
- tree对象的SHA:指向当前项目的目录快照。
- parent的SHA:指向父提交。普通提交有一个父提交;合并提交会有两个甚至多个父提交;根提交(仓库的第一次提交)没有父提交。
- 作者信息(Author):谁写的这段代码。
- 提交者信息(Committer):谁执行了提交。这俩在开源场景下经常不同,比如你给别人项目提PR,作者是你,提交者可能是项目维护者(通过rebase或merge方式合入后)。
- 提交信息(Commit Message):你写的
-m "xxx"内容。 - 时间戳。
理解了这一点,你就能理解为什么Git的提交历史是一条链:每个提交对象都保存着指向父提交的指针,一路回溯,最终能找到仓库的第一个提交。这也是为什么很多Git教程里把提交流程画成一条时间线上的节点,本质上就是commit对象通过parent指针串成的一条有向无环图(DAG)。
2.2 Git提交规范:开源项目里提交信息真的重要
我在代码审查里最常看到的问题,不是代码写得烂,而是提交信息写得一塌糊涂。比如:
code复制git commit -m "fix stuff"
这种提交信息在你自己本地仓库里随便写无所谓,但一旦进了开源项目,就是灾难。项目维护者靠提交信息来理解你改了什么、为什么改。如果提交信息含糊其辞,轻则被要求重写,重则PR直接被关掉。
开源项目里比较通行的规范是** Conventional Commits**(约定式提交),格式大致是:
code复制<type>(<scope>): <subject>
<body>
<footer>
类型(type)常见的包括:
| 类型 | 含义 | 典型场景 |
|---|---|---|
| feat | 新功能 | 新增了一个接口、一个组件 |
| fix | 修复bug | 修了一个崩溃问题、一个逻辑错误 |
| docs | 文档变更 | 改了README、加了注释 |
| style | 格式调整 | 加了分号、空格,不影响逻辑 |
| refactor | 重构 | 改了内部实现,不改变外部行为 |
| test | 测试变更 | 新增或修改测试用例 |
| chore | 杂务 | 改构建脚本、依赖配置等 |
scope是可选的,指影响的范围,比如fix(authentication): fix token refresh race condition,一眼就能看出影响的是认证模块。
body部分建议说明“为什么这么改”,而不是“改了什么”。因为改了什么看diff就知道了,为什么改才是维护者最想知道的。
我见过最优秀的提交信息长这样:
code复制fix(storage): resolve data loss when disk is full
When the disk becomes full, the write operation silently fails and
the error is not propagated to the caller, causing the application to
report success while data is actually lost.
Fix by checking the return value of fsync() and throwing an exception
if the write fails.
Closes #1234
第一行是简洁的总结,body里说明了问题现象、根因、解决方案,最后还关联了issue。这种提交信息,三个月后回头看依然能秒懂是干嘛的。
2.3 修改提交:commit --amend与交互式rebase
在开源项目里,你会经常需要修改已经写好的提交。比如审查之后发现某个文件少改了一行,或者提交信息写错了。
最简单的操作是git commit --amend:把暂存区的改动合并进上一次提交,同时可以修改提交信息。
bash复制git add src/file-that-i-forgot.js
git commit --amend -m "fix: proper message this time"
这样不会新增一条提交记录,而是把上一次提交“原地更新”了——严格来说并不是原地,而是生成了一个新提交对象,把原来的提交对象替换掉。这个操作在PR场景里特别有用,因为PR里的提交会被反复修改,你不想因为改个错别字就多出一条无意义的提交。
如果是修改更早的提交,比如要改倒数第二个,就得用git rebase -i交互模式:
bash复制git rebase -i HEAD~3
Git会打开一个编辑器,列出最近3条提交,你可以用reword改提交信息、用squash把提交合并到一起、用edit暂停下来修改内容、用drop删除某条提交。这个操作在开源贡献里的高频场景是:PR提交了好几次,审查过后要求“把这几条提交压成一条”。此时的命令就是:
bash复制git rebase -i origin/main
# 在编辑器里把除了第一条以外的全部改成squash
注意:
rebase会重写提交历史,所以绝对不要对已经推到远端并被别人拉取过的分支执行rebase。在开源项目的PR分支上,因为通常只有你自己在操作,所以可以放心用。但如果是共享分支(比如main),打死都不能rebase。
2.4 提交粒度:开源项目维护者最在意的细节
提交粒度这个问题,我在实战中体会特别深。刚开始贡献代码的时候,我喜欢憋一个大招,攒了四五天的改动,一次性提交一个“WIP: lots of changes”。后来作为维护者参与项目时才发现,这种提交对代码审查来说是灾难:审查者根本没法从一条巨型提交里定位问题。
正确的做法是按逻辑单元拆分子提交。比如你修一个bug,过程涉及:
- 改一个配置文件是加一个配置项
- 写一个工具函数
- 在主逻辑里调用这个函数
- 补测试用例
这四步可以拆成四个提交,每个提交都保证项目是可编译、可运行的状态。这样做的好处是:
- 审查者可以按提交逐个看,理解你的思考过程。
- 如果某个提交有问题,可以直接在PR里点出来,你只需要重新生成那一条提交,而不是整个PR。
- 别人后续用
git bisect二分法排查问题时,能精确定位到是哪一次改动引入了bug。
在开源项目里流传着一句话:“Make every commit a single logical change”。写代码的时候留个心眼,提交前花两分钟想一想“这次提交讲了一个完整的故事吗”,会让后续整个协作链路的体验提升好几个档次。
3. 分支:一张便利贴和一个开关
分支可能是Git里概念最简单、但玩起来最深的一个东西。很多人用分支只是图个方便——开个dev分支开发,完了合并回master。但如果你不理解分支的本质,遇到分支怪异的操作(比如rebase、cherry-pick、detached HEAD)就会一头雾水。
3.1 分支的本质:一个指向提交的可移动指针
Git的分支,本质上就是一个指向某个commit对象的指针。这个指针存在.git/refs/heads/目录下,每创建一个分支,就是往这个目录里写一个文件,内容是一个SHA-1哈希值。
bash复制git branch feature-login
这条命令做了什么?它创建了一个名为feature-login的分支,指向当前HEAD所在的提交,仅此而已。没有复制任何文件,没有创建新目录,就是在.git/refs/heads/下多了一个文件。
那git checkout feature-login(新版Git推荐用git switch feature-login)干什么?它把你当前的工作区文件还原成feature-login指针指向的那个提交的快照,同时把HEAD文件里的内容从ref: refs/heads/master改成ref: refs/heads/feature-login。
这里有个关键点:HEAD是一个“指针的指针”。它先指向分支指针,分支指针再指向提交对象。之所以这么设计,是因为Git需要区分“你在哪个分支上”和“你的分支指向哪里”这两个信息。当你在一个分支上执行git commit时,Git做的事情是:创建一个新提交对象,把父提交设为当前分支指针指向的提交,然后把当前分支的指针移动到新提交上。HEAD没有变——它仍然指向那个分支,但分支现在已经指向新提交了。
所以“切换分支”这个动作,实际包含两步:
- 把HEAD从指向A分支改成指向B分支。
- 把工作区文件的內容换成B分支指针指向的那个commit的快照。
理解了这个机制,你就明白了为什么“切换分支之前要把工作区清理干净”:因为Git切换分支时是用目标commit的快照去覆盖工作区的文件,如果你有未提交的改动,就有被覆盖的风险(实际上Git会检测并阻止危险操作,但它能检测的范围有限)。
3.2 合并:三方合并和那个著名的冲突标记
分支的生命周期里,合并(merge)是不可避免的一环。git merge的执行逻辑是:找到当前分支、目标分支以及它们的“共同祖先”(merge base),对这三个版本做一次三方合并。
为什么要“三方”?我举个例子。假设有这样一个历史:
code复制 A---B---C feature
/
D---E---F main
现在要把feature合并进main。git merge feature执行时:
- 共同祖先是
E。 E到F是main上的改动(假设这个例子中main在E之后没有新改动),E到C是feature上的改动。- 三方合并的目标是:在
E的基础上,同时应用两侧的改动。
如果两侧改的是文件的不同位置,Git能自动合并。如果两侧改了同一个文件的同一行,Git就束手无策,只能在代码里插入冲突标记,让你手动处理。
冲突标记长这样:
code复制<<<<<<< HEAD
这是main分支上的内容
=======
这是feature分支上的内容
>>>>>>> feature
处理冲突时,你或者保留一边,或者两边都保留,或者重新写一段,然后把<<<<<<<、=======、>>>>>>>这些标记行删掉,保存文件,再git add,最后git commit完成合并。
在开源项目里,冲突几乎是不可避免的——因为你fork出来的分支在本地改了几个月,上游主分支已经向前走了很远。而处理冲突的能力,恰恰是区分“会用Git”和“理解Git”的分水岭。
3.3 为什么开源项目里rebase比merge更常见
开源项目的PR流程里,你几乎不会用merge去把主分支的最新代码合并进你的PR分支,而是用rebase。为什么?
因为merge会产生一个合并提交(merge commit),导致PR分支和主分支之间出现一条绕来绕去的网状历史。而rebase的做法是:找到PR分支和主分支的共同祖先,然后把PR分支上所有独有的提交摘下来,在主分支最新的提交之后重新依次“播放”一遍。
bash复制git checkout my-feature
git rebase main
rebase之后,你的提交就像是在main的最新提交之后依次排列的,历史变成一条直线,没有多余的合并提交。维护者合并这类PR时,就能直接用fast-forward方式(快进合并),历史干净利落。
注意:rebase的代价是它改变了提交的哈希值,所以一旦你把这个分支push到了远端,并且其他人也拉取过这个分支,就不能再rebase了。在开源贡献场景里,你的PR分支只有你自己在动,所以可以放心rebase。但这个习惯不要带到团队共享分支上。
3.4 远程分支:origin/main和你本地的main不是一回事
很多新手在开源项目里犯迷糊,就是因为没分清“本地分支”和“远程分支”的区别。git branch -a会列出所有分支,你会看到:
code复制* main
remotes/origin/main
remotes/origin/HEAD -> origin/main
remotes/origin/main是远程分支在本地的一个“快照”,记录的是“上次我从远端拉取时,远程main分支指向哪个提交”。它不是实时同步的——你需要执行git fetch去更新这个快照,执行git pull等于git fetch + git merge(或git rebase)。
我给开源项目提PR时,经常遇到的情况是:本地main已经很旧了,远端的main已经向前走了50个提交。正确的操作流程是:
bash复制# 先切到本地main分支
git switch main
# 把本地main更新成和远程main一致
git pull origin main
# 切回你的PR分支
git switch my-feature
# 把main的最新改动rebase到你的PR分支上
git rebase main
这一套操作下来,你的PR分支就是基于最新main的,PR页面也会显示“This branch has no conflicts with the base branch”——这是你提PR时最想看到的一句话。
4. 工作流:从单打独斗到多人协作的路线图
仓库、提交、分支是Git的“零件”,工作流就是把这些零件组装起来的“设计图纸”。工作流解决的核心问题是:一群人怎么在同一个项目里协作,才能既高效又不互相踩脚。
4.1 集中式工作流:一上来就想当“中央集权”
最原始的工作流,是从SVN时代继承过来的思路:只有一个主分支(main/master),所有人在上面直接提交。为了不互相干扰,大家各自建本地分支,完成后再合并回main。
这种工作流对团队项目还能勉强运转,但放到开源项目里完全行不通。因为开源项目意味着“不是团队里的人也能参与”,你不可能给每个路过的贡献者开一个项目的写权限。所以集中式工作流基本只在私人仓库、个人项目或极少数强管控团队里见到。
4.2 Git Flow:功能完备但略显沉重
Git Flow在2010年被Vincent Driessen提出来,它定义了一套复杂的分支模型:
main:永远保持可发布状态。develop:日常开发的集成分支。feature/*:新功能开发分支,从develop分出来,完成后合并回develop。release/*:发布准备分支,从develop分出来,只做bug修复和版本号调整,完成后合并回main和develop。hotfix/*:线上紧急修复分支,从main分出来,修复后合并回main和develop。
这个工作流在存在“明确的发布周期”的项目里很好用,比如要出v1.2版本,有严格的RC阶段(release candidate),需要维护多个版本分支。但对很多开源项目来说,它显得有些笨重——每个改动可能要过feature分支、develop分支、release分支三道关卡,流程越长,贡献者的参与意愿越低。
4.3 GitHub Flow:开源项目的主流选择
GitHub Flow是一种极简的工作流,只有两条核心原则:
- 所有改动都基于main分支,开一个分支来做。
- 开分支后,随时向main分支提PR,通过审查后合并。
具体流程是:
- 从main分支切出一个新分支,命名为
feature/xxx或fix/xxx。 - 在这个分支上开发、提交。
- 推送到远端,开PR。
- PR被审查、讨论、修改。
- 通过后合并到main,删除分支。
GitHub Flow之所以在开源世界流行,是因为它把PR变成了协作的中心。贡献者不需要理解太复杂的流程,只要会“开分支、提交、推送、提PR”四步就能参与。项目的维护者通过PR来把关代码质量、引导讨论、做持续集成,一切都在一个界面里完成。
4.4 Forking Workflow:开源贡献的最终形态
前面三种工作流的共同前提是:所有参与者都在同一个仓库里操作。但开源项目显然不是这样——绝大多数贡献者并没有主仓库的写权限。于是Forking Workflow登场了,这也是开源贡献场景下最主流的工作流,不管你是给一个明星项目提PR,还是给一个个人项目修bug,几乎都跑这套流程:
- 在GitHub/GitLab上点击Fork,把主仓库复制一份到你自己的账号下。
git clone你fork下来的仓库到本地。- 添加远程仓库:
git remote add upstream <原仓库地址>。此时本地有两个远端引用,origin(你自己的fork)和upstream(原仓库)。 - 在本地创建一个新分支用来开发。
- 开发、提交、推送到
origin(你自己的fork)。 - 在原仓库页面发起Pull Request,请求把
origin/分支合入upstream/main。 - 维护者审查PR,提出修改意见;你继续在本地分支修改、推送,PR自动更新。
- PR被合并后,清理本地分支和远端fork里的对应分支。
Forking Workflow的关键在于刻意制造了“双重仓库结构”:origin让你拥有完全自由的开发空间,upstream保证代码最终能汇合到主线。这也是为什么很多开源项目贡献指南里都有一句话:“Please fork the repository and create a pull request from your fork.”
4.5 工作流选择的底层逻辑:你是在“存量维护”还是“增量贡献”
聊完几种工作流,我想多说一句选型的逻辑。其实没有“哪个工作流最好”,只有“哪个工作流在这个场景下最合适”。
判断的标准就一条:这个项目的分支生命周期长不长,发布节奏密不密。
- 生命周期短、发布节奏快(比如一个web项目,每天发布好几次):GitHub Flow足矣,再上Git Flow就是给自己找麻烦。
- 生命周期长、需要在多个版本线之间同时维护(比如一个SDK库,要同时维护v1.x和v2.x):Git Flow或类似的多重长期分支模型更稳。
- 多人协作、需要强代码审查和权限控制(开源项目):Forking Workflow几乎是唯一解。
我之前见过一个项目,明明就是个活跃度中等的开源库,非要上一套重型的Git Flow,搞出develop、release、hotfix一堆分支,结果核心维护者自己都被绕晕了,贡献者的PR不知道应该往哪个分支提。后来简化成主干开发+PR审查,反而活跃度上来了。
所以我平时给别人的建议是:工作流是协作习惯的固化,不是流程规定的堆砌。先用最简单的方式跑起来,觉得疼了再加规则。
5. 实战:给开源项目提PR前必须搞清楚的五个命令
概念说了这么多,最后回到实操。给开源项目贡献代码,你最终要落实到具体的命令上。以下五个命令,是你在PR流程中几乎每天都要用到的,把这五个命令的机制搞清楚,你对Git的理解就比大多数人强了。
5.1 git status:判断当前仓库状态的“仪表盘”
git status看起来最简单,但它是你日常最常用的命令。它的价值在于告诉你当前所有文件的状态。在一个大型开源项目里,改了几个文件,改了哪些,哪些已经add了,哪些还没,扫一眼git status就全清楚了。
两件小事值得注意:
git status的输出分了三个区块:Changes to be committed(已暂存)、Changes not staged for commit(已修改但未暂存)、Untracked files(未跟踪)。看到这三块就对应了仓库三层区域的概念。- 如果你在项目里配置了
git status --short的别名(git st),输出会非常紧凑,比如M file.txt表示文件被修改但未暂存,M file2.txt表示已暂存。
建议:进入任何你不熟悉的仓库,第一件事永远是git status,先把当前状态搞清楚。
5.2 git log:看历史,更准确地说是看“提交图”
git log虽然简单,但配合不同参数,功能差异很大。在开源项目里常用的组合:
bash复制# 单行查看最近N条提交
git log --oneline -10
# 查看某个作者最近的提交
git log --author="some-name" --oneline
# 图形化查看提交历史(这在开源项目里尤其有用,能看清分支走向)
git log --graph --oneline --all
# 查看某个文件的修改历史
git log --follow -- src/file.js
每次写提交信息的时候,我都会先看一眼前几天的提交格式,保持一致风格。很多项目在CONTRIBUTING.md里写了提交规范,但如果你没看,最保守的办法就是模仿已有提交的写法。
5.3 git diff:提交前必须确认的“改动清单”
git diff是审查自己代码的第一道关卡。提交前执行一次,看看自己到底改了什么,有没有不小心把调试代码或者无关的文件也改了。
几个实用场景:
bash复制# 查看工作区未暂存的改动
git diff
# 查看暂存区的改动(已经git add的)
git diff --cached
# 查看当前分支和main分支的差异
git diff main...
在开源项目里,我强烈建议你养成一个习惯:commit之前先git diff --cached过一遍,确认每一条改动都是预期内的。这个习惯能帮你拦住大量低级错误,比如泄露的密钥、不该提交的本地配置、误改的依赖版本。
5.4 git rebase -i:整理提交历史的瑞士军刀
前面已经详细说过rebase -i,这里再强调它在开源贡献里的一个高频用法:合并多个琐碎提交。
你在PR分支上开发时,可能是这样的:
code复制fix: typo in docs
wip: some changes
fix: another typo
feat: implement login
这些提交推上去之后,PR页面会显示一堆乱糟糟的提交。维护者大概率会说:“please squash your commits.”此时当你执行:
bash复制git rebase -i origin/main
编辑器里把所有提交标记为squash(除了第一条改为pick),保存退出,然后把历史重写后的分支强制推送到你的fork:
bash复制git push --force-with-lease origin my-feature
PR页面会立刻变成一个干净的单提交。注意这里用的是--force-with-lease而不是--force,前者会在推送前检查远端是否已被别人更新过,避免覆盖他人的提交。
重要:强制推送是危险操作。在使用前一定确认你推送的是PR分支,而不是共享分支。
--force-with-lease是在“可能出错”和“必须强推”之间最稳妥的选择。
5.5 git reflog:Git的后悔药
最后一个命令,也是很多新手不知道的:git reflog。它的作用是记录HEAD指针的每一次移动历史。这意味着即使你误删了分支、错误地reset回了错误的提交,都能通过reflog找到你之前所在的位置,然后恢复。
bash复制git reflog
输出类似:
code复制abc1234 HEAD@{0}: rebase finished: refs/heads/my-feature onto main
def5678 HEAD@{1}: rebase: checkout main
...
哪怕你执行了git reset --hard,只要reflog里还有记录,就能git reset --hard <那个SHA>恢复到之前的状态。在开源项目里,尤其是你执行了rebase、amend这些改变历史的操作后,说不定什么时候发现弄错了,reflog就是最后一根救命稻草。
我组建的团队里,新人入职第一天我会专门花半小时带他们把Git从git status到git reflog过一遍。因为根据我的经验,一个开发者对Git的理解深度,直接决定了他在复杂协作场景下的工作效率。概念扎实的人,遇到问题能自己推理出解决方案;概念模糊的人,只能靠搜索“Git pull冲突怎么办”来救火,而且下次换个错误场景又抓瞎。
仓库的三层区域、提交的历史链路、分支的指针本质、工作流的演进逻辑,这四个概念是贯穿一切的骨架。下次你再执行任何Git命令时,不妨停下来问一句:这条命令操作的是仓库的哪个区域?它改变了哪个指针?它会不会影响历史?
想清楚这一个问题,你对Git的理解,就已经超过了大半数嘴上说着“会用Git”的开发者了。
