Skip to content

Commit 8ba7dbd

Browse files
committed
Merge branch 'rs/diff-exit-code-with-external-diff'
"git diff --exit-code --ext-diff" learned to take the exit status of the external diff driver into account when deciding the exit status of the overall "git diff" invocation when configured to do so. * rs/diff-exit-code-with-external-diff: diff: let external diffs report that changes are uninteresting userdiff: add and use struct external_diff t4020: test exit code with external diffs
2 parents e631115 + d7b97b7 commit 8ba7dbd

File tree

8 files changed

+168
-19
lines changed

8 files changed

+168
-19
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 & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +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 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.
823828
endif::git-log[]
824829
endif::git-format-patch[]
825830

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: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ static int diff_color_moved_ws_default;
5757
static int diff_context_default = 3;
5858
static int diff_interhunk_context_default;
5959
static char *diff_word_regex_cfg;
60-
static char *external_diff_cmd_cfg;
60+
static struct external_diff external_diff_cfg;
6161
static char *diff_order_file_cfg;
6262
int diff_auto_refresh_index = 1;
6363
static int diff_mnemonic_prefix;
@@ -431,7 +431,11 @@ int git_diff_ui_config(const char *var, const char *value,
431431
return 0;
432432
}
433433
if (!strcmp(var, "diff.external"))
434-
return git_config_string(&external_diff_cmd_cfg, var, value);
434+
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"))
@@ -548,18 +552,22 @@ static char *quote_two(const char *one, const char *two)
548552
return strbuf_detach(&res, NULL);
549553
}
550554

551-
static const char *external_diff(void)
555+
static const struct external_diff *external_diff(void)
552556
{
553-
static const char *external_diff_cmd = NULL;
557+
static struct external_diff external_diff_env, *external_diff_ptr;
554558
static int done_preparing = 0;
555559

556560
if (done_preparing)
557-
return external_diff_cmd;
558-
external_diff_cmd = xstrdup_or_null(getenv("GIT_EXTERNAL_DIFF"));
559-
if (!external_diff_cmd)
560-
external_diff_cmd = external_diff_cmd_cfg;
561+
return external_diff_ptr;
562+
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;
565+
if (external_diff_env.cmd)
566+
external_diff_ptr = &external_diff_env;
567+
else if (external_diff_cfg.cmd)
568+
external_diff_ptr = &external_diff_cfg;
561569
done_preparing = 1;
562-
return external_diff_cmd;
570+
return external_diff_ptr;
563571
}
564572

565573
/*
@@ -4375,7 +4383,7 @@ static void add_external_diff_name(struct repository *r,
43754383
* infile2 infile2-sha1 infile2-mode [ rename-to ]
43764384
*
43774385
*/
4378-
static void run_external_diff(const char *pgm,
4386+
static void run_external_diff(const struct external_diff *pgm,
43794387
const char *name,
43804388
const char *other,
43814389
struct diff_filespec *one,
@@ -4385,8 +4393,21 @@ static void run_external_diff(const char *pgm,
43854393
{
43864394
struct child_process cmd = CHILD_PROCESS_INIT;
43874395
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+
}
43884409

4389-
strvec_push(&cmd.args, pgm);
4410+
strvec_push(&cmd.args, pgm->cmd);
43904411
strvec_push(&cmd.args, name);
43914412

43924413
if (one && two) {
@@ -4406,7 +4427,15 @@ static void run_external_diff(const char *pgm,
44064427
diff_free_filespec_data(one);
44074428
diff_free_filespec_data(two);
44084429
cmd.use_shell = 1;
4409-
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
44104439
die(_("external diff died, stopping at %s"), name);
44114440

44124441
remove_tempfile();
@@ -4512,7 +4541,7 @@ static void fill_metainfo(struct strbuf *msg,
45124541
}
45134542
}
45144543

4515-
static void run_diff_cmd(const char *pgm,
4544+
static void run_diff_cmd(const struct external_diff *pgm,
45164545
const char *name,
45174546
const char *other,
45184547
const char *attr_path,
@@ -4530,8 +4559,8 @@ static void run_diff_cmd(const char *pgm,
45304559
if (o->flags.allow_external || !o->ignore_driver_algorithm)
45314560
drv = userdiff_find_by_path(o->repo->index, attr_path);
45324561

4533-
if (o->flags.allow_external && drv && drv->external)
4534-
pgm = drv->external;
4562+
if (o->flags.allow_external && drv && drv->external.cmd)
4563+
pgm = &drv->external;
45354564

45364565
if (msg) {
45374566
/*
@@ -4597,7 +4626,7 @@ static void strip_prefix(int prefix_length, const char **namep, const char **oth
45974626

45984627
static void run_diff(struct diff_filepair *p, struct diff_options *o)
45994628
{
4600-
const char *pgm = external_diff();
4629+
const struct external_diff *pgm = external_diff();
46014630
struct strbuf msg;
46024631
struct diff_filespec *one = p->one;
46034632
struct diff_filespec *two = p->two;
@@ -4924,6 +4953,13 @@ void diff_setup_done(struct diff_options *options)
49244953
options->flags.exit_with_status = 1;
49254954
}
49264955

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+
49274963
options->diff_path_counter = 0;
49284964

49294965
if (options->flags.follow_renames)

t/t4020-diff-external.sh

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,72 @@ test_expect_success 'no diff with -diff' '
172172
grep Binary out
173173
'
174174

175+
check_external_diff () {
176+
expect_code=$1
177+
expect_out=$2
178+
expect_err=$3
179+
command_code=$4
180+
trust_exit_code=$5
181+
shift 5
182+
options="$@"
183+
184+
command="echo output; exit $command_code;"
185+
desc="external diff '$command' with trustExitCode=$trust_exit_code"
186+
with_options="${options:+ with }$options"
187+
188+
test_expect_success "$desc via attribute$with_options" "
189+
test_config diff.foo.command \"$command\" &&
190+
test_config diff.foo.trustExitCode $trust_exit_code &&
191+
echo \"file diff=foo\" >.gitattributes &&
192+
test_expect_code $expect_code git diff $options >out 2>err &&
193+
test_cmp $expect_out out &&
194+
test_cmp $expect_err err
195+
"
196+
197+
test_expect_success "$desc via diff.external$with_options" "
198+
test_config diff.external \"$command\" &&
199+
test_config diff.trustExitCode $trust_exit_code &&
200+
>.gitattributes &&
201+
test_expect_code $expect_code git diff $options >out 2>err &&
202+
test_cmp $expect_out out &&
203+
test_cmp $expect_err err
204+
"
205+
206+
test_expect_success "$desc via GIT_EXTERNAL_DIFF$with_options" "
207+
>.gitattributes &&
208+
test_expect_code $expect_code env \
209+
GIT_EXTERNAL_DIFF=\"$command\" \
210+
GIT_EXTERNAL_DIFF_TRUST_EXIT_CODE=$trust_exit_code \
211+
git diff $options >out 2>err &&
212+
test_cmp $expect_out out &&
213+
test_cmp $expect_err err
214+
"
215+
}
216+
217+
test_expect_success 'setup output files' '
218+
: >empty &&
219+
echo output >output &&
220+
echo "fatal: external diff died, stopping at file" >error
221+
'
222+
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
240+
175241
echo NULZbetweenZwords | perl -pe 'y/Z/\000/' > file
176242

177243
test_expect_success 'force diff with "diff"' '

userdiff.c

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ PATTERNS("scheme",
333333
"|([^][)(}{[ \t])+"),
334334
PATTERNS("tex", "^(\\\\((sub)*section|chapter|part)\\*{0,1}\\{.*)$",
335335
"\\\\[a-zA-Z@]+|\\\\.|([a-zA-Z0-9]|[^\x01-\x7f])+"),
336-
{ "default", NULL, NULL, -1, { NULL, 0 } },
336+
{ .name = "default", .binary = -1 },
337337
};
338338
#undef PATTERNS
339339
#undef IPATTERN
@@ -445,7 +445,11 @@ int userdiff_config(const char *k, const char *v)
445445
if (!strcmp(type, "binary"))
446446
return parse_tristate(&drv->binary, k, v);
447447
if (!strcmp(type, "command"))
448-
return git_config_string((char **) &drv->external, k, v);
448+
return git_config_string((char **) &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((char **) &drv->textconv, k, v);
451455
if (!strcmp(type, "cachetextconv"))

userdiff.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@ struct userdiff_funcname {
1111
int cflags;
1212
};
1313

14+
struct external_diff {
15+
char *cmd;
16+
unsigned trust_exit_code:1;
17+
};
18+
1419
struct userdiff_driver {
1520
const char *name;
16-
const char *external;
21+
struct external_diff external;
1722
const char *algorithm;
1823
int binary;
1924
struct userdiff_funcname funcname;

0 commit comments

Comments
 (0)