Files
git/diff-process.c
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

670 lines
23 KiB
C

/*
* The process provider of the hunk provider interface: consult a
* long-running external process via the pkt-line protocol for the
* hunks of a blob pair. The process answers from the pair's object
* names alone: it can serve a persistent cache keyed on the pair, or
* fetch the blobs from the repository itself (e.g. via "git cat-file
* --batch") and compute its own notion of which lines changed. The
* provider sits at the head of its repository's chain and gates
* itself per request; its state is the repository's pool of running
* processes, one per configured command, stopped when the provider
* is released.
*
* Protocol: pkt-line over stdin/stdout, following the pattern of
* the long-running filter process protocol (see convert.c).
*
* Handshake:
* git> git-diff-client / version=1 / flush
* process< git-diff-server / version=1 / flush
* git> capability=hunks-by-oid / flush
* process< capability=hunks-by-oid / flush
*
* Per-pair, when both sides are stored blobs:
* git> command=hunks-by-oid / pathname=<path>
* git> old-oid=<hex> / new-oid=<hex> / flush
* process< hunk <old_start> <old_count> <new_start> <new_count>
* process< ... / flush
* process< status=success / flush
*
* No content is sent. Because Git holds no content for the exchange,
* the answer is used as the process sent it: the hunks are not re-run
* through xdiff's compaction, and a status=success response with zero
* hunks asserts that the blobs are equivalent, including their
* trailing newlines. A process that cannot answer from the object names
* (or cannot rule out a trailing-newline-only difference) responds
* status=need-content; the pair then gets the builtin answer, served
* from the diff-hunks store or computed. A later
* protocol extension can define a content-carrying request for such
* processes and for sides that are not stored blobs.
*/
#include "git-compat-util.h"
#include "diff.h"
#include "diff-provider-internal.h"
#include "gettext.h"
#include "hex.h"
#include "odb.h"
#include "repository.h"
#include "sigchain.h"
#include "userdiff.h"
#include "sub-process.h"
#include "pkt-line.h"
#include "strbuf.h"
#define CAP_OID_HUNKS (1u << 0)
/*
* The provider's state: the repository's diff processes, keyed by
* their command string, so drivers that configure the same command
* share one process. An entry whose process failed stays in the
* pool with the failed bit set, so the command is not retried while
* the entry lives; the pool and its entries last until the provider
* is released.
*/
struct diff_process_state {
struct hashmap subprocesses;
};
struct diff_subprocess {
struct subprocess_entry subprocess;
/*
* Owns the string subprocess.cmd and the hashmap key borrow: the
* entry outlives the userdiff config a re-read may replace.
*/
char *cmd;
unsigned int supported_capabilities;
unsigned failed : 1;
};
static int start_diff_process_fn(struct subprocess_entry *subprocess)
{
static int versions[] = { 1, 0 };
static struct subprocess_capability capabilities[] = {
{ "hunks-by-oid", CAP_OID_HUNKS },
{ NULL, 0 }
};
struct diff_subprocess *entry =
container_of(subprocess, struct diff_subprocess, subprocess);
return subprocess_handshake(subprocess, "git-diff",
versions, NULL,
capabilities,
&entry->supported_capabilities);
}
/*
* The pool entry for a command, or NULL when its process fails to
* start here: the failure leaves a failed entry in the pool, so only
* the request that observed it maps it to an error and later
* requests pass the provider by.
*/
static struct diff_subprocess *get_or_launch_process(
struct diff_process_state *state,
struct userdiff_driver *drv)
{
struct subprocess_entry *running;
struct diff_subprocess *entry;
running = subprocess_find_entry(&state->subprocesses, drv->process);
if (running) {
entry = container_of(running, struct diff_subprocess,
subprocess);
return entry->failed ? NULL : entry;
}
entry = xcalloc(1, sizeof(*entry));
entry->cmd = xstrdup(drv->process);
if (subprocess_start_command(&entry->subprocess, entry->cmd,
start_diff_process_fn))
entry->failed = 1;
hashmap_entry_init(&entry->subprocess.ent, strhash(entry->cmd));
hashmap_add(&state->subprocesses, &entry->subprocess.ent);
if (entry->failed) {
warning(_("diff process '%s' failed to start;"
" using the builtin diff"), drv->process);
return NULL;
}
return entry;
}
/*
* A hunk in the diff process's presentation coordinates: the line
* numbering it reports over the protocol. Kept distinct from struct
* xdl_hunk (xdiff's coordinates) so that only translated hunks ever
* reach a consumer; diff_process_hunk_to_xdl() is the single
* crossing point.
*/
struct diff_process_hunk {
long old_start, old_count;
long new_start, new_count;
};
/*
* Parse one non-negative decimal field of a hunk line into *out and
* advance *line past it. Fields must be plain decimal with no leading
* whitespace or sign (isdigit() takes an unsigned char to stay defined
* for high-bit bytes). The first three fields are followed by a single
* space; the last (is_last) is followed by end-of-string or a space.
* Trailing space-separated tokens after the last field are allowed and
* ignored, so a future protocol version can append fields (e.g. a
* "moved" marker) without an older Git rejecting the line, mirroring
* the request-side rule that processes ignore unknown keys.
*
* A value that overflows strtol() is not a parse failure: the line is
* well-formed, so the stream stays in protocol sync. It is reported
* through *out_of_range, and the caller skips the pair the same way
* it skips any other out-of-range coordinate.
*/
static int parse_hunk_field(const char **line, long *out, int is_last,
int *out_of_range)
{
const char *p = *line;
char *end;
if (!isdigit((unsigned char)*p))
return -1;
errno = 0;
*out = strtol(p, &end, 10);
if (end == p)
return -1;
if (errno == ERANGE)
*out_of_range = 1;
else if (errno)
return -1;
if (is_last) {
if (*end != '\0' && *end != ' ')
return -1;
} else {
if (*end != ' ')
return -1;
end++;
}
*line = end;
return 0;
}
static int parse_hunk_line(const char *line,
struct diff_process_hunk *presented,
int *out_of_range)
{
*out_of_range = 0;
/* Format: "hunk <old_start> <old_count> <new_start> <new_count>" */
if (!skip_prefix(line, "hunk ", &line))
return -1;
if (parse_hunk_field(&line, &presented->old_start, 0, out_of_range) ||
parse_hunk_field(&line, &presented->old_count, 0, out_of_range) ||
parse_hunk_field(&line, &presented->new_start, 0, out_of_range) ||
parse_hunk_field(&line, &presented->new_count, 1, out_of_range))
return -1;
return 0;
}
/*
* Translate a hunk from the diff process's presentation coordinates
* into xdiff's.
*
* Protocol starts are already 1-based positions (the line a change
* sits before), the same numbering xdiff uses, so the only adjustment
* is for an empty file side: "git diff" addresses it with a start of 0
* and a count of 0 (e.g. "0 0 1 5" adds five lines to an empty old
* side), and since xdiff uses start-1 as an array index that 0 becomes
* 1 here. This is NOT the full inverse of xdl_emit_hunk_hdr()
* (xdiff/xutils.c): that emitter shifts a count-0 range to start-1 for
* the displayed "@@" header, but the protocol keeps the unshifted
* 1-based position for a mid-file insert or delete. This is the single
* point where presentation coordinates become xdiff coordinates, so
* any consumer of these coordinates may assume 1-based starts.
*
* Returns -1 for a start of 0 paired with a nonzero count, which names
* no line in either coordinate system. (parse_hunk_line() already
* guarantees non-negative starts and counts.)
*/
static int diff_process_hunk_to_xdl(const struct diff_process_hunk *presented,
struct xdl_hunk *xdl)
{
long old_start = presented->old_start;
long new_start = presented->new_start;
if ((!old_start && presented->old_count) ||
(!new_start && presented->new_count))
return -1;
if (!old_start)
old_start = 1;
if (!new_start)
new_start = 1;
xdl->old_start = old_start;
xdl->old_count = presented->old_count;
xdl->new_start = new_start;
xdl->new_count = presented->new_count;
return 0;
}
/*
* Validate the process's hunks (already in xdiff coordinates) before they
* bypass the diff algorithm. The content-independent rules (in-order,
* non-overlapping, lockstep-aligned, int32-bounded coordinates) are the
* provider interface's shared rule, diff_provider_check_hunk(); this
* function adds the two checks that need the blobs' line counts (a hunk
* past the end of a file, the run after the last hunk) and the
* per-rule diagnostics naming the process. On a bad response we warn
* and the caller falls back to the builtin diff. Returns 0 if valid,
* -1 (after warning) otherwise.
*
* old_lines/new_lines bound the line count of each side, or are
* negative when no bound is known. An oid-only answer arrives without
* content, so its caller passes upper bounds derived from the blobs'
* byte sizes, which caps coordinate magnitude but cannot support the
* run-after-the-last-hunk check: that one compares exact line counts,
* so it runs only when lines_exact is set, which no caller does today.
* It is kept for a content-carrying request, whose loaded buffers
* would provide exact counts.
*/
static int validate_external_hunks(const struct xdl_hunk *hunks, size_t nr,
long old_lines, long new_lines,
int lines_exact,
const char *process, const char *path)
{
struct diff_provider_hunks_check c = { 0 };
size_t i;
for (i = 0; i < nr; i++) {
const struct xdl_hunk *h = &hunks[i];
if (old_lines >= 0 &&
(h->old_count > old_lines - h->old_start + 1 ||
h->new_count > new_lines - h->new_start + 1)) {
warning(_("diff process '%s' returned a hunk past the "
"end of '%s'; using the builtin diff"),
process, path);
return -1;
}
switch (diff_provider_check_hunk(&c, h->old_start,
h->old_count, h->new_start,
h->new_count)) {
case DIFF_PROVIDER_HUNKS_OK:
break;
case DIFF_PROVIDER_HUNKS_RANGE:
warning(_("diff process '%s' returned out-of-range "
"coordinates for '%s'; using the builtin diff"),
process, path);
return -1;
case DIFF_PROVIDER_HUNKS_OVERLAP:
warning(_("diff process '%s' returned overlapping hunks "
"for '%s'; using the builtin diff"),
process, path);
return -1;
case DIFF_PROVIDER_HUNKS_MISALIGNED:
warning(_("diff process '%s' returned hunks that leave "
"'%s' misaligned; using the builtin diff"),
process, path);
return -1;
}
}
if (lines_exact &&
old_lines - c.prev_old_end != new_lines - c.prev_new_end) {
warning(_("diff process '%s' returned hunks that leave '%s' "
"misaligned; using the builtin diff"),
process, path);
return -1;
}
return 0;
}
/*
* The most lines a blob can hold, from its size alone: every line,
* even an empty one, costs at least one byte, so a blob of N bytes
* holds at most N lines. Returns -1 when the size is unavailable,
* leaving the response bounded only by the shared int32 rule. A size
* beyond INT32_MAX clamps to it, which loses nothing: a coordinate
* that large fails the shared rule anyway. In a partial clone the
* size lookup must not fetch the blob from the promisor remote:
* validating an answer that exists to avoid loading content must not
* itself download that content, so a missing blob reads as size
* unavailable instead.
*/
static long blob_line_cap(struct repository *r, const struct object_id *oid)
{
unsigned long size;
struct object_info oi = OBJECT_INFO_INIT;
oi.sizep = &size;
if (odb_read_object_info_extended(r->objects, oid, &oi,
OBJECT_INFO_SKIP_FETCH_OBJECT) < 0)
return -1;
if (size > INT32_MAX)
return INT32_MAX;
return (long)size;
}
/*
* The driver whose process a consultation for path would ask, or NULL
* when none applies (no driver, process not allowed, or xpp carries
* options the process is never told about). Needs no content, so
* the driver is picked before any blob is loaded.
*/
static struct userdiff_driver *diff_process_driver(struct diff_options *diffopt,
const char *path,
const xpparam_t *xpp)
{
struct userdiff_driver *drv;
if (!diffopt || !path)
return NULL;
if (!diffopt->flags.allow_diff_process || diffopt->ignore_driver_algorithm)
return NULL;
/*
* Whitespace-ignoring, regex-ignore (-I) and anchored options
* change which lines count as different, but the process is never
* told about them, so its hunks could not honor them. A forced
* diff algorithm (an option or configured algorithm setting)
* requests a specific builtin computation, which an
* authoritative answer would override. Rather than silently
* override the user's request, fall back to the builtin diff,
* which does honor these flags. Key this off xpp (the
* parameters this diff actually runs with) rather than diffopt,
* so a caller like blame, which keeps its algorithm and
* whitespace flags outside diffopt, is covered without a
* separate guard of its own.
*/
if ((xpp->flags & (XDF_WHITESPACE_FLAGS | XDF_IGNORE_BLANK_LINES |
XDF_DIFF_ALGORITHM_MASK)) ||
xpp->ignore_regex_nr || xpp->anchors_nr)
return NULL;
/*
* A path the protocol cannot carry never selects a process: an
* embedded newline would let the rest of the path forge further
* request keys, and the pathname must fit one packet. Passing
* here keeps the cost local to the path; a failed write would
* instead cost the whole command its process.
*/
if (strchr(path, '\n') ||
strlen(path) > LARGE_PACKET_DATA_MAX - strlen("pathname=\n"))
return NULL;
drv = userdiff_find_by_path(diffopt->repo->index, path);
if (!drv || !drv->process)
return NULL;
return drv;
}
/*
* Without content there is no size-derived bound on a response, so cap
* accumulation at a constant instead. A response that exceeds the
* cap is a protocol error: the process is disabled for the rest of
* the command and the caller falls back to the builtin diff.
*/
#define OID_HUNKS_MAX (1 << 20)
enum diff_process_result {
DIFF_PROCESS_ERROR = -1, /* failed; caller falls back to builtin */
DIFF_PROCESS_OK = 0, /* the process supplied hunks */
DIFF_PROCESS_SKIP, /* process did not apply: use builtin */
DIFF_PROCESS_EQUIVALENT, /* process says files are equivalent */
};
/*
* Ask drv's diff process to answer the request from the blob pair's
* object ids alone (the "hunks-by-oid" capability): no content is
* loaded or sent. On DIFF_PROCESS_OK the process's hunks are emitted
* through hunk_cb in 0-based emission coordinates, validated for order,
* overlap, and lockstep alignment first; because Git holds no content,
* the answer is used as the process sent it, without xdiff's compaction.
* DIFF_PROCESS_EQUIVALENT means the process asserts the pair equal.
* DIFF_PROCESS_SKIP covers everything that should fall through to the
* builtin computation: a missing capability, a missing object id, a
* status=need-content answer, or an invalid response.
*/
static enum diff_process_result diff_process_query_hunks(
struct diff_process_state *state,
struct userdiff_driver *drv,
const struct diff_provider_request *req,
xdl_emit_hunk_consume_func_t hunk_cb,
void *cb_data)
{
const char *path = req->path;
struct diff_subprocess *entry;
struct child_process *process;
int fd_in, fd_out;
struct packet_reader reader;
struct strbuf status = STRBUF_INIT;
struct xdl_hunk *hunks = NULL;
struct diff_process_hunk presented;
struct xdl_hunk hunk;
size_t nr_hunks = 0, alloc_hunks = 0, i;
int bad_coords = 0;
long old_cap, new_cap;
enum diff_process_result res;
if (!req->old_oid || !req->new_oid)
return DIFF_PROCESS_SKIP;
entry = get_or_launch_process(state, drv);
if (!entry)
return DIFF_PROCESS_ERROR;
if (!(entry->supported_capabilities & CAP_OID_HUNKS))
return DIFF_PROCESS_SKIP;
process = subprocess_get_child_process(&entry->subprocess);
fd_in = process->in;
fd_out = process->out;
sigchain_push(SIGPIPE, SIG_IGN);
if (packet_write_fmt_gently(fd_in, "command=hunks-by-oid\n") ||
packet_write_fmt_gently(fd_in, "pathname=%s\n", path) ||
packet_write_fmt_gently(fd_in, "old-oid=%s\n",
oid_to_hex(req->old_oid)) ||
packet_write_fmt_gently(fd_in, "new-oid=%s\n",
oid_to_hex(req->new_oid)) ||
packet_flush_gently(fd_in))
goto comm_error;
packet_reader_init(&reader, fd_out, NULL, 0,
PACKET_READ_CHOMP_NEWLINE |
PACKET_READ_GENTLE_ON_EOF |
PACKET_READ_GENTLE_ON_READ_ERROR);
for (;;) {
enum packet_read_status rs = packet_reader_read(&reader);
int out_of_range;
if (rs == PACKET_READ_FLUSH)
break;
/*
* Only a hunk line may precede the flush. EOF and a
* malformed frame end the session; an empty packet, which
* a length-only read cannot tell from a flush, would
* truncate the hunk section here and leave the status
* section to poison the next request, so it is a protocol
* error too.
*/
if (rs != PACKET_READ_NORMAL || !reader.pktlen)
goto comm_error;
if (parse_hunk_line(reader.line, &presented,
&out_of_range) < 0)
goto comm_error;
if (bad_coords)
continue;
if (out_of_range ||
diff_process_hunk_to_xdl(&presented, &hunk) < 0) {
/*
* Semantically invalid coordinates in a well-formed
* response: the stream stays in protocol sync, so
* drain the rest and fall back for this file while
* keeping the process alive, the same treatment
* validate_external_hunks() failures receive.
*/
bad_coords = 1;
continue;
}
if (nr_hunks >= OID_HUNKS_MAX) {
warning(_("diff process '%s' sent too many hunks"
" for '%s'; disabling it for the"
" remainder of this command"),
drv->process, path);
goto disable;
}
ALLOC_GROW(hunks, nr_hunks + 1, alloc_hunks);
hunks[nr_hunks++] = hunk;
}
if (subprocess_read_status_gently(fd_out, &status))
goto comm_error;
if (!strcmp(status.buf, "success")) {
if (bad_coords) {
warning(_("diff process '%s' returned out-of-range "
"coordinates for '%s'; using the builtin diff"),
drv->process, path);
res = DIFF_PROCESS_SKIP;
goto out;
}
if (!nr_hunks) {
res = DIFF_PROCESS_EQUIVALENT;
goto out;
}
/*
* Bound the coordinates by the blobs' sizes, read from the
* object database without loading content. Either both
* bounds hold or neither is applied: a partial bound would
* misclassify a response that the other side's size would
* have caught.
*/
old_cap = blob_line_cap(req->repo, req->old_oid);
new_cap = blob_line_cap(req->repo, req->new_oid);
if (old_cap < 0 || new_cap < 0)
old_cap = new_cap = -1;
if (validate_external_hunks(hunks, nr_hunks, old_cap, new_cap,
0, drv->process, path) < 0) {
res = DIFF_PROCESS_SKIP;
goto out;
}
/*
* Replay in the coordinates a hunk consumer receives from
* xdiff's emission: 0-based starts. The answer is used as
* the process sent it; with no content in hand it cannot be
* re-run through xdiff's compaction.
*/
for (i = 0; i < nr_hunks; i++)
hunk_cb(hunks[i].old_start - 1, hunks[i].old_count,
hunks[i].new_start - 1, hunks[i].new_count,
cb_data);
res = DIFF_PROCESS_OK;
goto out;
}
if (!strcmp(status.buf, "need-content")) {
/*
* The process cannot answer this pair from its object names;
* the caller computes the diff itself.
*/
res = DIFF_PROCESS_SKIP;
goto out;
}
if (!strcmp(status.buf, "abort")) {
/* The process withdrew: stop asking it for this session. */
entry->supported_capabilities &= ~CAP_OID_HUNKS;
res = DIFF_PROCESS_SKIP;
goto out;
}
/*
* An unrecognized status is a protocol error, not a per-pair
* failure: this Git did not request anything it does not know,
* so the process is answering some other protocol, and asking
* it again would warn on every pair of the traversal.
*/
warning(_("diff process '%s' sent unrecognized status '%s' for "
"'%s'; disabling it for the remainder of this command"),
drv->process, status.buf, path);
goto disable;
out:
free(hunks);
strbuf_release(&status);
sigchain_pop(SIGPIPE);
return res;
comm_error:
warning(_("diff process '%s' failed for '%s'; disabling it"
" for the remainder of this command"),
drv->process, path);
disable:
subprocess_stop_command(&entry->subprocess);
entry->failed = 1;
free(hunks);
strbuf_release(&status);
sigchain_pop(SIGPIPE);
return DIFF_PROCESS_ERROR;
}
/*
* The process outranks every later provider through its chain
* position: when it answers, the walk ends, so no later provider
* serves the pair, and an answered pair is never recorded. When it
* does not answer (it defers with need-content, lacks the
* capability, or failed), the caller computes the builtin diff for
* that pair. The store holds builtin results and nothing else, so
* an identity answer for such a pair equals what the caller would
* compute. Every non-answer is therefore a pass: a refusal would
* suppress that equal answer, and would keep a warming run from
* recording the builtin result the caller computes anyway.
*/
static enum diff_provider_disposition
diff_process_consult(struct diff_provider *provider,
const struct diff_provider_request *req,
diff_provider_fill_fn fill UNUSED, void *fill_data UNUSED,
xdl_emit_hunk_consume_func_t hunk_cb, void *cb_data)
{
struct diff_process_state *state = provider->state;
struct userdiff_driver *drv;
struct subprocess_entry *running;
drv = diff_process_driver(req->diffopt, req->path, req->xpp);
if (!drv)
return DIFF_PROVIDER_DISP_PASS;
running = subprocess_find_entry(&state->subprocesses, drv->process);
if (running && container_of(running, struct diff_subprocess,
subprocess)->failed)
return DIFF_PROVIDER_DISP_PASS;
switch (diff_process_query_hunks(state, drv, req,
hunk_cb, cb_data)) {
case DIFF_PROCESS_OK:
case DIFF_PROCESS_EQUIVALENT:
return DIFF_PROVIDER_DISP_ANSWERED;
case DIFF_PROCESS_SKIP:
case DIFF_PROCESS_ERROR:
break;
}
return DIFF_PROVIDER_DISP_PASS;
}
static void diff_process_release(struct diff_provider *provider)
{
struct diff_process_state *state = provider->state;
struct hashmap_iter iter;
struct diff_subprocess *entry;
/* A failed entry's process is already stopped or never ran. */
hashmap_for_each_entry(&state->subprocesses, &iter, entry,
subprocess.ent) {
if (!entry->failed)
subprocess_stop_command(&entry->subprocess);
free(entry->cmd);
}
hashmap_clear_and_free(&state->subprocesses,
struct diff_subprocess, subprocess.ent);
free(state);
}
struct diff_provider *diff_process_provider_new(void)
{
struct diff_process_state *state = xcalloc(1, sizeof(*state));
struct diff_provider *p = xcalloc(1, sizeof(*p));
hashmap_init(&state->subprocesses, cmd2process_cmp, NULL, 0);
p->consult = diff_process_consult;
p->release = diff_process_release;
p->state = state;
return p;
}