写Git教程往往容易陷入一个误区:一上来就铺开讲各种命令,讲完大家还是不知道实际开发中怎么用。尤其是分支——这个Git里最核心、也最容易被用乱的功能。这篇是Git系列教程的第五篇,我打算把分支的“查看、创建、合并”三条主线串起来,结合这些年实际项目里踩过的坑,把链路讲透。内容不追求覆盖所有冷门参数,而是聚焦日常工作流里最高频的操作:怎么看当前在哪个分支、怎么建一个新分支、怎么把分支上的改动合并回主干、冲突了怎么处理。不管你是刚接触Git的应届生,还是用了两年Git但一直没理清分支逻辑的“熟练工”,这篇都能让你少走几次弯路。
先说明一点:这篇文章默认你已经装好了Git,并且基本了解commit、push、pull这几个基础操作。如果连Git还没有装,建议先花十分钟把环境配好,再回来看分支操作。分支本身并不难,真正容易翻车的往往是把分支与其他概念混在一起的时候——比如“我切换分支怎么代码不见了”“我明明合并了怎么远程没反应”,这类问题我会在后面的常见问题章节专门梳理。
1. 先搞清楚分支到底是个什么东西
很多人用Git分支,用了一年可能都没想明白:分支本身到底存了什么?为什么创建分支那么快?为什么切换分支有时候会丢失改动?
1.1 分支本质上只是一个指针
Git里面的分支,本质上就是一个指向某个commit对象的可变指针。你可以把Git的提交历史想象成一条由提交节点连成的链,每个节点保存了当时项目文件的快照、父节点信息、提交人、提交信息等。分支名,就是贴在这条链上某个节点上的“便利贴”。当你执行git commit,当前分支这个“便利贴”就会自动挪到新生成的提交节点上。
这个设计带来的好处非常明显:创建分支几乎是瞬间完成的,因为它只是创建一个几字节的指针文件,而不是复制一份项目源码。我经常用一句话跟同事解释分支和拷贝的区别:“拷贝是把一本书复印一遍,分支是在书页上贴个标签,书还是那本,标签指向哪里,哪里就是当前版本。”
理解了这一点,很多现象就讲得通了:为什么你新建的分支能“看到”之前所有提交?因为新分支指针默认指向当前HEAD所在的提交,从那以后两个分支各自往前走,代码才真正分道扬镳。
1.2 HEAD:你当前到底站在哪里
与分支配套的核心概念是HEAD。HEAD也是一个指针,但它特殊在:它指向的是“当前所在的分支”,而不是某个具体的提交。当你执行git checkout或git switch切换分支时,Git做的事其实只有两步:把HEAD指向新的分支名,然后把工作目录里的文件替换成该分支最新提交对应的快照。
这里有个极容易踩坑的点:HEAD不等于分支名,但大多数时候你不需要关心HEAD的内部实现,只需要知道“HEAD指向哪个分支,我就在哪个分支上”。可以用git symbolic-ref HEAD查看HEAD到底指向什么,正常情况下输出类似refs/heads/main。如果你看到的是refs/heads/后面什么都不跟,那说明你处于detached HEAD状态,也就是直接检出了某个历史提交而不是分支,这种状态下做提交会找不到归属,新手经常在这里栽跟头。
1.3 分支的实际价值:隔离与并行
明白了分支是指针,再来看它的价值就清晰了。分支带来的核心能力是两个:隔离和并行。
所谓隔离,是指不同分支上的提交各自独立,互不影响。你可以在dev分支上随便改、随便提交、甚至把代码改坏,只要不合并回主分支,主干永远是稳定的。所谓并行,是指多条开发线可以同时前进——张三在feature/login上做登录模块,李四在feature/order上做订单模块,互不干扰,最后由项目经理或组长统一把功能分支合并回主干。
我在团队里经常用一句话强调分支的意义:“主干永远是可发布状态。”每个开发者在自己的特性分支上开发,合并前经过测试和评审,这才是一个成熟团队该有的协作模式。如果你现在还习惯所有人直接往master上提交,看完这篇之后建议赶紧改掉这个习惯。
2. 查看分支:先弄清家底再动手
查看分支是分支操作的第一步。很多人一上来就敲git branch,然后面对一堆输出懵了:到底是本地分支还是远程分支?我当前在哪个分支?哪些分支合并过了?别急,逐个来看。
2.1 最常用的分支查看命令组合
先说最高频的三条:
git branch:列出所有本地分支,当前分支前面会有一个*号标记,绿色高亮(如果你配了颜色)。git branch -v:在分支名后面显示该分支最新的提交哈希和提交说明,方便你快速看出每个分支分别停在哪里。git branch -vv:比-v再多一列,显示本地分支与远程分支的关联关系(也就是upstream信息),比如[origin/dev]这样的标记。
实际工作中,我几乎不用裸的git branch,最少也要用git branch -vv。原因很简单:光看分支名你不知道这个分支对应远程哪个分支、领先或落后几个提交,这些信息对判断下一步操作非常关键。
还有两个高频场景:查看远程分支用git branch -r,查看所有分支(本地加远程)用git branch -a。-a的输出里,远程分支会以remotes/origin/xxx的形式出现,注意远程分支你通常不能直接切换上去工作,需要先基于它创建本地分支。
2.2 怎么看当前在哪个分支
这个问题的答案其实不止一种方式。
最直观的方式是看命令行提示符。如果你配了git-prompt之类的工具,终端会显示类似(main)的前缀。没有配的话,执行git branch,看哪一行前面带*即可。更进一步,执行git status,第一行输出On branch xxx,一目了然。
还有一个被我经常用的方法:git log --oneline --graph --all -n 20。这条命令会以图形化的方式展示最近20条提交,并且用符号标识各个分支所在的位置。比如你会看到* feature/login、* main这样的标记,一眼就能看出当前主线停在哪、功能分支领先了多少个提交。这种“上帝视角”对理解分支关系帮助非常大。
平时在IDE里工作的话,IDEA的右下角会显示当前分支名,VS Code的左下角也有类似标记。不过IDE偶尔会“骗人”——提示信息没自动刷新,所以当我拿不准时,还是习惯回终端敲一下git branch确认,避免判断失误导致误操作。
2.3 查看分支之间的差异与状态
除了看分支本身,开发中更常需要的是对比分支之间的差异。这里列举几个我会用到的高频操作:
git log main..feature/login:查看feature/login上有而main上没有的提交。git diff main feature/login:对比两个分支最新提交之间的文件内容差异。git log --oneline --graph --all:按图形化方式看整个提交网络,这是排查分支结构问题最有效的命令,没有之一。
其中git log main..feature/login在合并前非常有用。它相当于提前告诉你“如果我执行合并,会带进来哪些提交”,这些提交是否都是你想要的?有没有夹带私人提交?提前筛查能减少很多麻烦。
另一个很实用的命令是git branch --merged和git branch --no-merged,用来列出已经合并进当前分支的以及尚未合并的分支。前者是“可以安全删除的候选名单”,后者是“千万别手滑删掉的黑名单”,单独记这两个命令不太起眼,但在做分支清理的时候简直救命。
3. 创建分支:建分支原来有这么多讲究
说实话,创建分支的命令本身非常简单,但很多人在“从哪儿建”“建完要不要切”“怎么跟远程关联”这几个问题上反复踩坑。这一节我们把场景拆开讲。
3.1 最简单的创建与切换组合
创建分支的命令有三兄弟,虽然都能完成“创建分支”这件事,但细节不同:
git branch <分支名>:创建一个新分支,但不切换过去。git checkout -b <分支名>:创建并立即切换过去。git switch -c <分支名>:Git 2.23+推出的更语义化的写法,功能与上一条完全一样。
我个人的习惯:如果只是想生成一个分支供以后使用,用git branch;如果创建完马上就要开始写代码,用git checkout -b或git switch -c。git switch系列命令是Git官方后来刻意推出用来替代checkout的部分功能的,因为checkout承担了太多职责(切换分支、恢复文件、检出新分支……),容易让人混乱。
实际场景举例:白天上班接到需求“给用户中心加一个找回密码页面”,我先git checkout main确保从最新的主干拉分支,再执行git checkout -b feature/password-reset,然后开始开发。注意一个原则:新分支最好从main、develop这类主干分支创建,而不是从某个功能分支再拉功能分支。不然功能之间的代码互相纠缠,合并的时候会非常痛苦。
3.2 从指定提交、标签或远程分支创建
git branch的完整用法是git branch <新分支名> <起点>,起点可以是任意提交哈希、已有的本地分支名、远程分支名或标签名。举几个例子:
git branch fix/hotfix abc1234:从提交abc1234位置拉出一个修复分支。git branch dev origin/dev:基于远程分支origin/dev创建本地分支dev。git branch demo v1.0.0:从标签v1.0.0处拉出demo分支。
这个能力在排查线上问题时特别好用。比如线上出了紧急bug,但main上已经合了很多新功能不方便直接改,这时候可以找到线上版本的tag(例如v2.3.1),从那个tag拉一个hotfix/xxx分支,修复后打补丁发布,完全不影响主干上的新功能开发。
还有一个非常常见的场景:从远程分支创建本地分支。输入git checkout -b dev origin/dev即可。这里有个不那么自动化的点:如果本地分支名和远程分支名相同,可以直接用git checkout --track origin/dev,Git会自动创建同名的本地分支并关联远程分支。关联上之后,git push和git pull就不用每次指定远程分支名了,能省不少事。
3.3 分支命名的规范与建议
分支名看似随便起,但起得不好后面会非常头疼。我见过有人用aaa、test1、new这种名字,过一周再回来看,根本不知道这个分支是干嘛的。这里给出我在团队里推行的命名风格,供参考:
功能分支:feature/功能描述,例如feature/user-login修复分支:bugfix/问题描述或hotfix/问题描述,例如hotfix/login-error发布分支:release/版本号,例如release/v1.2.0临时实验分支:experiment/描述,例如experiment/new-cache
层级目录式的命名不仅让分支归类清晰,还能让很多Git管理工具(包括GitLab网页端、SourceTree、IDEA自带的分支面板)自动按目录折叠,浏览体验好了不止一个档次。
另外强烈建议分支名尽量全小写英文加短横线,不要用中文、不要用空格、不要用.,这些符号在Shell和工具里多多少少会出幺蛾子。我见过同事用中文建分支,结果在Jenkins构建脚本里因为编码问题直接报错,白白折腾了一下午。
4. 合并分支:把成果汇入主干的艺术
创建分支容易,合并分支才是真正考验功力的时候。很多人怕合并,本质上是怕冲突。这里我要先说一句:合并本身不可怕,只要理解了合并的原理和策略,绝大多数冲突你都能从容解决。
4.1 两种合并方式:快进合并与三方合并
Git执行git merge时,根据具体情况会选择两种合并方式之一。
第一种叫快进合并(Fast-forward merge)。当目标分支(比如main)从你拉出feature分支之后没有任何新的提交,也就是main的指针还停在feature分支历史的一个祖先节点上,这时Git只需要把main指针直接挪到feature当前位置,不需要生成新的提交。这种合并最干净,历史是一条直线。
第二种叫三方合并(Three-way merge)。当main在拉出分支之后又走了几步,而feature也走了几步,两个分支产生了分叉,Git就需要找出三个快照:两个分支的最新提交,以及这两个提交的共同祖先(merge base),然后对比差异,把两边都改动的部分合并到一起。如果两遍改的是同一个文件的同一行,Git无法自己判断该保留哪边,就会产生冲突,需要人来做决策。
判断合并类型有一个偷懒的办法:执行git merge feature时,如果输出显示Fast-forward字样就是快进合并,如果显示Merge made by the 'ort' strategy(较新版本是ort,老版本是recursive)就是三方合并,会额外生成一个合并提交。
4.2 合并的主流程:从准备到推送
一个规范的合并操作,我建议按如下步骤走:
- 确保工作区干净。执行
git status确认没有未提交的改动,有的话先提交或暂存(git stash)。 - 切到目标分支。想合并到
main,就先git checkout main。 - 拉取最新代码。执行
git pull --rebase或git pull,确保本地main和远程保持一致。这一步很多人忽略,结果合并的时候把远程已经删除的旧文件又带了回来,或者在push时收到一堆冲突提示。 - 执行合并。
git merge feature/login。 - 冲突了就解决冲突(下一节细说),没有冲突会直接生成合并提交。
- 推送到远程。
git push。
这里面我要特别强调第3步。你可以回想一下:是不是经常遇到“我明明把分支合并了,怎么push上去没有变化”的情况?大概率就是你合并之前没有拉取远程最新代码,导致你的本地main已经落后,合并提交没有包含远程最新的东西。
4.3 冲突处理:从慌到不慌
冲突并不可怕,它只是Git在告诉你:“这里我搞不定,需要你判断。”真正可怕的是冲突之后一顿乱操作,把代码搞得更乱。
当冲突发生时,Git会在冲突文件里插入冲突标记。你会看到类似这样的内容:
<<<<<<< HEAD 这里的代码是当前分支(`main`)的版本 ======= 这里的代码是合并进来分支(`feature`)的版本 >>>>>>> feature/login<<<<<<<到=======之间是当前分支的内容,=======到>>>>>>>之间是要合并进来的分支的内容。你需要根据业务逻辑决定保留哪部分、调整哪部分,或者两者都要。记得把冲突标记本身删除干净,这是新手最常犯的错误——改了半天代码,结果把<<<<<<<、=======这些标记留在了文件里,编译直接报错。
解决完冲突之后,对每个冲突过的文件执行git add,然后执行git commit(注意这里不是git merge --continue也可以,但--continue更语义化,会自动带上默认的合并提交信息)。以IDEA为例,解决冲突有图形化界面,可以很方便地选择左边、右边还是both,强烈推荐用工具解决而不是手动在编辑器里删标记。
注意:如果你在合并过程中发现自己搞不定,想回到合并前的状态,执行
git merge --abort即可。Git会把你工作区恢复到合并开始之前的样子。这个命令是后悔药,心里要始终记得有它。
4.4 合并后验证:别急着推送
合并完成不代表万事大吉。我个人的习惯是,在推送之前先做一轮本地验证:
git log --oneline --graph -n 10:确认合并提交生成正确,分支网络是否符合预期。git diff main origin/main:对比本地与远程main的差异,确认没有意外文件被带进来。- 编译一把并跑相关测试。合并引起的编译失败不是罕见事,尤其是两个分支改动同一个接口定义时,Git可能不报冲突,但编译不通过。
推送之前多花两分钟验证,比推上去之后再回滚要划算得多。我在团队里见过太多次“推上去才发现少了一个文件”的尴尬场面。
5. 常见问题与排查技巧实录
这一节的内容全部来源于我实际开发中和带新人时遇到过的真实问题,挑高频的整理出来,每一条后面都会给出排查思路。
5.1 常见问题速查表
| 问题现象 | 原因 | 解决办法 |
|---|---|---|
| 切换分支后,之前写的代码不见了 | 切换分支前有未提交的改动,切到其他分支后改动还留在原工作区 | 切回来继续开发;如果你执行了git checkout .或git clean,那改动就被丢了,只能从git reflog碰运气 |
执行git merge提示“Already up to date” | 你已经在目标分支上,或者两个分支没有分叉 | 确认当前分支名;确认是否忘了git fetch拉远程最新信息 |
| 合并时一堆冲突,头都要大了 | 两个分支改了同一批文件的相同区域 | 不要慌,逐个文件解决;也可以先用git merge --abort退出,重新规划合并方式 |
| 合并完推送,远程无变化 | 本地main在合并前没有拉取远程最新代码 | 执行git pull --rebase之后重新合并;如果已经推过,用git push --force-with-lease覆盖(注意force是危险操作,确认只有你自己在动这个分支才用) |
| 误删了还没合并的分支 | 手滑执行了git branch -D xxx | 立即执行git reflog找到分支最后一个提交的哈希,用git branch xxx <hash>恢复 |
| IDEA右下角不显示分支名 | 一般是IDE版本或Git配置问题 | 点击右下角有“Git”字样的位置,呼出分支面板;如果完全消失,检查是否开了“Expose branches from all repos” |
| 拉取代码提示“Rebasing”字样卡住 | git pull默认行为或你在一个rebase进行中 | 如果不想rebase,用git pull --no-rebase;如果已经在rebase中途,用git rebase --abort退出 |
5.2 三个容易忽略的细节习惯
第一,提交前一定要看清自己在哪个分支。这句话听起来像废话,但我在代码评审中见过不止一次:有人本该在feature分支开发,结果所有提交都打在了main上。原因通常是早上切换到main拉了个最新代码,然后忘了切回功能分支就开始写代码了。破解方法很简单:写代码前养成执行git branch看一眼的习惯,或者把终端提示符配成分支名可见。
第二,git branch -D是危险操作。大小写区别很大:git branch -d只会删除已经合并进当前分支的分支,如果分支未合并会拒删;git branch -D则不管三七二十一强制删除。我建议日常清理分支优先用-d,报错说明分支还有独立提交,这时候重新思考是否真要删,而不是直接-D暴刀解决。真需要-D的时候,先用git log branch_name确认里面没有你需要的提交。
第三,合并策略不必迷信。Git还提供了git merge --no-ff(禁止快进合并)和git merge --squash(将分支上所有提交压缩成一个提交)等策略。很多团队强制要求--no-ff,因为这样能保留清晰的“功能合并”历史;--squash适合在合并实验性小改动时用,能让主干历史整洁。选择哪种策略没有对错,团队统一即可,但一定要明白:合并不仅仅是把代码并过来,也是有历史可读性的工程决策。
第四,配套命令git commit --amend和git stash的使用。你正在分支上开发,写完发现提交信息打错了,用git commit --amend -m "新的信息"就能修正上一次提交。如果你在分支A开发了一半,临时要去分支B修个紧急bug,又不想把半成品提交上去,git stash就是为你准备的——它把你的改动暂存起来,切到B分支改完了,再切回来git stash pop恢复现场。这两个命令和分支操作经常配合使用,用熟了整个开发流会非常顺。
5.3 综合实例:一次标准的多人协作合并
最后用一个完整场景串一下所有操作。假设你在做一个feature/user-center功能分支,开发完成后要合入develop分支:
# 1. 确认当前在feature分支,且工作区干净 git branch git status # 2. 切到develop并拉取最新代码 git checkout develop git pull --rebase # 3. 把feature分支合并进来 git merge feature/user-center # 4. 若有冲突,逐个文件解决后add和commit # ...解决冲突中... git add . git commit --no-edit # 5. 跑测试、看日志 npm test git log --oneline --graph -n 10 # 6. 推送到远程 git push这就是一个最标准的合并操作闭环。如果中间任何一步出问题,翻回上面的速查表对照排查即可。
我个人在实际项目中还有一个小习惯:合并完develop回到feature分支,执行git merge develop把最新的develop反向合并回来。这样做的目的是让功能分支尽早接触主干的新改动,减少合并时的大面积冲突。我们团队内部叫它“喂草”——让分支持续吸收主干的养分,等真正需要合回主干那天,冲突范围会小得多。这个习惯坚持下来,你会明显感觉到,合并不再是开发流程里的“高危环节”,而是一次平常得不能再平常的日常操作。