Re: [RFC PATCH v7 0/10] diff: add provider interface and initial providers

Phillip Wood <[email protected]> Tue, 4 Aug 2026 14:52:36 +0100
Newsgroups org.kernel.vger.git
Message-ID <[email protected]>
Hi Michael

On 01/08/2026 18:41, Michael Montalbo wrote:
> Every in-process diff in Git reduces, at one point, to a single
> question: given two blobs and the settings the diff runs under, which
> line ranges changed?  The answer is the diff's hunks: for each change,
> the position and length of the range on the old side and on the new.
> Each consumer asks in its own shape:
> 
> - blame diffs each suspect's blob against its parent's, taking only the
>    coordinates through xdiff's hunk callback;
> - the stat formats keep only the added and deleted counts;
> - patch output emits from the hunks, with xdiff interleaving context and
>    content around them;
> - log -L maps the tracked range across each commit from the coordinates.
> 
> In every case the answer is computed the same way: load both blobs and
> run xdiff.  That is the only source, so nothing that already holds the
> answer, or that would answer differently on purpose, can supply it
> instead.  Sometimes that is what we want, which is why patch-id and
> format-patch stay on the builtin computation throughout: patch-id needs
> identical hashes on every machine, and a format-patch must apply for
> recipients who share none of the sender's configuration.  Other times
> another source would be useful.

This explains the mechanics of the existing implementation, but doesn't 
really explain what the advantage of the change is beyond having a 
unified interface. What are things that are unlocked by this change? Are 
we doing it purely to cache the hunk headers or does it (as I suspect) 
unlock other features?

> This RFC sketches a direction.  The unified series shows one interface
> carrying two example providers and their interaction; it is not shaped
> to merge as one topic.  If the direction holds, the work returns as
> separate reviewable series (see Roadmap).  The two examples are
> demonstrations, each an RFC on its own: diff.<driver>.process, the RFC
> cooking as mm/diff-process-hunks, lets a configured external process
> answer with its own notion of which lines changed, and the diff-hunks
> store, new in this thread, remembers what xdiff computed and serves it
> back.  One is authoritative and external, one a cache and in-process.

What does it mean to be authoritative? Why isn't the xdiff code 
authoritative?

> Three pieces:
> 
> - A hunk provider interface (diff-provider.h) is the point of the
>    series.  A provider is an alternate source for the answer: asked with
>    the pair's object ids and the diff settings, before any blob is
>    loaded, it may supply the hunks in place of the builtin computation.
>    A miss falls through to that computation, and every answer passes one
>    shared validity check first.  The providers form a chain the
>    repository owns, built on first consultation and released in
>    repo_clear(), so provider state such as a running process never
>    outlives its repository.  

> Chain order is the authority,
I'm not sure what that phrase means

> and the
>    terminal provider is the builtin computation itself, so the interface
>    never exists without an implementor: patch 02 ships it answering every
>    request the way the consumers did before.  A consumer states its
>    request in one struct and reads one set of outcomes (answered,
>    unanswered, or failed); it never names a provider, and a provider>    added later maps onto those outcomes inside the interface, so consumer
>    code is written once.  Because every diff now walks the chain even
>    with no store or process configured, the default path was measured
>    against the pre-series base and runs within noise (a 5000-commit
>    log --stat and a long-history blame, ratio 1.00 either way).

So we can configure a provider for a particular file type via 
.gitattributes and diff.<driver>.process and then it is used 
automatically, if there is no provider configured we use xdiff?

> - The diff-hunks store shows the non-authoritative side: an in-process
>    cache at $GIT_DIR/objects/info/diff-hunks that may only reproduce the
>    builtin diff, so serving from it never changes a command's output.  It
>    is read by default and written only when a repository owner opts in,
>    warming it as a side effect of diff work the command already does:
> 
>        GIT_DIFF_HUNKS_WRITE=1 git log --all --stat >/dev/null
> 
>    A warmed store then serves the stat formats and blame from stored
>    coordinates instead of a fresh diff: on git.git a 5000-commit log
>    --stat runs about 1.9x faster, and blame reads the same entries
>    opportunistically (full numbers in [1]).  Its format and keying, what
>    it may not serve, and how it handles corruption and staleness are in
>    git-diff-hunks(1), gitformat-diff-hunks(5), and [2].  The interface
>    point is small: a cache drops in as the provider that stands aside
>    wherever an authoritative one answers.

I had a very quick look at the documentation in patch that implements 
this. Am I right in thinking it stores xdl_opts directly so that we 
cannot change the in-memory representation without breaking the cache? 
Are there plans to garbage collect cache entries when the corresponding 
blobs are removed?

> - diff.<driver>.process shows the authoritative side: an external
>    process, configured per driver, whose answers may deliberately differ
>    from the builtin diff and outrank the store.  Git asks it for a pair by
>    object names alone, so it answers before any blob is read, which suits
>    a cache or a process that fetches the blobs itself.  Consulting is
>    opt-in per command, following the allow_textconv precedent, and a pair
>    the process cannot answer falls back to the builtin diff.  The
>    protocol, the per-command gate, how failures are handled, and the
>    versioning that lets it grow are in gitattributes(5) and footnotes [3]
>    and [4].  The interface point, again, is small: an external,
>    authoritative provider joins the same chain ahead of the cache, and
>    neither consumer learns it is there.  A later content-carrying request
>    would extend it to the pairs and consumers this identity-only form
>    leaves on the builtin diff.
> 
> The series stops at the coordinates.

Does this mean the offsets and lengths in the hunk header? I can't see 
any reference to "coordinates" in the existing code.

> A consumer that needs the changed
> text, such as patch output, would have only its hunk selection replaced,
> with xdiff still emitting content from the blobs; that machinery is the
> content enrichment sketched in the Roadmap.  Establishing the framework
> on coordinates first keeps this series one design: the question, the
> interface, and two providers answering by identity.

This seems like an interesting proposal and the diff headers seem like a 
good place for initial phase to stop. It would be helpful to have a 
clearer explanation of the features this interface would support (i.e. 
what's the motivation for these changes) and a lot less jargon in the 
interface description.

Thanks

Phillip


> 
> Shape of the series:
> 
>    01     documentation: how external diff drivers relate to the
>           features layered on the diff
>    02     the provider interface: the request and outcome types, the
>           emit entry point, the shared validity check, and the
>           repository-owned chain with its terminal builtin provider
>    03     the store: on-disk format, library, and the diff-hunks command
>    04     recording: the stat walk computes, sums, and records
>           trim-stable pairs (writes gated off by default)
>    05     reading: the consult entry point and the store's registration
>           as a provider; the request gains the object ids and diff
>           options
>    06     blame reading through the interface's emit path
>    07-09  process preparation: sub-process lifecycle split, a gentle
>           status read for an optional process, and the
>           diff.<driver>.process config
>    10     the process provider, oid-only, at the head of the chain, with
>           the per-command gate; the request gains the path
> 
> Roadmap:
> 
> This RFC asks whether the direction is right, not for these ten patches
> to merge as one topic.  If it holds, the work returns in reviewable
> pieces:
> 
> - the interface and the store (patches 01 through 06): a cache with
>    measured numbers and no external-process machinery
> 
> - the process provider (patches 07 through 10) on the same interface
> 
> - the content enrichment (the content-carrying request, patch output and
>    log -L consulting, and the xdiff machinery that feeds a provider's
>    hunks into emission) once the identity-keyed framework settles.
> 
> Several design questions are left for those series.
> 
> mm/diff-process-hunks in seen would be dropped in favor of this thread
> and its split.
> 
> The series applies on the line-log topic (mm/line-log-limited-ops)
> rebased onto current master.  The topic rewrites the same
> builtin_diffstat() region this series touches, and current master
> includes 061a68e443 (sub-process: use gentle handshake to avoid die()
> on startup failure), which this topic leans on: a process that dies
> during the handshake degrades to the builtin diff like every other
> failure.  A trial merge against seen shows no interaction with other
> topics beyond the mm/diff-process-hunks replacement above.
> 
> The base (line-log topic on current master) and the full series are
> available at:
> 
>    git fetch https://github.com/mmontalbo/git mm/line-log-stat-formats-followup
>    git fetch https://github.com/mmontalbo/git mm/hunk-providers-oid-first
> 
> Changes since v6:
> 
> This is a restructuring, not an incremental reroll, so a range-diff
> against v6 is unreadable; the map of what changed:
> 
> - The series now leads with the hunk provider interface and brings the
>    diff-hunks store in as its in-process implementation (patches 02
>    through 06, new to this thread).  It keeps only the identity-keyed
>    half of the external diff process protocol from mm/diff-process-hunks.
> - v6's gitattributes documentation, sub-process split, and userdiff
>    config return close to their v6 form as patches 01, 07, and 09.  Patch
>    08 is new: a gentle status read so a protocol error in an optional
>    process degrades to the builtin diff instead of dying.
> - v6's protocol patch returns as patch 10, reduced to the oid-only
>    request, consulting through the interface, and carrying a per-command
>    gate (v6's bypass patch folds into it).
> - v6's blame and stat consults return as identity-keyed consults
>    (patches 05, 06, and 10); their content legs, along with v6's xdiff
>    external-hunks machinery, content-carrying request, and line-log
>    consult, are withheld for the content enrichment.
> 
> Footnotes:
> 
> [1] Store numbers, measured with hyperfine against the same build with
>      core.diffHunks=false.  The warm is a full cold build of the store;
>      the blame speedup is file-dependent (see the coverage limitation):
> 
>      git.git (82,912 commits, --all)
>        warm log --all --stat      20.9 s        store 28 MB, verify 39 ms
>        log --stat -5000           1.91x    (1.38 s -> 0.72 s)
>        blame diff.c               1.26x     (509 ms -> 403 ms)
>        blame hit rate             54% (896 of 1653 pairs)
> 
>      linux (1,445,548 commits, --all)
>        warm log --all --stat      714 s       store 298 MB, verify 415 ms
>        log --stat -5000           1.43x    (2.29 s -> 1.60 s)
>        blame kernel/sched/core.c  1.43x     (1.59 s -> 1.11 s)
>        blame hit rate             74% (2414 of 3263 pairs)
> 
> [2] The store is its own file because nothing existing is addressed by a
>      blob pair: notes attach to single objects, commit-graph chunks to
>      commits.  One entry per pair, keyed by (old blob, new blob,
>      xdl_opts) and recorded only when the pair's trimmed and untrimmed
>      diffs agree, serves blame at zero context and the stat formats at
>      any -U (divergent pairs are 0.4-0.5% of a warm and always compute).
>      The writer fsyncs and commits atomically, and a reader bounds-checks
>      every record and treats an unparsable file as absent; the trailing
>      checksum is checked by git diff-hunks verify, not on every read, the
>      same read-time trust the commit-graph and multi-pack-index take.
>      There is deliberately no fsck integration, expiry, or background
>      maintenance: the store is derivable at any time, so the recovery
>      path is git diff-hunks clear and a re-warm.  New commits make it
>      incomplete, not wrong; a later warm seeds from the file and pays
>      only for what is new.
> 
> [3] Consulting the process is allowed per command, like textconv: git
>      diff, git log and git show, and git blame consult it; the plumbing
>      diff commands do not unless --ext-diff or --diff-process is given,
>      and the interactive-patch machinery, format-patch, and range-diff
>      stay builtin.  Options the process is never told about select no
>      process, and an object id is sent only when it names the exact bytes
>      diffed (a pair under an active object replacement is not sent).  The
>      command comes from local configuration, as with filter.<name>
>      .process: attributes select only a driver name, so cloning cannot
>      cause a process to run.  gitattributes(5) has the full gate.
> 
> [4] The protocol is versioned and capability-negotiated, and extends
>      without breaking deployed processes: a process ignores request keys
>      it does not know, Git ignores trailing tokens on a hunk line so
>      fields can be appended, and new request forms arrive as capabilities
>      a process may decline.  Announcing a capability Git did not request
>      aborts the command, the filter protocol's handshake rule.  The
>      content-carrying request is the natural first extension; markers for
>      formatting-only changes and function or token boundaries are
>      candidates beyond it.
> 
> Michael Montalbo (10):
>    gitattributes: document how external diff drivers relate to diff
>      features
>    diff: introduce a hunk provider interface
>    diff-hunks: add the store format, library, and command
>    diff: record precomputed hunks during stat output
>    diff: read precomputed hunks for stat output
>    blame: read precomputed hunks
>    sub-process: separate process lifecycle from hashmap management
>    sub-process: add a gentle status read
>    userdiff: add diff.<driver>.process config
>    diff: consult oid-only hunk providers via diff.<driver>.process
> 
>   .gitignore                               |    1 +
>   Documentation/Makefile                   |    1 +
>   Documentation/config.adoc                |    2 +
>   Documentation/config/core.adoc           |   10 +-
>   Documentation/config/diff-hunks.adoc     |    8 +
>   Documentation/config/diff.adoc           |    6 +
>   Documentation/diff-algorithm-option.adoc |    3 +
>   Documentation/diff-options.adoc          |   15 +-
>   Documentation/git-diff-hunks.adoc        |  146 +++
>   Documentation/gitattributes.adoc         |  171 ++++
>   Documentation/gitformat-diff-hunks.adoc  |  129 +++
>   Documentation/meson.build                |    2 +
>   Makefile                                 |    5 +
>   blame.c                                  |   81 +-
>   builtin.h                                |    1 +
>   builtin/blame.c                          |    9 +-
>   builtin/diff-hunks.c                     |   53 ++
>   builtin/diff-tree.c                      |    3 +
>   builtin/diff.c                           |   11 +
>   builtin/log.c                            |   19 +
>   chunk-format.c                           |   62 +-
>   chunk-format.h                           |   14 +
>   command-list.txt                         |    2 +
>   diff-hunks.c                             | 1034 ++++++++++++++++++++++
>   diff-hunks.h                             |  141 +++
>   diff-process.c                           |  669 ++++++++++++++
>   diff-provider-internal.h                 |  130 +++
>   diff-provider.c                          |  190 ++++
>   diff-provider.h                          |  159 ++++
>   diff.c                                   |  294 +++++-
>   diff.h                                   |   47 +
>   environment.c                            |    1 +
>   git.c                                    |    1 +
>   meson.build                              |    4 +
>   odb.c                                    |    2 +
>   odb.h                                    |    4 +
>   range-diff.c                             |    6 +
>   repo-settings.c                          |    1 +
>   repo-settings.h                          |    1 +
>   repository.c                             |    3 +
>   repository.h                             |    8 +
>   sub-process.c                            |   52 +-
>   sub-process.h                            |   19 +-
>   t/helper/meson.build                     |    1 +
>   t/helper/test-diff-process-backend.c     |  349 ++++++++
>   t/helper/test-tool.c                     |    1 +
>   t/helper/test-tool.h                     |    1 +
>   t/meson.build                            |    3 +
>   t/perf/p4218-diff-hunks.sh               |   48 +
>   t/t4080-diff-process.sh                  |  593 +++++++++++++
>   t/t4220-diff-hunks.sh                    |  819 +++++++++++++++++
>   t/t4220/README                           |   55 ++
>   t/t4220/trim-divergent-new               |  319 +++++++
>   t/t4220/trim-divergent-old               |  316 +++++++
>   userdiff.c                               |    7 +
>   userdiff.h                               |    2 +
>   write-or-die.h                           |    7 +-
>   xdiff-interface.h                        |   12 +
>   58 files changed, 5989 insertions(+), 64 deletions(-)
>   create mode 100644 Documentation/config/diff-hunks.adoc
>   create mode 100644 Documentation/git-diff-hunks.adoc
>   create mode 100644 Documentation/gitformat-diff-hunks.adoc
>   create mode 100644 builtin/diff-hunks.c
>   create mode 100644 diff-hunks.c
>   create mode 100644 diff-hunks.h
>   create mode 100644 diff-process.c
>   create mode 100644 diff-provider-internal.h
>   create mode 100644 diff-provider.c
>   create mode 100644 diff-provider.h
>   create mode 100644 t/helper/test-diff-process-backend.c
>   create mode 100755 t/perf/p4218-diff-hunks.sh
>   create mode 100755 t/t4080-diff-process.sh
>   create mode 100755 t/t4220-diff-hunks.sh
>   create mode 100644 t/t4220/README
>   create mode 100644 t/t4220/trim-divergent-new
>   create mode 100644 t/t4220/trim-divergent-old
> 
> 
> base-commit: 9a0c4701dcd5725c4184599322b52933ff5005ca
> prerequisite-patch-id: 6270dea79c9f06530737cefa3e1a0a39a1be7877
> prerequisite-patch-id: 46fcc16a7a2ed760a1134d2a92c87699f3ec7bdb
> prerequisite-patch-id: c1e3da243003d060e429bc2196ae02b3453f01f9
> prerequisite-patch-id: 4ad4e273494d4e8503706c21bfdc90a5d7ce116a
> prerequisite-patch-id: f7fa1367756daafa83f4f030a5c7b6dc3dbb70d7
> prerequisite-patch-id: 829e76c9fec655a07f9383086a35bff3290b1c74
> prerequisite-patch-id: 5c5a0d61ae9b6d628d05f1eb5df046758f3111a8