rebase后旧提交去哪了?用真实Git检查parent链、冲突继续与中止

发布时间:2026/10/16 6:27:15
rebase后旧提交去哪了?用真实Git检查parent链、冲突继续与中止
rebase 之后提交图整齐了提交 ID 却变了旧的 ID 有时还能读出来冲突时分支又处在一个看起来很奇怪的位置。只说一句“把分支变基”并没有回答这些问题。如果已经理解 cherry-pick 是把一个提交引入的变化应用到当前历史上就可以沿着这个方向想连续多个提交要按什么顺序重放中间停下来以后怎样知道已经做到了哪一步下面只使用标准 Git 和 PowerShell。实验在独立临时仓库运行使用 Git for Windows 2.45.2.windows.1不依赖我的 mini-git也不把教学实现的限制套到真实 Git 上。1. 同一份变化接到不同parent后还是原提交吗先准备 A、B、C、DA 有 base.txtmaster 的 B 增加 b.txtfeature 的 C、D 分别增加 c.txt、d.txt。暂时让各次修改互不冲突。C---D (feature) / A---B (master)下面各块在同一个 PowerShell 窗口按顺序执行。第一个块会创建临时目录不要在有价值的现有仓库里省略这一步。$ErrorActionPreference Stop $PSNativeCommandUseErrorActionPreference $false function G { git args; if ($LASTEXITCODE -ne 0) { throw git failed: $args } } $lab Join-Path $env:TEMP (git-rebase- [guid]::NewGuid().ToString(N)) New-Item -ItemType Directory -Path $lab | Out-Null Set-Location $lab G init -b master G config user.name Blog experiment G config user.email blogexample.invalid G config core.autocrlf false Set-Content base.txt base G add base.txt G commit -m A G switch -c feature Set-Content c.txt C G add c.txt G commit -m C $oldC G rev-parse HEAD Set-Content d.txt D G add d.txt G commit -m D $oldD G rev-parse HEAD G switch master Set-Content b.txt B G add b.txt G commit -m B $B G rev-parse HEAD G switch feature现在只在本机重放G rebase master $newD G rev-parse HEAD $newC G rev-parse HEAD^ master_still_B ((G rev-parse master) -eq $B) new_C_parent_is_B ((G rev-parse ${newC}^) -eq $B) new_D_parent_is_new_C ((G rev-parse ${newD}^) -eq $newC) C_id_changed ($newC -ne $oldC) D_id_changed ($newD -ne $oldD) G cat-file -t $oldD G log --oneline --graph --all本轮五项关系检查都是 True旧 D 的类型仍是 commit。实际结构变成下面这样对象或入口重放前重放后master引用B仍为Bfeature引用D改为Dfeature的新parent链D → C → AD → C → B → AC、D原ID对应原commit对象仍可作为旧对象ID查询当前分支日志显示原链显示新链commit 的内容不只有文件它还包含 tree、parent、作者和提交说明等信息。新 base 改变了 C 的 parentD 又接到 C 后面即便对应修改很相似这次也得到了新的 commit ID。不能反过来说“每次 rebase 都重建所有提交”。已经位于目标 base 之上的分支、空提交、已应用补丁、不同重放模式都有各自规则。本篇只验证这个特意制造的分叉。Git rebase手册2. 日志不显示旧提交不等于对象马上被删除图中保留旧D到C再到A的这一行是为了区分“对象是否存在”和“当前分支能否到达它”。普通git log --all从当前引用出发未必再显示失去这些入口的旧链。我们提前保存了$oldD所以可以直接用 cat-file 读取还能检查 feature 的 reflogG reflog show feature --oneline G cat-file -p $oldD这不意味着旧对象永久保留。reflog、其他引用以及后续过期和垃圾回收策略都会影响保留情况。这里没有运行回收只能证明“刚完成这次rebase后仍能读取”不能承诺任何时间都能找回。3. 为什么必须先C再D本例 C、D 增加不同文件可以只看 parent 链检查顺序。若 D 修改的是 C 才新增的文件D 的变化还依赖 C 提供的前置状态随意倒过来执行就不再是同一套操作。当前base是B 重放C引入的变化 - CparentB 重放D引入的变化 - DparentC 最后让feature指向D因此 rebase 不是把 D 的完整快照盖到 B 上也不是只挪一下图里的线。新链、文件结果和分支入口需要分别检查。接下来增加同一行修改看看顺利路径之外发生什么。4. 制造冲突先验证abort在刚才成功重放的仓库里分别让 feature 和 master 修改 base.txt 的同一行。这里预期 rebase 返回非零退出码不把它当作实验失败。Set-Content base.txt feature side G add base.txt G commit -m feature conflict $beforeConflict G rev-parse HEAD G switch master Set-Content base.txt master side G add base.txt G commit -m master conflict G switch feature git rebase master $conflictExit $LASTEXITCODE if ($conflictExit -eq 0) { throw expected a conflict } if (-not (git ls-files -u -- base.txt)) { throw rebase failed without the expected unmerged file } Get-Content base.txt G status --short G rebase --abort abort_restores_HEAD ((G rev-parse HEAD) -eq $beforeConflict) abort_restores_file ((Get-Content base.txt -Raw).Trim() -eq feature side)本轮能看到冲突标记abort 后两项检查都为 True。它放弃这次重放并恢复操作前状态不是“把冲突标记删掉以后勉强继续”。辅助函数G会检查Git退出码使本应成功的步骤在失败时立即停止。只有这里故意制造冲突时使用原始git先保存退出码再检查base.txt的未合并index记录非零退出也可能来自其他错误不能仅凭“失败”就认定冲突场景成立。设置$PSNativeCommandUseErrorActionPreference$false是为了让支持该变量的PowerShell版本也能显式处理这些预期失败。5. continue不是从头再跑一次因为上一块已经 abort现在需要重新进入同一场冲突。不能在 abort 以后直接 continue。git rebase master if ($LASTEXITCODE -eq 0) { throw expected the conflict again } if (-not (git ls-files -u -- base.txt)) { throw expected unmerged base.txt again } Set-Content base.txt resolved G diff G add base.txt G -c core.editortrue rebase --continue resolved_content_retained ((Get-Content base.txt -Raw).Trim() -eq resolved) worktree_clean (-not [bool](G status --porcelain)) G log --oneline --graph --allgit diff用来检查我们打算保留的内容git add把解决结果写入 indexcontinue 再据此完成当前重放并处理剩余队列。core.editortrue仅在这次命令中跳过编辑器等待不是修改全局编辑器配置。动作实际目的不代表什么修改冲突文件决定最终内容Git替你判断业务逻辑正确git add将解决结果放入index测试已通过rebase --continue继续当前重放流程重新从第一步随意重放rebase --abort放弃并恢复操作前状态保留本轮手工解决内容把所有冲突标记删除不等于解决方案就是对的。Git 能处理提交关系不能替业务代码选择“应该保留哪一边”。真实项目里仍要检查 diff 和运行测试。6. 把可观察状态变成断言下图是本轮实际 PowerShell 执行记录的整理图保留重放后对象关系、中止恢复与继续完成的标准输出排版后截图不是桌面终端窗口截图。在同一个窗口继续执行验证if ((G rev-parse ${newC}^) -ne $B) { throw new base mismatch } if ((G rev-parse ${newD}^) -ne $newC) { throw replay order mismatch } if ($newC -eq $oldC -or $newD -eq $oldD) { throw expected rewritten commits } if ((G cat-file -t $oldD) -ne commit) { throw old object unexpectedly missing } if ((Get-Content base.txt -Raw).Trim() -ne resolved) { throw resolution missing } if (G status --porcelain) { throw worktree not clean } if (Test-Path .git/rebase-merge) { throw merge-backend rebase still active } if (Test-Path .git/rebase-apply) { throw apply-backend rebase still active } REBASE_CHECKS_PASSED验证程序分三个阶段先检查无冲突路径的新 parent 链再确认旧对象仍可读最后检查冲突的停止、中止和继续。只跑最后一项“工作区干净”无法证明前面每一步做对。本轮没有接触远程仓库没有强制推送也没有测试故障断电、并发操作或所有 rebase 选项。所有命令和断言都在正文里读者只用标准 Git 就能复现不需要另外索取私有程序。rebase 真正改变的不是一句“基底”而是变化重新应用的位置以及当前分支最终指向的提交链。别把这个实验照搬到别人已经基于它继续工作的共享历史多人协作时改写历史的代价是需要协调其他人的入口。相关cherry-pick和revert的parent、tree实验、merge为什么有时只是移动指针以及stash保存的是哪几份状态。