Git 2.56 版本亮点速览
开源 Git 项目刚刚发布了 Git 2.56.0,包含来自 104 位贡献者的功能和 bug 修复,其中 39 位是新人。我们上次是在 2.55 发布时 向大家汇报 Git 最新动态。
为了庆祝这次新版本的发布,下面来看看 GitHub 总结的一些自上次以来最有趣的功能和变更。
解决冲突而不暂存其他更改
解决合并冲突分为两个不同步骤。首先,你需要编辑工作树,直到每个冲突路径都包含你期望的结果。然后,将这些路径加入暂存区,以告知 Git 冲突已解决。
假设在 recipe.txt 发生了合并冲突,而 notes.txt 中包含一个与冲突无关的本地编辑。
第二步看似简单,但现有命令很容易让人暂存超出预期的内容。git add -u 会更新所有已跟踪的修改路径。在合并期间,这可能包括与冲突无关的本地更改。即使遗漏了冲突标记,它也可能暂存一个仍包含冲突标记的文件。
Git 2.56 提供了一种更安全的操作流程:
$ git add --resolved
fatal: the following paths still have conflict markers:
recipe.txt
$ # Edit recipe.txt and remove the conflict markers.
$ git add --resolved
$ git status --short
M notes.txt
M recipe.txt
新的 git add --resolved 模式专为这一步骤设计。它只考虑索引中当前处于未合并状态的路径。在暂存任何内容之前,它会扫描未合并的常规文件,检查是否有残留的冲突标记。如果发现冲突标记,它会报告受影响的文件并保留索引不变。
git status --short 中的两列分别代表已暂存和未暂存的更改。recipe.txt 被暂存为已解决状态,而 notes.txt 的无关更改仍保持未暂存状态。
你还可以通过传入路径规范来限制考虑哪些未合并路径。在该选择范围内,“全有或全无”的标记检查仍然生效:如果任何选定的常规文件包含冲突标记,Git 将不会暂存其中的任何一个。已解决的删除和二进制冲突不包含文本标记,因此 Git 可以正常暂存它们。
该选项的范围有意比 `git add -u` 或 `git add -A` 更窄。`--resolved` 不能与上述任一模式组合使用,且会忽略从未发生过冲突的已跟踪文件。这使其成为维护者工作流中一道有用的安全防线,尤其适用于合并操作开始时已存在无关本地变更的场景。
当不存在更多合并基点时,停止搜索历史
许多 Git 操作需要查找两个提交的最佳公共祖先。合并操作将其作为合并变更的起点;三点 diff 则利用最佳公共祖先来确定主题分支工作的起始位置。托管代码仓库的平台也会执行相同的计算,用于 Pull Request diff、可合并性检查、审查范围及其他比较场景。
Git 通过从两端提交头部向后遍历来寻找这些公共祖先。可以想象,将一侧可达的提交涂成蓝色,另一侧涂成红色,同时被两种颜色标记到的提交即为合并基点候选者。
找到一个候选者并不一定足够。交叉合并(criss-cross merges)可能产生多个合并基点,且彼此互不为祖先,因此 Git 必须持续遍历,直到确认已找到所有基点。然而,旧的终止规则在另一个合并基点已不可能存在后,仍可能继续处理一大段陈旧的、已是共同历史的数据尾部。
Git 2.56 会跟踪由每一侧独占涂色的待处理提交数量。该实现在此规则周围增加了额外防护机制,但从高层逻辑看,一旦某侧独占队列耗尽,就不会再出现新的交汇点。Git 此时可以停止遍历,同时确保返回所有合并基点。

当较旧的分支合并进一段远为漫长的历史时,这种差异会非常明显。在一个真实的 monorepo 案例中,遍历耗时从 0.68 秒降到 0.01 秒。在两个大型 monorepo 上的生产环境评估显示,一个仓库中许多场景快了约 70 倍,另一个平均提升约 20 倍。
最后一批补丁还解决了一个存在已久的 Linux 内核仓库性能问题。在使用 Git 默认 v2 commit-graph 的情况下,命令 git merge-base --all v4.8 v4.9 的遍历步数从 167,441 步降到 3,887 步,耗时从 0.29 秒降到 0.01 秒。
[来源]
让服务器也能用更小的 path-walk repack
Git 在 repack 仓库时,会寻找相似的对象,把它们以 delta 方式高效存储。传统做法用 name hash 对候选对象分组,而 path-walk repack 则按照对象在目录树中的位置来遍历,把同一路径的各个版本聚在一起,往往能找到好得多的 delta 关系。
存储空间的节省相当可观。在一个使用 Fluent UI 仓库最新克隆并强制 Git 重算 delta 的基准测试中,普通的 bitmapped repack 生成了 558.5 MB 的 pack,而使用 --path-walk repack 只生成 164.4 MB,小了约 71%。
但对代码托管平台来说,仅有更小的 pack 还不够。它们通常依靠 reachability bitmap 快速响应对象枚举查询,有些还使用 delta island,防止一组 ref 中的对象依赖于只存在于另一组的对象。此前 path-walk repack 与这两者都不兼容,导致其存储优势无法在更多场景落地。
Git 2.56 移除了这两个限制。path-walk repack 现在可以为新的 bitmap 挑选 commit;后续调用 git pack-objects 时,如果现有 bitmap 能满足请求就直接复用,只在必要时才回退到 path walk。
path-walk 现在也执行 delta island 所需的状态记录:在选择 delta 基对象前,先通过 commit 和 tree 传播 island 归属关系。这让托管平台在试验 path-walk 更小的磁盘表示格式时,仍能维持其隔离规则。
这些变更并未默认启用 path-walk 重新打包。相反,它们移除了两个重要的采用障碍,使大型仓库托管方能在不放弃基于 bitmap 的高速服务或 delta-island 约束的前提下,评估其存储节省效果。
冰山一角……
这些只是 Git 2.56 中的部分变更。以下是其他值得关注的功能和更新。
- 实验性命令
git history持续扩展。Git 2.54 引入该命令,包含reword和split;Git 2.55 增加了fixup。Git 2.56 新增:$ git history drop <commit>
该命令删除指定 commit,并将其后续提交重放至该 commit 的父节点上。若HEAD发生移动,Git 会在保留无关本地修改的前提下更新 index 和工作树。若重放后续提交时产生冲突或覆盖本地修改,则中止操作。该命令仍处于实验阶段,无法处理包含 merge commit 的历史记录,也不能删除根 commit 或 merge commit。
[source]
- 引用写入命令过去分散在
git update-ref、git symbolic-ref及其他底层命令中。Git 2.56 继续将底层引用管理整合到git refs工具集中:$ git refs create refs/heads/topic <new-value>$ git refs update refs/heads/topic <new-value> [<old-value>]$ git refs delete refs/heads/topic [<old-value>]$ git refs rename refs/heads/old refs/heads/new
可选的旧值为更新和删除操作提供比较并交换(compare-and-swap)保护。这些命令是故意设计在较低层面的。特别是,`git refs rename` 会移动引用及其 reflog,但不会执行 `git branch -m` 所做的分支配置调整。
[source]
- 清理已合并到上游的本地主题分支时,通常需要将每个分支与其配置的上游跟踪分支进行比较。Git 2.56 新增了一种批量处理方式:
$ git branch --delete-merged 'origin/*' 'topic-*' --dry-run
在此命令中,`origin/*` 匹配上游属于 `origin` 下的分支,`topic-*` 将候选范围限定为以 `topic-` 开头的本地分支名称,`--dry-run` 则列出将被删除的内容而不实际执行删除。
若未使用 `--dry-run`,Git 仅删除分支尖端可从其对应上游到达的分支。当前在 worktree 中检出的分支、缺少上游的分支以及存在多种模糊推送配置的分支将被跳过。通过设置 `branch..deleteMerged = false`,可以保护特定分支免受批量清理。
[source]
- `git bisect run` 在查找引入回归问题的提交时非常高效,但完成后通常会停留在问题提交上,直到你运行 `git bisect reset`。新的 `--reset-when-found[=
]` 选项将这两个步骤合并。其默认值为 `original`,即返回到二分查找开始前检出的提交;`found` 会清理二分查找状态,但保留问题提交处于检出状态。该选项不支持与 `--no-checkout` 同时使用。
[source]
- 实验性的
git replay命令现在支持用--linearize将合并拓扑扁平化:Git 会把提交重放到一条线性链上并去掉 merge commit,效果与git rebase --no-rebase-merges生成的拓扑一致,但不依赖工作区。该选项不能与--contained或一次重放多个分支的操作同时使用。
[source]
- Git 现在能识别两种常见的命令行笔误并提示正确写法:输入
git push origin/main会建议改用git push origin main,输入git branch --set-upstream-to origin main则会提示git branch --set-upstream-to=origin/main。Git 会在给出建议前先验证修改是否合理,避免盲目猜测。
[source]
- 部分克隆(partial clone)在初始阶段会省略部分对象,但按需下载的 blob 一直以来都会永久保存在本地。Git 2.56 现在可以丢弃较大的、可重新获取的 blob,重新依赖 promisor remote:
$ git repack -a --filter=blob:limit=1m --drop-filtered --dry-run$ git repack -a --filter=blob:limit=1m --drop-filtered
dry run 会列出候选对象;正式的 repack 则在本地删除它们,之后再访问时会重新拉取。这是一种手动清理机制,并非自动控制大小的缓存,目前也只支持blob:limit过滤器。该功能要求配置 promisor remote,且会拒绝不安全的场景,比如丢弃当前 index 引用的对象。这项工作由 Siddharth Shrimali 作为 GSoC 项目贡献,导师为 Christian Couder 和 Siddharth Asthana。
[source, discussion]
git log --follow现在能更可靠地追踪非线性历史中的路径。以往,该命令只维护一个全局的“当前路径”。当不同的父提交对同一路径进行了不同方式的重命名时,恰好先被访问的那一侧会决定 Git 在后续遍历中跟随哪个路径。Git 2.56 为每个父提交单独记录路径,使结果不再依赖遍历顺序,并修复了子树合并等场景下的问题。
[来源]
- 在具有多个根节点的历史图中,无关提交可能被放在根提交正下方同一列,造成二者看似相连的错觉。Git 2.56 在必要时会缩进这些“视觉根节点”:
* 某段可见历史的根节点* 无关提交
此功能默认在 git log --graph 中启用。如需恢复旧的渲染方式,请使用 --no-graph-indent;或通过设置 log.graphIndent 来指定默认行为。
[来源]
- 多项内部改动消除了原本常规操作中存在的性能断崖。Reftable 写入在获取锁后不再冗余重新加载,使文件系统 stat 调用从线性复杂度降至常数级。加载已知为新文件的 packfile 时,不再在每次插入前扫描现有列表,从而消除了一个 O(N²) 的性能回退——此前该问题导致一个与 prompt 相关的命令在拥有 37,815 个 pack 的仓库中耗时 4.5 秒。
其他改动还移除了大量 tombstone 场景下 reftable 的二次方扫描,以及路径受限的工作区 diff 中的类似瓶颈,同时使未跟踪文件收集逻辑稳定保持 O(n log n) 复杂度,不再依赖输入已排序的假设。Reftable 的 tombstone 性能测试耗时从约 13 秒降至 0.2 秒。在约含 50 万条索引条目的 Chromium 检出中,受影响的git diff操作耗时从约 8 分钟缩短至 0.07 秒。
冰山之下