| #!/bin/sh |
| |
| test_description='diff process via long-running process' |
| |
| . ./test-lib.sh |
| |
| # See t/helper/test-diff-process-backend.c for the backend implementation |
| # and available --mode= options. |
| |
| BACKEND="test-tool diff-process-backend" |
| |
| test_expect_success 'setup' ' |
| echo "*.c diff=cdiff" >.gitattributes && |
| git add .gitattributes && |
| |
| # boundary.c: 10 lines, changes at 5-6 and 9-10. |
| # Used by: hunk boundaries, error fallback, crash, bad hunks, overlap. |
| cat >boundary.c <<-\EOF && |
| line1 |
| line2 |
| line3 |
| line4 |
| OLD5 |
| OLD6 |
| line7 |
| line8 |
| OLD9 |
| OLD10 |
| EOF |
| git add boundary.c && |
| |
| # worddiff.c: single-line function, value changes 1 -> 999. |
| # Used by: word-diff, --diff-algorithm, --no-ext-diff, --stat. |
| cat >worddiff.c <<-\EOF && |
| int value(void) { return 1; } |
| EOF |
| git add worddiff.c && |
| |
| # newfile.c: single-line function, value changes 42 -> 99. |
| # Used by: modified file, --exit-code, multiple drivers. |
| cat >newfile.c <<-\EOF && |
| int new_func(void) { return 42; } |
| EOF |
| git add newfile.c && |
| |
| # logtest.c: single-line function for log/format-patch tests. |
| # Needs two commits so log -1 has a diff. |
| cat >logtest.c <<-\EOF && |
| int logfunc(void) { return 1; } |
| EOF |
| git add logtest.c && |
| |
| # two.c/one.c: two-file pair for error/abort/startup-failure tests. |
| cat >one.c <<-\EOF && |
| int first(void) { return 1; } |
| EOF |
| cat >two.c <<-\EOF && |
| int second(void) { return 2; } |
| EOF |
| git add one.c two.c && |
| |
| git commit -m "initial" && |
| |
| # Second commit for logtest.c (so log -1 has something to show). |
| cat >logtest.c <<-\EOF && |
| int logfunc(void) { return 2; } |
| EOF |
| git add logtest.c && |
| git commit -m "change logtest.c" && |
| |
| # Working tree modifications (not committed). |
| cat >boundary.c <<-\EOF && |
| line1 |
| line2 |
| line3 |
| line4 |
| NEW5 |
| NEW6 |
| line7 |
| line8 |
| NEW9 |
| NEW10 |
| EOF |
| |
| cat >worddiff.c <<-\EOF && |
| int value(void) { return 999; } |
| EOF |
| |
| cat >newfile.c <<-\EOF && |
| int new_func(void) { return 99; } |
| EOF |
| |
| cat >one.c <<-\EOF && |
| int first(void) { return 10; } |
| EOF |
| |
| cat >two.c <<-\EOF |
| int second(void) { return 20; } |
| EOF |
| ' |
| |
| # |
| # Core behavior: the tool controls which lines are marked as changed. |
| # |
| |
| test_expect_success 'diff process hunk boundaries affect output' ' |
| # The file has changes at lines 5-6 and 9-10, but fixed-hunk |
| # only reports lines 5-6 as changed. Lines 9-10 should not |
| # appear as changed in the output. |
| git -c diff.cdiff.process="$BACKEND --mode=fixed-hunk" \ |
| diff boundary.c >actual && |
| test_grep "^-OLD5" actual && |
| test_grep "^-OLD6" actual && |
| test_grep "^+NEW5" actual && |
| test_grep "^+NEW6" actual && |
| test_grep ! "^-OLD9" actual && |
| test_grep ! "^-OLD10" actual && |
| test_grep ! "^+NEW9" actual && |
| test_grep ! "^+NEW10" actual |
| ' |
| |
| test_expect_success 'diff process works with modified file' ' |
| test_when_finished "rm -f backend.log" && |
| git -c diff.cdiff.process="$BACKEND --log=backend.log" \ |
| diff -- newfile.c >actual 2>stderr && |
| test_grep "return 99" actual && |
| test_grep "pathname=newfile.c" backend.log && |
| test_must_be_empty stderr |
| ' |
| |
| test_expect_success 'diff process works with added file (empty old side)' ' |
| cat >added.c <<-\EOF && |
| int added(void) { return 1; } |
| EOF |
| git add added.c && |
| |
| test_when_finished "rm -f backend.log" && |
| git -c diff.cdiff.process="$BACKEND --log=backend.log" \ |
| diff --cached -- added.c >actual 2>stderr && |
| test_grep "added" actual && |
| test_grep "pathname=added.c" backend.log && |
| test_must_be_empty stderr |
| ' |
| |
| test_expect_success 'diff process works with deleted file (empty new side)' ' |
| git add added.c && |
| git commit -m "commit added.c" && |
| git rm added.c && |
| |
| test_when_finished "rm -f backend.log" && |
| git -c diff.cdiff.process="$BACKEND --log=backend.log" \ |
| diff --cached -- added.c >actual 2>stderr && |
| test_grep "deleted file" actual && |
| test_grep "pathname=added.c" backend.log && |
| test_must_be_empty stderr |
| ' |
| |
| test_expect_success 'diff process skipped for binary files' ' |
| printf "\\0binary" >binary.c && |
| git add binary.c && |
| git commit -m "add binary" && |
| printf "\\0changed" >binary.c && |
| |
| test_when_finished "rm -f backend.log" && |
| git -c diff.cdiff.process="$BACKEND --log=backend.log" \ |
| diff -- binary.c >actual && |
| test_grep "Binary files" actual && |
| test_path_is_missing backend.log |
| ' |
| |
| test_expect_success 'diff process not consulted for unmatched driver' ' |
| echo "not tracked by cdiff" >unmatched.txt && |
| git add unmatched.txt && |
| git commit -m "add unmatched.txt" && |
| |
| echo "modified" >unmatched.txt && |
| |
| test_when_finished "rm -f backend.log" && |
| git -c diff.cdiff.process="$BACKEND --log=backend.log" \ |
| diff -- unmatched.txt >actual && |
| test_grep "modified" actual && |
| test_path_is_missing backend.log |
| ' |
| |
| test_expect_success 'multiple drivers use separate processes' ' |
| echo "*.h diff=hdiff" >>.gitattributes && |
| git add .gitattributes && |
| |
| cat >multi.h <<-\EOF && |
| int header(void) { return 1; } |
| EOF |
| git add multi.h && |
| git commit -m "add multi.h" && |
| |
| cat >multi.h <<-\EOF && |
| int header(void) { return 2; } |
| EOF |
| |
| test_when_finished "rm -f backend-c.log backend-h.log" && |
| git -c diff.cdiff.process="$BACKEND --log=backend-c.log" \ |
| -c diff.hdiff.process="$BACKEND --log=backend-h.log" \ |
| diff -- newfile.c multi.h >actual 2>stderr && |
| test_grep "pathname=newfile.c" backend-c.log && |
| test_grep "pathname=multi.h" backend-h.log && |
| test_must_be_empty stderr |
| ' |
| |
| test_expect_success 'diff process works alongside textconv' ' |
| write_script uppercase-filter <<-\EOF && |
| tr "a-z" "A-Z" <"$1" |
| EOF |
| |
| cat >textconv.c <<-\EOF && |
| hello world |
| EOF |
| git add textconv.c && |
| git commit -m "add textconv.c" && |
| |
| cat >textconv.c <<-\EOF && |
| goodbye world |
| EOF |
| |
| test_when_finished "rm -f backend.log" && |
| git -c diff.cdiff.textconv="./uppercase-filter" \ |
| -c diff.cdiff.process="$BACKEND --log=backend.log" \ |
| diff -- textconv.c >actual 2>stderr && |
| # The diff process receives textconv-transformed (uppercase) content. |
| test_grep "pathname=textconv.c" backend.log && |
| test_grep "old=HELLO WORLD" backend.log && |
| test_grep "new=GOODBYE WORLD" backend.log && |
| test_must_be_empty stderr |
| ' |
| |
| # |
| # Downstream features: word diff, log, equivalent files, exit code. |
| # |
| |
| test_expect_success 'diff process with --word-diff' ' |
| test_when_finished "rm -f backend.log" && |
| git -c diff.cdiff.process="$BACKEND --log=backend.log" \ |
| diff --word-diff worddiff.c >actual 2>stderr && |
| test_grep "\[-1;-\]" actual && |
| test_grep "{+999;+}" actual && |
| test_grep "pathname=worddiff.c" backend.log && |
| test_must_be_empty stderr |
| ' |
| |
| test_expect_success 'diff process works with git log -p' ' |
| # With no-hunks mode, the tool says the files are equivalent, |
| # so log -p should show the commit but no diff content. |
| test_when_finished "rm -f backend.log" && |
| git -c diff.cdiff.process="$BACKEND --mode=no-hunks --log=backend.log" \ |
| log -1 -p -- logtest.c >actual 2>stderr && |
| test_grep "change logtest.c" actual && |
| test_grep ! "return 2" actual && |
| test_grep "command=hunks pathname=logtest.c" backend.log && |
| test_must_be_empty stderr |
| ' |
| |
| test_expect_success 'diff process no hunks suppresses diff output' ' |
| cat >nohunks.c <<-\EOF && |
| int zero(void) { return 0; } |
| EOF |
| git add nohunks.c && |
| git commit -m "add nohunks.c" && |
| |
| cat >nohunks.c <<-\EOF && |
| int zero(void) { return 999; } |
| EOF |
| |
| git -c diff.cdiff.process="$BACKEND --mode=no-hunks" \ |
| diff nohunks.c >actual && |
| test_must_be_empty actual |
| ' |
| |
| test_expect_success 'diff process no hunks with --exit-code returns success' ' |
| git -c diff.cdiff.process="$BACKEND --mode=no-hunks" \ |
| diff --exit-code nohunks.c |
| ' |
| |
| test_expect_success 'diff process with --exit-code and hunks returns failure' ' |
| test_expect_code 1 git -c diff.cdiff.process="$BACKEND" \ |
| diff --exit-code newfile.c |
| ' |
| |
| # |
| # Bypass mechanisms: flags and commands that skip the diff process. |
| # |
| |
| test_expect_success 'diff process bypassed by --diff-algorithm' ' |
| test_when_finished "rm -f backend.log" && |
| git -c diff.cdiff.process="$BACKEND --log=backend.log" \ |
| diff --diff-algorithm=patience worddiff.c >actual && |
| test_grep "return 999" 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 --log=backend.log" \ |
| diff --no-ext-diff worddiff.c >actual && |
| test_grep "return 999" actual && |
| test_path_is_missing backend.log |
| ' |
| |
| test_expect_success 'diff process not used by format-patch' ' |
| test_when_finished "rm -f backend.log" && |
| git -c diff.cdiff.process="$BACKEND --log=backend.log" \ |
| format-patch -1 --stdout -- logtest.c >actual && |
| test_grep "return 2" actual && |
| test_path_is_missing backend.log |
| ' |
| |
| test_expect_success 'diff process not used by --stat' ' |
| test_when_finished "rm -f backend.log" && |
| git -c diff.cdiff.process="$BACKEND --log=backend.log" \ |
| diff --stat worddiff.c >actual && |
| test_grep "worddiff.c" actual && |
| test_path_is_missing backend.log |
| ' |
| |
| # |
| # Error handling and fallback. |
| # |
| |
| test_expect_success 'diff process fallback on tool error status' ' |
| test_when_finished "rm -f backend.log" && |
| git -c diff.cdiff.process="$BACKEND --mode=error --log=backend.log" \ |
| diff boundary.c >actual 2>stderr && |
| # Fallback produces the full builtin diff (both change regions). |
| test_grep "^-OLD5" actual && |
| test_grep "^+NEW5" actual && |
| test_grep "^-OLD9" actual && |
| test_grep "^+NEW9" actual && |
| # Tool was contacted (it replied with error, not crash). |
| test_grep "command=hunks pathname=boundary.c" backend.log && |
| test_grep "diff process.*failed" stderr |
| ' |
| |
| test_expect_success 'diff process error keeps tool available for next file' ' |
| test_when_finished "rm -f backend.log" && |
| git -c diff.cdiff.process="$BACKEND --mode=error --log=backend.log" \ |
| diff -- one.c two.c >actual 2>stderr && |
| # Unlike abort, error keeps the tool available: both files |
| # are sent to the tool (and both fall back). |
| test_grep "pathname=one.c" backend.log && |
| test_grep "pathname=two.c" backend.log && |
| test_grep "return 10" actual && |
| test_grep "return 20" actual && |
| test_grep "diff process.*failed" stderr |
| ' |
| |
| test_expect_success 'diff process abort disables for session' ' |
| test_when_finished "rm -f backend.log" && |
| git -c diff.cdiff.process="$BACKEND --mode=abort --log=backend.log" \ |
| diff -- one.c two.c >actual 2>stderr && |
| # Both files should still produce diff output via fallback. |
| test_grep "return 10" actual && |
| test_grep "return 20" actual && |
| # The tool aborts on the first file and git clears its |
| # capability. The second file never contacts the tool. |
| test_grep "pathname=one.c" backend.log && |
| test_grep ! "pathname=two.c" backend.log && |
| test_must_be_empty stderr |
| ' |
| |
| test_expect_success 'diff process fallback on tool crash' ' |
| git -c diff.cdiff.process="$BACKEND --mode=crash" \ |
| diff boundary.c >actual 2>stderr && |
| test_grep "^-OLD5" actual && |
| test_grep "^+NEW5" actual && |
| test_grep "^-OLD9" actual && |
| test_grep "^+NEW9" actual && |
| # Crash is a communication failure, so a warning is emitted. |
| test_grep "diff process.*failed" stderr |
| ' |
| |
| test_expect_success 'diff process startup failure only warns once' ' |
| git -c diff.cdiff.process="/nonexistent/tool" \ |
| diff -- one.c two.c >actual 2>stderr && |
| # Both files produce diff output via fallback. |
| test_grep "return 10" actual && |
| test_grep "return 20" actual && |
| # Sentinel prevents repeated warnings: only one, not one per file. |
| test_grep "diff process.*failed" stderr >warnings && |
| test_line_count = 1 warnings |
| ' |
| |
| |
| test_expect_success 'diff process fallback on bad hunks' ' |
| git -c diff.cdiff.process="$BACKEND --mode=bad-hunk" \ |
| diff boundary.c >actual 2>stderr && |
| test_grep "^-OLD5" actual && |
| test_grep "^+NEW5" actual && |
| test_grep "^-OLD9" actual && |
| test_grep "^+NEW9" actual && |
| test_grep "exceeds.*lines" stderr |
| ' |
| |
| test_expect_success 'diff process fallback on mismatched unchanged totals' ' |
| cat >synctest.c <<-\EOF && |
| line1 |
| line2 |
| line3 |
| EOF |
| git add synctest.c && |
| git commit -m "add synctest.c" && |
| |
| cat >synctest.c <<-\EOF && |
| line1 |
| changed |
| line3 |
| EOF |
| |
| # bad-sync reports hunk 1 2 1 1: marks 2 old lines and 1 new |
| # line as changed, leaving 1 unchanged old vs 2 unchanged new. |
| # The synchronization invariant fails and git falls back. |
| git -c diff.cdiff.process="$BACKEND --mode=bad-sync" \ |
| diff synctest.c >actual 2>stderr && |
| test_grep "changed" actual && |
| test_grep "unchanged line count mismatch" stderr |
| ' |
| |
| test_expect_success 'diff process fallback on overlapping hunks' ' |
| # boundary.c has 10 lines, so both hunks are in bounds |
| # but they overlap at lines 3-5, triggering the ordering check. |
| git -c diff.cdiff.process="$BACKEND --mode=overlap" \ |
| diff boundary.c >actual 2>stderr && |
| test_grep "NEW5" actual && |
| test_grep "overlaps with previous" stderr |
| ' |
| |
| test_expect_success 'diff process fallback on malformed hunk line' ' |
| git -c diff.cdiff.process="$BACKEND --mode=bad-parse" \ |
| diff boundary.c >actual 2>stderr && |
| test_grep "^-OLD5" actual && |
| test_grep "^+NEW5" actual |
| ' |
| |
| test_expect_success 'diff process skipped when tool omits capability' ' |
| git -c diff.cdiff.process="$BACKEND --mode=no-cap" \ |
| diff boundary.c >actual 2>stderr && |
| test_grep "^-OLD5" actual && |
| test_grep "^+NEW5" actual && |
| test_must_be_empty stderr |
| ' |
| |
| # |
| # Blame integration. |
| # |
| |
| test_expect_success 'blame uses tool-provided hunks' ' |
| cat >blame-hunk.c <<-\EOF && |
| line1 |
| line2 |
| line3 |
| line4 |
| original5 |
| original6 |
| line7 |
| line8 |
| line9 |
| line10 |
| EOF |
| git add blame-hunk.c && |
| git commit -m "add blame-hunk.c" && |
| ORIG=$(git rev-parse --short HEAD) && |
| |
| cat >blame-hunk.c <<-\EOF && |
| line1 |
| line2 |
| line3 |
| line4 |
| changed5 |
| changed6 |
| line7 |
| line8 |
| changed9 |
| changed10 |
| EOF |
| git add blame-hunk.c && |
| git commit -m "change blame-hunk.c" && |
| CHANGE=$(git rev-parse --short HEAD) && |
| |
| # With fixed-hunk mode the tool reports only lines 5-6 as changed, |
| # so blame should attribute lines 9-10 to the original commit |
| # even though the builtin diff would show them as changed. |
| git -c diff.cdiff.process="$BACKEND --mode=fixed-hunk" \ |
| blame blame-hunk.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 && |
| sed -n "6p" actual >line6 && |
| test_grep "$CHANGE" line5 && |
| test_grep "$CHANGE" line6 |
| ' |
| |
| test_expect_success 'blame skips commits with no hunks from diff process' ' |
| cat >blame.c <<-\EOF && |
| int main(void) { |
| return 0; |
| } |
| EOF |
| git add blame.c && |
| git commit -m "add blame.c" && |
| ORIG_COMMIT=$(git rev-parse --short HEAD) && |
| |
| cat >blame.c <<-\EOF && |
| int main(void) |
| { |
| return 0; |
| } |
| EOF |
| git add blame.c && |
| git commit -m "reformat blame.c" && |
| BLAME_COMMIT=$(git rev-parse --short HEAD) && |
| |
| # Without no-hunks mode, blame attributes the change. |
| git blame blame.c >without && |
| test_grep "$BLAME_COMMIT" without && |
| |
| # With no-hunks mode, the process considers the files equivalent |
| # and blame skips the reformat commit, attributing to the original. |
| git -c diff.cdiff.process="$BACKEND --mode=no-hunks" \ |
| blame blame.c >with && |
| test_grep ! "$BLAME_COMMIT" with && |
| test_grep "$ORIG_COMMIT" with |
| ' |
| |
| test_expect_success 'blame --no-ext-diff bypasses diff process' ' |
| test_when_finished "rm -f backend.log" && |
| git -c diff.cdiff.process="$BACKEND --mode=no-hunks --log=backend.log" \ |
| blame --no-ext-diff blame.c >actual && |
| # Without the process, blame attributes the reformat commit normally. |
| test_grep "$BLAME_COMMIT" actual && |
| test_path_is_missing backend.log |
| ' |
| |
| test_expect_success 'blame --no-ext-diff uses builtin hunks' ' |
| # fixed-hunk mode would narrow blame to lines 5-6, but |
| # --no-ext-diff should bypass it and use the builtin diff. |
| test_when_finished "rm -f backend.log" && |
| git -c diff.cdiff.process="$BACKEND --mode=fixed-hunk --log=backend.log" \ |
| blame --no-ext-diff blame-hunk.c >actual && |
| # Builtin diff attributes lines 9-10 to the change commit. |
| sed -n "9p" actual >line9 && |
| test_grep "$CHANGE" line9 && |
| test_path_is_missing backend.log |
| ' |
| |
| test_done |