git/t/t4080-diff-process.sh

594 lines
23 KiB
Bash
Executable File

#!/bin/sh
test_description='diff.<driver>.process: oid-only hunk requests'
TEST_PASSES_SANITIZE_LEAK=true
. ./test-lib.sh
# See t/helper/test-diff-process-backend.c for the process implementation
# and available --mode= options.
BACKEND="test-tool diff-process-backend"
test_expect_success 'setup' '
echo "*.c diff=cdiff" >.gitattributes &&
git add .gitattributes &&
# 10 lines, changes at 5-6 and 9-10 between the two commits.
cat >pair.c <<-\EOF &&
line1
line2
line3
line4
original5
original6
line7
line8
line9
line10
EOF
git add pair.c &&
git commit -m "add pair.c" &&
cat >pair.c <<-\EOF &&
line1
line2
line3
line4
changed5
changed6
line7
line8
changed9
changed10
EOF
git add pair.c &&
git commit -m "change pair.c"
'
test_expect_success 'an oid-capable process answers blame by object names alone' '
test_when_finished "rm -f backend.log" &&
ORIG=$(git rev-parse --short HEAD~1) &&
CHANGE=$(git rev-parse --short HEAD) &&
# The process reports only lines 5-6 as changed, so blame attributes
# lines 9-10 to the original commit even though the builtin diff
# would show them as changed.
git -c diff.cdiff.process="$BACKEND --mode=oid-fixed --log=backend.log" \
blame pair.c >actual &&
sed -n "9p" actual >line9 &&
sed -n "10p" actual >line10 &&
test_grep "$ORIG" line9 &&
test_grep "$ORIG" line10 &&
sed -n "5p" actual >line5 &&
test_grep "$CHANGE" line5 &&
test_grep "command=hunks-by-oid pathname=pair.c" backend.log
'
test_expect_success 'an oid-capable process answers --numstat by object names alone' '
test_when_finished "rm -f backend.log" &&
git -c diff.cdiff.process="$BACKEND --mode=oid-fixed --log=backend.log" \
log -1 --format= --numstat -- pair.c >actual &&
printf "2\t2\tpair.c\n" >expect &&
test_cmp expect actual &&
test_grep "command=hunks-by-oid pathname=pair.c" backend.log
'
test_expect_success 'need-content falls through to the builtin diff' '
test_when_finished "rm -f backend.log" &&
git -c diff.cdiff.process="$BACKEND --mode=oid-need-content --log=backend.log" \
log -1 --format= --numstat -- pair.c >actual &&
printf "4\t4\tpair.c\n" >expect &&
test_cmp expect actual &&
test_grep "command=hunks-by-oid pathname=pair.c" backend.log
'
test_expect_success 'a warmed hunk store does not override process hunks' '
test_when_finished "git diff-hunks clear" &&
ORIG=$(git rev-parse --short HEAD~1) &&
GIT_DIFF_HUNKS_WRITE=1 git log -2 --stat -- pair.c >/dev/null &&
# Control: without a process, blame is served from the store.
git blame --show-stats pair.c >stats &&
test_grep "num precomputed hits: 1" stats &&
# The store holds the builtin hunks, but a process-capable driver
# makes the process authoritative, so blame must reflect the
# process hunks (only lines 5-6), not a store hit.
git -c diff.cdiff.process="$BACKEND --mode=oid-fixed" \
blame pair.c >actual &&
sed -n "9p" actual >line9 &&
test_grep "$ORIG" line9
'
test_expect_success 'a worktree side is not asked by object names' '
test_when_finished "rm -f backend.log && git checkout -- pair.c" &&
echo "worktree edit" >>pair.c &&
git -c diff.cdiff.process="$BACKEND --mode=oid-fixed --log=backend.log" \
diff --numstat -- pair.c >actual &&
printf "1\t0\tpair.c\n" >expect &&
test_cmp expect actual &&
test_path_is_missing backend.log
'
test_expect_success 'diff process bypassed by --no-ext-diff' '
test_when_finished "rm -f backend.log" &&
git -c diff.cdiff.process="$BACKEND --mode=oid-fixed --log=backend.log" \
log -1 --format= --numstat --no-ext-diff -- pair.c >actual &&
printf "4\t4\tpair.c\n" >expect &&
test_cmp expect actual &&
test_path_is_missing backend.log
'
test_expect_success 'format-patch keeps its diffstat off the process' '
test_when_finished "rm -f backend.log" &&
# format-patch emits a diffstat, and a diffstat consults the
# process, but the gate keeps it builtin so a generated patch
# applies for recipients without the process.
git -c diff.cdiff.process="$BACKEND --mode=oid-fixed --log=backend.log" \
format-patch -1 --stdout --stat -- pair.c >actual &&
test_grep "^+changed9" actual &&
test_path_is_missing backend.log
'
test_expect_success 'format-patch --ext-diff keeps its diffstat off the process' '
test_when_finished "rm -f backend.log" &&
# The gate holds even when --ext-diff enables the external command.
git -c diff.cdiff.process="$BACKEND --mode=oid-fixed --log=backend.log" \
format-patch -1 --stdout --ext-diff --stat -- pair.c >actual &&
test_grep "^+changed9" actual &&
test_path_is_missing backend.log
'
test_expect_success 'diff process not consulted by plumbing diff commands' '
test_when_finished "rm -f backend.log && git checkout -f HEAD -- pair.c" &&
# diff-tree diffs the two commits, a real pair a defeated gate would
# consult the process for.
git -c diff.cdiff.process="$BACKEND --mode=oid-fixed --log=backend.log" \
diff-tree --numstat HEAD >actual &&
test_grep "pair.c" actual &&
# diff-index needs a change to diff, or there is no pair and a
# missing log proves nothing; stage one and diff it against HEAD.
echo "staged change" >>pair.c &&
git add pair.c &&
git -c diff.cdiff.process="$BACKEND --mode=oid-fixed --log=backend.log" \
diff-index --cached --numstat HEAD -- pair.c >actual &&
test_grep "pair.c" actual &&
test_path_is_missing backend.log
'
test_expect_success 'add -p stages from the builtin diff with a process configured' '
test_when_finished "rm -f backend.log" &&
cat >gate.c <<-\EOF &&
int gate(void) { return 1; }
EOF
git add gate.c &&
git commit -m "add gate.c" &&
cat >gate.c <<-\EOF &&
int gate(void) { return 2; }
EOF
# add -p builds its hunks from patch text, which is not a provider
# consumer today, so a configured process cannot shape what it
# offers. This pins that interactive patch stays builtin for the
# current consumers, rather than exercising the plumbing gate.
test_write_lines y |
git -c diff.cdiff.process="$BACKEND --mode=oid-fixed --log=backend.log" \
add -p gate.c &&
git diff --cached -- gate.c >staged &&
test_grep "return 2" staged &&
test_path_is_missing backend.log &&
git commit -m "gate.c v2"
'
test_expect_success 'blame withholds identity for the working-tree pair' '
test_when_finished "rm -f backend.log && git checkout -- pair.c" &&
echo "uncommitted" >>pair.c &&
wt=$(git hash-object pair.c) &&
git -c diff.cdiff.process="$BACKEND --mode=oid-fixed --log=backend.log" \
blame pair.c >actual &&
# The dirty working-tree side is not a stored blob: no request
# may name its bytes by object id.
test_grep ! "new-oid=$wt" backend.log
'
test_expect_success 'a replaced blob makes the process step aside' '
new_blob=$(git rev-parse HEAD:pair.c) &&
test_when_finished "rm -f backend.log && git replace -d $new_blob" &&
# Replacing the new-side blob redirects the content the diff reads
# under the id the process would be sent, so a raw-id request would
# name bytes other than the ones diffed. Identity is withheld: the
# process is not consulted and the builtin computes the pair from
# the replaced content.
repl=$(printf "just one line\n" | git hash-object -w --stdin) &&
git replace "$new_blob" "$repl" &&
git log -1 --format= --numstat -- pair.c >expect &&
git -c diff.cdiff.process="$BACKEND --mode=oid-fixed --log=backend.log" \
log -1 --format= --numstat -- pair.c >actual &&
test_cmp expect actual &&
test_path_is_missing backend.log
'
test_expect_success 'an equivalence answer omits the pair from the stat' '
test_when_finished "rm -f backend.log" &&
git -c diff.cdiff.process="$BACKEND --mode=oid-equal --log=backend.log" \
log -1 --format= --numstat -- pair.c >actual &&
# An equivalent pair sums to a zero-count entry, and the stat
# code omits zero-count modified entries, the same way a
# whitespace-only pair prints nothing under -w. The builtin
# diff would print nonzero counts here, and the log proves the
# process was consulted.
test_must_be_empty actual &&
test_grep "command=hunks-by-oid pathname=pair.c" backend.log
'
test_expect_success 'blame passes equivalent pairs through to the boundary' '
ORIG=$(git rev-parse --short ":/add pair.c") &&
# The process asserts every consulted pair equal, so no line is
# ever treated as changed: every line passes through to the
# commit that added the file, marked as the blame boundary.
git -c diff.cdiff.process="$BACKEND --mode=oid-equal" \
blame pair.c >actual &&
sed -n "5p" actual >line5 &&
test_grep "^\^$ORIG" line5 &&
sed -n "10p" actual >line10 &&
test_grep "^\^$ORIG" line10
'
test_expect_success 'a warming run records a pair the process defers' '
test_when_finished "git diff-hunks clear" &&
git diff-hunks clear &&
# The process owns the path but defers this pair with
# need-content, so the pair gets the builtin diff; that is the
# result the store holds, so the warming run records it and a
# later read is served from the store.
GIT_DIFF_HUNKS_WRITE=1 git -c core.diffHunks=true \
-c diff.cdiff.process="$BACKEND --mode=oid-need-content" \
log -1 --format= --stat -- pair.c >/dev/null &&
git -c core.diffHunks=true blame --show-stats pair.c >stats 2>&1 &&
test_grep "num precomputed hits: 1" stats
'
# The protocol error paths: each adversarial response shape must warn,
# fall back to the builtin output, and either keep the process alive
# (a per-pair rejection) or disable it for the rest of the command (a
# protocol error). The request log tells the two apart: the log walk
# below consults two pairs (gate.c first, then pair.c), so a disabled
# process shows one logged request and a live one shows two.
test_expect_success 'a malformed hunk line disables the process for the command' '
test_when_finished "rm -f backend.log err" &&
git log --format= --numstat -- "*.c" >expect &&
git -c diff.cdiff.process="$BACKEND --mode=oid-malformed --log=backend.log" \
log --format= --numstat -- "*.c" >actual 2>err &&
test_cmp expect actual &&
test_grep "disabling it for the remainder" err &&
test_line_count = 1 backend.log
'
test_expect_success 'coordinates past the blob size skip the pair, process stays alive' '
test_when_finished "rm -f backend.log err" &&
git log --format= --numstat -- "*.c" >expect &&
git -c diff.cdiff.process="$BACKEND --mode=oid-huge --log=backend.log" \
log --format= --numstat -- "*.c" >actual 2>err &&
test_cmp expect actual &&
test_grep "past the end" err &&
test_line_count = 2 backend.log
'
test_expect_success 'a count that overflows long skips the pair, process stays alive' '
test_when_finished "rm -f backend.log err" &&
git log --format= --numstat -- "*.c" >expect &&
git -c diff.cdiff.process="$BACKEND --mode=oid-erange --log=backend.log" \
log --format= --numstat -- "*.c" >actual 2>err &&
test_cmp expect actual &&
test_grep "out-of-range coordinates" err &&
test_line_count = 2 backend.log
'
test_expect_success 'overlapping hunks are rejected per pair' '
test_when_finished "rm -f backend.log err" &&
git log --format= --numstat -- "*.c" >expect &&
git -c diff.cdiff.process="$BACKEND --mode=oid-overlap --log=backend.log" \
log --format= --numstat -- "*.c" >actual 2>err &&
test_cmp expect actual &&
test_grep "overlapping hunks" err &&
test_line_count = 2 backend.log
'
test_expect_success 'misaligned hunks are rejected per pair' '
test_when_finished "rm -f backend.log err" &&
git log --format= --numstat -- "*.c" >expect &&
git -c diff.cdiff.process="$BACKEND --mode=oid-misaligned --log=backend.log" \
log --format= --numstat -- "*.c" >actual 2>err &&
test_cmp expect actual &&
test_grep "misaligned" err &&
test_line_count = 2 backend.log
'
test_expect_success 'a start of zero with a nonzero count is rejected per pair' '
test_when_finished "rm -f backend.log err" &&
git log --format= --numstat -- "*.c" >expect &&
# A start of 0 names an empty side, so a nonzero count beside it
# names no line; the coordinate is rejected and the pair falls back
# to the builtin diff while the process stays alive.
git -c diff.cdiff.process="$BACKEND --mode=oid-badstart --log=backend.log" \
log --format= --numstat -- "*.c" >actual 2>err &&
test_cmp expect actual &&
test_grep "out-of-range coordinates" err &&
test_line_count = 2 backend.log
'
test_expect_success 'an unrecognized status disables the process for the command' '
test_when_finished "rm -f backend.log err" &&
git log --format= --numstat -- "*.c" >expect &&
git -c diff.cdiff.process="$BACKEND --mode=oid-unknown-status --log=backend.log" \
log --format= --numstat -- "*.c" >actual 2>err &&
test_cmp expect actual &&
test_grep "unrecognized status .frobnicate." err &&
test_line_count = 1 backend.log
'
test_expect_success 'status=abort withdraws the capability without a warning' '
test_when_finished "rm -f backend.log err" &&
git log --format= --numstat -- "*.c" >expect &&
git -c diff.cdiff.process="$BACKEND --mode=oid-abort --log=backend.log" \
log --format= --numstat -- "*.c" >actual 2>err &&
test_cmp expect actual &&
test_grep ! "disabling" err &&
test_line_count = 1 backend.log
'
test_expect_success 'a bare status without the hunk-section flush is a protocol error' '
test_when_finished "rm -f backend.log err" &&
git log --format= --numstat -- "*.c" >expect &&
git -c diff.cdiff.process="$BACKEND --mode=oid-bare-status --log=backend.log" \
log --format= --numstat -- "*.c" >actual 2>err &&
test_cmp expect actual &&
test_grep "disabling it for the remainder" err &&
test_line_count = 1 backend.log
'
test_expect_success 'an empty packet in the hunk section is a protocol error' '
test_when_finished "rm -f backend.log err" &&
git log --format= --numstat -- "*.c" >expect &&
git -c diff.cdiff.process="$BACKEND --mode=oid-empty-packet --log=backend.log" \
log --format= --numstat -- "*.c" >actual 2>err &&
test_cmp expect actual &&
test_grep "disabling it for the remainder" err &&
test_line_count = 1 backend.log
'
test_expect_success 'a process that dies mid-response fails the command over to builtin' '
test_when_finished "rm -f backend.log err" &&
git log --format= --numstat -- "*.c" >expect &&
git -c diff.cdiff.process="$BACKEND --mode=oid-crash --log=backend.log" \
log --format= --numstat -- "*.c" >actual 2>err &&
test_cmp expect actual &&
test_grep "disabling it for the remainder" err &&
test_line_count = 1 backend.log
'
test_expect_success 'garbage bytes on stdout fail the command over to builtin' '
test_when_finished "rm -f backend.log err" &&
git log --format= --numstat -- "*.c" >expect &&
git -c diff.cdiff.process="$BACKEND --mode=oid-garbage --log=backend.log" \
log --format= --numstat -- "*.c" >actual 2>err &&
test_cmp expect actual &&
test_grep "disabling it for the remainder" err &&
test_line_count = 1 backend.log
'
test_expect_success 'a process announcing no capability is never asked' '
test_when_finished "rm -f backend.log" &&
git log --format= --numstat -- "*.c" >expect &&
git -c diff.cdiff.process="$BACKEND --mode=cap-none --log=backend.log" \
log --format= --numstat -- "*.c" >actual &&
test_cmp expect actual &&
test_must_be_empty backend.log
'
test_expect_success 'a trailing token on a hunk line is ignored' '
test_when_finished "rm -f backend.log" &&
git -c diff.cdiff.process="$BACKEND --mode=oid-trailing --log=backend.log" \
log -1 --format= --numstat -- pair.c >actual &&
printf "2\t2\tpair.c\n" >expect &&
test_cmp expect actual &&
test_grep "command=hunks-by-oid pathname=pair.c" backend.log
'
test_expect_success 'a failed start warns once and the store may serve the path' '
git init failrepo &&
(
cd failrepo &&
echo "*.c diff=cdiff" >.gitattributes &&
git add .gitattributes &&
test_commit f1 f.c "one" &&
test_commit f2 f.c "one
two" &&
test_commit f3 f.c "one
two
three" &&
GIT_DIFF_HUNKS_WRITE=1 git log --format= --stat -- f.c >/dev/null &&
# The command has no shell metacharacters, so it fails at
# exec time; a shell-wrapped command would fail at the
# handshake, which the gentle handshake in the base
# (061a68e443) likewise degrades to the builtin diff.
git blame f.c >expect &&
git -c diff.cdiff.process=/does-not-exist-diff-backend \
blame f.c >actual 2>err &&
test_cmp expect actual &&
# One warning even though the blame consults two pairs.
test $(grep -c "failed to start" err) = 1 &&
# A failure is a non-answer like any other: the request
# that observes it and every later request pass to the
# store, so the warmed store serves both pairs.
git -c diff.cdiff.process=/does-not-exist-diff-backend \
blame --show-stats f.c >stats 2>&1 &&
test_grep "num precomputed hits: 2" stats
)
'
test_expect_success 'the store serves a pair the process defers' '
test_when_finished "git diff-hunks clear" &&
git diff-hunks clear &&
GIT_DIFF_HUNKS_WRITE=1 git log --format= --stat -- pair.c >/dev/null &&
git blame pair.c >expect &&
# need-content defers the pair to the builtin diff, which is
# what the store holds, so the walk continues past the process
# and the store serves the pair.
git -c diff.cdiff.process="$BACKEND --mode=oid-need-content" \
blame pair.c >actual &&
test_cmp expect actual &&
git -c diff.cdiff.process="$BACKEND --mode=oid-need-content" \
blame --show-stats pair.c >stats 2>&1 &&
test_grep "num precomputed hits: 1" stats
'
test_expect_success 'git diff between commits consults the process' '
test_when_finished "rm -f backend.log" &&
ORIG=$(git rev-parse ":/add pair.c") &&
CHANGE=$(git rev-parse ":/change pair.c") &&
git -c diff.cdiff.process="$BACKEND --mode=oid-fixed --log=backend.log" \
diff --numstat $ORIG $CHANGE -- pair.c >actual &&
printf "2\t2\tpair.c\n" >expect &&
test_cmp expect actual &&
test_grep "command=hunks-by-oid pathname=pair.c" backend.log
'
test_expect_success 'git show consults the process' '
test_when_finished "rm -f backend.log" &&
CHANGE=$(git rev-parse ":/change pair.c") &&
git -c diff.cdiff.process="$BACKEND --mode=oid-fixed --log=backend.log" \
show --format= --numstat $CHANGE -- pair.c >actual &&
printf "2\t2\tpair.c\n" >expect &&
test_cmp expect actual &&
test_grep "command=hunks-by-oid pathname=pair.c" backend.log
'
test_expect_success 'diff-tree --ext-diff consults the process' '
test_when_finished "rm -f backend.log" &&
CHANGE=$(git rev-parse ":/change pair.c") &&
git -c diff.cdiff.process="$BACKEND --mode=oid-fixed --log=backend.log" \
diff-tree --ext-diff --no-commit-id --numstat $CHANGE >actual &&
printf "2\t2\tpair.c\n" >expect &&
test_cmp expect actual &&
test_grep "command=hunks-by-oid pathname=pair.c" backend.log
'
test_expect_success '--no-diff-process forbids consulting alone' '
test_when_finished "rm -f backend.log" &&
git -c diff.cdiff.process="$BACKEND --mode=oid-fixed --log=backend.log" \
log -1 --no-diff-process --format= --numstat -- pair.c >actual &&
printf "4\t4\tpair.c\n" >expect &&
test_cmp expect actual &&
test_path_is_missing backend.log
'
test_expect_success '--diff-process allows plumbing to consult' '
test_when_finished "rm -f backend.log" &&
CHANGE=$(git rev-parse ":/change pair.c") &&
git -c diff.cdiff.process="$BACKEND --mode=oid-fixed --log=backend.log" \
diff-tree --diff-process --no-commit-id --numstat $CHANGE >actual &&
printf "2\t2\tpair.c\n" >expect &&
test_cmp expect actual &&
test_grep "command=hunks-by-oid pathname=pair.c" backend.log
'
test_expect_success 'a forced blame diff algorithm bypasses the process' '
test_when_finished "rm -f backend.log" &&
CHANGE=$(git rev-parse --short ":/change pair.c") &&
# The process would attribute lines 9-10 to the original commit
# (see the oid-fixed blame test above); a forced builtin
# algorithm must produce the builtin attribution and never start
# the process.
git -c diff.cdiff.process="$BACKEND --mode=oid-fixed --log=backend.log" \
blame --histogram pair.c >actual &&
sed -n "9p" actual >line9 &&
test_grep "$CHANGE" line9 &&
test_path_is_missing backend.log
'
test_expect_success 'textconv output is never identified to the process' '
test_when_finished "rm -f backend.log" &&
echo "*.tcv diff=tcv" >>.gitattributes &&
git add .gitattributes &&
git commit -m tcv-attr &&
test_config diff.tcv.textconv cat &&
test_commit tcv1 file.tcv "alpha" &&
test_commit tcv2 file.tcv "alpha
beta" &&
git blame file.tcv >expect &&
git -c diff.tcv.process="$BACKEND --mode=oid-fixed --log=backend.log" \
blame file.tcv >actual &&
test_cmp expect actual &&
test_path_is_missing backend.log
'
test_expect_success 'a gitlink side is never identified to the process' '
test_when_finished "rm -f backend.log" &&
echo "sub diff=cdiff" >>.gitattributes &&
git add .gitattributes &&
git commit -m sub-attr &&
C1=$(git rev-parse HEAD) &&
C2=$(git rev-parse HEAD~1) &&
git update-index --add --cacheinfo 160000,$C1,sub &&
git commit -m sub-1 &&
git update-index --add --cacheinfo 160000,$C2,sub &&
git commit -m sub-2 &&
git -c diff.cdiff.process="$BACKEND --mode=oid-fixed --log=backend.log" \
log -1 --format= --numstat -- sub >actual &&
printf "1\t1\tsub\n" >expect &&
test_cmp expect actual &&
test_path_is_missing backend.log
'
test_expect_success 'a relative diff consults by the repo-relative path' '
test_when_finished "rm -f backend.log" &&
echo "reldir/*.rel diff=rdrv" >>.gitattributes &&
git add .gitattributes &&
git commit -m rel-attr &&
mkdir reldir &&
test_write_lines line1 line2 line3 line4 original5 original6 \
line7 line8 line9 line10 >reldir/x.rel &&
git add reldir/x.rel &&
git commit -m "add x.rel" &&
test_write_lines line1 line2 line3 line4 changed5 changed6 \
line7 line8 changed9 changed10 >reldir/x.rel &&
git add reldir/x.rel &&
git commit -m "change x.rel" &&
# diff.relative strips the prefix from the displayed name; the
# driver lookup and the request pathname must still use the
# repo-relative path, or the directory-scoped attribute above
# would not match and the process would never be consulted.
# The process runs at the repository root, so its log lands there.
(
cd reldir &&
git -c diff.relative=true \
-c diff.rdrv.process="$BACKEND --mode=oid-fixed --log=backend.log" \
log -1 --format= --numstat -- x.rel
) >actual &&
printf "2\t2\tx.rel\n" >expect &&
test_cmp expect actual &&
test_grep "command=hunks-by-oid pathname=reldir/x.rel" backend.log
'
test_expect_success 'an empty file side is answered with a start of zero' '
test_when_finished "rm -f backend.log" &&
>empty.c &&
git add empty.c &&
git commit -m "add empty.c" &&
printf "x\ny\nz\n" >empty.c &&
git add empty.c &&
git commit -m "fill empty.c" &&
# The process addresses the empty old side with a start of 0 and a
# count of 0, and claims two of the three added lines. The answer is
# used as sent, so the stat shows the two lines it named, not the
# three the builtin would.
git -c diff.cdiff.process="$BACKEND --mode=oid-empty --log=backend.log" \
log -1 --format= --numstat -- empty.c >actual &&
printf "2\t0\tempty.c\n" >expect &&
test_cmp expect actual &&
test_grep "command=hunks-by-oid pathname=empty.c" backend.log
'
test_done