Files
git/diff-provider.h
Michael Montalbo 91c8989d4e diff: consult oid-only hunk providers via diff.<driver>.process
The provider chain so far holds the diff-hunks store in front of the
terminal builtin computation.  Open it to external processes: a pair on
a path whose driver configures diff.<driver>.process is answered by a
long-running process speaking a pkt-line protocol (following the filter
process protocol), registered at the head of the chain and consulted
before the store and before any blob is loaded.

The protocol starts with the smallest request that can carry an answer:
object names alone.  A request is the pathname and the pair's
old-oid/new-oid, with no content.  The process answers with hunk lines,
with a zero-hunk success that asserts the blobs equivalent (trailing
newlines included), or with status=need-content, on which the pair
falls through to the builtin answer.  This serves the two shapes that
need no content pushed to them: a cache keyed on the blob pair, and a
process that fetches the blobs itself (for example over "git cat-file
--batch").  A pair whose side is not a stored blob carries a NULL id;
the provider sends no request and passes it.  Because Git holds no
content for the exchange, the answer is used as sent: hunks are
validated for order, overlap, lockstep alignment, and magnitude, then
replayed without the normalization xdiff applies to diffs it computes
itself.  The magnitude bound is the blobs' sizes, read from the object
database without loading content: a blob of N bytes holds at most N
lines.

Because the process's answer is authoritative, it outranks the store,
and its head-of-chain position says so.  A pair the process answers
never reaches the store and is never recorded, so nothing it produces
enters the store, which holds the builtin answer only.  A request it
does not answer, whether need-content, a missing capability, or a
missing id, passes down the chain to the builtin answer, which is what
the store serves, so the store may serve such a pair and a warming run
may record it.  Entries recorded before a process was configured are
not purged; a pair the process answers ignores them, and "git diff-hunks
clear" discards them.

The provider gates itself per request.  The driver is looked up by the
old-side path, so a renamed file resolves to the same driver, and by
the repository-relative path, so a diff.relative run from a
subdirectory names the pair the same way.  Options the process is never
told about select no process: the whitespace-ignoring options, -I,
--anchored, and an algorithm forced by option or configuration (blame
routes its algorithm through xdl_opts, so --histogram is covered).  The
request gains its last field, the path; the consumers change only by
filling it, and neither names the process.

The provider's state is its repository's pool of running processes,
keyed by the configured command, so drivers sharing a command share a
process, a submodule speaks to its own, and releasing the provider
(from repo_clear()) stops them.  The pool owns a copy of each command
string, so an entry outlives a config re-read.  A command that fails
stays as an entry that is not retried: its request and every later one
pass, so the store may serve the path for the rest of the command.

A protocol error in a response never kills the command.  The response
is read through a packet reader gentle about framing, so an error takes
one path: a single warning, the process stopped and marked failed, and
the builtin diff for the rest of the command.  That covers garbage
bytes, a truncated response, an empty packet, a bare status, and an
unrecognized status.  Semantically invalid coordinates cost only their
pair: the response is drained, the pair is computed, and the process
stays alive.  A path the protocol cannot carry (an embedded newline, or
one too long for a packet) falls back per path rather than costing the
command its process.  The handshake keeps one fatal check: a process
that announces a capability Git did not request aborts the command, as
the long-running filter protocol does.

Consulting is allowed per command, following the allow_textconv
precedent.  "git diff", "git log" and "git show", and "git blame" set
allow_diff_process; the plumbing diff commands and the interactive-patch
machinery never set it, so scripted and staging output stays builtin.
The options adjust the flag:

- --no-ext-diff clears it and --ext-diff sets it;
- --diff-process and --no-diff-process set and clear it alone, leaving
  external diff drivers as they were;
- format-patch clears it unconditionally, so a generated patch applies
  for recipients without the process;
- range-diff passes --no-ext-diff to the "git log" it compares.

git blame and the summary formats consult the process.  For blame, a
pair reported equivalent emits no hunks, so the whole commit passes to
its parent.  In the stat formats such a pair sums to a zero-count entry,
which the "nothing changed" rule omits, as under -w.  The subprocess is
long-running: one startup cost across a traversal, one round-trip per
consulted pair.  Answers travel in struct xdl_hunk, new in
xdiff-interface.h, holding xdiff's 1-based coordinates; nothing feeds
them back to xdiff, since only coordinate consumers consult.

A content-carrying request is the natural extension: it would serve
sides that are not stored blobs and processes that want content pushed
to them, and bring patch output and log -L's range tracking to the same
answer.  As it stands, a process's answers show in blame and the summary
formats while patch output stays builtin.

t4080 exercises the protocol, the per-command gate, and the error paths:

- each adversarial response shape warns and falls back to builtin, the
  request log proving which failures disable the process and which keep
  it alive (a malformed hunk line, coordinates past the blob size, a
  count overflowing strtol(), overlapping or misaligned hunks, an
  unrecognized status, a bare status, an empty packet, a mid-response
  crash, and raw garbage);
- a capability-less process and status=abort degrade without noise, and
  a failed start warns once and returns the path to the store;
- a trailing token on a hunk line is ignored, pinning field
  appendability;
- positive consults for git diff, git show, and diff-tree under
  --ext-diff and --diff-process; textconv output and gitlink sides are
  never identified; a diff.relative run consults by the repo-relative
  path;
- the equivalence answer is pinned from both consumers, and a warming
  run past a deferring process records the pair for a later read.

Helped-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Signed-off-by: Michael Montalbo <mmontalbo@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
2026-08-01 15:09:52 -07:00

160 lines
6.5 KiB
C

#ifndef DIFF_PROVIDER_H
#define DIFF_PROVIDER_H
#include "xdiff-interface.h"
/*
* The hunk provider interface sits between naming a pair of file
* versions to diff and computing their changed line ranges.
* Consumers that operate on hunk coordinates route their diff
* through here, so that a provider can answer for the pair before
* its content is loaded.
*
* A hunk provider answers a consumer's request from the pair's
* identity, its blob object ids and the settings that determine the
* diff, before any content is loaded; a request no provider answers
* falls through to the consumer's own computation. Two providers implement this
* interface with different authority. The diff-hunks store
* (diff-hunks.h) is in-process and not authoritative: it may only
* reproduce the builtin result, so it never asserts a pair
* equivalent, and it stands aside wherever a process outranks it. A
* process configured in diff.<driver>.process (diff-process.c) is
* authoritative for its paths: its answer may deliberately differ
* from the builtin diff, including asserting a pair equivalent. The
* interface resolves that authority through a provider chain owned
* by the repository, built on first consultation and released by
* repo_clear(): chain order is the resolution, and the builtin
* computation itself is the chain's terminal provider. A consumer
* never names a provider; it reads the outcome below. Every answer a
* provider serves from identity passes the shared coordinate check
* (diff-provider-internal.h) before any consumer sees it.
*/
struct diff_options;
struct object_id;
struct repository;
/*
* The result of a consultation: two dependent axes flattened into
* their four valid points. The first axis is the state of the
* response: the pair was answered, no provider answered, or (from
* diff_provider_emit_hunks() alone) the attempt failed. The second
* axis exists only in the unanswered state: whether what the caller
* computes for this request may be recorded, the one rule the
* interface imposes on an otherwise free caller. The rule travels
* in the outcome because the knowledge is a provider's while the
* recording is the caller's, and it shares the enum with the state,
* rather than riding a separate flag, so that no meaningless
* combination is representable and -Wswitch forces every consumer
* that switches to place the no-record arm.
*
* These values describe consultations, not providers: the set does
* not grow when a provider is added; a new provider maps onto these
* values inside the interface, so consumer code is written once.
* Each entry point returns a subrange of the set (stated at its
* declaration); a switch over this enum should list every value and
* omit "default:" so -Wswitch keeps it exhaustive, and a caller for
* whom only one value is actionable may compare against that value
* alone.
*/
enum diff_provider_outcome {
/*
* Loading or diffing the pair failed. Returned only by
* diff_provider_emit_hunks(), whose compute leg is the only
* part of a consultation that can fail.
*/
DIFF_PROVIDER_ERROR = -1,
/*
* The request is answered: every hunk of the pair has been
* emitted through the callback. An authoritative provider
* that finds the pair equivalent answers with no hunks at
* all, so a callback that never fired is an answer, not an
* accident.
*/
DIFF_PROVIDER_ANSWERED = 0,
/*
* No provider answered. What happens next is the caller's
* business, typically computing the diff itself; a result it
* computes for this request may be recorded.
*/
DIFF_PROVIDER_UNANSWERED,
/*
* No provider answered, and what the caller computes for
* this request must not be recorded: either an authoritative
* provider owns the pair and declined this request, or the
* request is shaped by parameters outside the recording key,
* the key a recorded result is later served by.
*/
DIFF_PROVIDER_UNANSWERED_NO_RECORD,
};
/*
* A consultation request. The interface consults providers from
* these fields alone; no content is loaded before an answer.
*
* repo owns the provider chain the request walks. old_oid/new_oid
* name the blobs whose bytes are diffed; pass NULL for a side whose
* bytes are not a stored blob (a working-tree file, textconv output,
* a gitlink), so no provider answers from an id it cannot look up.
* path names the file the pair is diffed as; a provider selected by
* path applies only where it is set. diffopt carries the diff
* settings that live outside xpp; xpp carries the parameters the
* diff runs with. Each provider gates itself on the fields that
* concern it.
*/
struct diff_provider_request {
struct repository *repo;
const struct object_id *old_oid;
const struct object_id *new_oid;
const char *path;
struct diff_options *diffopt;
const xpparam_t *xpp;
};
/*
* Consult the providers for the request's pair without computing.
* On DIFF_PROVIDER_ANSWERED the hunks were emitted through hunk_cb
* (0-based emission coordinates, context 0) and were validated
* before the first callback ran, so a consumer may accumulate
* directly into its result. Never returns DIFF_PROVIDER_ERROR.
* The callback's return value is not consulted: emission of a
* validated answer has no error leg, so the callback must return 0.
*/
enum diff_provider_outcome
diff_provider_consult(const struct diff_provider_request *req,
xdl_emit_hunk_consume_func_t hunk_cb, void *cb_data);
/*
* Load the pair's content. Called at most once per request, only
* when the ranges are computed rather than provided. The buffers
* borrow storage owned by the callback's owner.
*/
typedef int (*diff_provider_fill_fn)(void *data, mmfile_t *old_file,
mmfile_t *new_file);
/*
* Consult the providers and, when no identity answer serves the
* request, load the pair's content through fill and compute its
* exact changed ranges (context 0). Emits to hunk_cb either way and
* returns DIFF_PROVIDER_ANSWERED, or DIFF_PROVIDER_ERROR when fill
* or the diff fails. The unanswered outcomes are never returned: a
* pair no provider answers is computed here instead of in the caller.
*/
enum diff_provider_outcome
diff_provider_emit_hunks(const struct diff_provider_request *req,
diff_provider_fill_fn fill, void *fill_data,
xdl_emit_hunk_consume_func_t hunk_cb,
void *cb_data);
/*
* Release the repository's provider chain: stop any provider-owned
* processes and free the providers. Called by repo_clear(); the
* chain builds again on the next consultation.
*/
void diff_providers_clear(struct repository *r);
#endif /* DIFF_PROVIDER_H */