Skip to content

Commit d7b97b7

Browse files
rscharfegitster
authored andcommitted
diff: let external diffs report that changes are uninteresting
The options --exit-code and --quiet instruct git diff to indicate whether it found any significant changes by exiting with code 1 if it did and 0 if there were none. Currently this doesn't work if external diff programs are involved, as we have no way to learn what they found. Add that ability in the form of the new configuration options diff.trustExitCode and diff.<driver>.trustExitCode and the environment variable GIT_EXTERNAL_DIFF_TRUST_EXIT_CODE. They pair with the config options diff.external and diff.<driver>.command and the environment variable GIT_EXTERNAL_DIFF, respectively. The new options are off by default, keeping the old behavior. Enabling them indicates that the external diff returns exit code 1 if it finds significant changes and 0 if it doesn't, like diff(1). The name of the new options is taken from the git difftool and mergetool options of similar purpose. (There they enable passing on the exit code of a diff tool and to infer whether a merge done by a merge tool is successful.) The new feature sets the diff flag diff_from_contents in diff_setup_done() if we need the exit code and are allowed to call external diffs. This disables the optimization that avoids calling the program with --quiet. Add it back by skipping the call if the external diff is not able to report empty diffs. We can only do that check after evaluating the file-specific attributes in run_external_diff(). If we do run the external diff with --quiet, send its output to /dev/null. I considered checking the output of the external diff to check whether its empty. It was added as 11be65c (diff: fix --exit-code with external diff, 2024-05-05) and quickly reverted, as it does not work with external diffs that do not write to stdout. There's no reason why a graphical diff tool would even need to write anything there at all. I also considered using a non-zero exit code for empty diffs, which could be done without adding new configuration options. We'd need to disable the optimization that allows git diff --quiet to skip calling external diffs, though -- that might be quite surprising if graphical diff programs are involved. And assigning the opposite meaning of the exit codes compared to diff(1) and git diff --exit-code to the external diff can cause unnecessary confusion. Suggested-by: Phillip Wood <[email protected]> Signed-off-by: René Scharfe <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 54443bb commit d7b97b7

File tree

8 files changed

+101
-12
lines changed

8 files changed

+101
-12
lines changed

Documentation/config/diff.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ diff.external::
7979
you want to use an external diff program only on a subset of
8080
your files, you might want to use linkgit:gitattributes[5] instead.
8181

82+
diff.trustExitCode::
83+
If this boolean value is set to true then the
84+
`diff.external` command is expected to return exit code
85+
0 if it considers the input files to be equal or 1 if it
86+
considers them to be different, like `diff(1)`.
87+
If it is set to false, which is the default, then the command
88+
is expected to return exit code 0 regardless of equality.
89+
Any other exit code causes Git to report a fatal error.
90+
8291
diff.ignoreSubmodules::
8392
Sets the default value of --ignore-submodules. Note that this
8493
affects only 'git diff' Porcelain, and not lower level 'diff'
@@ -164,6 +173,15 @@ diff.<driver>.command::
164173
The custom diff driver command. See linkgit:gitattributes[5]
165174
for details.
166175

176+
diff.<driver>.trustExitCode::
177+
If this boolean value is set to true then the
178+
`diff.<driver>.command` command is expected to return exit code
179+
0 if it considers the input files to be equal or 1 if it
180+
considers them to be different, like `diff(1)`.
181+
If it is set to false, which is the default, then the command
182+
is expected to return exit code 0 regardless of equality.
183+
Any other exit code causes Git to report a fatal error.
184+
167185
diff.<driver>.xfuncname::
168186
The regular expression that the diff driver should use to
169187
recognize the hunk header. A built-in pattern may also be used.

Documentation/diff-options.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -820,7 +820,11 @@ ifndef::git-log[]
820820

821821
--quiet::
822822
Disable all output of the program. Implies `--exit-code`.
823-
Disables execution of external diff helpers.
823+
Disables execution of external diff helpers whose exit code
824+
is not trusted, i.e. their respective configuration option
825+
`diff.trustExitCode` or `diff.<driver>.trustExitCode` or
826+
environment variable `GIT_EXTERNAL_DIFF_TRUST_EXIT_CODE` is
827+
false.
824828
endif::git-log[]
825829
endif::git-format-patch[]
826830

Documentation/git.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,16 @@ parameter, <path>.
644644
For each path `GIT_EXTERNAL_DIFF` is called, two environment variables,
645645
`GIT_DIFF_PATH_COUNTER` and `GIT_DIFF_PATH_TOTAL` are set.
646646

647+
`GIT_EXTERNAL_DIFF_TRUST_EXIT_CODE`::
648+
If this Boolean environment variable is set to true then the
649+
`GIT_EXTERNAL_DIFF` command is expected to return exit code
650+
0 if it considers the input files to be equal or 1 if it
651+
considers them to be different, like `diff(1)`.
652+
If it is set to false, which is the default, then the command
653+
is expected to return exit code 0 regardless of equality.
654+
Any other exit code causes Git to report a fatal error.
655+
656+
647657
`GIT_DIFF_PATH_COUNTER`::
648658
A 1-based counter incremented by one for every path.
649659

Documentation/gitattributes.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,11 @@ with the above configuration, i.e. `j-c-diff`, with 7
776776
parameters, just like `GIT_EXTERNAL_DIFF` program is called.
777777
See linkgit:git[1] for details.
778778

779+
If the program is able to ignore certain changes (similar to
780+
`git diff --ignore-space-change`), then also set the option
781+
`trustExitCode` to true. It is then expected to return exit code 1 if
782+
it finds significant changes and 0 if it doesn't.
783+
779784
Setting the internal diff algorithm
780785
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
781786

diff.c

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,10 @@ int git_diff_ui_config(const char *var, const char *value,
432432
}
433433
if (!strcmp(var, "diff.external"))
434434
return git_config_string(&external_diff_cfg.cmd, var, value);
435+
if (!strcmp(var, "diff.trustexitcode")) {
436+
external_diff_cfg.trust_exit_code = git_config_bool(var, value);
437+
return 0;
438+
}
435439
if (!strcmp(var, "diff.wordregex"))
436440
return git_config_string(&diff_word_regex_cfg, var, value);
437441
if (!strcmp(var, "diff.orderfile"))
@@ -556,6 +560,8 @@ static const struct external_diff *external_diff(void)
556560
if (done_preparing)
557561
return external_diff_ptr;
558562
external_diff_env.cmd = xstrdup_or_null(getenv("GIT_EXTERNAL_DIFF"));
563+
if (git_env_bool("GIT_EXTERNAL_DIFF_TRUST_EXIT_CODE", 0))
564+
external_diff_env.trust_exit_code = 1;
559565
if (external_diff_env.cmd)
560566
external_diff_ptr = &external_diff_env;
561567
else if (external_diff_cfg.cmd)
@@ -4387,6 +4393,19 @@ static void run_external_diff(const struct external_diff *pgm,
43874393
{
43884394
struct child_process cmd = CHILD_PROCESS_INIT;
43894395
struct diff_queue_struct *q = &diff_queued_diff;
4396+
int quiet = !(o->output_format & DIFF_FORMAT_PATCH);
4397+
int rc;
4398+
4399+
/*
4400+
* Trivial equality is handled by diff_unmodified_pair() before
4401+
* we get here. If we don't need to show the diff and the
4402+
* external diff program lacks the ability to tell us whether
4403+
* it's empty then we consider it non-empty without even asking.
4404+
*/
4405+
if (!pgm->trust_exit_code && quiet) {
4406+
o->found_changes = 1;
4407+
return;
4408+
}
43904409

43914410
strvec_push(&cmd.args, pgm->cmd);
43924411
strvec_push(&cmd.args, name);
@@ -4408,7 +4427,15 @@ static void run_external_diff(const struct external_diff *pgm,
44084427
diff_free_filespec_data(one);
44094428
diff_free_filespec_data(two);
44104429
cmd.use_shell = 1;
4411-
if (run_command(&cmd))
4430+
cmd.no_stdout = quiet;
4431+
rc = run_command(&cmd);
4432+
if (!pgm->trust_exit_code && rc == 0)
4433+
o->found_changes = 1;
4434+
else if (pgm->trust_exit_code && rc == 0)
4435+
; /* nothing */
4436+
else if (pgm->trust_exit_code && rc == 1)
4437+
o->found_changes = 1;
4438+
else
44124439
die(_("external diff died, stopping at %s"), name);
44134440

44144441
remove_tempfile();
@@ -4926,6 +4953,13 @@ void diff_setup_done(struct diff_options *options)
49264953
options->flags.exit_with_status = 1;
49274954
}
49284955

4956+
/*
4957+
* External diffs could declare non-identical contents equal
4958+
* (think diff --ignore-space-change).
4959+
*/
4960+
if (options->flags.allow_external && options->flags.exit_with_status)
4961+
options->flags.diff_from_contents = 1;
4962+
49294963
options->diff_path_counter = 0;
49304964

49314965
if (options->flags.follow_renames)

t/t4020-diff-external.sh

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -177,15 +177,17 @@ check_external_diff () {
177177
expect_out=$2
178178
expect_err=$3
179179
command_code=$4
180-
shift 4
180+
trust_exit_code=$5
181+
shift 5
181182
options="$@"
182183

183184
command="echo output; exit $command_code;"
184-
desc="external diff '$command'"
185+
desc="external diff '$command' with trustExitCode=$trust_exit_code"
185186
with_options="${options:+ with }$options"
186187

187188
test_expect_success "$desc via attribute$with_options" "
188189
test_config diff.foo.command \"$command\" &&
190+
test_config diff.foo.trustExitCode $trust_exit_code &&
189191
echo \"file diff=foo\" >.gitattributes &&
190192
test_expect_code $expect_code git diff $options >out 2>err &&
191193
test_cmp $expect_out out &&
@@ -194,6 +196,7 @@ check_external_diff () {
194196

195197
test_expect_success "$desc via diff.external$with_options" "
196198
test_config diff.external \"$command\" &&
199+
test_config diff.trustExitCode $trust_exit_code &&
197200
>.gitattributes &&
198201
test_expect_code $expect_code git diff $options >out 2>err &&
199202
test_cmp $expect_out out &&
@@ -204,6 +207,7 @@ check_external_diff () {
204207
>.gitattributes &&
205208
test_expect_code $expect_code env \
206209
GIT_EXTERNAL_DIFF=\"$command\" \
210+
GIT_EXTERNAL_DIFF_TRUST_EXIT_CODE=$trust_exit_code \
207211
git diff $options >out 2>err &&
208212
test_cmp $expect_out out &&
209213
test_cmp $expect_err err
@@ -216,14 +220,23 @@ test_expect_success 'setup output files' '
216220
echo "fatal: external diff died, stopping at file" >error
217221
'
218222

219-
check_external_diff 0 output empty 0
220-
check_external_diff 128 output error 1
221-
222-
check_external_diff 1 output empty 0 --exit-code
223-
check_external_diff 128 output error 1 --exit-code
224-
225-
check_external_diff 1 empty empty 0 --quiet
226-
check_external_diff 1 empty empty 1 --quiet # we don't even call the program
223+
check_external_diff 0 output empty 0 off
224+
check_external_diff 128 output error 1 off
225+
check_external_diff 0 output empty 0 on
226+
check_external_diff 0 output empty 1 on
227+
check_external_diff 128 output error 2 on
228+
229+
check_external_diff 1 output empty 0 off --exit-code
230+
check_external_diff 128 output error 1 off --exit-code
231+
check_external_diff 0 output empty 0 on --exit-code
232+
check_external_diff 1 output empty 1 on --exit-code
233+
check_external_diff 128 output error 2 on --exit-code
234+
235+
check_external_diff 1 empty empty 0 off --quiet
236+
check_external_diff 1 empty empty 1 off --quiet # we don't even call the program
237+
check_external_diff 0 empty empty 0 on --quiet
238+
check_external_diff 1 empty empty 1 on --quiet
239+
check_external_diff 128 empty error 2 on --quiet
227240

228241
echo NULZbetweenZwords | perl -pe 'y/Z/\000/' > file
229242

userdiff.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,10 @@ int userdiff_config(const char *k, const char *v)
446446
return parse_tristate(&drv->binary, k, v);
447447
if (!strcmp(type, "command"))
448448
return git_config_string(&drv->external.cmd, k, v);
449+
if (!strcmp(type, "trustexitcode")) {
450+
drv->external.trust_exit_code = git_config_bool(k, v);
451+
return 0;
452+
}
449453
if (!strcmp(type, "textconv"))
450454
return git_config_string(&drv->textconv, k, v);
451455
if (!strcmp(type, "cachetextconv"))

userdiff.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ struct userdiff_funcname {
1313

1414
struct external_diff {
1515
char *cmd;
16+
unsigned trust_exit_code:1;
1617
};
1718

1819
struct userdiff_driver {

0 commit comments

Comments
 (0)