我们团队上个月做一次版本迭代,开发到一半,产品经理突然说线上有个紧急bug要马上修。当时新功能的代码已经散落在两个feature分支上,线上分支的代码和它们差了差不多几十个提交。那一刻我在终端里敲git branch,看着屏幕上的分支列表,突然意识到:理解git分支的创建、合并、删除这套基本功,是真的能在关键时刻救命的。
不少开发干了两三年,git clone和git commit用得贼溜,但一碰到多分支协作就慌:不知道远程分支怎么建,合并代码时遇到冲突直接傻眼,分支删不干净弄得本地分支列表越来越脏。这篇文章我把这几块东西拆开揉碎了讲一遍。不追求把git文档翻译一遍,只讲实际开发中真正用得上的操作,以及这些操作背后为什么是这样设计的。
1. 分支的设计思路:先搞懂Git到底在解决什么问题
1.1 分支不是文件夹拷贝,是指针
很多新手理解的“分支”,是代码复制了一份,各改各的,最后再想办法合到一起。这个理解不算错,但会把后面很多操作带偏。
Git里的分支,本质上是一个指向某次提交(commit)的可移动指针。你执行git branch feature-login,它不复制任何代码文件,只是在.git/refs/heads/目录下新建了一个文件,里面存了当前提交的哈希值。真正的代码对象全部存在对象库(object database)里,靠哈希值引用。所以git创建分支是瞬时操作,不管你的仓库是10MB还是10GB,创建分支都是毫秒级完成。理解了“指针”这个模型,很多行为就说得通了:
- 分支名带不带
origin/前缀,含义完全不同。origin/feature-login是远程仓库的“快照指针”,它只在fetch或push时更新,不跟着你的本地提交走。 - 切换分支时,工作区文件会变,是因为git用指针找到对应提交,然后把文件内容从对象库里解出来填进工作区。
- 所谓“删除分支”,本质就是删掉那个指针文件。只要提交还在对象库里,用
git reflog把它找回来不过是几秒钟的事。
1.2 为什么要有远程分支这个概念
把分支想象成“在某个时间点给代码拍了一张快照并起了个名字”,本地分支就是你自己手里的快照,远程分支就是托管服务器上的快照。多人协作时,谁也不希望自己的本地快照直接暴露给其他人看,于是git设计了origin/xxx这种远程跟踪分支(remote-tracking branch)来做“同步的中间态”。
我见过不少团队,协作流程是“谁做完谁push到master”,然后靠聊天工具吼一声让大家拉最新代码。这种流程在小团队、低并发时能跑,但只要两个人同时改了同一个文件,痛苦的合并过程就开始了。规范的做法是用feature分支隔离开发,用远程分支做同步边界,用合并(merge)或变基(rebase)把各自的工作成果汇到一起。这套流程的根基,就是先把分支的“指针模型”和“远程跟踪机制”吃透。
2. 创建远程分支:从本地到远端的完整链路
2.1 最常见的操作路径
先说结论:git没有一个叫“创建远程分支”的独立命令。远程分支的创建,实际是“本地分支 + push”的组合动作。过程分三步:
# 第一步:基于当前所在分支,创建并切换到新分支 git checkout -b feature/payment-v2 # 第二步:在新分支上正常开发、提交 git add . git commit -m "feat: 新增支付v2接口" # 第三步:把本地分支推送到远程,并建立跟踪关系 git push -u origin feature/payment-v2执行完第三步,git branch -r会看到origin/feature/payment-v2,远程分支就建好了。-u参数是--set-upstream的简写,它的作用是给本地分支设置上游分支。设置之后,以后直接敲git push和git pull,git就知道该跟哪个远程分支打交道,不用每次把分支名写全。
2.2 push命令的参数拆解
那条git push -u origin feature/payment-v2值得细说。origin是远程仓库的别名,通常在git clone时自动生成,指向你拷贝的远程地址。feature/payment-v2是你要推送的本地分支名,没有加冒号,所以它既表示“本地分支名”,也表示“远程分支名”,这是一条简写规则。
如果你想把本地分支推到远程另一个名字下,可以写全映射关系:
git push origin feature/payment-v2:feature/payment-release这样远程会多出一个叫feature/payment-release的分支,内容与本地feature/payment-v2一致。但这种改名推送在日常协作里用得不多,反而容易在团队里造成“远程名和本地名对不上”的困惑,建议只在临时应急时用。
2.3 实际操作时会踩的坑
第一个坑:不小心把开发分支推到了master。有些人习惯用git push origin master推送,但你当前在feature/payment-v2分支上时,这条命令并不会把feature分支推上去,而是会把本地的master分支推上去,如果你本地master落后远程很多,Git会拒绝推送(non-fast-forward)。但这已经够吓人了。更稳妥的做法是推之前先看一眼当前分支:git branch --show-current。
第二个坑:clone下来的仓库,本地没有远程的新分支。你同事推了一个feature/order-export上去,你想切到这个分支开发,直接git checkout feature/order-export通常是不行的(较新版本的git可以,但老版本不行)。规范操作是先抓取远程分支信息,再基于它创建本地分支:
git fetch origin git checkout -b feature/order-export origin/feature/order-export第二句也可以简写成git checkout --track origin/feature/order-export,git会自动创建一个同名的本地分支并设置跟踪关系。
第三个坑:分支名不规范。我见过分支名叫final、test2、bugfix2222的,等过一个月再看,谁也不知道这分支是干嘛的。推荐的分支命名规范大概是:feature/功能、bugfix/缺陷修复、hotfix/紧急修复、release/发版分支,后面接简短描述,比如feature/user-center-refactor。
2.4 创建远程分支之前,先想想从哪个分支切出来
很多人创建分支时不太在意基于哪个分支,这是个隐患。比如你在develop上有一些本地未提交的修改,这时执行git checkout -b feature/abc,新分支会带着工作区里那些还没提交的改动一起切过去。你以为是新起点,实际上把一堆“半成品”也带过去了。
我自己的习惯是:切新分支前,先git status确认工作区干净。如果有未提交的改动,要么先提交到当前分支,要么用git stash暂存,等新分支建好后再git stash pop恢复。这个习惯看着啰嗦,但能省掉后面合并时大量“怎么这个文件有这个改动”的排查时间。
3. 分支合并:merge与rebase的选择题
3.1 合并的方法不止一种
分支合并的最终目的,是把另一个分支的提交整合到当前分支。但实现方式分成两大流派:git merge和git rebase。两者的工作方式完全不同,理解它们的区别是处理合并冲突的关键。
git merge是“把两个分支的历史汇合在一起”。它会找两个分支的最近共同祖先(merge base),然后把当前分支和待合并分支各自相对于这个祖先的改动都保留下来,生成一个新的合并提交(merge commit)。这个提交有两个父提交,git的提交历史上会看到明显的“分叉再汇合”痕迹。
# 在develop分支上,把feature/login的改动合并进来 git checkout develop git merge feature/logingit rebase则是“把当前分支的提交,在目标分支最新的提交之上重新放一遍”。它不做“汇合”,而是把你分支上的每一次提交取出来,在目标分支的新提交之后逐个重放。结果是当前分支的历史变成一条直线,没有合并提交,提交历史非常干净。
# 在feature/login分支上,把develop的更新变基进来 git checkout feature/login git rebase develop两者的核心差异可以总结为:merge保留“真实发生过什么”,rebase重塑“看起来发生了什么”。
3.2 什么时候用merge,什么时候用rebase
我见过最混乱的git协作场景,就是把merge和rebase混用且没有任何规则。我自己的经验是分场景对待:
合并功能分支回主线(比如feature合并到develop/master),用merge。理由很简单:合并提交记录了“这个功能在什么时候、从哪个分支合进来的”,团队其他人review历史时,能清楚地看到功能开发的脉络。而且merge是“非破坏性”操作,它不会改动已有提交的哈希,也不改写历史,任何人都可以放心执行。
更新自己的功能分支(比如把develop的最新代码同步到feature分支),用rebase。理由也很实际:如果你在feature分支上反复merge develop,分支历史里会出现大量“Merge branch 'develop' into feature/login”这种无意义的合并提交,看着很吵。用rebase把feature分支的提交“搬”到develop最新代码之上,历史是一条直线,干净清爽。
但rebase有一个必须注意的前提:只能对那些“没有推到远程共享分支”的提交执行。因为rebase会改写提交哈希,你本地feature/login的提交哈希和远程origin/feature/login上的哈希会变得完全不一样。如果别人已经基于远程那个分支做了开发,你rebase再强推,就是把别人脚下的瓷砖抽了。
三句话原则:
- 要合并到共享分支:merge,保留历史。
- 要把共享分支的更新同步到个人分支:rebase,保持线性。
- 共享分支上的提交,永远不要rebase。
3.3 合并冲突:它是流程的一部分,不是错误
多人开发同一批文件,冲突几乎是必然的。Git解决冲突的核心机制是:当两个分支都修改了同一个文件的同一块区域时,git自己无法判断该保留哪个覆盖哪个,于是把决定权交还给人。
实际遇到冲突时,文件里会出现这样的标记:
<<<<<<< HEAD 这里是你当前分支(比如develop)上的内容 ======= 这里是待合并分支(比如feature/login)上的内容 >>>>>>> feature/login处理步骤是:
git status查看冲突文件列表。- 逐个打开冲突文件,手动决定保留哪部分、删除哪部分标记符号。
- 改完后
git add标记为已解决。 - 如果是merge,执行
git merge --continue,编辑器里填一下合并提交信息,保存退出。 - 如果是rebase,执行
git rebase --continue,这会重放下一个提交,如果还有冲突,继续重复上面步骤。
这里有两条血泪教训:
第一,不要用编辑器自带的“接受当前/接受传入”按钮无脑处理冲突。vscode、IDEA里确实有这个按钮,但“接受传入”意味着你完全丢弃了当前分支这一段修改,如果那两个分支本身有相互依赖的逻辑,这样简单粗暴处理常常会编出一个逻辑上不自洽的代码来。正确做法是把冲突两边的代码都看一遍,理解各自意图,再决定怎么合并。
第二,冲突文件多的时候,一个一个来,别着急。我记得有一次合并,冲突文件有23个,我头昏眼花半小时处理完,以为自己搞定了,结果编译报错十几处。后来养成了习惯:冲突解决后,先跑一遍项目构建或测试,确认没问题再提交,这个步骤能拦下一大批“合并冲突虽然解决但代码逻辑坏掉”的事故。
3.4 两个容易翻车的合并操作
操作一:revert了一个分支之后,再合并其他分支时会冲突。比如master上有一个提交A,后来发现有问题,用git revert A把这个改动“回滚”掉。此时master上的这个文件回到了A之前的状态。接着你有一个feature分支,它基于A之后的状态开发,也改了同一文件,等你把feature合并回master时,git会看到master上“A+revert”这两次提交都接触过该文件,feature也接触过,于是大概率冲突,而且冲突内容极其晦涩,因为A和revert A正好把改动抵消了。处理这种冲突,真的只能静下心,把三个时间点的文件版本逐一对比,有时甚至要git log -p查看完整修改脉络才能理清。
操作二:rebase时把提交弄丢了。如果你在rebase过程中发现冲突太多,想放弃这次rebase,可以用git rebase --abort回到rebase开始前的状态。但如果你已经做了一次git rebase --continue,再想反悔,就不能简单地abort了。此时可以用git reflog找到rebase之前所在的那个提交哈希,然后git reset --hard 那个哈希,整个分支就回到rebase前的样子了。reflog是git的“后悔药开关”,强烈建议每个开发者都记住这个命令。
4. 删除分支:本地、远程,以及那些残留引用
4.1 删除本地分支的正确姿势
删除分支的核心命令是git branch -d:
# 删除本地分支 git branch -d feature/old-feature-d是--delete的简写。git在这里做了一个保护:如果这个分支还有未被合并的提交,它会拒绝删除,并提示“not fully merged”。这是git在防止你误删“还没合入主线的代码”。
如果确认这个分支上的提交确实不需要保留了,可以用大写-D强制删除:
git branch -D feature/abandoned-work-D是--delete --force的合并简写。注意,强制删除之前请务必确认两点:一是这个分支没有推到远程,或者虽然推了但远程也打算删;二是这些提交确实没有任何保留价值。一旦删掉,虽然git reflog里还能找回,但时间长了就会淹没在大量记录里。
还有个细节:不能删除当前所在的分支。Git会提示“Can not delete branch checked out”,意思是“你不能删掉自己正站着的树枝”。必须先切到别的分支,再执行删除。
4.2 删除远程分支
远程分支的删除,是push的反向操作,用的是--delete参数:
git push origin --delete feature/old-feature也可以写成简写形式:
git push origin -d feature/old-feature这个命令的本质,是告诉远程仓库“把这个分支引用删掉”。执行成功后,远程的分支就不存在了,你可以在GitHub/GitLab的页面上确认。
需要注意,删除远程分支不会自动删除你本地的同名分支,也不会自动删除本地的远程跟踪引用(origin/feature/old-feature)。所以正常情况下,删完远程之后还要删一遍本地分支,并且清理跟踪引用:
git branch -d feature/old-feature git fetch --prune--prune会清理本地的远程跟踪分支引用,把那些远程已经不存在但本地还留着的origin/xxx标记删掉。
4.3 清理残留分支的三种场景
场景一:本地一堆+(号开头)的分支。终端里执行git branch时,明明远程分支在GitLab上已经没了,本地却还显示着一大堆,列表很长。这是远程跟踪引用没清干净,执行:
git fetch --prune或者更明确一点:
git remote prune origin两条命令作用类似,推荐用git fetch --prune,因为fetch还能顺便把其他远程分支的最新状态拉下来。
场景二:vscode里看不到远程已删除的分支,但本地分支列表还能看到。vscode的Git插件(GitLens等)通常会读取本地的远程跟踪引用,如果引用没清理,界面上就会挂着一些“幽灵分支”。执行一遍git fetch --prune,刷新一下Git视图,基本就干净了。
场景三:idea里分支列表特别长,想统一清理。在IDEA的Git工具窗口里,右键分支节点可以选择“Delete”删除本地分支;远程分支在origin/节点下右键也可以删除。但如果远程分支很多,一条条右键显然不现实,我一般直接在终端里批量处理:
# 把本地所有的远程跟踪分支列出来看看 git branch -r # 把确认不需要的远程分支批量删掉(一条条来,别用通配符乱删)批量操作时务必一条条列清楚再删。git没有“按前缀批量删除远程分支”的开箱用法,用脚本来删也不是不行,但要非常谨慎,别把同事正在用的分支误删了。
4.4 关于删除分支和找回提交的补充
说一个我踩过的坑:删除了一个看似“已经完全合并”的功能分支,结果后来发现那个合并本身被revert过,等于功能代码实际上没有生效。此时那个分支上的提交已经找不到了(revert后再合并,git认为分支已合并,所以git branch -d不会拦截)。后来同事在代码里找不到某个接口实现,我凭着印象用git log --all -- <文件名>找到那个提交的哈希,再用git cherry-pick把关键提交救回来了。
这件事实在有点反直觉。git branch -d提示“分支已完全合并”,看起来是安全的,但如果你曾经对这个分支做过revert、reset这类“反悔操作”,“完全合并”的判断不一定代表“代码最终生效”。所以删除分支前,最稳妥的做法是看一眼git log里这个分支的实际提交记录,确认它们真的存活在目标分支上,再删。
5. 实际开发中绕不开的典型场景问答
5.1 场景一:合并master时冲突了,如何处理max
热搜词里有一条“master分支revert后,其他分支合并master冲突”,这个前面讲过原理了。这里补充一下实际处理步骤:
- 先
git merge master,git会报告冲突文件。 - 打开每个冲突文件,看
<<<<<<<和>>>>>>>标记。 - 如果某一块改动是“master上revert,而feature分支正好改的是同一代码”,你基本只能人工判断:这个功能到底要不要保留。要保留就把feature侧的改动手动合进去;不要保留就把两边内容一起删掉,换成旧版本。
- 每次处理完一个文件,
git add,最后git merge --continue。
我的体会是,这种“revert叠加合并”的冲突,是git里最烧脑的冲突类型之一,因为它本质上不是代码冲突,而是“需求决策冲突”——到底哪个版本才是最终要的,机器判断不了,代码本身也看不出端倪,只能靠人来定。
5.2 场景二:在idea里commit了但还没push,怎么撤掉
热搜词里有一条“怎么删除idea上git某个分支上commit但未push的代码?”。这个问题很常见,一般出现在“啊,我刚才commit错了分支”或者“这个提交里有一个不该提交的文件”。
方法分两种:
如果只是想撤销提交,但保留工作区改动(也就是把提交“退”回暂存区):
git reset --soft HEAD~1HEAD~1表示当前提交的前一个提交。--soft的意思是“移动指针,但不动暂存区和工作区”。执行后,那个commit里的改动会回到暂存区(绿色),你可以重新组织文件,再提交。
如果是想撤销提交,同时把工作区改动一并清除(彻底回到上一个提交的状态):
git reset --hard HEAD~1这个操作会把工作区里所有未提交的改动也一并丢掉,慎用。如果提交里有些文件改动很重要,只是提交信息写错了,千万别用--hard,改用git commit --amend修改提交信息:
git commit --amend -m "正确的提交信息"--amend其实就是“把当前暂存区的改动和上一个提交合并,生成一个新提交,替换掉原来的提交”。很多新手以为它是“修改最后一次提交信息专用”,实际上它也能用来往最后一次提交里追加遗漏的文件。
在IDEA里也有对应的图形化操作,在Git工具窗口右键提交记录,会看到“Undo Commit”和“Drop Commit”之类的选项,原理和reset一致。但图形界面容易让人忘了底层原理,我还是推荐大家先把命令行的reset练明白。
5.3 场景三:git commit --amend用错了,怎么补救
--amend确实好用,但它也会改写提交哈希。如果这个提交已经push到远程共享分支,你再amend并强行push,就会造成远程历史被改写,其他同事拉代码时会看到一堆奇怪的冲突甚至“丢失提交”的错觉。
补救方法:
# 先找到amend之前的提交哈希 git reflog # 记住想要恢复的那条记录 git reset --hard <那个哈希>reflog记录了你本地分支指针移动的历史,包括amend、reset、rebase等操作都记录在案。只要操作还没超过默认的保留时间(通常90天),都有机会找回。
5.4 场景四:gitlab上分支合并的权限管理
企业里用GitLab做代码托管时,通常会给master或main分支设置保护(Protected Branches)。被保护的分支,普通开发者不能直接push,只能通过Merge Request(合并请求)来合入代码,而且通常还要求至少一个评审人approved之后才能点Merge。
这种情况下,“分支合并”的动作往往不是开发者自己执行的,而是你提交Merge Request、评审人审核通过、最后由自动化流水线或具备权限的维护者完成merge。这时你自己就只需要保证:功能分支干净、冲突已解决、commit信息规范。如果MR页面显示“Cannot merge due to conflicts”,你先在本地把目标分支合并进自己的功能分支,解决冲突后再push,MR就变绿了:
git fetch origin git checkout feature/your-branch git merge origin/master # 把目标分支的最新代码合并进来 # 解决冲突 git push origin feature/your-branch5.5 场景五:清理“已经删除的远程分支”在vscode里的残留
vscode用过一段时间后,分支列表经常混进一堆已经不存在的远程分支。简单操作路径:
- 在vscode的终端里执行
git fetch --prune。 - 点击左侧源代码管理图标,刷新一下Git视图。
- 如果还残留,可以执行
git remote prune origin再刷一次。
这个操作很安全,它只清理“远程已删除”的跟踪引用,不会动任何本地未推送的提交。
5.6 常见问题速查表
| 问题现象 | 原因 | 解决方法 |
|---|---|---|
git push提示non-fast-forward | 本地落后于远程 | git pull --rebase再push |
| 删除本地分支提示未合并 | 分支有未合并提交 | 确认不需要后改用-D |
| 远程分支删了本地还显示 | 远程跟踪引用残留 | git fetch --prune |
| 合并后代码丢了 | 可能revert或reset过 | git reflog找回 |
| 提交信息写错了 | 最后一次提交 | git commit --amend |
| commit后想撤回 | 提交未push | git reset --soft HEAD~1 |
| rebase冲突太多想反悔 | rebase进行中 | git rebase --abort |
| 改了错误分支的代码 | 代码在working tree | git stash后切分支git stash pop |
6. 把git操作变成肌肉记忆的练习建议
Git这个东西,光看文章不实操,很快就忘了。我自己带了几个实习生,总结出一个规律:真正能稳定上手git的,不是背命令背得最多的人,而是把每一条命令的效果想明白的人。
我建议你做这样几组练习,每次都用同一个测试仓库,随便造点文件就行:
第一组练习:创建和推送。在本地建一个仓库,创建三个分支,分别推送到远程,然后用git branch -vv查看每个分支的跟踪关系,看看和你想的是否一致。
第二组练习:合并和冲突。故意在两个分支上修改同一个文件的同一行,然后merge,体会冲突标记的呈现方式和解决流程。这个练习要多做几次,直到你能不看教程就独立处理完冲突。
第三组练习:删除和找回。创建一个分支,提交几次,然后删掉它,再用git reflog把它找回来。经历过一次“删除可恢复”,你对分支本质的理解会瞬间加深。
第四组练习:rebase与revert。在feature分支上做几个提交,rebase到develop上,然后用git log --graph观察历史形态变化。再手动造一个“合并后revert再合并”的场景,体验那种最烧脑的冲突。
这四组练习做完,你的git基本功就基本夯实了。后面遇到复杂的协作流程,比如cherry-pick、submodule、bisect,就都是在此基础上的增量学习了。我在实际开发里越用越觉得,git命令不多,关键是把“指针”“引用”“提交图”这几个底层概念吃透,好多操作根本不用背,推演一遍就知道该怎么写。