git/diff-process.c

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)
{
size_t 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;
}