上周帮一个团队看仓库,打开git log --graph --oneline --all那一屏,主干上密密麻麻全是合并节点,一半写着 "Merge branch 'main' into feature/xxx",另一半写着 "Merge branch 'feature/xxx' into main",真正的功能提交被夹在中间,得往下翻五六屏才看得清一条线。我问他们为什么这么合,回答是"git pull默认就这样"。同一个仓库里,另一位同学的做法完全相反,天天rebase,结果有一次把已经推上去的分支改写了,同事本地一拉,历史直接分叉成两条,两个人对着屏幕找了两个小时才把线接回去。
git merge和git rebase这两条命令,几乎所有写过代码的人都用过,但真正能说清"它们到底改了什么、什么时候该用哪条、出事了怎么救"的人并不多。大部分教程只告诉你"merge 保留历史,rebase 让历史变干净",这句话没错,但它解释不了为什么变基之后提交哈希全变了、为什么冲突时 ours 和 theirs 感觉是反的、为什么同事的分支会突然多出一堆重复提交。这篇就把这两条命令拆到底层动作,再落到我平时实际在用的几条操作流,最后给一套变基翻车之后的完整救援路径。刚学会git commit的新手能看懂原理部分,天天带队做代码评审的老手可以直接跳到冲突和救援那两节抄作业。
1. 两条命令在仓库里留下的痕迹完全不同
1.1 git merge 的底层动作:造一个新提交,把两条线缝起来
要理解合并,先得理解一个词:合并基(merge base)。假设主干main和你自己的feature/login从同一个点分出去,之后各自往前走。Git 会在这两个分支的共同祖先里找"最新的那一个",这个提交就是合并基。找到之后,Git 做的不是"把两个文件夹对比一下取新的",而是三方比较:拿合并基、main的最新提交、feature/login的最新提交,这三份快照放一起看。凡是只有一边改过的文件,直接采用改过的那份;凡是一边改了、另一边没动的地方,也算干净;只有两边都动了同一片区域,才叫冲突。
这个机制决定了合并的一个关键特性:它是一次性的、结论性的操作。合并完成后,Git 生成一个全新的提交,这个提交有两个父提交,一个指向main原来的头,一个指向feature/login的头。
git switch main git merge feature/login执行完,main的头往前挪了一格,这一格就是那个双亲提交。你在图上看到的分叉再汇合的形状,就是它留下的痕迹。这个痕迹的价值在于:它记录了"这两段工作是在什么时间点、以什么状态汇到主干的",日后查问题时,git log --graph能一眼看出某个功能是从哪进来的。
有一个容易忽略的分支情况:如果main自从分出去之后一步都没动过,那么feature/login的所有提交其实是直接接在main头上的,Git 会走快进(fast-forward),什么都不新建,只是把main的指针往前推。这种情况下你根本看不到合并节点,因为确实没有"两条线"需要缝。
git merge --no-ff feature/login # 强制生成合并提交,即使能快进 git merge --ff-only feature/login # 只允许快进,不能快进就报错退出 git merge --squash feature/login # 不生成提交,把所有改动塞进暂存区这三个参数我后面会专门讲怎么选,先记住一点:--squash走的是完全不同的路子,它不产生任何合并关系,只是把对方分支的净变更当成一堆未提交的修改放到你工作区,需要你自己再git commit一次。这种做法会把对方分支的所有提交历史丢掉,只留一个结果。
1.2 rebase 是把提交逐个"重放",不是把分支搬过去
很多人对变基的误解在于把它想象成"剪切粘贴"——把feature上的提交剪下来,粘到main后面。这个比喻方向对,但会让人低估它的破坏力。
真实的过程是这样的:Git 先找到feature/login和main的合并基,把合并基之后属于feature/login的提交按顺序列出来,然后把当前分支的头临时挪到main的最新提交上,接着从最早的提交开始,一个一个地把每个提交"当作补丁"重新应用一遍。每应用成功一个,就在当前位置生成一个全新的提交对象。
git switch feature/login git rebase main这里有三个必须知道的细节。
第一,变基过程中你会短暂处于游离 HEAD 状态。git status会显示interactive rebase in progress或者You are currently rebasing branch 'feature/login' on 'xxxxxxx'。这不是出错,是正常中间态,别慌着git checkout。
第二,每个提交都会被重新计算一次。提交对象的哈希由树内容、父提交、作者信息、提交信息、提交时间戳共同决定。父提交变了,树内容也可能因为基线不同而变(比如main上删掉了一个文件,你这条分支又改过它),所以哈希必然变。一条有 8 个提交的分支变基一次,等于凭空造出 8 个新提交对象,原来那 8 个还躺在对象库里,只是没有任何分支指向它们了。
第三,变基是"一条一条来"的,所以冲突也会一条一条出现。这点和合并完全不同:合并是整个分支一次性比较,冲突一次性暴露;变基是重放第一个提交时冲突一次,你可能解决完git rebase --continue,重放第二个提交时又冲突一次,来解决的还是同一片代码。第一次做变基的人经常在这里崩溃,以为是命令出问题了,其实这是设计如此。
想让合并历史也被保留下来(比如你的功能分支内部本身就有合并节点),得加参数:
git rebase --rebase-merges main不加这个参数,默认行为是把所有提交拍平成一条直线,原来的合并节点会被拆掉重放。
1.3 哈希变了,意味着一整串关联的东西都失效了
变基改写的不是"分支的名字指向",而是"提交本身"。这个区别在实际协作里能引发一串连锁反应,我按踩坑频率排一下。
同事基于你旧提交开的分支会变成孤儿线。他从你的feature/login上拉了一个feature/login-fix,你变基之后强推,他本地那条分支的父提交还在旧历史上,一拉取就出现"两边都有新提交"的分叉,git log --graph看上去像是同一份改动出现了两遍。
代码评审里挂在具体提交上的讨论会变成悬空引用。评审系统一般按提交哈希记录行内评论,哈希被换掉,那些评论就找不着位置了,有的平台会整段隐藏。
提交签名会失效。如果仓库启用了提交签名校验,变基后的新提交没有对应签名,某些严格的分支保护策略会直接拒收。
构建缓存和产物标签对不上。CI 系统通常用提交哈希做缓存键和镜像标签,哈希全变意味着这些缓存全部 miss,一次变基能让你多等十几分钟的流水线。
把这四条放在一起看,就能得出一个很实用的结论:变基的安全性不取决于命令本身,而取决于这条历史有没有别人依赖。个人分支上随便变,公共主干上变基就是在拆别人的地基。
2. 什么时候该留合并节点,什么时候该把历史捋直
2.1 公共主干上的合并节点是有信息量的,别急着嫌它丑
我见过不少团队一开始嫌合并节点乱,定了个规矩"所有人必须 rebase,主干保持一条直线"。执行了半年,问题开始冒出来:某次线上故障回溯,想知道某个改动是哪次集成进来的、跟哪批改动一起上的线,结果历史是一条笔直的线,每个提交看起来都像是独立开发完直接放上去的,完全看不出"这批功能是一起合的"。
合并节点真正记录的是集成事件——某个时间点,一批工作以什么状态进入了主干。这个信息在排查线上问题时极其有用。你能看到"这个提交是跟另外 12 个提交一起合进来的,那批改动里还有一个调整配置的提交,八成就是它"。直线历史把这个信息彻底抹掉了。
所以我的建议是:主干接收功能分支时保留合并节点,用git merge --no-ff,哪怕能快进也强制造一个。一个功能一个节点,主干的图谱就是一张清晰的集成时间线。
2.2 个人分支上的变基是在做"发布前整理",不是洁癖
反过来,在功能分支内部,变基几乎总是对的。原因很实在:开发过程中的提交质量参差不齐。你会看到 "fix typo"、"再改一下"、"真的最后一次"、"调试用的日志" 这种提交混在里面,这些提交对日后读历史的人毫无价值,反而增加噪音。
功能分支在提交评审之前,用交互式变基把它们整理成 2 到 3 个语义完整的提交,每个提交都对应一个可独立理解的改动单元,这才是变基最有价值的用法。这件事发生在你自己分支上,没有别人依赖,风险接近于零。
2.3 判断该用哪条的三条标准
我把平时的判断浓缩成一张表,遇到纠结的时候按顺序对一遍,基本不会有争议。
| 判断维度 | 倾向 merge | 倾向 rebase |
|---|---|---|
| 这条分支有没有被别人拉取过 | 有,且对方可能在其上继续工作 | 没有,纯本地 |
| 这条历史是不是公共记录 | 是,主干、发布分支、长期维护分支 | 否,个人功能分支、临时试验分支 |
| 你想要的结果是什么 | 保留集成事实,可回溯 | 提交整洁,便于逐条评审 |
| 提交本身的质量 | 已经足够干净 | 需要合并、拆分、改信息 |
| 冲突出现的节奏 | 一次性暴露,好规划时间 | 逐个提交暴露,可能反复 |
| 出问题后的恢复成本 | 低,git merge --abort一步回退 | 中,需要reflog或--abort |
有一格我要单独强调:"有没有被别人拉取过"这条的优先级高于其他所有条。技术上讲,只要分支还没推到远程、或者推上去之后确认没人基于它工作,怎么改都行。一旦有人依赖,不管历史多难看,都应该用合并而不是变基。历史难看只是不美观,历史被改写是协作事故,这两个代价不在一个量级上。
2.4 一个反直觉的经验:先合并再变基,有时候最省事
有些场景里,硬要二选一是自找麻烦。比如你的功能分支已经开了两周,中途main上动了 200 多个提交,你直接rebase会连续撞十几次冲突,每次解决的都是同一批代码,极其消耗耐心。
我的做法是分两步走:先用git merge main把主干合进来一次,把所有冲突一次性解决掉,这一步冲突再多也是一次性的;等分支开发完、准备提交评审之前,再整体git rebase main做一次整理。这时候因为分歧已经被那次合并消化掉了,变基大概率零冲突,只是把提交捋直。
3. 我日常在用的几条操作流
3.1 功能分支追主干:先 fetch,再 rebase,别直接 pull
git pull的默认行为是fetch加merge,这也是主干上大量无意义合并节点的来源。两个人一天各拉三次,就能造出六个 "Merge branch 'main' of ..." 的提交,这些提交不含任何实际改动,纯粹是同步动作留下的垃圾。
我更倾向于把拉取和整合拆成两步,看清楚再动:
git fetch origin git log --oneline HEAD..origin/main # 先看看主干上多了什么 git rebase origin/main # 把本地提交重放到最新主干上如果确定自己就是要保持线性,可以把默认行为改掉,省掉每次都敲参数:
git config --global pull.ff only # 只允许快进,不能快进就报错,逼你想清楚 git config --global pull.rebase true # 或者让 pull 默认走 rebasepull.ff only这个配置我推荐给所有团队新人。它的好处是不会悄悄帮你做出决定——一旦出现需要整合的情况,它直接报错退出,你得自己判断该 merge 还是 rebase。这比默认帮你造一个合并提交要安全得多。
还有一种情况要单独说:如果你本地分支上还没提交任何东西,只是想同步,那用git pull --ff-only或者干脆git reset --hard origin/main(确认工作区没东西的前提下)都行,别去 rebase 一个空分支。
3.2 交互式变基:把随手提交整理成能见人的提交
这是变基最实用的场景,也是我最常用的功能。命令本身很简单,关键是理解那个编辑界面里每个指令干什么。
git rebase -i HEAD~5它会打开一个文本编辑器,列出最近 5 个提交,每行开头是一个指令词。你需要做的就是把这些指令词改掉,保存退出,Git 按你写的顺序重新执行一遍。
| 指令 | 作用 | 典型用法 |
|---|---|---|
pick | 原样保留这个提交 | 默认状态,不动它 |
reword | 保留改动,但重新编辑提交信息 | 提交信息写错了、写得太随意 |
edit | 停下来,允许你修改这个提交的内容 | 需要往这个提交里补东西,或拆成两个 |
squash | 合并到上一个提交,保留两条提交信息让你编辑 | 两个提交想合成一个,且要重写信息 |
fixup | 合并到上一个提交,丢弃这条提交信息 | 纯粹的"改错字"型提交 |
drop | 删掉这个提交 | 误提交的调试代码、日志 |
| 调整行顺序 | 改变提交的应用顺序 | 想让某个改动提前进来 |
一个高频用法是把一串零碎提交压成一个。比如你写了 4 个提交,前 3 个是功能本身,第 4 个是"修复上一个提交里的笔误",那就把第 4 行改成fixup,保存,历史里就只剩 3 个干净的提交。
配合--fixup和--autosquash可以做到不改编辑器自动整理:
git commit --fixup=3f2a1b9 # 生成一个专门"修补 3f2a1b9"的提交 git rebase -i --autosquash HEAD~6 # Git 自动把它排到对应提交后面并标成 fixup这个组合在评审反馈阶段特别好用。评审人指出某个提交里的问题,你不用再单独造一个"根据评审意见修改"的提交污染历史,直接--fixup打到原提交上,最后一次性整理干净。
注意:交互式变基会重写你选中的所有提交,包括它们之后的所有提交。整理之前先确认这条分支没有推给别人,或者推了但没人基于它工作。
3.3--no-ff、--ff-only、--squash到底怎么选
这三个参数经常被混着用,但它们的语义差别很大,选错了会在历史里留下很难改的后果。
--no-ff是强制留痕。无论能不能快进,都生成一个合并提交。适用场景是主干接收功能分支,你要保留"这批改动是一起进来的"这个事实。代价是图上会多节点,但主干本来就该有节点。
--ff-only是强制约束。只允许快进,遇到需要真正合并的情况就报错退出。这个参数我最常用在两类场景:一是拉取远程更新时防止自动造合并提交;二是合并一个自己刚做完、确认可以直接接上去的分支时,用它做一次安全检查——如果它报错了,说明分支已经分叉,你得先想清楚怎么处理。
--squash是只取结果不取历史。所有变更进暂存区,不产生提交,不建立父子关系。适用场景是:对方分支上 30 个提交全是"改一点、试试、再改回来"的调试过程,你只想要最终结果,一个提交搞定。要清楚它的副作用——合并关系彻底丢失,日后git log里追不到这些改动来自哪个分支,只能靠提交信息里手工写一句。
我的默认组合是:主干merge --no-ff,个人分支同步用rebase,垃圾分支用merge --squash一把收掉,--ff-only当安全阀用。
3.4rebase --onto和cherry-pick:接错基的时候怎么救
有时候分支开错了基。比如你从feature/A上拉了feature/B,做着做着发现feature/A被砍掉了,feature/B得改接在main上。这时候普通rebase main会把feature/A的提交也一起带过来,因为它们在合并基之后。
git rebase --onto main feature/A feature/B这条命令读法是:把feature/B上属于feature/A之后的那些提交,重新接到main上。三个参数分别是新基、旧基、要移动的分支。我第一次用的时候记不住顺序,后来用了个笨办法记:从左到右念"接到哪、从哪切、搬哪个"。
cherry-pick是同一类需求的轻量版,适合只挑一两个提交:
git cherry-pick 3f2a1b9 # 挑一个 git cherry-pick 3f2a1b9..7c8d9e0 # 挑一段(左开右闭,不含起点)两者的区别在于:rebase --onto是批量搬家,会保持提交顺序和依赖关系;cherry-pick是单点摘取,摘出来的提交在目标分支上是全新的哈希,和原分支没有任何关联。想把某个线上热修同步回开发分支,用cherry-pick更合适;想整个分支改换门庭,用rebase --onto。
4. 变基出问题之后的完整救援链路
4.1 第一步永远是 reflog,任何其他操作都排在它后面
我见过最可惜的情况是:变基搞砸了,第一反应是关掉终端重开、或者git reset --hard,结果把能救回来的引用记录也一并清掉了。实际上 Git 对"移动过的 HEAD"有完整记录,这个东西叫reflog,默认保留 90 天。
git reflog输出是一串HEAD@{n}的移动记录,每一行左边是哈希,右边是动作描述,比如rebase (start)、rebase (finish)、commit、reset、checkout。你要找的是变基开始之前那一行,也就是rebase (start)上面那条。
找到之后,用两种方式之一恢复:
git reset --hard HEAD@{5} # 方式一:直接把分支指回旧位置 git switch -c rescue HEAD@{5} # 方式二:新建一个分支接住旧历史,更安全我一律推荐方式二。原因很朴素:在不熟悉的状态下,不要移动已有分支。新建一个分支接住旧历史,你的原分支和救援分支同时存在,可以来回对比着看,确认哪边是对的再决定怎么处理。reset --hard一旦执行,工作区里没提交的改动就没了,虽然 reflog 还能救提交,但救不回未提交的文件。
还有一个更省事的路径:如果你只是刚执行完变基、还没做其他操作,git rebase --abort就能直接回到变基前的状态。但如果你已经--continue走完了整个流程,--abort就无能为力了,只能走 reflog。
另外有个小工具能帮你确认"变基前后到底改了什么":
git range-diff main old-branch-hash new-branch-hash它会按补丁内容对比两段提交序列,告诉你哪些提交是等价的、哪些内容发生了变化、哪些被丢弃了。变基完想复核一遍有没有漏东西,这个命令比逐个git show高效得多。
4.2 变基中途卡住时的三个出口,以及什么时候该用哪个
变基过程中遇到冲突,Git 会停下来等你处理,状态提示里会给出三个选项。这三个选项不是随便选的,用错了会把历史搞得更乱。
git rebase --continue是正常路径。你手动解决完冲突、git add标记为已解决,然后执行它,Git 继续重放下一个提交。要记住的是不要在解决完冲突后又git commit——变基过程中 Git 自己在管提交,你额外提交会打乱它的节奏,虽然现代版本能识别,但容易出现空提交或者重复提交。
git rebase --skip是丢弃当前这个提交。适用于"这个提交的改动在目标分支上已经存在了"这种情况。典型的例子是你把某个补丁手工应用到了main上,然后变基时 Git 发现无内容可应用,冲突一片空白。这时候--continue会告诉你没有改动可提交,--skip才对。
git rebase --abort是整体放弃,回到变基开始之前。适用于"我发现整个思路错了"或者"冲突太多,我决定改用合并",一步回退,干净利落。
还有第四种情况,严格说不算出口:你想临时看一眼别的东西。这时候千万别git checkout 别的分支,那会让你脱离当前变基状态,后续操作全乱。正确做法是开个新终端窗口干活,让变基停在那儿。
提示:变基中断期间,仓库处于游离 HEAD 状态,此时任何
git commit、git reset、git switch都可能让变基状态丢失。要么解决冲突继续,要么--abort,别夹在中间操作。
4.3 已经推到远程的分支被改写:补救和告知同样重要
如果你变基了一条已经推到远程的分支,本地和远程的哈希分叉了,普通git push会被拒绝。这时候唯一能做的就是把本地历史强推上去,但不要用--force。
git push --force-with-lease origin feature/login两者的区别是:--force是无条件覆盖,哪怕远程在你上次拉取之后又有了新的提交,照样碾过去,别人的工作直接丢失。--force-with-lease会先检查远程分支是不是还停在你预期的位置,如果不是,直接拒绝推送。
git push --force-with-lease=feature/login:3f2a1b9 origin feature/login这个显式指定期望哈希的写法更严格,适合多人协作的分支。它的逻辑是"只有当远程还停在 3f2a1b9 时我才覆盖",只要有人在此期间推过东西,操作就会失败,给你一次重新评估的机会。
强推之后还有一步不能省:在团队频道里说清楚。告诉他们这条分支被改写了,本地如果保存着旧分支,请用下面这种方式重生:
git fetch origin git switch feature/login git reset --hard origin/feature/login如果对方在这条旧分支上有自己的提交,那不能简单 reset,得先git rebase --onto origin/feature/login 旧基 本地分支把他的提交摘出来重放。这也是我为什么反复强调"公共分支不要变基"——一旦发生,善后成本全部转嫁给别人。
4.4 一次完整的排查链路复现
说个虚构但很典型的场景。A 同学在feature/search上做了 6 个提交,推到了远程,同组的另一位同学基于它开了feature/search-ui。A 同学顺手做了个git rebase -i把 6 个提交压成了 2 个,然后git push --force。结果另一位同学拉取之后,本地多了 6 个提交,git log --graph显示两条历史并行,同名的改动出现了两次。
排查顺序是这样的。先让另一位同学什么都别做,尤其别 reset 也别 pull。第一步在他本地跑git reflog,找到他最后一次正常拉取前的哈希,通过git switch -c backup HEAD@{n}把当前状态先备份住。第二步跑git fetch,看远程的feature/search现在是什么样子,确认只有 2 个提交。第三步跑git range-diff,把本地的 6 个提交和远程的 2 个提交做内容对比,确认 6 个提交的内容是否已经被压进了那 2 个里——如果是,他那 6 个提交就是冗余的;如果他自己在旧分支上还加了新提交,那些提交得单独捞出来。
确认之后,处理方式分两种。他的feature/search-ui如果还没开始写,直接git switch feature/search-ui && git reset --hard origin/feature/search重建,再把自己要加的东西重新加上。如果已经写了几个提交,就git rebase --onto origin/feature/search 旧分支哈希 feature/search-ui,把新提交摘出来接到新基上。
整个链路里最关键的一步是"先备份再动手"。这次侥幸救回来了,是因为 reflog 还在。如果当时有人慌了先git gc --prune=now或者重装了一遍客户端,那 6 个提交可能就真没了。
5. 冲突本身:比命令更耗时间的那些细节
5.1 冲突标记怎么读,以及为什么我总开着 zdiff3
默认的冲突标记只有两段,<<<<<<<到=======是本分支的版本,=======到>>>>>>>是对方的版本。问题是这两段中间没有参照物,你得自己回忆"原来这里长什么样",改起来容易漏东西。
git config --global merge.conflictStyle zdiff3打开之后冲突块会变成三段,中间多出一段|||||||开头的内容,是合并基里的原始版本。有了这个参照,你就能清楚地看出两边各自改了什么、哪些是新增、哪些是删除,判断起来快得多。zdiff3是相对新一点的风格,比老式的diff3更紧凑,相邻的公共行会被收进冲突块外面,视觉上更清楚。如果 Git 版本较老不支持,用diff3也一样。
冲突块里的标记千万不要手工删漏。<<<<<<<、=======、>>>>>>>、|||||||这几行必须清理干净,漏一行提交上去,代码里就会留一堆尖括号,而且语法检查未必能发现(有些语言里它就是注释或者字符串的一部分)。
5.2 rerere:把重复冲突只解一次
rerere是 "reuse recorded resolution" 的缩写,这个功能被严重低估了。它的作用是记录你每次解决冲突的方案,下次遇到完全相同的冲突(同样的文件、同样的冲突块)时,自动帮你套用上次的解法。
git config --global rerere.enabled true它在两种场景下特别香。一是长时间的变基,一条分支重放十几个提交,同一个冲突反复出现五六次,开着rerere你只需要解第一次,后面自动过。二是反复切换的分支,比如你在main和功能分支之间来回切,每次都要解一遍同样的冲突,开着它就省事。
另外它还有个隐藏福利:git rerere会把解决前后的状态存下来,即使你后来--abort了,再重来一遍也能复用这些记录。不过要注意,rerere记录在本地,不会推到远程,团队里每个人得各自开。
5.3 换行符和格式化工具造成的"假冲突"
这类冲突最气人:打开文件一看,两边内容一模一样,Git 偏偏说冲突。原因通常是行尾符不一致——Windows 上是CRLF,Linux 和 mac 上是LF,同一个文件在两个人的机器上换行符不同,逐行对比时每一行都算"改动"。
处理办法是统一的.gitattributes配置:
* text=auto *.sh text eol=lf *.bat text eol=crlf第一行让 Git 自动判断文本文件并在仓库里统一存成 LF,后面两行按文件类型指定工作区的换行符。这个文件要提交到仓库,才能对所有人生效。已经乱掉的仓库,需要一次性做规范化(重写所有文件的换行符并单独提交一次),之后才不会反复冲突。
另一类假冲突来自格式化工具。两个人在不同时间保存了同一个文件,各自的编辑器按不同规则重排了缩进,内容没变但每一行都不同。这种冲突的根因不在 Git,在于团队没有统一的格式化配置。我的做法是把格式化配置文件和编辑器配置一起写进仓库,再配一个提交前自动格式化的钩子,让"保存即格式化"成为默认行为,从源头消灭这类冲突。
5.4-X ours和-X theirs不是"丢弃一边",变基时还是反的
这个参数我用错过一次,代价是丢了一整个文件的改动,所以单独拎出来说。
git merge -X ours feature/login它的真实语义是:在正常的合并流程基础上,当某一小块区域发生冲突时,自动采用本分支的版本。非冲突的部分照常合并,两边的其他改动都会保留。它绝对不是"整个文件都用我的"——那是git checkout --ours 文件名干的事,两者完全不同。
更麻烦的是变基时 ours 和 theirs 的含义是反的。因为变基过程中你处于"重放别人提交"的位置,此刻的ours指的是你已经重放到的那个基点(也就是目标分支),theirs指的是正在被重放的那个提交。所以git rebase -X theirs的意思是"冲突时采用被重放提交的内容",跟直觉正好相反。
# 变基过程中,这三个命令的 ours/theirs 全部是反的 git checkout --ours file.txt git checkout --theirs file.txt git rebase -X ours main我的建议是:不要在变基时用-X参数。它太容易搞反,而且一旦用错,被丢掉的内容不会报错,只会悄悄消失。宁可老老实实逐个解冲突,慢一点但不会错。真要用,用完立刻git diff检查一遍结果。
5.5 二进制文件冲突只有一条路
图片、压缩包、字体、设计稿这类二进制文件,Git 没法逐行合并,一旦两边都改了同一个文件,冲突就是"整个文件二选一",没有中间地带。
git checkout --ours assets/logo.png # 取本分支的版本 git checkout --theirs assets/logo.png # 取对方的版本 git add assets/logo.png解决完之后记得和改动方确认一句"这个文件我取了你这版,另一版的内容还要不要",因为被丢掉的那一版在 Git 里没有任何痕迹可查(它还在对象库里,但没人会去翻)。
更根本的解法是从流程上避免:二进制资源尽量不让两个人同时改,或者改成拆分成独立文件(比如把一张大图拆成多个小图,或者用文本格式描述配置),让冲突面变小。实在避免不了,就在.gitattributes里给这类文件标记-merge,让 Git 直接放弃自动合并、强制人工介入,比它悄悄选一个版本要安全。
最后再分享一个我自己养成的小习惯:每次执行rebase或merge之前,先跑一遍git status和git stash list,确认工作区干净、没有遗留的 stash 没处理。这两个命令在脏工作区上执行失败的情况太常见了,而且失败之后的报错信息往往指向别的地方,容易把人带偏。多花三秒钟看一眼状态,能省掉后面半小时的排查。