1. Git 分支到底在解决什么问题
我见过不少刚接触 Git 的朋友,一开始最懵的就是分支这个概念。其实你完全可以把它理解成“平行世界”:你在主线上开发到一半,突然需要加一个紧急功能或者修一个线上 bug,如果直接在主线改,改到一半发现方向错了,代码就成了一锅粥。分支就是让你在不影响主线的前提下,开一个独立的副本去尝试,试错了直接扔掉,试对了再合并回来。
Git 的分支之所以好用,核心在于它和 SVN 那套集中式版本管理的分支完全是两回事。SVN 的分支本质上是目录拷贝,你切个分支可能要等半天,分支之间还容易互相影响。而 Git 的分支只是一个指针,指向某一次提交(commit),创建和切换分支在本地几乎是一瞬间的事,成本极低。这也是为什么 Git 社区里流行“分支开发、主干发布”的工作流——每天开十几个分支都毫无压力。
这篇内容我会站在实际使用的角度,把分支的创建、切换、合并和冲突解决整个流程串起来讲,配合命令和场景说明,最后再补几段我在项目和 GUI 工具里踩过的坑。适合刚把 Git 装好、准备开始正经用分支的人,也适合已经会基本操作、但遇到冲突总是心里没底的开发者。
先说准备工作。Git 装好之后,请务必先设置用户名和邮箱,否则你提交的代码没有作者信息,团队协作时根本分不清这段代码是谁写的。Windows 下安装 Git 时一路默认选项就行,选 VS Code 或者默认编辑器都无所谓的,后面用命令行时主要靠的是常用命令,不太依赖编辑器。装完打开 Git Bash 或者终端,输入下面的命令验证一下:
git --version然后设置身份信息:
git config --global user.name "你的名字" git config --global user.email "你的邮箱"2. 分支的创建与切换:高频操作必须形成肌肉记忆
2.1 创建分支的三种姿势
创建分支这个动作,大多数人会直接执行git branch <branch-name>,但这个命令只创建分支,不切换过去。如果你马上要在这个分支上写代码,需要再执行一次git checkout <branch-name>,或者用更省事的写法:
git checkout -b feature/user-login这条命令等价于git branch feature/user-login加git checkout feature/user-login,一步到位创建并切换。Git 2.23 之后的版本还引入了git switch命令,语义更清晰:
git switch -c feature/user-login我自己实际用下来,git checkout -b已经足够日常使用,git switch主要优势是它和git restore做了职责分离,以后提测别人review你的命令历史时会更容易看懂。但这不是硬性要求,团队里统一一种写法就好。
除了命令行,现在主流 IDE 也都支持可视化创建分支。比如 IntelliJ IDEA,右下角点开 Git 分支小按钮,选择 New Branch,输入名字直接就创建并切换到新分支了。Eclipse 里则是右键项目 -> Team -> Switch To -> New Branch。工具虽然不同,背后的逻辑是一样的:先基于当前所在分支创建新分支,新分支默认包含当前分支的所有提交。
有一个细节必须注意:创建分支前,看准你当前在哪个分支。很多人想从 master 拉一个新分支,结果人还在 dev 分支上,直接执行了创建命令,新分支就从 dev 拉出来了,后面合并时引入一堆不相关的代码,排查起来非常痛苦。执行创建前先看一眼当前分支:
git branch当前分支前面会带一个星号。或者用git status也能看到。养成这个习惯后,分支拉错的情况会大幅减少。
2.2 切换分支时的“脏工作区”陷阱
切换分支最怕的是什么?当前分支有未提交的改动,然后你直接切走了。Git 的行为是这样的:如果这些改动和新分支的内容不冲突,Git 会把改动直接带过去;如果冲突,Git 会拒绝切换并提示你把改动提交或者暂存。
这个“改动被带过去”的特性坑过不少人。你本来在 feature/A 分支改了一个配置文件的调试参数,切回 dev 分支时忘了这件事,随手提交了,结果调试参数混进了 dev。所以我的习惯是:切分支前,只要工作区有任何改动,先git status看清楚。如果改动是临时的,可以用git stash存起来:
git stash push -m "临时调试配置" git checkout dev # 需要的时候再恢复 git stash pop如果用 IDEA,切换分支时它会弹窗提示你是否要把未提交的改动带过去,这时候看清楚再点。从命令行切分支,遇到 Git 拒绝时,先别慌,git status看一眼,通常都会告诉你冲突的文件列表,解决后再切。
另外有个在 IDEA 里非常容易踩的坑:当前分支有未提交的改动,你想checkout到另一个分支,IDEA 默认会弹窗给你三个选项,包括 Force Checkout(强制切换)。你如果点了 Force Checkout,本地的未提交改动直接就被覆盖丢掉了。别问我怎么知道的,我丢过一次重要的新写代码后,就再也没点过 Force Checkout。正确的做法是要先 commit 或者 stash,然后再切换。
2.3 本地分支与远程分支的关联
本地新建分支之后,如果想让代码推到远程仓库,需要执行一次推送并设置上游:
git push -u origin feature/user-login-u参数会把本地分支和远程分支关联起来。关联之后,以后你直接执行git push和git pull,Git 就知道该跟哪个远程分支交互,不用每次都写完整参数。
如果你是从远程仓库拉取别人新建的分支,常见做法有两种:
# 方式一:直接拉取并切换到对应分支 git checkout -b feature/xxx origin/feature/xxx # 方式二:先取回远程分支,再基于它创建本地分支 git fetch origin git checkout -b feature/xxx origin/feature/xxx还有一个更省事的命令,Git 会自动帮你创建跟踪关系:
git switch feature/xxx前提是你的本地没有这个名字的分支,而远程有,那这条命令会直接基于远程分支创建同名本地分支并关联。这个特性比较新,老版本的 Git 不支持,但我试过 GitHub 上近几年的 Git 版本都没问题。
查看本地分支和远程分支的关联关系,可以用:
git branch -vv输出的每一行中,带[origin/xxx]字样的就是已关联的,没有的就是还没关联。团队协作时,看到别人的分支但没有本地分支,执行上面任意一种方式就能进入开发状态,很方便。
3. 分支合并:merge 的底层逻辑与命令实操
3.1 两种合并:fast-forward 与三方合并
讲 merge 之前,建议大家先理解两种合并方式,因为 Git 在终端里输出的信息会直接把你绕晕。
第一种是 fast-forward(快进)合并。它的触发条件很特殊:当前分支没有产生新的提交,也就是说 master 一直停在原来的位置,而你要合并的分支是从 master 拉出来并往后走了几个提交。这时候 Git 不需要真正做“合并”,只需要把 master 的指针直接往前移动到 feature 分支的最新提交上。整个过程没有产生新的提交节点,历史是一条直线。
第二种是三方合并。feature 分支从 master 拉出来之后,master 自己也有别人提交的新代码,两边都发生了改变。这时候 Git 必须把三个版本拿来对比:两个分支的共同祖先版本、当前分支版本、目标分支版本,然后生成一个合并提交。这个合并提交有两个父提交,历史图上就会看到分叉和合流的线条。
理解这两种合并的关键在于:fast-forward 永远不会产生冲突,三方合并才是冲突的真正来源。有些团队喜欢禁掉 fast-forward,强制所有合并都生成一个合并节点,用git merge --no-ff实现,这样历史记录里能清晰看到“这是一个功能合入”的节点。反之,有些团队喜欢保持线性历史,会用 rebase 替代 merge。这两种理念没有绝对的对错,取决于团队怎么约定。
3.2 merge 命令的完整实操
要把 feature 分支合并到 master,标准流程是:
# 1. 先切到目标分支 git checkout master # 2. 拉取最新的远程代码 git pull # 3. 合并功能分支 git merge feature/user-login很多新人会犯一个错误:在 feature 分支上执行git merge master。这条命令不是不能用,但它的语义是“把 master 合并到 feature 中”,操作反了。如果你本来想把功能合并到 master,结果在 feature 分支上执行了这个命令,反而是把 master 的新代码拉进了 feature,最后 master 上没有你的功能代码,一脸懵。
正确的顺序就是上面的流程:先切到接收方,再执行 merge 操作。如果合并过程中没有冲突,Git 会自动生成一个合并提交。如果你希望提交信息更清晰,可以加--no-edit参数跳过编辑器,或者用-m直接传一个提交信息:
git merge feature/user-login -m "合并登录功能"合并完成后可以看一下历史:
git log --oneline --graph--graph参数会把提交历史以 ASCII 字符画的形式打印出来,分叉合流一眼就能看懂。强烈建议每个 Git 用户都把这个 alias 配上:
git config --global alias.tree "log --oneline --graph --all --decorate"之后直接输git tree就能看到非常直观的分支历史图。
3.3 合并后分支要删吗
功能合并完成之后,通常那个 feature 分支就不再需要了。删除本地分支:
git branch -d feature/user-login注意小写的-d会检查这个分支是否已经完全合并,如果还有未合并的提交,Git 会拒绝删除。如果确认不要了,可以用大写的-D强制删除。我个人建议始终先用-d,因为如果 Git 提示还有未合并内容,说明你自己可能漏了什么。
远程分支的删除:
git push origin --delete feature/user-login删除远程分支之后,本地如果还残留有远程分支的追踪引用(比如你 fetch 过,但本地没建分支),用下面的命令清理:
git fetch --prune清理之后git branch -r就不会再看到远程已经删掉的分支了。这条命令在团队协作中很常用——别人删了远程分支,你本地 fetch 之后还留着记录,时间久了远程分支列表一片混乱。
4. 冲突解决实战:从模拟冲突到批量处理的完整路径
4.1 冲突是怎么产生的
我直接给结论:当你要合并的两个分支,都在同一个文件的同一块区域改动了内容,Git 就不知道该听谁的,于是报冲突。如果两边改的是不同文件,或者同一个文件的不同区域,Git 能自动完成合并,不需要你介入。
举一个最经典的冲突场景:
master 分支上,代码文件hello.py有一行输出:
print("Hello, world!")有人在 master 上把这一行改成了:
print("Hello, master!")同时,另外有人从旧的 master 拉了一个分支 feature,在 feature 上把这行改成了:
print("Hello, feature!")当 feature 合并回 master 时,两个分支对同一行给出了不同的修改,Git 无法判断哪个是正确版本,于是提示冲突。
4.2 一步一步解决冲突
先模拟一个冲突。这里我建议所有人都在本地建一个测试仓库,把整个冲突过程亲手跑一遍,几十秒的事,但比看十篇文档都有用。
mkdir git-merge-test && cd git-merge-test git init创建文件app.txt,写入第一行内容:
hello world提交:
git add app.txt git commit -m "initial commit"从 master 创建分支 feat-a:
git checkout -b feat-a修改app.txt第一行为:
hello from feature a提交:
git commit -am "modify app.txt in feat-a"切回 master:
git checkout master此时 master 上的app.txt还是第一版。把第一行改成:
hello from master提交:
git commit -am "modify app.txt in master"现在尝试把 feat-a 合并进来:
git merge feat-a你会在终端里看到类似这样的输出:
Auto-merging app.txt CONFLICT (content): Merge conflict in app.txt Automatic merge failed; fix conflicts and then commit the result.这时候打开app.txt,里面是 Git 写好的冲突标记:
<<<<<<< HEAD hello from master ======= hello from feature a >>>>>>> feat-a三个标记的含义很明确:
<<<<<<< HEAD和=======之间,是当前所在分支(也就是 master)的改动。=======和>>>>>>> feat-a之间,是被合并分支(feat-a)的改动。
解决冲突就是在这两个版本之间做选择。如果你要保留 master 的版本,就把从===到>>>>>>的内容删掉,同时删掉三组标记;如果要保留 feat-a 的版本,反过来删除上半部分。如果两个都要,就手动编辑合并成一段新代码,再删掉标记。
下面是我实际处理时常用的一种思路:先看内容,再决定保留谁。
比如,如果 master 上这句是正式环境的输出,feat-a 上是测试环境的输出,那正确的做法可能是两个都保留,或者改成统一用配置控制。解决完后的文件内容可能是:
hello from master hello from feature a或者干脆:
hello world取决于你的业务逻辑。
改完后执行:
git add app.txt git commit -m "resolve merge conflict between master and feat-a"注意:解决冲突后的提交不需要带-a参数,因为git add已经标明了这个文件已被解决。直接提交即可。合并完成之后,可以再用git tree看一眼历史图,应该能看到两个分支分叉后汇合到一个合并提交上。
4.3 冲突标记的可视化工具处理
如果你用的是 IDE 自带的合并工具,整个过程会更直观一些。以 IntelliJ IDEA 为例:合并出冲突时,IDEA 会弹出一个冲突列表,双击文件会打开三栏对比界面。左边是本地版本,右边是远程版本,中间是合并结果。你可以在中间区域手动编辑,然后点击按钮接受左边/右边的某个 hunk,或者两个都接受。
IDEA 的冲突处理界面右上角还有几个箭头按钮,可以快速跳到下一处冲突。处理完所有冲突点后,点击 Apply 就生成了解决后的文件。之后回到 Git 窗口,把项目标记为已合并并提交即可。
TortoiseGit(俗称小乌龟)的操作路径也很相似:合并出现冲突时,在文件上右键 -> Edit Conflicts,会打开 TortoiseGitMerge 工具,左边是“我的”版本,右边是“他们的”版本,下方是合并预览,把需要的内容复制到下方并保存,然后标记为已解决。这个工具对不熟悉命令行的同事比较友好,因为它是纯图形化操作。
SourceTree 也是同样的逻辑,它的冲突解决界面会把每一处冲突单独列出来,你可以逐个选择使用别人的版本还是自己的版本,或者手动编辑。SourceTree 还有个优势是它的提交图形展示非常直观,适合给新人讲分支流转。
4.4 大规模冲突的批量处理技巧
如果一次合并产生了十几个冲突文件,逐个打开编辑器处理会很痛苦。我的建议是先分类,别盲目打开就改。
第一步,先看文件列表:
git statusGit 会把所有冲突文件标记出来,格式为both modified: 路径。按类型分一下:
- 纯文本代码冲突:必须手工合并,逐个看上下文。
- 配置文件冲突(如
application.yml、pom.xml):大概率是别人新增了配置项,你也在同一位置新增了不同的配置项。这种冲突比较危险,因为两边可能都是必要的,不能只留一边。建议把两边内容都合并,再检查格式。
如果是二进制文件冲突(图片、jar 包之类),Git 没法自动合并,你只能在两个版本中选一个,或者让同事重新生成一个版本。这种冲突一般不会多,但一旦出现,处理前最好和持有另一个版本的人沟通一下,别自己拍板。
批量选择“完全以某一边为准”的场景也经常出现。比如合并时你发现自己分支对这个文件只是一些临时改动,完全可以用对方的版本覆盖,命令行操作如下:
# 以当前分支(HEAD)为准 git checkout --ours 文件路径 # 以被合并分支为准 git checkout --theirs 文件路径执行后文件内容已经被替换,再执行git add 文件路径标记为已解决即可。注意--ours和--theirs在合并时的指向,要看清楚当前所在分支是谁:你正在执行合并的那个分支是ours,被合进来的那个分支是theirs。
如果冲突的多个文件都想统一以某一边为准,可以配合git ls-files -u | awk '{print $4}'列出所有冲突文件再批量处理。不过命令比较复杂,实际操作时我一般用 IDE 的文件列表配合右键操作更快。
4.5 冲突解决后的提交与验证
冲突全部解决后,最后一步是把结果提交。这一步最容易犯的错误是忘记git add就直接 commit。如果没有 add,Git 会提示还有未解决的冲突,提交会被拒绝。
提交之后,还有一个我强烈建议做的动作:跑一遍构建和测试。合并冲突往往发生在你不熟悉的代码区域,手工会合时可能引入语法错误或者逻辑错误。尤其是代码里如果有注释、字符串带特殊字符,很容易在删标记的时候误删内容。
我之前遇到一次很隐蔽的问题:解决冲突时,有一个文件的缩进格式被打乱了,编辑器本身没问题,但代码风格检查直接挂了,CI 被红牌警告。这种问题跑一次 CI 就能发现,比合并完就不管要稳得多。
5. 常见的分支合并问题与排查思路
5.1 “已经是最新”的坑
有时候你执行git merge feature/xxx,Git 提示Already up to date。很多人第一反应是“代码呢?怎么没合并?”
这个提示的真实含义是:目标分支已经包含 feature 分支的全部提交,无需再合并。换句话说,你的 feature 分支没有新的东西可以合进当前分支。
出现这个情况常见原因有三个。第一,你根本不在目标分支上,看看git branch当前在哪个分支。很多人以为自己在 master,其实还在自己的功能分支上。第二,feature 分支确实没有新提交。第三,feature 分支的改动已经通过别的途径合进来了,比如有人执行了 cherry-pick。
碰到这种情况先别怀疑 Git 坏了,按照上面三个方向逐个排查。
5.2 合并之后发现错了,怎么回滚
合并错了不可怕,Git 给了你反悔的机会。如果合并还没有提交,也就是冲突解决到一半想放弃,可以用:
git merge --abort这个命令会把工作区恢复到合并之前的状态,所有合并过程中的改动都会消失。如果合并已经提交了,你想撤销这次合并,可以用:
git revert -m 1 <merge-commit-id>-m 1表示保留哪个父分支的代码,这里的1指的是合并提交的第一个父提交,也就是合并操作发起前你所在的那个分支。revert不会删除历史,而是新增一个反向提交来抵消合并的改动,这个操作更安全,适合在共享分支上使用。
如果想彻底抹掉合并的痕迹,可以git reset --hard HEAD~1,但这个命令会丢弃提交历史,如果是公共分支,会直接影响其他同事,除非你确定只有你在用,否则不要这样做。
5.3 IDEA 右下角突然不显示分支
有朋友遇到过 IDEA 右下角分支按钮消失的情况,这个和 Git 本身没有关系,纯粹是 IDE 的小问题。常见原因是你当前项目的 Git 根目录识别出了问题,或者打开了一个非 Git 项目。排查方式很简单:打开 Version Control 设置,确认项目被正确识别为 Git 项目;或者把右下角的 Git Widget 手动加回状态栏,在 View -> Appearance -> Status Bar Widgets 里勾选 Git Branch 即可。这类问题不影响仓库和分支本身,只是 UI 显示问题。
5.4 小乌龟拉取时卡在“更新”
TortoiseGit 在合并前通常会自动 fetch/pull 一次。如果执行过程中卡住,多半是网络问题,也可能是远端仓库地址配错了。排查命令:
git remote -v确认远端地址没问题之后,可以先手动执行git fetch,如果 fetch 也失败,检查本地的代理设置。这个问题大多数时候是网络环境导致的,和 Git 本身的分支逻辑无关。
6. 与 merge 并行的进阶操作:rebase、stash 与 cherry-pick
6.1 rebase 会把提交摔平,用前想清楚
git rebase是另一个高频操作。它和 merge 最大的区别是:merge 生成一个合并提交,保留分支的分叉历史;rebase 则是把你当前分支的提交“摘下来”,重新接到目标分支的最新提交之后,历史变成一条直线。
举个例子,你在 feature 分支上有两个提交,master 在这期间也往前走了三个提交。rebase 之后,feature 分支会基于最新的 master 重新排列你的两个提交,看起来就像你从新 master 才拉出来分支一样。这样做的最大好处是历史非常干净,开发团队 review 代码时不用在分叉图上找半天。
但 rebase 有个臭名昭著的副作用:它改写了提交的哈希值。你的提交 A 变成了新的 A',哈希完全变了。如果这个分支已经推送到远程,并且有其他人基于它开发了内容,你 rebase 之后再强推,别人的本地历史会直接错乱。所以铁律是:不要对公共分支执行 rebase,不要对共享分支强推。
我个人的习惯是:本地开发分支可以随意 rebase,保持历史整洁;一旦推到远程并通知了团队,后续同步一律用 merge,不再 rebase。
6.2 stash 适合在切分支前保存现场
git stash前面已经提过,它可以把当前未提交的改动暂存起来,让工作区恢复干净,之后可以在任意分支上恢复。
我实际使用中比较常用的场景是:正在 feature 分支开发一个新功能,测出来线上有 bug,需要立刻切到 master 去修。feature 的新功能写了一半,没法提交,这时候 stash 一下,切到 master 修 bug,修完再切回来 pop 继续开发。整个流程干净利落。
如果 stash 了多个现场,可以用git stash list查看,恢复时指定编号:
git stash list git stash apply stash@{1}这里apply和pop的区别是:pop恢复并删除对应 stash 记录,apply恢复但不删除。不确定会不会还需要这个现场时,用apply更稳。
6.3 cherry-pick 解决“只要一个提交”
有时候你不需要合并整个分支,只想从其他分支拿一个提交改动到当前分支。比如 hotfix 分支上有一个提交修了 bug,你想把这个修复同步到当前开发分支,但不想把 hotfix 分支的其他乱七八糟改动都带过来。这时用:
git cherry-pick <commit-hash>这个命令会把指定的提交应用到当前分支,生成一个新的提交。如果应用时发生冲突,解决方式和 merge 冲突完全一样。cherry-pick 是多人协作里很常用的操作,但它也有一个坑:如果源提交后来又被修改了,cherry-pick 不会自动更新你之前应用过的内容。也就是说,它是一次性的拷贝,不是引用。
6.4 merge 和 rebase 到底怎么选
很多文章喜欢告诉你一个标准答案,但实际工程里真的没有唯一的“正确”。我根据自己几个项目的经验整理了一个判断逻辑,不一定适合所有团队,但可以参考:
- 如果分支会推送到远程,并且被团队其他人使用,绝对不要 rebase,老老实实用 merge。
- 如果分支完全是自己本地开发,还没推送过,或者你有充分把握团队其他人不会基于它开发,rebase 可以帮你把历史整理得很干净。
- 如果团队要求保持提交历史的“事实记录”,那 merge 更合适,合并提交会真实反映“什么时候合了哪条分支”。
- 如果团队重视线性历史和简洁的 log,rebase 结合 squash 会更合适。
说到这里必须提一下git merge --squash。它会把你当前分支的所有提交压缩成一个提交,然后放到目标分支上。这个操作很适合功能分支合并前使用——一个功能十几个小提交,每个都是“改个 typo”、“再改改”,直接合进去历史太吵,压缩成一个“完成登录功能”的提交就很清爽。注意 squash 合并生成的新提交不会保留原始提交的逐条信息,在跟踪粒度上要自己想清楚。
7. 我用过之后觉得值得分享的命令与习惯
文章讲到这里,Git 分支的核心操作基本覆盖完了。最后我把自己平时积累的几个习惯写在下面,希望能给你一些参考。
第一,提交信息写成“动词 + 描述”的格式。fix: 修复登录接口空指针比fix更容易让人看懂。分支名也尽量统一风格,我见到比较合理的一套是:功能用feature/开头,修复用fix/开头,发布用release/开头。这个约定不需要工具强制,文本一致就行。
第二,合并前先看一眼历史再操作。git log --oneline -5只需要两秒钟,但能帮你确认当前分支是不是你要合并进去的那个,能省掉很多无谓的返工。
第三,冲突解决后务必全量构建一次。我说的构建不是本地手动点点,而是至少在本地执行一次完整的编译和单测。因为冲突解决过程中,人的注意力很容易集中在冲突标记上,忽略了标记附近被无意间动过的代码。一次构建就能暴露出绝大多数字符串、语法、引用的问题。
第四,遇到不理解的 Git 行为,先看提示再搜索。Git 的报错信息虽然唠叨,但大多数说的都是人话。比如your local changes would be overwritten by checkout,翻译过来就是“切分支会把本地没提交的改动覆盖掉”。看懂提示再去搜解决方案,比直接复制网上的命令更不容易出错。
第五,一个整合了git fetch+git checkout的习惯。团队协作时我会频繁从远程拉取同事新建的分支。一条git fetch origin && git checkout -b branch_name origin/branch_name就能完成,不需要每次都用图形界面。这个习惯熟练之后,效率提升非常明显。
最后再分享一个我觉得很有价值的小技巧:在本地建一个临时测试仓库,谁都可以在任何时间用几十秒的时间验证自己对 Git 的理解。比如你怀疑某个参数的行为,直接建一个目录跑一遍,比自己对着文档猜半天快得多。Git 本身的学习路径就是这样一条一条试出来的,从来没有哪个人能靠看书就完全弄明白所有细节。希望这篇文章能帮你少走一些弯路,尤其是那些我在操作中真正踩过的坑,对你也有参考价值。