作为一名开发者,你是否曾对 git rebase 心怀忐忑,而更倾向于使用 git merge?遇到需要版本回退的情况时,是否感到无从下手?这或许是很多开发者从“会用Git”到“精通Git”之间的一道坎。
要跨越这道坎,仅仅记住命令的语法是不够的,关键在于理解其背后的设计思想与运作原理。本文将从Git的核心概念出发,逐步深入到分支管理、代码合并等高级操作,帮助你从本质层面掌握Git,从而在开发中游刃有余。也欢迎你前往 云栈社区 的“基础 & 综合”板块,与更多开发者交流底层技术原理。
1. 基本概念
1.1 Git的优势
Git是一个分布式代码管理工具。在讨论分布式之前,我们先了解一下什么是中央式代码管理仓库。
- 中央式:所有的代码都保存在中央服务器,提交必须依赖网络,并且每次提交都会直接进入中央仓库。在协同开发时,可能频繁触发代码合并,增加了提交的成本和复杂度。SVN就是典型的中央式工具。
- 分布式:可以在本地完成提交,无需依赖网络,每次提交都会在本地自动备份。每个开发者都可以将远程仓库完整地克隆(clone)到本地,包括所有的提交历史。Git就是分布式工具的代表。
那么,Git相比SVN的优势在哪里?
举个例子:当你写了一大堆代码后,突然发现一个小时前写的部分有问题,希望能回退到那时的状态。对于这种情况,Git的优势就非常明显了。因为Git鼓励频繁的本地提交(成本小),并且本地保存了完整的提交记录,可以随时随地进行精准回退。
这并非说SVN无法完成类似操作,只是Git的实现方式更加优雅和高效。Git相比于中央式工具还有许多其他优点,在此不一一列举,感兴趣的读者可以自行探索。
1.2 文件状态
在Git中,文件主要分为三种状态:已修改(modified)、已暂存(staged)、已提交(committed)。
- 修改(modified):Git能够感知到工作目录中哪些文件被修改了,然后将这些修改的文件标记为“已修改”状态。
- 暂存(staged):通过
add 命令,可以将工作目录中已修改的文件提交到暂存区,等待被最终提交。
- 提交(committed):将暂存区的文件通过
commit 命令永久保存到Git的版本库中。
1.3 Commit节点
为了表述方便,下文将以“节点”来代称一次提交(commit)。
在Git中,每次提交都会生成一个唯一的节点,并通过SHA-1算法计算出一个哈希值作为其唯一标识。多次提交会形成一个线性的节点链(在不考虑合并的情况下),如图1-1所示。

图1-1:线性提交历史
节点上方的 2d...、ab...、c1... 就是通过SHA-1计算的哈希值。此外,每个节点都包含了其父节点的内容。例如,C2节点包含了C1的提交内容;同样,C3节点包含了C1和C2的所有提交内容。
1.4 HEAD
HEAD是Git中一个非常重要的概念,你可以将它理解为一个“指针”或“引用”。它可以指向任何一个提交节点,并且它所指向的节点内容,就是当前工作目录所呈现的代码状态。
仍以图1-1为例,如果HEAD指向C2节点,那么你当前看到的工作目录代码就是C2节点对应的版本。HEAD也可以指向某个分支,从而间接指向该分支所指向的节点。如何移动HEAD的指向将在后续章节讲解。
1.5 远程仓库
尽管Git会将代码和历史完整地保存在本地,但在团队协作中,最终仍需要将代码同步到一个公共的服务器,即远程仓库。
通过 git clone 命令,你可以将远程仓库的代码、提交历史、分支、HEAD等状态完整地下载到本地。但需要注意的是,这些本地副本(如远程分支引用 origin/master)不会自动更新,需要你手动执行命令从远程仓库拉取(fetch)最新的状态。
远程仓库作为协作的中枢,你和你的同事可以基于它进行协同开发。开发新功能后,可以推送(push)到远程仓库;同样,也可以从远程仓库拉取(pull)同事的代码。
注意点:由于远程仓库是团队协作的基准,务必保证其代码质量。切勿将未经充分测试和验证的代码直接提交到远程仓库的主分支。
2. 分支
2.1 什么是分支?
分支是Git另一个核心概念。当一个分支指向某个节点时,当前节点的内容即为该分支的内容。分支本质上也是一个指针或引用,与HEAD类似。不同之处在于,HEAD通常只有一个,而分支可以同时存在多个。在实际开发中,我们通常会根据不同的功能或版本需求创建各自的分支。
那么,分支有什么用呢?
设想一个场景:你们的App历经艰辛终于发布了v1.0版本。由于需求紧急,v1.0上线后团队立刻开始v1.1版本的开发。正当你开发得起劲时,QA同学反馈用户发现了一些v1.0的bug,需要紧急修复并重新发布。修复v1.0的bug必须基于v1.0的代码,可你现在已经基于master分支开发了一部分v1.1的功能,代码已经发生了变化。此时该怎么办?
引入分支概念可以优雅地解决这个问题,如图2-1所示。

图2-1:使用分支进行hotfix
- 先看左侧示意图,假设C2节点对应v1.0版本的代码。上线后,我们基于C2节点创建一个用于修复bug的分支,例如
ft-1.0。
- 再看右侧示意图,v1.0上线后,团队继续在
master 分支上开发v1.1内容,并提交了节点C3。当收到QA反馈后,切换到 ft-1.0 分支进行bug修复,修复完成后提交生成节点C4。最后,切换回 master 分支并合并 ft-1.0 分支,即可将修复内容同步到主线上。
分支的适用场景非常广泛,例如,当你有一个不确定是否上线的功能需要预先开发时,可以为其单独创建一个特性分支。待功能确定上线时,直接将该分支合并到主分支即可。许多优秀的开源项目也都在GitHub上采用类似的工作流进行协作。
注意点:在某个节点创建分支时,Git并不会复制该节点对应的代码文件,而只是创建一个新的引用(分支)指向该节点。无论是HEAD还是分支,它们都是非常轻量级的引用,这是Git高效设计的关键之一。
3. 命令详解
3.1 提交相关
我们之前提到,想要提交代码,必须先将改动加入暂存区。Git中通过 add 命令实现。
添加某个文件到暂存区:
git add 文件路径
添加所有文件到暂存区:
git add .
同时,Git也提供了撤销工作区和暂存区改动的命令。
撤销工作区改动(危险操作,未加入暂存区的改动会丢失):
git checkout -- 文件名
清空暂存区(将文件从暂存区撤回,但工作区改动保留):
git reset HEAD 文件名
提交:
将改动加入暂存区后,即可进行提交。提交后会生成一个新的提交节点。
git commit -m “该节点的描述信息”
3.2 分支相关
创建分支
创建一个新分支,该分支会与HEAD指向同一个节点。简单说,HEAD指向哪里,新创建的分支就指向哪里。
git branch 分支名
切换分支
切换分支后,默认情况下HEAD会指向当前分支,即HEAD间接指向当前分支指向的节点。
git checkout 分支名
你也可以创建分支后立即切换,这是一个常用组合操作:
git checkout -b 分支名
删除分支
为了保持仓库分支的简洁,当一个分支完成使命后(例如其功能已被合并),应及时删除它。
git branch -d 分支名
3.3 合并相关
合并命令是Git中较难掌握但至关重要的部分。常用的合并命令主要有三个:merge、rebase 和 cherry-pick。
merge
merge 是最常用的合并命令,它可以将某个分支或某个节点的代码合并到当前分支。
git merge 分支名/节点哈希值
如果需要合并的分支完全领先于当前分支,如图3-1所示,ft-1 完全包含 ft-2 的内容。

图3-1:Fast-forward 合并
此时,ft-2 执行 git merge ft-1 后会触发 快速合并(fast-forward),两个分支直接指向同一个节点,这是最理想的情况。
但实际开发中更常见的是图3-2(左)所示的情况:两个分支各自有了新的提交。

图3-2:三方合并
此时,ft-2 执行 git merge ft-1 后,Git会将节点C3和C4的内容合并,并生成一个新的提交节点C5(这个过程称为三方合并),最后将 ft-2 分支指向C5,如图3-2(右)所示。
注意点:如果C3和C4同时修改了同一个文件的同一行代码,合并时会发生冲突。因为Git无法自动决定该保留谁的修改,这时就需要你手动解决冲突,编辑文件,然后完成合并提交。
rebase
rebase(变基)是另一种合并方式。
git rebase 分支名/节点哈希值
与 merge 不同,rebase 的合并结果看起来不会产生新的合并节点,而是将当前分支的提交“重新播放”在目标分支的最新提交之后,如图3-3所示。

图3-3:Rebase 变基
当左侧示意图的 ft-1.0 分支执行 git rebase master 后,会将C4节点复制一份(记为C4')放到C3节点之后。C4与C4'内容对应,但拥有不同的哈希值。
rebase 能使提交历史看起来更加线性、整洁,它将并行的开发流程在历史记录中呈现为串行,更符合直观。既然 rebase 这么好,是否可以完全取代 merge?并非如此,下面我们来比较一下它们的优缺点:
- merge 优缺点:
- 优点:每个节点都严格按照实际提交时间排列。发生冲突时,通常只需解决两个分支末端节点的冲突。
- 缺点:合并两个分叉的分支会生成新的合并节点,长期使用可能导致提交历史图错综复杂。
- rebase 优缺点:
- 优点:使提交历史保持线性,易于阅读。
- 缺点:历史被重写了,不再是严格的按时间排序。例如图3-3中,无论C4实际提交时间早于或晚于C3,它在变基后都会排在C3之后。此外,变基过程中可能需要重复解决冲突(如果多个提交都修改了相同代码)。
对于网络上一味推崇只使用 rebase 的观点,笔者持保留态度。在不同分支(如长期存在的特性分支)合并时使用 rebase,可能会因重复解决冲突而得不偿失。但在将本地分支推送到远程同一条分支前,优先使用 rebase 来整理提交历史是个好习惯。笔者的观点是:根据不同场景合理搭配使用 merge 和 rebase,如果两者皆可,优先考虑 rebase。
cherry-pick
cherry-pick(遴选)不同于上述两种合并,它允许你选择某个(或某几个)特定的提交节点,将其更改应用到当前分支。
git cherry-pick 节点哈希值
如图3-4所示,假设当前分支是 master,执行 git cherry-pick C3 C4 后,会将C3和C4节点的修改抓过来,在当前分支末端依次形成新的提交C3'和C4'。

图3-4:Cherry-pick 遴选
3.4 回退相关
分离HEAD
默认情况下,HEAD指向某个分支。但你也可以让HEAD脱离分支,直接指向某个提交节点,这个过程称为“分离HEAD”。
git checkout 节点哈希值
// 或者直接脱离当前分支,指向当前节点
git checkout --detach
由于哈希值冗长难记,Git提供了基于相对引用的快捷方式:
// HEAD分离并指向前一个节点
git checkout 分支名/HEAD^
// HEAD分离并指向前N个节点
git checkout 分支名~N
将HEAD分离出来指向特定节点有什么用?例如,你发现某个历史提交有问题,可以将HEAD指向那个节点,修改代码后重新提交。如果你不希望生成一个新的提交节点,而只是想修正那个历史提交,可以在提交时加上 --amend 选项:
git commit --amend
回退
回退是开发中的常见需求。比如你提交了代码后发现问题,想撤销这次提交,回到上一个版本。这可以通过 reset 命令实现。
// 回退N个提交
git reset HEAD~N
reset 与相对引用类似,但区别在于 reset 在移动HEAD的同时,也会移动当前分支的指向,一同回退。
3.5 远程相关
当我们接触一个新项目时,首先要获取代码。Git中通过 clone 命令从远程仓库复制代码到本地。
git clone 仓库地址
前文提到,clone 不仅仅是复制文件,它还会把远程仓库的所有分支、HEAD等引用状态一并保存到本地,如图3-5所示。

图3-5:Clone 操作后的本地与远程引用
其中 origin/master 和 origin/ft-1 是本地的远程分支引用,它们代表了上一次与远程仓库通信时,远程分支的状态。这些本地引用不会自动更新。当远程仓库的 master 分支有了新提交,你需要手动执行命令来更新本地的这些“远程快照”。
小提示:技术上,任何Git仓库都可以作为另一个仓库的“远程”,甚至是本地的一个目录。这有助于理解Git的分布式本质,但在实际协作中,我们总是使用一个共享的服务器仓库。
fetch
通俗地说,fetch 是一次下载操作。它会从远程仓库拉取所有新增的提交节点,并更新本地的远程分支引用(如 origin/master),但不会改变你本地的工作目录和当前分支。
git fetch 远程仓库地址/分支名
pull
pull 命令可以从远程仓库的某个分支拉取代码并合并到当前分支。
git pull 远程分支名
实际上,pull 的本质是 fetch + merge。它先执行 fetch 更新本地远程仓库状态,然后将远程分支的内容合并(merge)到当前本地分支。合并完成后,本地分支会指向最新的节点。
你也可以使用 rebase 方式进行拉取合并:
git pull --rebase 远程分支名
push
push 命令将本地提交推送至远程仓库。
git push 远程分支名
直接推送可能会失败,例如当远程分支有你本地没有的新提交时,会产生冲突。因此,在 push 之前,通常先执行一次 pull 或 pull --rebase 来合并远程的更改并解决潜在冲突。推送成功后,本地的远程分支引用(如 origin/master)会更新,与本地分支指向同一节点。
总结
- 引用是核心:无论是HEAD还是分支,都是指向提交节点的轻量级引用。“引用 + 节点”构成了Git分布式版本控制的基石。
- 合并策略的选择:
merge 保留了更真实的时间历史,而 rebase 创造了更线性的提交历史。应根据场景选择,在推送前整理历史时可优先使用 rebase。
- 时光机:通过移动HEAD,你可以查看任何一个历史提交对应的代码状态。
- 同步机制:
clone 或 fetch 都会将远程仓库的完整状态(提交和引用)在本地保存一份副本。
- 拉取的本质:
pull 实质上是 fetch 加上合并操作(默认merge,可选rebase)。
理解这些原理后,再面对复杂的版本管理场景时,你就能清晰地知道每个命令背后发生了什么,从而做出最合适的选择,真正驾驭Git,而非仅仅记住命令。