1. 主分支推送被拒的那一刻问题从来不在 push 本身几乎每个用 Git 做协作的人都遇到过屏幕上那行红字! [rejected] main - main (non-fast-forward)。第一次看到时最容易做出的动作是加个-f再来一次命令确实成功了但几分钟后同事那边就会炸开锅。我在早期也干过这种事后来才彻底想明白推送主分支被拒根因几乎从来不在 push 这一步而在你按下 push 之前本地这支主分支和远端那支主分支已经分叉了。围绕这个分叉git fetch负责把远端发生的事情如实取回来、让你看见git rebase负责把你本地多出来的提交重新安放到最新的基点上、让历史变成一条直线最后 push 才有可能顺利通过。这三步是配套的少看一步就容易盲目操作少整理一步就会制造一堆无意义的合并提交。这篇内容适合已经会git add、git commit、git push但一遇到主分支同步、历史整理、冲突处理就心里发虚的人也适合那些每次都用git pull一把梭、出了事只会git reset --hard的人。我先把结论摆出来在多人协作的仓库里git fetch不是一个可选的前置动作而是你做任何判断的信息来源git rebase不是高级玩家的玩具而是把本地零碎提交整理成可评审历史的标准手段。这两件事合起来解决的正是我本地的主分支该怎么安全地跟上远端、并且体面地推回去这个问题。1.1 non-fast-forward 到底在拦什么Git 的推送有一条很朴素的规则远端分支的指针只允许向前移动不允许被替换成一条不包含远端当前最新提交的历史。换句话说如果你要推上去的历史里找不到远端分支此刻指向的那个提交服务端就会拒绝并且贴心地告诉你Updates were rejected because the remote contains work that you do not have locally。值得追问的是Git 为什么要有这条规则因为远端分支是所有人共享的引用它一旦被覆盖其他已经拉取过旧历史的人本地的提交链就和远端对不上了。Git 默认选择了保守策略——宁可拒绝你也不悄悄改写别人脚下的地基。这也是为什么后面讲到--force-with-lease时我会反复强调强推不是不能用而是要知道自己在覆盖谁的东西。我习惯用文档协作来类比A 和 B 同时基于同一版文档修改A 先提交了新增的第三章B 手里还是只有前两章B 想直接把整份文档覆盖上去那 A 的第三章就没了。Git 拦的就是这个动作。所以正确的处理顺序永远是先把远端的新内容取回来再把自己的修改放上去。1.2 fetch 之后本地主分支为什么还是旧的这是新手最困惑的一点明明执行了git fetchgit status却说你的分支比 origin/main 领先 3 个提交打开文件也看不到别人改的东西。原因在于git fetch做了两件事但恰恰没做第三件。fetch 做的事说明下载对象把远端新增的 commit、tree、blob 打包拉进本地.git/objects更新远程跟踪分支把refs/remotes/origin/main指向远端最新的提交不动本地分支本地refs/heads/main原地不动工作区文件也不动所以git fetch是纯粹的侦察动作它对工作区零影响可以随时执行、反复执行。你本地的主分支指针还是老位置工作区的文件当然也没变化。真正让本地主分支动起来的是你接下来选的merge或者rebase。这个分离设计其实非常友好你可以先看远端的改动、再决定怎么处理而不是被强制拉进一个可能满是冲突的合并现场。1.3 我为什么不再直接 git pullgit pull就是git fetch加一个合并动作默认是merge。它的问题不在于错而在于把获取信息和改变状态打包成了一步中间没有留给你观察的窗口。一旦远端有人改了你正在改的文件你会在毫无准备的情况下被丢进冲突现场甚至连这次合并会引入哪些提交都不知道。更隐蔽的坑是默认行为不一致老版本 Git 在没有任何配置时会给出警告不同环境里pull.rebase的取值可能不同导致同一个git pull在不同机器上一个产生合并提交、一个做变基团队里的历史风格就此分叉。我的建议很明确在同步主分支这件事上把 fetch 和 rebase 拆开手动执行需要偷懒时再用配置好的pull --rebase。分开执行多花的十几秒换来的是你对每次历史变更的完全知情。2. git fetch 的三个层次从拉对象到看差异很多人对 fetch 的理解停留在从远端拿东西但真正决定你能不能用好它的是你能不能读懂 fetch 之后本地多出来的那几个引用。我一般把 fetch 的用法分成三个层次第一层是知道它更新了什么第二层是知道怎么对比第三层是知道那些边缘参数在什么场景下必须加上。这三层走完你在主分支同步这件事上就不会再有我到底拉下来什么了的迷惑。2.1 refspecfetch 的搬运规则fetch 的搬运规则由 refspec 描述形如refs/heads/*:refs/remotes/origin/*。冒号左边是源远端引用右边是目标本地引用前面的表示允许非快进更新。默认配置下这条规则的意思是把远端所有分支映射成本地的origin/xxx远程跟踪分支。理解这一点有实际价值。比如你想临时看一眼某个远端分支而不创建本地分支# 只把远端某个分支拉到本地一个临时远程跟踪引用 git fetch origin feature/login:refs/remotes/origin/feature/login # 看看这个分支上有什么本地没有的提交 git log --oneline main..origin/feature/login再比如你想知道 fetch 到底更新了哪些引用可以打开详细输出# 显示 fetch 过程中每个引用的变化A 表示新增M 表示移动D 表示删除 git fetch --verbose origin # 或者事后查看引用变化日志 git reflog show origin/maingit reflog show origin/main这一条我强烈推荐养成习惯它记录的是远程跟踪分支自身的移动轨迹能清楚看到上一次 fetch 时远端在哪这一次在哪。排查为什么我的远程跟踪分支和同事看到的不一样时这个日志往往能直接给出答案——多半是你很久没 fetch 了。2.2 fetch 之后必做的三件事fetch 只是把信息搬回来不消费这些信息等于白搬。我固定会做三件事顺序基本不变。第一件看远端多了什么提交# 本地 main 有、远端 main 没有的提交我这边待推送的 git log --oneline origin/main..main # 远端 main 有、本地 main 没有的提交别人推上去的 git log --oneline main..origin/main # 两边分叉的完整图景 git log --oneline --graph --decorate main origin/main --not $(git merge-base main origin/main)第三条命令里的--not配合merge-base意思是排除掉双方的共同祖先及其之前的所有提交剩下的就只有真正分叉的部分。图形化输出能一眼看出是一个交叉还是多条并行。第二件看远端到底改了哪些文件# 只看远端相对本地的文件级差异 git diff --stat main...origin/main # 三点语法等价于 diff $(git merge-base main origin/main) origin/main # 也就是远端从分叉点开始改了什么不会把你本地的改动混进来这里的三点...和两点..的区别值得记牢在git diff里A..B是直接比较两个端点会把 A 侧独有的改动也显示成反向差异A...B是从共同祖先比到 B更符合远端改了什么的直觉。我第一次被这个坑绊住是因为用..看 diff结果一堆自己写的东西以删除的形式出现白白紧张了一场。第三件确认工作区状态干净git status这一步看起来多余其实是安全阀。rebase 要求工作区和暂存区没有未提交的改动否则要么被拒绝要么需要--autostash。在动手之前花两秒确认状态比事后从 rebase 中断里捞文件省事得多。2.3 prune、tag 与浅克隆下的 fetch 行为默认的 fetch 不会删除本地已经不存在的远程跟踪分支。也就是说同事把远端分支删了你本地的origin/那个分支还会一直挂着时间长了git branch -r输出里全是幽灵分支。解决办法是加--prune# 拉取并清理已删除的远程跟踪分支 git fetch --prune origin # 更省事的做法把它设为全局默认 git config --global fetch.prune true关于 tag默认 fetch 会自动跟随远端新增的 tag这在 CI 环境里偶尔会造成困扰远端 tag 被删掉又重建本地会报 tag 冲突。如果不需要自动跟随可以关掉git config --global fetch.pruneTags true git config --global fetch.writeCommitGraph true再一个是浅克隆。用--depth拉下来的仓库是截断历史fetch 默认不会自动加深。如果你在主分支上需要做 rebase 却发现找不到共同祖先很可能就是这个原因# 逐步加深历史直到能算出共同祖先 git fetch --deepen50 # 或者直接补全为完整历史 git fetch --unshallow我在处理一次 CI 构建日志里的异常时就栽在这上面容器里用的是浅克隆git rebase origin/main报了一堆莫名其妙的东西把历史补全后立刻恢复正常。所以看到rebase 报错但本地明明干净这种诡异现象先怀疑浅克隆。3. git rebase 到底对你的提交做了什么git rebase被讲得玄乎本质上它只做一件事把你当前分支上独有的那批提交一个一个地重新应用到另一个基点上并生成一批全新的提交。注意全新的三个字——新提交的哈希值必然不同因为它的父提交变了。理解这一点后面所有关于强推、关于历史重写、关于别人拉过旧分支的风险都是顺理成章的推论。3.1 提交被摘下再插回去的完整过程拿最常见的场景举例本地main和origin/main在提交 C 处分叉你本地多了 D、E 两个提交远端多了 F、G 两个提交。执行git rebase origin/main时Git 的动作序列是这样的。找到共同祖先也就是 C。把 D、E 这两个提交对应的改动patch临时保存起来分支指针先回退到origin/main此时指向 G。按顺序把 D 的改动应用到 G 之上生成 D再应用 E 的改动生成 E。如果某一步应用失败内容冲突暂停并让你解决全部成功后main指向 E。整个过程可以用一条命令直观验证# 执行前后对比提交哈希能看到 D、E 变成了 D、E git log --oneline --graph main git rebase origin/main git log --oneline --graph main结果是一条干净的直线C → F → G → D → E。原来的 D、E 并没有消失它们还在 reflog 和悬空对象里这也是后面误操作回滚一章的基础。一个容易忽略的细节rebase 应用的是每个提交的 diff不是文件快照。所以如果你的某次提交里有一行代码是新加的而新基点上同一位置已经有人加了完全一样的一行Git 可能判定为这个改动已经在目标里了而自动跳过该提交git status里会提示all conflicts fixed but you are still merging或者更常见的dropping xxx -- patch contents already upstream。看到日志里出现dropping不要慌这是正常行为。3.2 冲突现场的三个目录和三个出口rebase 中途停下来时很多人第一反应是乱敲命令。我建议先搞清楚现场此时 HEAD 处于游离状态detached HEAD停在某个中间提交上Git 用三份信息帮你看清冲突。位置内容用途工作区文件带冲突标记的合并结果手改的地方--oursstage 2变基目标新基点那一侧想知道别人/远端怎么写--theirsstage 3你正在被应用的提交想知道我原来怎么写注意这里的 ours/theirs 在 rebase 语境下和 merge 是反过来的这是老手偶尔也会绕晕的地方。rebase 时--ours指的是你正在往上贴的那个新基线--theirs才是你自己那个要被应用的提交。三个出口要记清楚# 解决冲突后标记已解决并继续应用下一个提交 git add 冲突文件 git rebase --continue # 放弃整个 rebase回到动手之前的状态 git rebase --abort # 跳过当前这个提交注意这个提交的改动会彻底不进入结果 git rebase --skip--skip是最危险的一个因为它会让某个提交的内容凭空消失而你可能毫无察觉。我的习惯是解冲突时先看git status里列出的文件清单逐个处理处理完再用git diff --cached复核一遍准备提交的内容确认无误才--continue。中途如果想反悔当前这一步git checkout -- file可以把单个文件恢复到冲突标记状态重来。3.3 rebase 和 merge 在主分支场景下怎么选这两个命令都能把远端变化合进本地但产物完全不同。维度mergerebase历史形状分叉后交汇多一个合并提交一条直线提交哈希原有提交不变全部重写冲突处理次数一次性在合并提交里解决逐个提交解决可能反复遇到同一处对已推送分支的安全性安全危险需强推适合的场景已共享的分支、长期并行的大特性本地待推送的提交、保持主分支线性我在主分支同步上的取舍是本地尚未推送的提交一律 rebase已经推送到远端、并且可能有别人在基于它工作的分支一律 merge。前者能保证主分支历史是一条可读的直线git log一眼看到底后者能避免改写别人手里的历史。团队如果强制要求主分支线性历史通常的做法是在合并请求环节统一做 rebase 或 squash而不是让每个人在自己机器上随意强推主分支。还有一点实测体会冲突次数多的时候merge 反而更省心因为它只让你解一次rebase 可能让你在三个提交上重复解同一处冲突。这时候git rerere就派上用场了后面会讲。4. 把本地主分支对齐远端一套可复现的操作序列理论讲完落到手上的是一套固定动作。我在任何需要往主分支推送的场景里基本都走同一个流程区别只在于中途有没有冲突。下面这套序列可以直接照着做每一步我都标了意图出问题时也好定位。4.1 完整命令序列与每步意图# 0. 确认在正确的分支上且工作区干净 git status git branch --show-current # 1. 取回远端最新状态同时清理已删除的远程跟踪分支 git fetch --prune origin # 2. 看清楚两边各有什么 git log --oneline main..origin/main # 别人推的 git log --oneline origin/main..main # 我待推的 git diff --stat main...origin/main # 别人改了哪些文件 # 3. 把自己的提交重新安放到远端最新之上 git rebase origin/main # 4. 如果远端没有新东西rebase 会直接提示 up to date相当于空操作 # 如果有冲突进入 3.2 里的处理流程 # 5. 复核结果历史应为一条直线 git log --oneline --graph -10 # 6. 推送 git push origin main第 1 步加--prune是我长期保留的习惯。原因很实际在多分支并行的仓库里远程跟踪分支越堆越多git branch -r的输出会变得没法看而且在写脚本做批量操作时容易误伤已经删除的分支。第 3 步之后如果你发现 rebase 的结果和自己预期不符别急着往下走先看git log。rebase 是一个一次性重写一批提交的动作越早发现问题越好处理# 查看 rebase 过程中分支指针的移动轨迹 git reflog # 想退回 rebase 之前找到 ORIG_HEAD 或者 reflog 里的位置 git reset --hard ORIG_HEADORIG_HEAD是 Git 在执行 rebase、merge 这类会大幅移动指针的操作前自动记录的位置非常方便。但要注意如果你在 rebase 之后又做了别的移动指针的操作ORIG_HEAD会被覆盖这时候就只能靠reflog里按时间翻找了。第 6 步如果还是被拒说明在你 fetch 之后到 push 之前远端又有人推了东西这在活跃仓库里完全可能尤其是 fetch 和 push 之间隔了几分钟。处理方法很简单再走一遍第 1 到第 6 步。4.2 交互式 rebase 整理提交在把本地提交推上去之前很多人的提交历史是这样的fix typo、再改一下、真的最后一次、临时提交。这种历史推到主分支上评审的人会很痛苦。交互式 rebase 就是专门用来整理这一段的。# 整理最近 4 个提交不含更早的 git rebase -i HEAD~4 # 或者整理从远端 main 分叉以来的所有提交这个写法更稳妥 git rebase -i origin/main编辑器里会出现一份清单每行一个提交前面的动词决定怎么处理动词缩写作用pickp保留该提交rewordr保留改动但修改提交信息edite停在该提交上允许修改内容或拆分squashs合并进上一个提交保留两条提交信息fixupf合并进上一个提交丢弃本条提交信息dropd删除该提交日常最常用的是fixup。至于提交信息的编辑窗口我一般的习惯是把squash那几条合并成一条完整、能说清这次改了什么、为什么改的信息而不是把四条零碎信息拼在一起。还有一个非常实用的技巧可以让你不用手动排顺序# 提交时就把这是修上一个提交的标记打进去 git commit --fixup目标提交哈希 git commit --fixupHEAD~2 # 之后自动把它们折叠到对应位置不需要打开编辑器 git rebase -i --autosquash origin/main # 设为默认从此 -i 就带自动排序 git config --global rebase.autosquash true这个组合我用得极多写代码时发现前一个提交漏了个边界判断直接git commit --fixupHEAD补上最后统一rebase -i --autosquash一次收拾干净全程不用记提交顺序。注意 autosquash 依赖提交信息里的fixup!、squash!前缀手写提交信息时如果误加了这些前缀会被意外折叠这点要留神。4.3 推送阶段普通 push 与 --force-with-lease 的边界rebase 之后本地主分支的哈希全变了git push必然被拒。这时候面临选择。方式风险适用git push被拒安全默认先试这个git push --force-with-lease若远端在你 fetch 后被人推过会拒绝推送自己整理过历史的分支git push --force无条件覆盖远端可能删掉别人的提交几乎不用--force-with-lease的机制是推送前检查远端分支是否还停在你上次 fetch 时看到的位置如果不是说明中间有人推了新东西直接拒绝。这正好堵住了我强推把同事刚推的提交冲掉这个最典型的事故。所以如果非要强推我只用这个参数不用--force。还要提醒一句--force-with-lease的保护依赖于你本地的远程跟踪引用是准确的。如果你之前用git fetch更新过origin/main那它是准的如果你用了git remote update之类的批量操作或者手动git update-ref改过引用保护可能失真。更严格的写法是显式指定期望值# 明确要求远端 main 必须还停在这个哈希上否则拒绝 git push --force-with-leasemain:你 fetch 到的哈希 origin main另外如果目标分支开启了保护策略强推通常会被服务端直接拒绝这其实是好事相当于给你加了一道外部保险。5. 我踩过的那些坑rebase 现场的排错链路下面这几个场景都是真实踩过、并且事后复盘过很多次的。我把排查链路完整写出来是因为这类问题只看结论没有意义下次换个形态你依然认不出来。5.1 历史被重写后 push 再次被拒现象rebase 顺利完成git log看着没问题push 却被拒提示non-fast-forward。排查链路是这样的。第一步确认远端在这期间有没有新提交git fetch origin git log --oneline main..origin/main如果输出为空说明远端没动那问题出在你这边。第二步确认本地是不是真的做了 rebasegit log --oneline --graph -10 git reflog -20reflog 里能看到rebase (finish)之类的记录。第三步也是最容易被忽略的一步确认你推的分支和远端分支的对应关系对不对。有一次我 rebase 的是主分支push 时却因为上游追踪配置问题推到了一个名字相近的分支上报错信息里的分支名被我扫了一眼就跳过了。所以git push我总是显式写全git push origin main而不是依赖git push的默认行为。显式写全虽然啰嗦但能省掉大量到底推哪去了的困惑。还有一种情况远端分支确实没动但服务端拒绝了非快进推送而你确认这次改写是安全的。这时才轮到--force-with-lease出场。5.2 误 abort、误操作导致提交消失场景rebase 途中冲突太多心烦随手git rebase --abort发现工作区回到起点但自己改了半天的东西好像不见了。或者更糟手滑执行了git reset --hard。先说结论只要提交过基本都能捞回来。Git 的对象库在垃圾回收之前不会真的删除这些提交reflog就是那个索引。# 1. 看完整的 HEAD 移动历史找到误操作之前的位置 git reflog --dateiso # 2. 找到目标哈希后先建一个分支保住它别直接 reset git branch rescue 目标哈希 # 3. 确认内容无误后再决定怎么合并回 main git log --oneline rescue git diff main rescue我用git branch rescue而不是直接git reset --hard是因为 reset 会再次移动指针万一哈希记错了就又多了一层麻烦。先建分支相当于给自己留了个锚点怎么折腾都不怕。如果是--abort之前有很多手工解决的冲突内容这些内容不一定在 reflog 里因为它们可能从未被提交。这种情况只能靠编辑器或 IDE 的本地历史功能找回。所以我养成了一个习惯rebase 解冲突的过程中每解决完一个文件就git add解决完一个提交就--continue不要攒着一堆一次性处理。分批推进中途出问题的损失最小。还有一个隐蔽的坑rebase 时如果对某个文件用了git checkout --ours file你以为保留了自己的版本实际上保留的是新基线的版本前面 3.2 讲过 ours/theirs 在 rebase 里是反的。这个错误不会报错只会静默地丢掉你的改动。我的做法是解冲突时尽量不用--ours/--theirs一把梭而是打开文件手动合并尤其是业务逻辑代码。5.3 rerere 和几个日常配置git rererereuse recorded resolution是我认为被严重低估的一个功能。开启之后Git 会记录你解决过的冲突片段和解决方案下次遇到完全相同的冲突时自动套用。# 全局开启 git config --global rerere.enabled true它的价值在长生命周期的分支上体现得最明显你 rebase 一次解了冲突隔几天远端又改了同一个位置你再 rebasererere 会把上次的解法直接填进去你只需要确认一遍。缺点是它记性好到可能套用错误的旧方案所以开启后建议加上rerere.autoupdate之外仍然保持人工复核的习惯至少在--continue前扫一眼改动。几个我常驻的配置配合本文主题# 让 pull 默认走 rebase避免无意中产生合并提交 git config --global pull.rebase true # 新分支自动以 rebase 方式跟随上游 git config --global branch.autoSetupRebase always # fetch 时自动清理已删除的远程跟踪分支 git config --global fetch.prune true # 让 diff 显示更清晰便于解冲突时判断 git config --global diff.algorithm histogram关于pull.rebase true有一点必须说明它只影响拉取时的合并方式不影响你的主分支被推上去时的历史。也就是说它在本地很安全但如果你的分支已经推送并被别人拉取开启它反而可能让你在不知情的情况下改写了共享历史。这也是很多团队不敢全局开启它的原因。我的做法是个人仓库全局开、协作仓库按项目配置并且始终保留手动fetchrebase的习惯作为兜底。6. 团队协作里的边界哪些分支能 rebase技术细节掌握之后真正决定你会不会给别人添麻烦的是边界感。同样一条git rebase用在自己没推过的本地分支上叫整理用在别人已经拉取过的共享分支上叫事故。6.1 黄金法则的工程化落地那条被反复引用的规则是不要对已经推送到公共仓库、并且可能有人在基于它工作的分支执行 rebase。这条规则听起来抽象落地时可以转成三个自查问题。第一这个分支我推上去之后有没有告诉别人可以基于它开发如果有就别动历史。第二这个分支是不是主分支、发布分支这类多人共享的引用如果是一律用 merge 或平台提供的合并策略不要本地强推。第三我 rebase 之后要推是不是必须加 force 参数如果必须那就说明这次操作会改写公共历史先停下来确认有没有人已经拉取过。我在自己的机器上做了一个简单的区分本地特性分支feature/xxx尚未推送随便 rebase、随便 squash、随便 reset一旦推送上去并开了合并请求就只做追加提交不再改写历史主分支本地副本永远只做跟上远端的 rebase不做其他整理。这三档规则记熟之后基本不会踩到公共历史的红线。6.2 主分支保护与合并策略的配合现在多数代码托管平台都提供分支保护禁止直接推送主分支、要求合并请求通过评审和检查、限制谁能强推。这些设置不是限制自由而是把改写公共历史这个动作收拢到一个可控的入口。和本文主题直接相关的一点是平台提供的几种合并方式合并方式产物适合创建合并提交保留全部原始提交 一个合并提交特性分支提交本身清晰、需要保留演进过程变基合并把特性分支的提交逐个重放到主分支上保持线性提交已经整理干净、数量适中压缩合并全部提交压成一个提交零碎、只需保留最终结果这三种方式本质上都是把rebase 还是 merge的选择权从个人转移到了仓库策略上。团队统一用变基合并或压缩合并时主分支历史自然就是线性的个人再在本地随意 merge 也不会污染主分支。这其实比我早期推崇的每个人自觉 rebase要靠谱得多——靠约定不如靠工具链。另一个实践细节如果主分支保护开启了要求分支与主分支同步的选项你的合并请求可能被要求先解决冲突。这时候在本地做一次git fetch origin git rebase origin/main再推上去比在网页上点合并要清楚得多因为你可以在本地跑测试验证冲突解决后的结果。6.3 我常用的几条配置和习惯最后分享几条和这个主题相关的个人习惯都是长期用下来觉得收益大于成本的。第一永远显式指定远端和分支git push origin main而不是git push。省下的几个字符不值得用一次推错分支来换。第二动手前先看一眼git status和当前分支名。这条听起来像废话但我见过太多在错误分支上做 rebase 的例子包括我自己。第三保留一份git log --oneline --graph --all -20的心智图。每次 rebase 前后各看一次确认形状符合预期。图形化输出能暴露很多命令输出里看不出来的问题比如意外多出来的交叉、重复的提交、跑到别的分支上的提交。第四不要在 rebase 中途做别的事。rebase 是一个半完成状态此时 HEAD 游离、索引里有暂存内容、可能有冲突标记任何额外的checkout、stash pop、reset都可能让现场变得难以复原。我现在的做法是遇到复杂冲突就先把整个操作推到重来用--abort回到干净状态重新规划后再动手而不是在混乱的现场上打补丁。第五给别人讲这个流程时先讲 fetch 再讲 rebase。很多人觉得 rebase 难其实难的不是命令本身而是不清楚自己在对哪两个东西做变基。把git log --oneline main..origin/main这条命令教会对方他自己就能看清局势后面的事情都是水到渠成。这套流程用熟了之后从推送被拒到干净地推上去通常只需要三条命令中间加一次git log确认。真正花时间的地方永远是冲突解决本身而那是业务问题不是 Git 问题。