Git 分支、Merge 与 Rebase
一次“代码没丢,历史却错了”的合并
一个功能分支通过了测试,开发者为了让提交图变直,在共享分支上执行 rebase,随后用普通强推覆盖远端。代码表面完整,另一位开发者刚推送的修复却从分支尖端消失;原有 Commit 签名、审批基线和构建记录也全部指向了旧对象。事故不在 rebase 命令本身,而在团队把“整理自己的提交”误当成“可以改写任何分支”。
Git 分支不是复制出来的代码目录,而是 refs/heads/* 下指向某个 Commit 的可移动引用。Commit 保存树对象、父 Commit、作者与提交者信息;父指针把对象连成有向无环图。HEAD 再指向当前分支或某个具体 Commit。理解这些对象之后,三种整合动作的差异就很清楚:merge 连接既有历史,rebase 在新父节点上复制提交,cherry-pick 只选择若干补丁重新提交。
这条差异决定了操作边界。merge commit 保留原 Commit ID 并新增一个双亲节点;rebase 和 cherry-pick 会创建新 Commit,原签名、SHA、审批与外部链接不会自动迁移。分支尚未共享时,改写通常只影响作者自己;分支已经成为他人的上游后,同一动作就会变成协作协议和恢复能力问题。
先确认正在移动哪些引用
Branch、merge、rebase 和 cherry-pick 都是 Git 内置能力,不需要插件。第一次操作前先确认客户端、工作区、远端和 upstream:
git --version
git status --short --branch
git remote -v
git branch -vv这些输出要回答三个问题:当前分支指向哪个 Commit,默认上游是谁,工作区与 index 是否干净。随后建立三个习惯:
工作区和暂存区应干净。Rebase、Merge 前未提交的改动会增加状态判断成本;确需暂存时明确记录 stash 用途。先 git fetch --prune 更新远端跟踪引用,再比较提交图。不要把本地的 origin/main 当成实时远端状态。对已共享分支执行历史改写前,确认是否有人基于它继续工作,并留下改写前的 Commit ID 或备份引用。
示例使用 main、feature/example、fix/example 和 <remote> 等抽象名称。实际项目需替换为团队约定,不要复制真实远端地址、凭证或内部项目名到文档和脚本。
Git 官方的 git-branch 将分支操作定义在引用上,git-merge、git-rebase 和 git-cherry-pick 分别给出连接、重放和序列器状态。实际能力仍以 git --version 与本机 git help <command> 为准,尤其是团队跨操作系统或跨发行版维护客户端基线时。
先在可丢弃的测试仓库设置默认拉取策略,避免 git pull 在分叉后临时询问或执行团队不期望的整合方式。三选一,不要同时照抄:
# 明确用 merge 整合远端变化
git config --local pull.rebase false
# 或:只允许 fast-forward,出现分叉就停下人工判断
git config --local pull.ff only
# 或:团队明确采用 rebase 更新个人主题分支
git config --local pull.rebase true对初学者和受保护主分支,pull.ff=only 是容易解释的失败前置:它不会替你决定如何处理分叉。主题分支是否 rebase,再由人根据共享程度判断。
建议启用自动保存 rebase 冲突解决前的工作区只在团队理解其行为后使用;更重要的是先保留冲突标记和状态可见性。任何 GUI 都只是调用这些对象与命令,排障时应回到 CLI 观察提交图和进行中的操作。
让分支行为变得可预测
建立分支跟踪关系
创建并切换新分支:
git switch -c feature/example
git push -u <remote> feature/example-u 会把本地分支关联到远端跟踪分支,之后 git status、无参数 git pull 和 git push 才知道默认比较对象。检查最终关系:
git branch -vv
git config --get-regexp '^branch\.'不要仅按分支名相同推断它们已经跟踪。错误 upstream 会让 ahead/behind、pull 和 push 指向意外分支。
选择历史整合策略
团队通常从三种策略中选主路径:
Merge commit:保留分支汇合点和原 Commit ID,适合需要看见并行上下文、审计合并动作或保留签名对象的场景;代价是主线图更复杂。Rebase 后 fast-forward:把主题提交复制到最新基线上,主线线性、便于 bisect 与阅读;代价是 Commit ID 和原签名改变,不适合随意改写共享分支。Squash merge:平台把一个 PR/MR 压成一个新 Commit,主线紧凑;代价是分支内提交身份、签名与粒度不进入主线。平台保护规则还要明确谁能选择该策略,以及合并后是否撤销旧审批。
没有一种策略能同时最大化原始拓扑、线性、原签名和最少 Commit。团队应按审计需求、回滚粒度、提交质量和协作规模选择,而不是争论哪张提交图更漂亮。
给危险操作设置显式边界
不要把 push --force 写进全局 alias 或自动脚本。确需改写个人主题分支时,使用明确分支和租约:
git fetch <remote>
git push --force-with-lease=<remote-branch>:<expected-commit> <remote> HEAD:<remote-branch>精确 <expected-commit> 表达“只有远端仍在我确认过的位置才覆盖”。git-push 说明,仅写 --force-with-lease 会依赖本地远端跟踪引用;编辑器或后台任务自动 fetch 后,这个引用可能已更新,保护假设随之变化。
在本地看见三种对象变化
下面用纯本地仓库观察 merge、rebase 和 cherry-pick 的对象变化,不需要真实远端和凭证。在空测试目录创建仓库:
git init history-lab
cd history-lab
git config user.name "Example Developer"
git config user.email "developer@example.invalid"
git switch -c main
printf "base\n" > app.txt
git add app.txt
git commit -m "chore: create base"观察 merge commit
创建一条主题分支和一条主线提交,使两边分叉:
git switch -c feature/merge-demo
printf "feature\n" >> app.txt
git commit -am "feat: add merge example"
git switch main
printf "main\n" > main.txt
git add main.txt
git commit -m "docs: add main note"
git merge --no-ff feature/merge-demo -m "merge: integrate example"
git log --graph --oneline --decorate --all预期图中出现一个有两个父 Commit 的 merge commit。验证父节点:
git show --no-patch --format='%H%nparents: %P' HEAD观察线性 rebase
从主线创建新主题,随后让主线再前进:
git switch -c feature/rebase-demo
printf "rebase\n" > rebase.txt
git add rebase.txt
git commit -m "feat: add rebase example"
git rev-parse HEAD
git switch main
printf "main-2\n" >> main.txt
git commit -am "docs: extend main note"
git switch feature/rebase-demo
git rebase main
git log --graph --oneline --decorate --all记下的旧 Commit ID 与 rebase 后的新 ID 应不同,因为 Git 在新父节点上创建了内容相近的新 Commit。随后 fast-forward 合并:
git switch main
git merge --ff-only feature/rebase-demo
git log --first-parent --oneline --decorate -5预期 --ff-only 成功,主线没有新增 merge commit。
观察 cherry-pick 复制的是提交而不是身份
在实验仓库中创建一条维护线和一个独立修复,记录来源 Commit 后再重放:
git switch -c maintenance HEAD~1
git switch -c fix/validation main
printf "validation\n" > validation.txt
git add validation.txt
git commit -m "fix: add validation"
source_commit=$(git rev-parse HEAD)
git switch maintenance
git cherry-pick -x "$source_commit"
git show --no-patch --format='commit: %H%nparent: %P%nsubject: %s%n%b' HEAD预期新 Commit 的树中包含修复,提交说明记录来源 SHA,但当前 Commit ID 与 $source_commit 不同,父节点也属于维护线。反向实验是在同一维护线再次 cherry-pick 同一个来源:Git 可能报告补丁为空或发生上下文冲突。不要用 --allow-empty 掩盖重复搬运;先检查目标线是否已有等价补丁,再选择 --skip 或修正历史。完成后退出目录,确认路径确为专用实验目录,再删除整个 history-lab;不要把实验分支推到真实团队仓库。
新功能分支的日常路径
从已更新的主线开始:
git fetch <remote> --prune
git switch main
git merge --ff-only <remote>/main
git switch -c feature/example开发期间提交可审查的小步变更。准备 PR/MR 前,用提交图确认基线:
git log --graph --oneline --decorate --boundary <remote>/main...HEAD
git diff --stat <remote>/main...HEAD若分支只有自己使用,可以 rebase 到最新远端主线:
git fetch <remote>
git rebase <remote>/main若已有同事基于该分支工作,优先 merge 最新主线,或先协调改写窗口。官方 Rebase 文档明确提醒:改写他人已经基于其工作的上游,会迫使下游人工修复历史。
独立修复的 cherry-pick 路径
当一个已审核修复需要进入另一条维护线,而不希望合并整个来源分支时:
git switch <target-branch>
git cherry-pick -x <source-commit>-x 会在无冲突的 Cherry-pick 消息中记录来源 Commit,适合可公开追溯的分支间搬运。它仍创建新 Commit,不会复用原 Commit ID 或原签名。先确认目标分支没有等价变更,否则会形成重复修复。
Cherry-pick merge commit 必须用 -m <parent-number> 指定主线父节点;这不是常规捷径。选错父节点会得到与预期相反的差异,应先用 git show --cc 和 git rev-list --parents -n 1 <merge-commit> 理解父关系。
合并前验证
不论采用哪种历史策略,合并前都至少验证:
git status --short --branch
git log --graph --oneline --decorate <remote>/main..HEAD
git diff --check <remote>/main...HEAD
<project-test-command>git diff --check 只检查空白错误,不等于业务测试。项目测试、静态检查和评审门禁仍由项目脚本与托管平台执行。
分支创建、查看和跟踪:
git switch -c feature/example
git branch -vv
git branch --show-current
git branch --contains <commit>
git branch --merged main
git branch --no-merged mainMerge 的安全入口:
git merge --ff-only <branch>
git merge --no-ff <branch>
git merge --abortRebase 的常用入口:
git rebase <upstream>
git rebase -i <upstream>
git rebase --onto <new-base> <old-base> <branch>
git rebase --continue
git rebase --skip
git rebase --abort普通 rebase 默认会把待重放提交变成线性序列,并省略其中的 merge commit。确需保留分支拓扑时才评估:
git rebase --rebase-merges <upstream>它会尝试重建 merge commit,但原 merge 中的手工冲突解决可能需要重新处理,不能把它当作无损复制拓扑。
Cherry-pick 的状态控制:
git cherry-pick <commit>
git cherry-pick <oldest>^..<newest>
git cherry-pick --continue
git cherry-pick --skip
git cherry-pick --abort清理本地分支前先确认可达性:
git branch --merged main
git branch -d feature/example-d 只在分支已合并到 upstream 或当前 HEAD 时删除;-D 会跳过该保护。删除分支只删除引用,不等于立刻删除对象,但不能把 reflog 与垃圾回收前的暂时可恢复性当备份策略。
fatal: Not possible to fast-forward, aborting
git pull --ff-only 或 git merge --ff-only 失败。运行 git log --graph --oneline --decorate --left-right HEAD...<upstream>,确认双方是否各有独立提交。本地与上游已经分叉,fast-forward 无法同时保留两边尖端。根据分支共享程度选择 merge 或 rebase;不要用强推掩盖主线分叉。整合后 git merge-base --is-ancestor <upstream> HEAD 应成功,项目测试再次通过。
You have divergent branches and need to specify how to reconcile them
git pull 要求选择策略。检查 git config --show-origin --get-regexp '^pull\.(rebase|ff)$'。分支发生分叉,而仓库没有明确 pull 行为。先 fetch 和查看提交图,再为当前仓库设置 pull.ff only、pull.rebase true 或 pull.rebase false 中的一种。用测试分支模拟分叉,确认命令按团队策略成功或可预期地停止。
Rebase 停在冲突,工作区看起来“坏了”
git status 显示正在 rebase,部分文件含冲突标记。先读 git status,再用 git diff --name-only --diff-filter=U 列出未合并文件。待重放 Commit 与新基线修改了相同区域,Git 无法自动决定结果。理解当前正在重放的 Commit,解决并 git add 后执行 git rebase --continue;确认该 Commit 已无意义才 --skip;方向错误则 --abort 回到开始前。rebase 完成后检查提交范围、运行测试,并用 git range-diff <old-range> <new-range> 比较改写前后语义。
Cherry-pick 提示空提交
解决冲突后 Git 报当前 Cherry-pick 为空。用 git diff 和 git log --cherry-mark --left-right <target>...<source> 判断目标是否已有等价补丁。修复已经通过其他 Commit 进入目标分支,或冲突解决选择了目标分支现状。确认确实无需变化后执行 git cherry-pick --skip;若需要保留审计占位,按团队政策显式创建空 Commit,而不是盲目继续。运行针对该修复的测试,并确认没有重复逻辑。
--force-with-lease 被拒绝
改写主题分支后 push 报 stale info 或租约不匹配。git fetch <remote> 后比较 git log --left-right --graph HEAD...<remote>/<branch>,查看是否有他人新提交。远端已不在你确认的位置,租约正确地阻止覆盖。整合或保留他人提交,重新评审后再推;不要改用 --force。用精确 expected Commit 执行租约推送,并在平台确认远端历史包含所有预期变更。
Rebase 后 PR/MR 突然出现大量重复提交
差异范围远超本次改动。运行 git merge-base HEAD <remote>/main、git log --cherry-mark --left-right <remote>/main...HEAD,检查是否选错 upstream 或 old base。从错误基线 rebase、把已合并分支再次重放,或对含 merge 的历史使用了不合适的普通 rebase。立即停止继续推送;用 reflog 找到改写前尖端,建立备份分支,再以正确基线重新执行。git diff <remote>/main...HEAD 只包含预期文件,range-diff 没有意外新增提交。
本地 Branch、Merge、Rebase 和 Cherry-pick 不需要网络凭证;fetch、pull、push 才经过 HTTPS/SSH、代理和托管平台权限。排障时先区分“本地提交图操作失败”和“远端传输或授权失败”。
对远端历史的权限要收窄:
主分支和共享长期分支由服务端保护,普通开发者不能强推或删除。允许改写的个人主题分支也应使用 --force-with-lease,并在 PR/MR 讨论中通知正在审查的人。机器人只获得完成职责所需的分支权限,不能因为要更新一个主题分支就拥有所有受保护分支的 force push。
代理凭证、PAT、SSH 私钥和远端 URL 不写入 Git alias、示例命令或仓库配置模板。--no-verify 只能在有审计的故障处置中使用;跳过本地 Hook 不代表服务端门禁会或应该放行。
远端拒绝 non-fast-forward 可能来自 Git 自身,也可能来自平台保护规则。查看完整错误和平台审计,不要先提升令牌权限。
团队策略首先要定义分支的“共享等级”,再定义命令:
个人短期主题分支:允许作者在评审前 rebase 和整理,强推使用租约。多人协作主题分支:默认不改写;确需改写时约定冻结窗口、备份引用和下游恢复方式。主分支、维护分支、发布相关长期分支:禁止客户端强推,由 PR/MR 与平台规则整合;具体发布 SOP 归发布主线维护。
其次定义主线历史目标。若采用 merge commit,应要求分支内提交可读并保留合并语义;若采用 rebase/fast-forward,应禁止改写共享上游并要求改写后重新验证;若采用 squash,应把 PR/MR 作为细粒度审计载体,并让 squash Commit 信息包含问题单和风险摘要。
策略必须能被服务端执行。仅在团队 Wiki 写“不要 force push”没有约束力;保护分支、必需评审、状态检查和绕过审计应在托管平台落实。客户端操作前仍要让开发者看懂这些门禁正在保护哪条引用、哪些 Commit 和哪组审批证据。
培训不要从背命令开始。让新人画出 HEAD、本地分支、远端跟踪分支和 merge base,再让他在测试仓库制造分叉、停止操作、恢复和比较历史。能解释对象变化的人,才更可能在真实仓库做对选择。
线性历史并不天然更真实
Rebase 能让主线更易读,但它用新对象表达整理后的叙事,隐藏了原始并行拓扑。审计高度敏感、依赖 Commit 签名或需要保留集成节点时,merge commit 可能更合适。团队应明确“主线用于解释最终演进”还是“主线还要保留协作过程”。
租约安全依赖你观察到的远端状态
--force-with-lease 的价值是拒绝覆盖未见过的远端变化。若 IDE 在后台 fetch,远端跟踪引用可能悄悄前移;不带精确期望值的租约就可能把“已见过”误判成“可覆盖”。高风险脚本应保存明确的远端 Commit,并把 refname 与 expected Commit 都写进命令。
Rebase 会重建签名与审查上下文
Rebase、Cherry-pick 和 Squash 都创建新 Commit。原 Commit 的 GPG/SSH/S/MIME 签名不会迁移,基于旧 SHA 的审批、构建结果和外部链接也可能失效。平台是否自动撤销旧审批取决于规则配置,必须在治理层验证,不能仅靠 Git 客户端假设。
Cherry-pick 容易制造长期分叉
它适合把一个明确修复带入维护线,但若后续主线与维护线反复双向 Cherry-pick,等价补丁、不同 Commit ID 和冲突会快速累积。每次搬运应记录来源和目标,指定哪条线是事实源,并约定后续是 Merge 回主线还是保持独立维护。
大型分支的风险不是命令耗时
持续数周、跨越多个边界的大分支会放大 Review 失焦、冲突集中、功能开关缺失和回滚困难。Git 策略无法替代拆分交付。架构上应优先让变更可渐进合并,用兼容迁移、功能开关和小 PR/MR 降低分支寿命,而不是寻找更激进的 Rebase 技巧。
恢复能力必须在改写前建立
Reflog 常能找回本地旧尖端,但它有保留期限,也不覆盖所有机器和服务端场景。高风险改写前创建明确备份引用、记录旧 SHA、确认远端保护与恢复 owner;完成并验证后再按期限删除备份。把“以后 reflog 能救”当流程,会在清理或换机后失效。
能解释分支是引用、Commit 是带父指针的对象,不把分支理解为复制目录。操作前工作区干净,并已 fetch --prune 更新远端跟踪引用。本地分支 upstream 正确,git branch -vv 的 ahead/behind 对象符合预期。
仓库已明确 pull.ff 或 pull.rebase 策略,分叉时不会临时猜测。已根据共享等级选择 merge、rebase 或 squash,而不是只追求线性外观。Rebase 前确认没有下游协作者,或已约定冻结、备份与恢复方式。
Cherry-pick 前确认目标线没有等价补丁,跨维护线搬运使用可追溯来源。强推只用于允许改写的主题分支,并使用明确 ref 与 expected Commit 的租约。已考虑 IDE 后台 fetch 对不带精确期望值 --force-with-lease 的影响。
冲突时会用 status 判断当前操作,并知道 continue、skip、abort 的区别。历史改写后使用 range-diff、提交图、项目测试和 PR/MR 差异重新验证。已评估改写对 Commit 签名、审批、构建结果和外部 SHA 引用的影响。
主分支和长期共享分支的禁止强推由服务端规则执行,绕过有审计。实验只在抽象测试仓库进行,正文不含真实远端、凭证、内网地址或本机路径。
