← 文章 / 开源项目
GitHub Blog 3小时前 · 2026-09-29 16:19:44 · 3 阅读

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 此时可以停止遍历,同时确保返回所有合并基点。

动画展示了在某一侧遍历变空之前找到两个 merge base 的过程。Git 2.56 在此时即停止,而早期版本的 Git 会继续遍历大片已经确认共有的区域,即使不可能再找到新的 merge base。

当较旧的分支合并进一段远为漫长的历史时,这种差异会非常明显。在一个真实的 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 约束的前提下,评估其存储节省效果。

[source, source]

冰山一角……

这些只是 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 秒。

    冰山之下

    这仅仅是最新发布的部分示例。若想查看完整内容,请参阅 2.56 发布说明,或访问 Git 仓库 中更早版本的说明。

原始来源: GitHub Blog

评论 (0)