Git Mailing List Archive mirror
 help / color / mirror / Atom feed
* Possible git-diff bug when using exit-code with diff filters
@ 2024-04-20  1:13 German Lashevich
  2024-04-21 10:42 ` René Scharfe
  0 siblings, 1 reply; 32+ messages in thread
From: German Lashevich @ 2024-04-20  1:13 UTC (permalink / raw
  To: git

> What did you do before the bug happened? (Steps to reproduce your issue)

I configured a diff filter via gitattributes to use a custom script that,
sometimes, can change the files being compared in a way that there are no
differences between them.
Then I used `git diff --exit-code` on the changed file.

> What did you expect to happen? (Expected behavior)

I expected `git diff --exit-code` to return 0, since there are no differences
between the files after the filter is applied.

> What happened instead? (Actual behavior)

`git diff --exit-code` correctly produces no output, but returns 1.

> What's different between what you expected and what actually happened?

The difference is that `git diff --exit-code`, instead of returning 0, returns
1 even when there is no output.

> Anything else you want to add:

I have prepared a repository with a test case that reproduces the issue.
You can find it at https://github.com/Zebradil/git-diff-exit-code-bug-repro
The Readme file in the repository contains instructions on how to reproduce
the issue.

[System Info]
git version: 2.44.0
cpu: x86_64
no commit associated with this build
sizeof-long: 8
sizeof-size_t: 8
shell-path: /bin/sh
uname: Linux 6.8.7-arch1-1 #1 SMP PREEMPT_DYNAMIC Wed, 17 Apr 2024
15:20:28 +0000 x86_64
compiler info: gnuc: 13.2
libc info: glibc: 2.39
$SHELL (typically, interactive shell): /bin/zsh

Best regards,
German Lashevich

^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: Possible git-diff bug when using exit-code with diff filters
  2024-04-20  1:13 Possible git-diff bug when using exit-code with diff filters German Lashevich
@ 2024-04-21 10:42 ` René Scharfe
  2024-04-21 18:17   ` Junio C Hamano
                     ` (2 more replies)
  0 siblings, 3 replies; 32+ messages in thread
From: René Scharfe @ 2024-04-21 10:42 UTC (permalink / raw
  To: German Lashevich, git

Am 20.04.24 um 03:13 schrieb German Lashevich:
>> What did you do before the bug happened? (Steps to reproduce your issue)
>
> I configured a diff filter via gitattributes to use a custom script that,
> sometimes, can change the files being compared in a way that there are no
> differences between them.
> Then I used `git diff --exit-code` on the changed file.
>
>> What did you expect to happen? (Expected behavior)
>
> I expected `git diff --exit-code` to return 0, since there are no differences
> between the files after the filter is applied.
>
>> What happened instead? (Actual behavior)
>
> `git diff --exit-code` correctly produces no output, but returns 1.
>
>> What's different between what you expected and what actually happened?
>
> The difference is that `git diff --exit-code`, instead of returning 0, returns
> 1 even when there is no output.
>
>> Anything else you want to add:
>
> I have prepared a repository with a test case that reproduces the issue.
> You can find it at https://github.com/Zebradil/git-diff-exit-code-bug-repro
> The Readme file in the repository contains instructions on how to reproduce
> the issue.

You can more easily reproduce it by setting the environment variable
GIT_EXTERNAL_DIFF or the configuration setting diff.external -- no
attributes needed.

The output of external diffs does not influence the exit code currently.
We don't even have a way to find out if there was any output.

The patch below pipes it through a buffer and sets found_changes only if
something was written.  It fixes the error code for external diffs
producing no output, but breaks several am, pull and merge tests (t4150,
t4151, t5520, t6424, t6430, t7611).  So diff_from_contents doesn't work
quite as I expected.

And slurping in the whole output of external diffs is inefficient, of
course.  That could be fixed later by using a small buffer and writing
directly as we read.

René


diff --git a/diff.c b/diff.c
index 108c187577..76631644e5 100644
--- a/diff.c
+++ b/diff.c
@@ -40,6 +40,7 @@
 #include "setup.h"
 #include "strmap.h"
 #include "ws.h"
+#include "write-or-die.h"

 #ifdef NO_FAST_WORKING_DIRECTORY
 #define FAST_WORKING_DIRECTORY 0
@@ -4404,8 +4405,18 @@ static void run_external_diff(const char *pgm,
 	diff_free_filespec_data(one);
 	diff_free_filespec_data(two);
 	cmd.use_shell = 1;
-	if (run_command(&cmd))
-		die(_("external diff died, stopping at %s"), name);
+	if (o->flags.exit_with_status) {
+		struct strbuf out = STRBUF_INIT;
+		if (capture_command(&cmd, &out, 0))
+			die(_("external diff died, stopping at %s"), name);
+		if (out.len)
+			o->found_changes = 1;
+		write_or_die(1, out.buf, out.len);
+		strbuf_release(&out);
+	} else {
+		if (run_command(&cmd))
+			die(_("external diff died, stopping at %s"), name);
+	}

 	remove_tempfile();
 }
@@ -4851,6 +4862,7 @@ void diff_setup_done(struct diff_options *options)
 	 */

 	if ((options->xdl_opts & XDF_WHITESPACE_FLAGS) ||
+	    options->flags.exit_with_status ||
 	    options->ignore_regex_nr)
 		options->flags.diff_from_contents = 1;
 	else
diff --git a/t/t4020-diff-external.sh b/t/t4020-diff-external.sh
index fdd865f7c3..26430ca66b 100755
--- a/t/t4020-diff-external.sh
+++ b/t/t4020-diff-external.sh
@@ -172,6 +172,14 @@ test_expect_success 'no diff with -diff' '
 	grep Binary out
 '

+test_expect_success 'diff.external and --exit-code with output' '
+	test_expect_code 1 git -c diff.external=echo diff --exit-code
+'
+
+test_expect_success 'diff.external and --exit-code without output' '
+	git -c diff.external=true diff --exit-code
+'
+
 echo NULZbetweenZwords | perl -pe 'y/Z/\000/' > file

 test_expect_success 'force diff with "diff"' '


^ permalink raw reply related	[flat|nested] 32+ messages in thread

* Re: Possible git-diff bug when using exit-code with diff filters
  2024-04-21 10:42 ` René Scharfe
@ 2024-04-21 18:17   ` Junio C Hamano
  2024-04-21 18:32     ` rsbecker
  2024-05-05 10:19     ` René Scharfe
  2024-05-05 10:19   ` [PATCH 1/2] diff: report unmerged paths as changes in run_diff_cmd() René Scharfe
  2024-05-05 10:20   ` [PATCH 2/2] diff: fix --exit-code with external diff René Scharfe
  2 siblings, 2 replies; 32+ messages in thread
From: Junio C Hamano @ 2024-04-21 18:17 UTC (permalink / raw
  To: René Scharfe; +Cc: German Lashevich, git

René Scharfe <l.s.r@web.de> writes:

> You can more easily reproduce it by setting the environment variable
> GIT_EXTERNAL_DIFF or the configuration setting diff.external -- no
> attributes needed.

Indeed.

A much simpler fix may be to declare that these two features are
imcompatible and fail the execution upfront, instead of just
silently ignoring one of the two options.

As a person who is very much used to the external diff not
contributing to the exit status (who also invented the external diff
driver interface), I would be a wrong person to judge if such a
simplified approach is desirable, of course, but just throwing it
out as a food for thought.

Thanks.

^ permalink raw reply	[flat|nested] 32+ messages in thread

* RE: Possible git-diff bug when using exit-code with diff filters
  2024-04-21 18:17   ` Junio C Hamano
@ 2024-04-21 18:32     ` rsbecker
  2024-04-21 19:09       ` Junio C Hamano
  2024-05-05 10:19     ` René Scharfe
  1 sibling, 1 reply; 32+ messages in thread
From: rsbecker @ 2024-04-21 18:32 UTC (permalink / raw
  To: 'Junio C Hamano', 'René Scharfe'
  Cc: 'German Lashevich', git

On Sunday, April 21, 2024 2:18 PM, Junio C Hamano wrote:
>René Scharfe <l.s.r@web.de> writes:
>
>> You can more easily reproduce it by setting the environment variable
>> GIT_EXTERNAL_DIFF or the configuration setting diff.external -- no
>> attributes needed.
>
>Indeed.
>
>A much simpler fix may be to declare that these two features are imcompatible and
>fail the execution upfront, instead of just silently ignoring one of the two options.
>
>As a person who is very much used to the external diff not contributing to the exit
>status (who also invented the external diff driver interface), I would be a wrong
>person to judge if such a simplified approach is desirable, of course, but just
>throwing it out as a food for thought.

My suggestion would be to keep with a priority approach, where GIT_EXTERNAL_DIFF overrides diff.external, assuming they set hold to the same specification (the git-config page implies they do) and GIT_EXTERNAL_DIFF overrides diff.external as I would expect. The behaviour of the two should be identical and definitely not in conflict. From my experience with this feature, and what I would prefer to preserve, is a formal separation between the diff engine and the underlying textconv. If the textconv processor fails in some way, the git diff should also fail, but the exit code should reflect that git diff failed, not the status of the textconv processor. If the external diff wrapper fails, the completion should be passed up since git would/should/could delegate the diff processing to the external engine. The only real completion code requirement I see is that 0 = no difference, 1 = difference exists, other is an indeterminate failure.

I might be restating the obvious, but this is an important capability, at least to me and my teams. We use this extensively for binary content diffs. It is also pretty important for structured document (e.g., MS-Word, RTF, PDF) and executable diffs that git does not handle out of the box.
--Randall


^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: Possible git-diff bug when using exit-code with diff filters
  2024-04-21 18:32     ` rsbecker
@ 2024-04-21 19:09       ` Junio C Hamano
  2024-04-21 20:18         ` rsbecker
  0 siblings, 1 reply; 32+ messages in thread
From: Junio C Hamano @ 2024-04-21 19:09 UTC (permalink / raw
  To: rsbecker; +Cc: 'René Scharfe', 'German Lashevich', git

<rsbecker@nexbridge.com> writes:

> On Sunday, April 21, 2024 2:18 PM, Junio C Hamano wrote:
>>René Scharfe <l.s.r@web.de> writes:
>>
>>> You can more easily reproduce it by setting the environment variable
>>> GIT_EXTERNAL_DIFF or the configuration setting diff.external -- no
>>> attributes needed.
>>
>>Indeed.
>>
>>A much simpler fix may be to declare that these two features are imcompatible and
>>fail the execution upfront, instead of just silently ignoring one of the two options.
>>
>>As a person who is very much used to the external diff not contributing to the exit
>>status (who also invented the external diff driver interface), I would be a wrong
>>person to judge if such a simplified approach is desirable, of course, but just
>>throwing it out as a food for thought.
>
> My suggestion would be to keep with a priority approach, where
> GIT_EXTERNAL_DIFF overrides diff.external, assuming they set hold
> to the same specification (the git-config page implies they do)
> and GIT_EXTERNAL_DIFF overrides diff.external as I would expect.

Nobody in this discussion thread is hinting to change that, so I am
a bit confused where the above suggestion comes from...


^ permalink raw reply	[flat|nested] 32+ messages in thread

* RE: Possible git-diff bug when using exit-code with diff filters
  2024-04-21 19:09       ` Junio C Hamano
@ 2024-04-21 20:18         ` rsbecker
  0 siblings, 0 replies; 32+ messages in thread
From: rsbecker @ 2024-04-21 20:18 UTC (permalink / raw
  To: 'Junio C Hamano'
  Cc: 'René Scharfe', 'German Lashevich', git

On Sunday, April 21, 2024 3:09 PM, Junio C Hamano wrote:
><rsbecker@nexbridge.com> writes:
>
>> On Sunday, April 21, 2024 2:18 PM, Junio C Hamano wrote:
>>>René Scharfe <l.s.r@web.de> writes:
>>>
>>>> You can more easily reproduce it by setting the environment variable
>>>> GIT_EXTERNAL_DIFF or the configuration setting diff.external -- no
>>>> attributes needed.
>>>
>>>Indeed.
>>>
>>>A much simpler fix may be to declare that these two features are
>>>imcompatible and fail the execution upfront, instead of just silently ignoring one
>of the two options.
>>>
>>>As a person who is very much used to the external diff not
>>>contributing to the exit status (who also invented the external diff
>>>driver interface), I would be a wrong person to judge if such a
>>>simplified approach is desirable, of course, but just throwing it out as a food for
>thought.
>>
>> My suggestion would be to keep with a priority approach, where
>> GIT_EXTERNAL_DIFF overrides diff.external, assuming they set hold to
>> the same specification (the git-config page implies they do) and
>> GIT_EXTERNAL_DIFF overrides diff.external as I would expect.
>
>Nobody in this discussion thread is hinting to change that, so I am a bit confused
>where the above suggestion comes from...

I must have misinterpreted. Please ignore my suggestion.


^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: Possible git-diff bug when using exit-code with diff filters
  2024-04-21 18:17   ` Junio C Hamano
  2024-04-21 18:32     ` rsbecker
@ 2024-05-05 10:19     ` René Scharfe
  2024-05-06 17:22       ` Junio C Hamano
  1 sibling, 1 reply; 32+ messages in thread
From: René Scharfe @ 2024-05-05 10:19 UTC (permalink / raw
  To: Junio C Hamano; +Cc: German Lashevich, git

Am 21.04.24 um 20:17 schrieb Junio C Hamano:
> A much simpler fix may be to declare that these two features are
> imcompatible and fail the execution upfront, instead of just
> silently ignoring one of the two options.

It would not be *that* simple -- if we want to error out upfront we'd
have to evaluate the attributes of all files (or all changed files)
first to see whether they require an external diff.

Reporting the incompatibility in the middle of a diff would be easier,
but I don't see why we shouldn't support that combination.  It takes
some effort, sure, but not prohibitively much.

René


^ permalink raw reply	[flat|nested] 32+ messages in thread

* [PATCH 1/2] diff: report unmerged paths as changes in run_diff_cmd()
  2024-04-21 10:42 ` René Scharfe
  2024-04-21 18:17   ` Junio C Hamano
@ 2024-05-05 10:19   ` René Scharfe
  2024-05-05 10:20   ` [PATCH 2/2] diff: fix --exit-code with external diff René Scharfe
  2 siblings, 0 replies; 32+ messages in thread
From: René Scharfe @ 2024-05-05 10:19 UTC (permalink / raw
  To: German Lashevich, git; +Cc: Junio C Hamano

You can ask the diff machinery to let the exit code indicate whether
there are changes, e.g. with --quiet.  It as two ways to calculate that
bit: The quick one assumes blobs with different hashes have different
content, and the more elaborate way actually compares the contents,
possibly applying transformations like ignoring whitespace.

The quick way considers an unmerged file to be a change and reports
exit code 1, which makes sense.

The slower path uses the struct diff_options member found_changes to
indicate whether the blobs differ even with the transformations applied.
It's not set for unmerged files, though, resulting in exit code 0.

Set found_changes in run_diff_cmd() for unmerged files, for a consistent
exit code of 1 if there's an unmerged file, regardless of whether
whitespace is ignored.

Signed-off-by: René Scharfe <l.s.r@web.de>
---
 diff.c                   | 1 +
 t/t4046-diff-unmerged.sh | 8 ++++++++
 2 files changed, 9 insertions(+)

diff --git a/diff.c b/diff.c
index 108c187577..ded9ac70df 100644
--- a/diff.c
+++ b/diff.c
@@ -4555,6 +4555,7 @@ static void run_diff_cmd(const char *pgm,
 			     o, complete_rewrite);
 	} else {
 		fprintf(o->file, "* Unmerged path %s\n", name);
+		o->found_changes = 1;
 	}
 }

diff --git a/t/t4046-diff-unmerged.sh b/t/t4046-diff-unmerged.sh
index ffaf69335f..c606ee4c40 100755
--- a/t/t4046-diff-unmerged.sh
+++ b/t/t4046-diff-unmerged.sh
@@ -96,4 +96,12 @@ test_expect_success 'diff --stat' '
 	test_cmp diff-stat.expect diff-stat.actual
 '

+test_expect_success 'diff --quiet' '
+	test_expect_code 1 git diff --cached --quiet
+'
+
+test_expect_success 'diff --quiet --ignore-all-space' '
+	test_expect_code 1 git diff --cached --quiet --ignore-all-space
+'
+
 test_done
--
2.45.0

^ permalink raw reply related	[flat|nested] 32+ messages in thread

* [PATCH 2/2] diff: fix --exit-code with external diff
  2024-04-21 10:42 ` René Scharfe
  2024-04-21 18:17   ` Junio C Hamano
  2024-05-05 10:19   ` [PATCH 1/2] diff: report unmerged paths as changes in run_diff_cmd() René Scharfe
@ 2024-05-05 10:20   ` René Scharfe
  2024-05-05 15:25     ` Phillip Wood
                       ` (2 more replies)
  2 siblings, 3 replies; 32+ messages in thread
From: René Scharfe @ 2024-05-05 10:20 UTC (permalink / raw
  To: German Lashevich, git; +Cc: Junio C Hamano

You can ask the diff machinery to let the exit code indicate whether
there are changes, e.g. with --exit-code.  It as two ways to calculate
that bit: The quick one assumes blobs with different hashes have
different content, and the more elaborate way actually compares the
contents, possibly applying transformations like ignoring whitespace.

Always use the slower path by setting the flag diff_from_contents,
because any of the files could have an external diff driver set via an
attribute, which might consider binary differences irrelevant, like e.g.
diff -b.

And don't stop walking that path in diff_flush() just because
output_format is unset.  Instead run diff_flush_patch() with output
redirected to /dev/null to get the exit status, like we already do for
DIFF_FORMAT_NO_OUTPUT.  That's consistent with the comments in diff.h:

   /* Same as output_format = 0 but we know that -s flag was given
    * and we should not give default value to output_format.
    */
   #define DIFF_FORMAT_NO_OUTPUT	0x0800

An unset output_format is given e.g. by repo_index_has_changes().  We
could set it right there, but there may be other places, so simply let
diff_flush() handle it for all of them consistently.

Finally, capture the output of the external diff and only register a
change by setting found_changes if the program wrote anything.

Reported-by: German Lashevich <german.lashevich@gmail.com>
Signed-off-by: René Scharfe <l.s.r@web.de>
---
 diff.c                   | 33 ++++++++++++++++++++++++++++++---
 t/t4020-diff-external.sh |  8 ++++++++
 2 files changed, 38 insertions(+), 3 deletions(-)

diff --git a/diff.c b/diff.c
index ded9ac70df..cd3c8a3978 100644
--- a/diff.c
+++ b/diff.c
@@ -40,6 +40,7 @@
 #include "setup.h"
 #include "strmap.h"
 #include "ws.h"
+#include "write-or-die.h"

 #ifdef NO_FAST_WORKING_DIRECTORY
 #define FAST_WORKING_DIRECTORY 0
@@ -4404,8 +4405,33 @@ static void run_external_diff(const char *pgm,
 	diff_free_filespec_data(one);
 	diff_free_filespec_data(two);
 	cmd.use_shell = 1;
-	if (run_command(&cmd))
-		die(_("external diff died, stopping at %s"), name);
+	if (o->flags.diff_from_contents) {
+		int got_output = 0;
+		cmd.out = -1;
+		if (start_command(&cmd))
+			die(_("external diff died, stopping at %s"), name);
+		for (;;) {
+			char buffer[8192];
+			ssize_t len = xread(cmd.out, buffer, sizeof(buffer));
+			if (!len)
+				break;
+			if (len < 0)
+				die(_("unable to read from external diff,"
+				      " stopping at %s"), name);
+			got_output = 1;
+			if (write_in_full(1, buffer, len) < 0)
+				die(_("unable to write output of external diff,"
+				      " stopping at %s"), name);
+		}
+		close(cmd.out);
+		if (finish_command(&cmd))
+			die(_("external diff died, stopping at %s"), name);
+		if (got_output)
+			o->found_changes = 1;
+	} else {
+		if (run_command(&cmd))
+			die(_("external diff died, stopping at %s"), name);
+	}

 	remove_tempfile();
 }
@@ -4852,6 +4878,7 @@ void diff_setup_done(struct diff_options *options)
 	 */

 	if ((options->xdl_opts & XDF_WHITESPACE_FLAGS) ||
+	    options->flags.exit_with_status ||
 	    options->ignore_regex_nr)
 		options->flags.diff_from_contents = 1;
 	else
@@ -6742,7 +6769,7 @@ void diff_flush(struct diff_options *options)
 	if (output_format & DIFF_FORMAT_CALLBACK)
 		options->format_callback(q, options, options->format_callback_data);

-	if (output_format & DIFF_FORMAT_NO_OUTPUT &&
+	if ((!output_format || output_format & DIFF_FORMAT_NO_OUTPUT) &&
 	    options->flags.exit_with_status &&
 	    options->flags.diff_from_contents) {
 		/*
diff --git a/t/t4020-diff-external.sh b/t/t4020-diff-external.sh
index fdd865f7c3..26430ca66b 100755
--- a/t/t4020-diff-external.sh
+++ b/t/t4020-diff-external.sh
@@ -172,6 +172,14 @@ test_expect_success 'no diff with -diff' '
 	grep Binary out
 '

+test_expect_success 'diff.external and --exit-code with output' '
+	test_expect_code 1 git -c diff.external=echo diff --exit-code
+'
+
+test_expect_success 'diff.external and --exit-code without output' '
+	git -c diff.external=true diff --exit-code
+'
+
 echo NULZbetweenZwords | perl -pe 'y/Z/\000/' > file

 test_expect_success 'force diff with "diff"' '
--
2.45.0

^ permalink raw reply related	[flat|nested] 32+ messages in thread

* Re: [PATCH 2/2] diff: fix --exit-code with external diff
  2024-05-05 10:20   ` [PATCH 2/2] diff: fix --exit-code with external diff René Scharfe
@ 2024-05-05 15:25     ` Phillip Wood
  2024-05-06 17:31       ` Junio C Hamano
  2024-05-06 18:23       ` René Scharfe
  2024-06-05  8:31     ` [PATCH v2 0/3] " René Scharfe
  2024-06-09  7:35     ` [PATCH v3 " René Scharfe
  2 siblings, 2 replies; 32+ messages in thread
From: Phillip Wood @ 2024-05-05 15:25 UTC (permalink / raw
  To: René Scharfe, German Lashevich, git; +Cc: Junio C Hamano

Hi René

Thanks for working on this

On 05/05/2024 11:20, René Scharfe wrote:
> Finally, capture the output of the external diff and only register a
> change by setting found_changes if the program wrote anything.

I worry that this will lead to regression reports like 
https://lore.kernel.org/git/CA+dzEBn108QoMA28f0nC8K21XT+Afua0V2Qv8XkR8rAeqUCCZw@mail.gmail.com/ 
as the external diff will not see a terminal on stdout anymore. I'm not 
really clear why we ignore the exit code of the external diff command. 
Merge strategies are expected to exit 0 on success, 1 when there are 
conflicts and another non-zero value for other errors - it would be nice 
to do something similar here where 1 means "there were differences" but 
it is probably too late to do that without a config value to indicate 
that we should trust the exit code.

Best Wishes

Phillip

^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: Possible git-diff bug when using exit-code with diff filters
  2024-05-05 10:19     ` René Scharfe
@ 2024-05-06 17:22       ` Junio C Hamano
  0 siblings, 0 replies; 32+ messages in thread
From: Junio C Hamano @ 2024-05-06 17:22 UTC (permalink / raw
  To: René Scharfe; +Cc: German Lashevich, git

René Scharfe <l.s.r@web.de> writes:

> Am 21.04.24 um 20:17 schrieb Junio C Hamano:
>> A much simpler fix may be to declare that these two features are
>> imcompatible and fail the execution upfront, instead of just
>> silently ignoring one of the two options.
>
> It would not be *that* simple -- if we want to error out upfront we'd
> have to evaluate the attributes of all files (or all changed files)
> first to see whether they require an external diff.

You're absolutely right.

> Reporting the incompatibility in the middle of a diff would be easier,
> but I don't see why we shouldn't support that combination.  It takes
> some effort, sure, but not prohibitively much.

OK.  Thanks.

^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: [PATCH 2/2] diff: fix --exit-code with external diff
  2024-05-05 15:25     ` Phillip Wood
@ 2024-05-06 17:31       ` Junio C Hamano
  2024-05-06 18:23       ` René Scharfe
  1 sibling, 0 replies; 32+ messages in thread
From: Junio C Hamano @ 2024-05-06 17:31 UTC (permalink / raw
  To: Phillip Wood; +Cc: René Scharfe, German Lashevich, git

Phillip Wood <phillip.wood123@gmail.com> writes:

>> Finally, capture the output of the external diff and only register a
>> change by setting found_changes if the program wrote anything.
>
> I worry that this will lead to regression reports like
> https://lore.kernel.org/git/CA+dzEBn108QoMA28f0nC8K21XT+Afua0V2Qv8XkR8rAeqUCCZw@mail.gmail.com/
> as the external diff will not see a terminal on stdout anymore.

Hmph.  If it were Git that the external diff script eventually
invokes that changes the behaviour (e.g. color and use of pager)
based on the tty-ness of the output, we could use tricks like
GIT_PAGER_IN_USE to propagate tty-ness down to the subprocess,
but Git is not the only thing involved here, so it is not a very
good general solution.

> I'm
> not really clear why we ignore the exit code of the external diff
> command. Merge strategies are expected to exit 0 on success, 1 when
> there are conflicts and another non-zero value for other errors - it
> would be nice to do something similar here where 1 means "there were
> differences" but it is probably too late to do that without a config
> value to indicate that we should trust the exit code.

Yeah, that thought crossed my mind.  If we were designing the system
from scratch today, that would certainly be what we would do.  I
suspect that it is because external diff drivers were invented way
before "diff --exit-code" was introduced.

Thanks.

^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: [PATCH 2/2] diff: fix --exit-code with external diff
  2024-05-05 15:25     ` Phillip Wood
  2024-05-06 17:31       ` Junio C Hamano
@ 2024-05-06 18:23       ` René Scharfe
  2024-05-08 15:25         ` phillip.wood123
  1 sibling, 1 reply; 32+ messages in thread
From: René Scharfe @ 2024-05-06 18:23 UTC (permalink / raw
  To: phillip.wood, German Lashevich, git; +Cc: Junio C Hamano

Am 05.05.24 um 17:25 schrieb Phillip Wood:
> On 05/05/2024 11:20, René Scharfe wrote:
>> Finally, capture the output of the external diff and only register a
>> change by setting found_changes if the program wrote anything.
>
> I worry that this will lead to regression reports like
> https://lore.kernel.org/git/CA+dzEBn108QoMA28f0nC8K21XT+Afua0V2Qv8XkR8rAeqUCCZw@mail.gmail.com/
> as the external diff will not see a terminal on stdout anymore.

So external diffs might no longer color their output if invoked using
the option --exit-code.  I assume this can be worked around by adding an
option like --color=always to the diff command.  This warrants a note in
the docs at the very least, though.

But thinking about it, the external diff could be a GUI program that
doesn't write to its stdout at all.  Or it could write something if the
files' contents match and stay silent if there are differences, weird as
that may be.

> I'm not really clear why we ignore the exit code of the external diff
> command.

Historical reasons, I guess.

> Merge strategies are expected to exit 0 on success, 1 when there are
> conflicts and another non-zero value for other errors - it would be
> nice to do something similar here where 1 means "there were
> differences" but it is probably too late to do that without a config
> value to indicate that we should trust the exit code.
Right, such a diff command protocol v2 would not need to pipe the
output through an inspection loop.  Sounds like a good idea.  It's
unfortunate that it would increase the configuration surface, which is
not in an acute need to expand.  We could advertise the new option when
dying due to the unsupported combination of --exit-code and external
diff, but that's in equal parts helpful and obnoxious, I feel.

René

^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: [PATCH 2/2] diff: fix --exit-code with external diff
  2024-05-06 18:23       ` René Scharfe
@ 2024-05-08 15:25         ` phillip.wood123
  2024-05-11 20:32           ` René Scharfe
  0 siblings, 1 reply; 32+ messages in thread
From: phillip.wood123 @ 2024-05-08 15:25 UTC (permalink / raw
  To: René Scharfe, phillip.wood, German Lashevich, git; +Cc: Junio C Hamano

Hi René

On 06/05/2024 19:23, René Scharfe wrote:
> Am 05.05.24 um 17:25 schrieb Phillip Wood:
>> Merge strategies are expected to exit 0 on success, 1 when there are
>> conflicts and another non-zero value for other errors - it would be
>> nice to do something similar here where 1 means "there were
>> differences" but it is probably too late to do that without a config
>> value to indicate that we should trust the exit code.
> Right, such a diff command protocol v2 would not need to pipe the
> output through an inspection loop.  Sounds like a good idea.  It's
> unfortunate that it would increase the configuration surface, which is
> not in an acute need to expand.  We could advertise the new option when
> dying due to the unsupported combination of --exit-code and external
> diff, but that's in equal parts helpful and obnoxious, I feel.

Yes, diff dying would be annoying but the message would be useful.

Thinking about the external diff and some of the other diff options I 
wonder what we should do when options like "--quiet" and "--name-only" 
are combined with an external diff (I haven't checked the current 
behavior). If we introduced a diff command protocol v2 we could include 
a way to pass through "--quiet" though maybe just redirecting the stdout 
of the external command to /dev/null and using the exit code would be 
sufficient.

Best Wishes

Phillip

P.S. I haven't forgotten about our unit-test discussion but I'm afraid 
it will probably be the middle of next month before I have time to reply.

^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: [PATCH 2/2] diff: fix --exit-code with external diff
  2024-05-08 15:25         ` phillip.wood123
@ 2024-05-11 20:32           ` René Scharfe
  2024-05-12  9:38             ` René Scharfe
  0 siblings, 1 reply; 32+ messages in thread
From: René Scharfe @ 2024-05-11 20:32 UTC (permalink / raw
  To: phillip.wood, German Lashevich, git; +Cc: Junio C Hamano

Am 08.05.24 um 17:25 schrieb phillip.wood123@gmail.com:
> Hi René
>
> On 06/05/2024 19:23, René Scharfe wrote:
>> Am 05.05.24 um 17:25 schrieb Phillip Wood:
>>> Merge strategies are expected to exit 0 on success, 1 when there are
>>> conflicts and another non-zero value for other errors - it would be
>>> nice to do something similar here where 1 means "there were
>>> differences" but it is probably too late to do that without a config
>>> value to indicate that we should trust the exit code.
>> Right, such a diff command protocol v2 would not need to pipe the
>> output through an inspection loop.  Sounds like a good idea.  It's
>> unfortunate that it would increase the configuration surface, which is
>> not in an acute need to expand.  We could advertise the new option when
>> dying due to the unsupported combination of --exit-code and external
>> diff, but that's in equal parts helpful and obnoxious, I feel.
>
> Yes, diff dying would be annoying but the message would be useful.

Having poked at it a bit more, I wonder if we need to add some further
nuance/trick to avoid having to reject certain combinations of options.

Currently external diffs can't signal content equality.  That doesn't
matter for trivial equality (where content and mode of the two files
match), as this case is always handled by the diff machinery already.
Only lossy comparisons (e.g. ignoring whitespace) even have the need to
signal equality.

If we (continue to) assume that external diffs are lossless then we
don't need to change the code, just document that assumption.  And add a
way to specify lossy external diffs that can signal whether they found
interesting differences, to address the originally reported shortcoming.

Is there an official term for comparisons that ignore some details?
"Lossy" is rather used for compression.  Filtered, partial, selective?

> Thinking about the external diff and some of the other diff options I
> wonder what we should do when options like "--quiet" and
> "--name-only" are combined with an external diff (I haven't checked
> the current behavior). If we introduced a diff command protocol v2 we
> could include a way to pass through "--quiet" though maybe just
> redirecting the stdout of the external command to /dev/null and using
> the exit code would be sufficient.

The current code uses shortcuts like that.  For lossy external diffs we
need to turn (some of) them off.  Lots of possible combinations with
special handling -- needs lots of tests for reasonable coverage. :-/

> P.S. I haven't forgotten about our unit-test discussion but I'm
> afraid it will probably be the middle of next month before I have
> time to reply.
No worries; reminds me to polish some unit-test patches, though.  I
get distracted a lot these days..

René

^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: [PATCH 2/2] diff: fix --exit-code with external diff
  2024-05-11 20:32           ` René Scharfe
@ 2024-05-12  9:38             ` René Scharfe
  0 siblings, 0 replies; 32+ messages in thread
From: René Scharfe @ 2024-05-12  9:38 UTC (permalink / raw
  To: phillip.wood, German Lashevich, git; +Cc: Junio C Hamano

Am 11.05.24 um 22:32 schrieb René Scharfe:
> Am 08.05.24 um 17:25 schrieb phillip.wood123@gmail.com:
>> Hi René
>>
>> On 06/05/2024 19:23, René Scharfe wrote:
>>> Am 05.05.24 um 17:25 schrieb Phillip Wood:
>>>> Merge strategies are expected to exit 0 on success, 1 when there are
>>>> conflicts and another non-zero value for other errors - it would be
>>>> nice to do something similar here where 1 means "there were
>>>> differences" but it is probably too late to do that without a config
>>>> value to indicate that we should trust the exit code.
>>> Right, such a diff command protocol v2 would not need to pipe the
>>> output through an inspection loop.  Sounds like a good idea.  It's
>>> unfortunate that it would increase the configuration surface, which is
>>> not in an acute need to expand.  We could advertise the new option when
>>> dying due to the unsupported combination of --exit-code and external
>>> diff, but that's in equal parts helpful and obnoxious, I feel.
>>
>> Yes, diff dying would be annoying but the message would be useful.
>
> Having poked at it a bit more, I wonder if we need to add some further
> nuance/trick to avoid having to reject certain combinations of options.
>
> Currently external diffs can't signal content equality.  That doesn't
> matter for trivial equality (where content and mode of the two files
> match), as this case is always handled by the diff machinery already.
> Only lossy comparisons (e.g. ignoring whitespace) even have the need to
> signal equality.
>
> If we (continue to) assume that external diffs are lossless then we
> don't need to change the code, just document that assumption.  And add a
> way to specify lossy external diffs that can signal whether they found
> interesting differences, to address the originally reported shortcoming.

One more step in that direction: If we continue to use exit code 0 to
mean "notable changes present" and redefine 1 to mean "non-trivial
equality"  instead of "fatal error" then we don't need to add any config
options.

A downside is that the exit codes of diff(1) have the opposite meaning.
Which is weird in itself: You say "give me a diff" and it answers "true,
I got nothing for you" or "false, I actually had to print that dang
thing".  Inherited from cmp(1), I guess.  Which I further guess got it
from bcmp(3)?

But we can't directly use diff(1) anyway because we pass either one or
six parameters instead of the two that it would expect.  Our external
diff calling protocol is already special enough that we can freely
chose the meaning of exit codes.

Any other downsides?  Am I missing something?

René

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [PATCH v2 0/3] diff: fix --exit-code with external diff
  2024-05-05 10:20   ` [PATCH 2/2] diff: fix --exit-code with external diff René Scharfe
  2024-05-05 15:25     ` Phillip Wood
@ 2024-06-05  8:31     ` René Scharfe
  2024-06-05  8:35       ` [PATCH v2 1/3] t4020: test exit code with external diffs René Scharfe
                         ` (3 more replies)
  2024-06-09  7:35     ` [PATCH v3 " René Scharfe
  2 siblings, 4 replies; 32+ messages in thread
From: René Scharfe @ 2024-06-05  8:31 UTC (permalink / raw
  To: git; +Cc: Junio C Hamano, German Lashevich, Phillip Wood

Changes since v1:
- old patch 1 was merged
- old patch 2 was merged and reverted
- new patch 1 adds tests that exercises diff --exit-code and --quiet
  together with all methods for specifying external diffs
- new patch 2 adds a struct to store the flag that patch 3 adds
- patch 3 adds configuration options and an environment variable to
  accept diff(1)-style exit codes from external diffs

  t4020: test exit code with external diffs
  userdiff: add and use struct external_diff
  diff: let external diffs report that changes are uninteresting

 Documentation/config/diff.txt   | 14 ++++++++
 Documentation/git.txt           |  7 ++++
 Documentation/gitattributes.txt |  5 +++
 diff.c                          | 62 ++++++++++++++++++++++++---------
 t/t4020-diff-external.sh        | 44 +++++++++++++++++++++++
 userdiff.c                      |  8 +++--
 userdiff.h                      |  7 +++-
 7 files changed, 128 insertions(+), 19 deletions(-)

--
2.45.2

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [PATCH v2 1/3] t4020: test exit code with external diffs
  2024-06-05  8:31     ` [PATCH v2 0/3] " René Scharfe
@ 2024-06-05  8:35       ` René Scharfe
  2024-06-05  8:36       ` [PATCH v2 2/3] userdiff: add and use struct external_diff René Scharfe
                         ` (2 subsequent siblings)
  3 siblings, 0 replies; 32+ messages in thread
From: René Scharfe @ 2024-06-05  8:35 UTC (permalink / raw
  To: git; +Cc: Junio C Hamano, German Lashevich, Phillip Wood

Add tests to check the exit code of git diff with its options --quiet
and --exit-code when using an external diff program.  Currently we
cannot tell whether it found significant changes or not.

Signed-off-by: René Scharfe <l.s.r@web.de>
---
 t/t4020-diff-external.sh | 33 +++++++++++++++++++++++++++++++++
 1 file changed, 33 insertions(+)

diff --git a/t/t4020-diff-external.sh b/t/t4020-diff-external.sh
index fdd865f7c3..bed640b2b1 100755
--- a/t/t4020-diff-external.sh
+++ b/t/t4020-diff-external.sh
@@ -172,6 +172,39 @@ test_expect_success 'no diff with -diff' '
 	grep Binary out
 '

+check_external_exit_code () {
+	expect_code=$1
+	command_code=$2
+	option=$3
+
+	command="exit $command_code;"
+	desc="external diff '$command'"
+
+	test_expect_success "$desc via attribute with $option" "
+		test_config diff.foo.command \"$command\" &&
+		echo \"file diff=foo\" >.gitattributes &&
+		test_expect_code $expect_code git diff $option
+	"
+
+	test_expect_success "$desc via diff.external with $option" "
+		test_config diff.external \"$command\" &&
+		>.gitattributes &&
+		test_expect_code $expect_code git diff $option
+	"
+
+	test_expect_success "$desc via GIT_EXTERNAL_DIFF with $option" "
+		>.gitattributes &&
+		test_expect_code $expect_code env \
+			GIT_EXTERNAL_DIFF=\"$command\" \
+			git diff $option
+	"
+}
+
+check_external_exit_code   1 0 --exit-code
+check_external_exit_code   1 0 --quiet
+check_external_exit_code 128 1 --exit-code
+check_external_exit_code   1 1 --quiet # we don't even call the program
+
 echo NULZbetweenZwords | perl -pe 'y/Z/\000/' > file

 test_expect_success 'force diff with "diff"' '
--
2.45.2

^ permalink raw reply related	[flat|nested] 32+ messages in thread

* [PATCH v2 2/3] userdiff: add and use struct external_diff
  2024-06-05  8:31     ` [PATCH v2 0/3] " René Scharfe
  2024-06-05  8:35       ` [PATCH v2 1/3] t4020: test exit code with external diffs René Scharfe
@ 2024-06-05  8:36       ` René Scharfe
  2024-06-05  8:38       ` [PATCH v2 3/3] diff: let external diffs report that changes are uninteresting René Scharfe
  2024-06-05 16:47       ` [PATCH v2 0/3] diff: fix --exit-code with external diff Junio C Hamano
  3 siblings, 0 replies; 32+ messages in thread
From: René Scharfe @ 2024-06-05  8:36 UTC (permalink / raw
  To: git; +Cc: Junio C Hamano, German Lashevich, Phillip Wood

Wrap the string specifying the external diff command in a new struct to
simplify adding attributes, which the next patch will do.

Make sure external_diff() still returns NULL if neither the environment
variable GIT_EXTERNAL_DIFF nor the configuration option diff.external is
set, to continue allowing its use in a boolean context.

Use a designated initializer for the default builtin userdiff driver to
adjust to the type change of the second struct member.  Spelling out
only the non-zero members improves readability as a nice side-effect.

No functional change intended.

Signed-off-by: René Scharfe <l.s.r@web.de>
---
 diff.c     | 32 +++++++++++++++++---------------
 userdiff.c |  4 ++--
 userdiff.h |  6 +++++-
 3 files changed, 24 insertions(+), 18 deletions(-)

diff --git a/diff.c b/diff.c
index ded9ac70df..286d093bfa 100644
--- a/diff.c
+++ b/diff.c
@@ -57,7 +57,7 @@ static int diff_color_moved_ws_default;
 static int diff_context_default = 3;
 static int diff_interhunk_context_default;
 static const char *diff_word_regex_cfg;
-static const char *external_diff_cmd_cfg;
+static struct external_diff external_diff_cfg;
 static const char *diff_order_file_cfg;
 int diff_auto_refresh_index = 1;
 static int diff_mnemonic_prefix;
@@ -429,7 +429,7 @@ int git_diff_ui_config(const char *var, const char *value,
 		return 0;
 	}
 	if (!strcmp(var, "diff.external"))
-		return git_config_string(&external_diff_cmd_cfg, var, value);
+		return git_config_string(&external_diff_cfg.cmd, var, value);
 	if (!strcmp(var, "diff.wordregex"))
 		return git_config_string(&diff_word_regex_cfg, var, value);
 	if (!strcmp(var, "diff.orderfile"))
@@ -546,18 +546,20 @@ static char *quote_two(const char *one, const char *two)
 	return strbuf_detach(&res, NULL);
 }

-static const char *external_diff(void)
+static const struct external_diff *external_diff(void)
 {
-	static const char *external_diff_cmd = NULL;
+	static struct external_diff external_diff_env, *external_diff_ptr;
 	static int done_preparing = 0;

 	if (done_preparing)
-		return external_diff_cmd;
-	external_diff_cmd = xstrdup_or_null(getenv("GIT_EXTERNAL_DIFF"));
-	if (!external_diff_cmd)
-		external_diff_cmd = external_diff_cmd_cfg;
+		return external_diff_ptr;
+	external_diff_env.cmd = xstrdup_or_null(getenv("GIT_EXTERNAL_DIFF"));
+	if (external_diff_env.cmd)
+		external_diff_ptr = &external_diff_env;
+	else if (external_diff_cfg.cmd)
+		external_diff_ptr = &external_diff_cfg;
 	done_preparing = 1;
-	return external_diff_cmd;
+	return external_diff_ptr;
 }

 /*
@@ -4373,7 +4375,7 @@ static void add_external_diff_name(struct repository *r,
  *               infile2 infile2-sha1 infile2-mode [ rename-to ]
  *
  */
-static void run_external_diff(const char *pgm,
+static void run_external_diff(const struct external_diff *pgm,
 			      const char *name,
 			      const char *other,
 			      struct diff_filespec *one,
@@ -4384,7 +4386,7 @@ static void run_external_diff(const char *pgm,
 	struct child_process cmd = CHILD_PROCESS_INIT;
 	struct diff_queue_struct *q = &diff_queued_diff;

-	strvec_push(&cmd.args, pgm);
+	strvec_push(&cmd.args, pgm->cmd);
 	strvec_push(&cmd.args, name);

 	if (one && two) {
@@ -4510,7 +4512,7 @@ static void fill_metainfo(struct strbuf *msg,
 	}
 }

-static void run_diff_cmd(const char *pgm,
+static void run_diff_cmd(const struct external_diff *pgm,
 			 const char *name,
 			 const char *other,
 			 const char *attr_path,
@@ -4528,8 +4530,8 @@ static void run_diff_cmd(const char *pgm,
 	if (o->flags.allow_external || !o->ignore_driver_algorithm)
 		drv = userdiff_find_by_path(o->repo->index, attr_path);

-	if (o->flags.allow_external && drv && drv->external)
-		pgm = drv->external;
+	if (o->flags.allow_external && drv && drv->external.cmd)
+		pgm = &drv->external;

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

 static void run_diff(struct diff_filepair *p, struct diff_options *o)
 {
-	const char *pgm = external_diff();
+	const struct external_diff *pgm = external_diff();
 	struct strbuf msg;
 	struct diff_filespec *one = p->one;
 	struct diff_filespec *two = p->two;
diff --git a/userdiff.c b/userdiff.c
index 82bc76b910..f47e2d9f36 100644
--- a/userdiff.c
+++ b/userdiff.c
@@ -333,7 +333,7 @@ PATTERNS("scheme",
 	 "|([^][)(}{[ \t])+"),
 PATTERNS("tex", "^(\\\\((sub)*section|chapter|part)\\*{0,1}\\{.*)$",
 	 "\\\\[a-zA-Z@]+|\\\\.|([a-zA-Z0-9]|[^\x01-\x7f])+"),
-{ "default", NULL, NULL, -1, { NULL, 0 } },
+{ .name = "default", .binary = -1 },
 };
 #undef PATTERNS
 #undef IPATTERN
@@ -445,7 +445,7 @@ int userdiff_config(const char *k, const char *v)
 	if (!strcmp(type, "binary"))
 		return parse_tristate(&drv->binary, k, v);
 	if (!strcmp(type, "command"))
-		return git_config_string(&drv->external, k, v);
+		return git_config_string(&drv->external.cmd, k, v);
 	if (!strcmp(type, "textconv"))
 		return git_config_string(&drv->textconv, k, v);
 	if (!strcmp(type, "cachetextconv"))
diff --git a/userdiff.h b/userdiff.h
index d726804c3e..7e2b28e88d 100644
--- a/userdiff.h
+++ b/userdiff.h
@@ -11,9 +11,13 @@ struct userdiff_funcname {
 	int cflags;
 };

+struct external_diff {
+	const char *cmd;
+};
+
 struct userdiff_driver {
 	const char *name;
-	const char *external;
+	struct external_diff external;
 	const char *algorithm;
 	int binary;
 	struct userdiff_funcname funcname;
--
2.45.2

^ permalink raw reply related	[flat|nested] 32+ messages in thread

* [PATCH v2 3/3] diff: let external diffs report that changes are uninteresting
  2024-06-05  8:31     ` [PATCH v2 0/3] " René Scharfe
  2024-06-05  8:35       ` [PATCH v2 1/3] t4020: test exit code with external diffs René Scharfe
  2024-06-05  8:36       ` [PATCH v2 2/3] userdiff: add and use struct external_diff René Scharfe
@ 2024-06-05  8:38       ` René Scharfe
  2024-06-06  6:39         ` Johannes Sixt
  2024-06-06  9:48         ` Phillip Wood
  2024-06-05 16:47       ` [PATCH v2 0/3] diff: fix --exit-code with external diff Junio C Hamano
  3 siblings, 2 replies; 32+ messages in thread
From: René Scharfe @ 2024-06-05  8:38 UTC (permalink / raw
  To: git; +Cc: Junio C Hamano, German Lashevich, Phillip Wood

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().

I considered checking the output of the external diff to check whether
its empty.  It was added as 11be65cfa4 (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 <phillip.wood123@gmail.com>
Signed-off-by: René Scharfe <l.s.r@web.de>
---
 Documentation/config/diff.txt   | 14 ++++++++++++++
 Documentation/git.txt           |  7 +++++++
 Documentation/gitattributes.txt |  5 +++++
 diff.c                          | 30 +++++++++++++++++++++++++++++-
 t/t4020-diff-external.sh        | 23 +++++++++++++++++------
 userdiff.c                      |  4 ++++
 userdiff.h                      |  1 +
 7 files changed, 77 insertions(+), 7 deletions(-)

diff --git a/Documentation/config/diff.txt b/Documentation/config/diff.txt
index 5ce7b91f1d..485a13236d 100644
--- a/Documentation/config/diff.txt
+++ b/Documentation/config/diff.txt
@@ -79,6 +79,13 @@ diff.external::
 	you want to use an external diff program only on a subset of
 	your files, you might want to use linkgit:gitattributes[5] instead.

+diff.trustExitCode::
+	If this boolean value is set to true then the `diff.external`
+	command is expected to return exit code 1 if it finds
+	significant changes and 0 if it doesn't, like diff(1).  If it's
+	false then the `diff.external` command is expected to always
+	return exit code 0.  Defaults to false.
+
 diff.ignoreSubmodules::
 	Sets the default value of --ignore-submodules. Note that this
 	affects only 'git diff' Porcelain, and not lower level 'diff'
@@ -164,6 +171,13 @@ diff.<driver>.command::
 	The custom diff driver command.  See linkgit:gitattributes[5]
 	for details.

+diff.<driver>.trustExitCode::
+	If this boolean value is set to true then the
+	`diff.<driver>.command` command is expected to return exit code
+	1 if it finds significant changes and 0 if it doesn't, like
+	diff(1).  If it's false then the `diff.external` command is
+	expected to always return exit code 0.  Defaults to false.
+
 diff.<driver>.xfuncname::
 	The regular expression that the diff driver should use to
 	recognize the hunk header.  A built-in pattern may also be used.
diff --git a/Documentation/git.txt b/Documentation/git.txt
index a31a70acca..81b806416c 100644
--- a/Documentation/git.txt
+++ b/Documentation/git.txt
@@ -644,6 +644,13 @@ parameter, <path>.
 For each path `GIT_EXTERNAL_DIFF` is called, two environment variables,
 `GIT_DIFF_PATH_COUNTER` and `GIT_DIFF_PATH_TOTAL` are set.

+`GIT_EXTERNAL_DIFF_TRUST_EXIT_CODE`::
+	Setting this environment variable indicates the the program
+	specified with `GIT_EXTERNAL_DIFF` returns exit code 1 if it
+	finds significant changes and 0 if it doesn't, like diff(1).
+	If it's not set, the program is expected to always return
+	exit code 0.
+
 `GIT_DIFF_PATH_COUNTER`::
 	A 1-based counter incremented by one for every path.

diff --git a/Documentation/gitattributes.txt b/Documentation/gitattributes.txt
index 4338d023d9..80cae17f37 100644
--- a/Documentation/gitattributes.txt
+++ b/Documentation/gitattributes.txt
@@ -776,6 +776,11 @@ with the above configuration, i.e. `j-c-diff`, with 7
 parameters, just like `GIT_EXTERNAL_DIFF` program is called.
 See linkgit:git[1] for details.

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

diff --git a/diff.c b/diff.c
index 286d093bfa..3dff81ec7d 100644
--- a/diff.c
+++ b/diff.c
@@ -430,6 +430,10 @@ int git_diff_ui_config(const char *var, const char *value,
 	}
 	if (!strcmp(var, "diff.external"))
 		return git_config_string(&external_diff_cfg.cmd, var, value);
+	if (!strcmp(var, "diff.trustexitcode")) {
+		external_diff_cfg.trust_exit_code = git_config_bool(var, value);
+		return 0;
+	}
 	if (!strcmp(var, "diff.wordregex"))
 		return git_config_string(&diff_word_regex_cfg, var, value);
 	if (!strcmp(var, "diff.orderfile"))
@@ -554,6 +558,8 @@ static const struct external_diff *external_diff(void)
 	if (done_preparing)
 		return external_diff_ptr;
 	external_diff_env.cmd = xstrdup_or_null(getenv("GIT_EXTERNAL_DIFF"));
+	if (git_env_bool("GIT_EXTERNAL_DIFF_TRUST_EXIT_CODE", 0))
+		external_diff_env.trust_exit_code = 1;
 	if (external_diff_env.cmd)
 		external_diff_ptr = &external_diff_env;
 	else if (external_diff_cfg.cmd)
@@ -4385,6 +4391,18 @@ static void run_external_diff(const struct external_diff *pgm,
 {
 	struct child_process cmd = CHILD_PROCESS_INIT;
 	struct diff_queue_struct *q = &diff_queued_diff;
+	int rc;
+
+	/*
+	 * Trivial equality is handled by diff_unmodified_pair() before
+	 * we get here.  If we don't need to show the diff and the
+	 * external diff program lacks the ability to tell us whether
+	 * it's empty then we consider it non-empty without even asking.
+	 */
+	if (!(o->output_format & DIFF_FORMAT_PATCH) && !pgm->trust_exit_code) {
+		o->found_changes = 1;
+		return;
+	}

 	strvec_push(&cmd.args, pgm->cmd);
 	strvec_push(&cmd.args, name);
@@ -4406,7 +4424,10 @@ static void run_external_diff(const struct external_diff *pgm,
 	diff_free_filespec_data(one);
 	diff_free_filespec_data(two);
 	cmd.use_shell = 1;
-	if (run_command(&cmd))
+	rc = run_command(&cmd);
+	if ((!pgm->trust_exit_code && !rc) || (pgm->trust_exit_code && rc == 1))
+		o->found_changes = 1;
+	else if (!pgm->trust_exit_code || rc)
 		die(_("external diff died, stopping at %s"), name);

 	remove_tempfile();
@@ -4924,6 +4945,13 @@ void diff_setup_done(struct diff_options *options)
 		options->flags.exit_with_status = 1;
 	}

+	/*
+	 * External diffs could declare non-identical contents equal
+	 * (think diff --ignore-space-change).
+	 */
+	if (options->flags.allow_external && options->flags.exit_with_status)
+		options->flags.diff_from_contents = 1;
+
 	options->diff_path_counter = 0;

 	if (options->flags.follow_renames)
diff --git a/t/t4020-diff-external.sh b/t/t4020-diff-external.sh
index bed640b2b1..bbb49b8a0a 100755
--- a/t/t4020-diff-external.sh
+++ b/t/t4020-diff-external.sh
@@ -175,19 +175,22 @@ test_expect_success 'no diff with -diff' '
 check_external_exit_code () {
 	expect_code=$1
 	command_code=$2
-	option=$3
+	trust_exit_code=$3
+	option=$4

 	command="exit $command_code;"
-	desc="external diff '$command'"
+	desc="external diff '$command' with trustExitCode=$trust_exit_code"

 	test_expect_success "$desc via attribute with $option" "
 		test_config diff.foo.command \"$command\" &&
+		test_config diff.foo.trustExitCode $trust_exit_code &&
 		echo \"file diff=foo\" >.gitattributes &&
 		test_expect_code $expect_code git diff $option
 	"

 	test_expect_success "$desc via diff.external with $option" "
 		test_config diff.external \"$command\" &&
+		test_config diff.trustExitCode $trust_exit_code &&
 		>.gitattributes &&
 		test_expect_code $expect_code git diff $option
 	"
@@ -196,14 +199,22 @@ check_external_exit_code () {
 		>.gitattributes &&
 		test_expect_code $expect_code env \
 			GIT_EXTERNAL_DIFF=\"$command\" \
+			GIT_EXTERNAL_DIFF_TRUST_EXIT_CODE=$trust_exit_code \
 			git diff $option
 	"
 }

-check_external_exit_code   1 0 --exit-code
-check_external_exit_code   1 0 --quiet
-check_external_exit_code 128 1 --exit-code
-check_external_exit_code   1 1 --quiet # we don't even call the program
+check_external_exit_code   1 0 off --exit-code
+check_external_exit_code   1 0 off --quiet
+check_external_exit_code 128 1 off --exit-code
+check_external_exit_code   1 1 off --quiet # we don't even call the program
+
+check_external_exit_code   0 0 on --exit-code
+check_external_exit_code   0 0 on --quiet
+check_external_exit_code   1 1 on --exit-code
+check_external_exit_code   1 1 on --quiet
+check_external_exit_code 128 2 on --exit-code
+check_external_exit_code 128 2 on --quiet

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

diff --git a/userdiff.c b/userdiff.c
index f47e2d9f36..6cdfb147c3 100644
--- a/userdiff.c
+++ b/userdiff.c
@@ -446,6 +446,10 @@ int userdiff_config(const char *k, const char *v)
 		return parse_tristate(&drv->binary, k, v);
 	if (!strcmp(type, "command"))
 		return git_config_string(&drv->external.cmd, k, v);
+	if (!strcmp(type, "trustexitcode")) {
+		drv->external.trust_exit_code = git_config_bool(k, v);
+		return 0;
+	}
 	if (!strcmp(type, "textconv"))
 		return git_config_string(&drv->textconv, k, v);
 	if (!strcmp(type, "cachetextconv"))
diff --git a/userdiff.h b/userdiff.h
index 7e2b28e88d..5f1c8cc49c 100644
--- a/userdiff.h
+++ b/userdiff.h
@@ -13,6 +13,7 @@ struct userdiff_funcname {

 struct external_diff {
 	const char *cmd;
+	unsigned trust_exit_code:1;
 };

 struct userdiff_driver {
--
2.45.2

^ permalink raw reply related	[flat|nested] 32+ messages in thread

* Re: [PATCH v2 0/3] diff: fix --exit-code with external diff
  2024-06-05  8:31     ` [PATCH v2 0/3] " René Scharfe
                         ` (2 preceding siblings ...)
  2024-06-05  8:38       ` [PATCH v2 3/3] diff: let external diffs report that changes are uninteresting René Scharfe
@ 2024-06-05 16:47       ` Junio C Hamano
  3 siblings, 0 replies; 32+ messages in thread
From: Junio C Hamano @ 2024-06-05 16:47 UTC (permalink / raw
  To: René Scharfe; +Cc: git, German Lashevich, Phillip Wood

René Scharfe <l.s.r@web.de> writes:

>   t4020: test exit code with external diffs
>   userdiff: add and use struct external_diff
>   diff: let external diffs report that changes are uninteresting

OK, we now need to mark each external diff driver if we want to
honor their exit status, but that awkwardness is to be blamed on the
original design.  Thanks for addressing the misdesign I made years
ago in the mildest way possible not to introduce any unnecessary
regressions.

^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: [PATCH v2 3/3] diff: let external diffs report that changes are uninteresting
  2024-06-05  8:38       ` [PATCH v2 3/3] diff: let external diffs report that changes are uninteresting René Scharfe
@ 2024-06-06  6:39         ` Johannes Sixt
  2024-06-06  8:28           ` René Scharfe
  2024-06-06  9:48         ` Phillip Wood
  1 sibling, 1 reply; 32+ messages in thread
From: Johannes Sixt @ 2024-06-06  6:39 UTC (permalink / raw
  To: René Scharfe; +Cc: Junio C Hamano, German Lashevich, Phillip Wood, git

Am 05.06.24 um 10:38 schrieb René Scharfe:
> +diff.trustExitCode::
> +	If this boolean value is set to true then the `diff.external`
> +	command is expected to return exit code 1 if it finds
> +	significant changes and 0 if it doesn't, like diff(1).  If it's
> +	false then the `diff.external` command is expected to always
> +	return exit code 0.  Defaults to false.

I find this somewhat unclear. What are the consequences when this value
is set to false, but the command exits with code other than 0? Is it

    If it's false then any exit code other than 0 of the `diff.external`
    command is treated as an error.

-- Hannes


^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: [PATCH v2 3/3] diff: let external diffs report that changes are uninteresting
  2024-06-06  6:39         ` Johannes Sixt
@ 2024-06-06  8:28           ` René Scharfe
  2024-06-06 15:49             ` Junio C Hamano
  0 siblings, 1 reply; 32+ messages in thread
From: René Scharfe @ 2024-06-06  8:28 UTC (permalink / raw
  To: Johannes Sixt; +Cc: Junio C Hamano, German Lashevich, Phillip Wood, git

Am 06.06.24 um 08:39 schrieb Johannes Sixt:
> Am 05.06.24 um 10:38 schrieb René Scharfe:
>> +diff.trustExitCode::
>> +	If this boolean value is set to true then the `diff.external`
>> +	command is expected to return exit code 1 if it finds
>> +	significant changes and 0 if it doesn't, like diff(1).  If it's
>> +	false then the `diff.external` command is expected to always
>> +	return exit code 0.  Defaults to false.
>
> I find this somewhat unclear. What are the consequences when this value
> is set to false, but the command exits with code other than 0? Is it
>
>     If it's false then any exit code other than 0 of the `diff.external`
>     command is treated as an error.

Yes, unexpected exit codes are reported as errors.

If trustExitCode is false and --quiet is given then the execution of
external diffs is skipped, so in that situation there is no exit code to
expect, though.  Not sure how to express it concisely, though.  This
attempt looks a bit bloated:

--quiet::
        Disable all output of the program. Implies `--exit-code`.
        Disables execution of external diff helpers whose exit code
        is not trusted, i.e. their respective configuration option
	`diff.trustExitCode` or `diff.<driver>.trustExitCode` or
	environment variable `GIT_EXTERNAL_DIFF_TRUST_EXIT_CODE` is
	false.

Might be worth documenting this original behavior somehow, anyway.  It
makes sense in hindsight, but surprised me a bit when I wrote the tests.

René

^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: [PATCH v2 3/3] diff: let external diffs report that changes are uninteresting
  2024-06-05  8:38       ` [PATCH v2 3/3] diff: let external diffs report that changes are uninteresting René Scharfe
  2024-06-06  6:39         ` Johannes Sixt
@ 2024-06-06  9:48         ` Phillip Wood
  2024-06-07  8:19           ` René Scharfe
  1 sibling, 1 reply; 32+ messages in thread
From: Phillip Wood @ 2024-06-06  9:48 UTC (permalink / raw
  To: René Scharfe, git
  Cc: Junio C Hamano, German Lashevich, Phillip Wood, Johannes Sixt

Hi René

On 05/06/2024 09:38, René Scharfe wrote:
> 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().
> 
> I considered checking the output of the external diff to check whether
> its empty.  It was added as 11be65cfa4 (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.

Thanks for the comprehensive commit message, I agree that it is much 
less confusing to follow the exit code convention of diff(1). This is 
looking good, I've left a couple of comments below.


> +diff.trustExitCode::
> +	If this boolean value is set to true then the `diff.external`
> +	command is expected to return exit code 1 if it finds
> +	significant changes and 0 if it doesn't, like diff(1).  If it's
> +	false then the `diff.external` command is expected to always
> +	return exit code 0.  Defaults to false.

I wonder if "significant changes" is a bit ambiguous and as Johannes 
said it would be good to mention that other exit codes are errors. Perhaps

	If this boolean value is set to true then the `diff.external`
	command is expected to return exit code 0 if it considers the
	input files to be equal and 1 if they are not, like diff(1).
	If it is false then the `diff.external` command is expected to
	always return exit code 0. In both cases any other exit code
	is considered to be an error. Defaults to false.


>   	strvec_push(&cmd.args, pgm->cmd);
>   	strvec_push(&cmd.args, name);
> @@ -4406,7 +4424,10 @@ static void run_external_diff(const struct external_diff *pgm,
>   	diff_free_filespec_data(one);
>   	diff_free_filespec_data(two);
>   	cmd.use_shell = 1;

Should we be redirecting stdout to /dev/null here when the user passes 
--quiet?

> -	if (run_command(&cmd))
> +	rc = run_command(&cmd);
> +	if ((!pgm->trust_exit_code && !rc) || (pgm->trust_exit_code && rc == 1))
> +		o->found_changes = 1;
> +	else if (!pgm->trust_exit_code || rc)
>   		die(_("external diff died, stopping at %s"), name);

This is a bit fiddly because we may, or may not trust the exit code but 
the logic here looks good.

> -check_external_exit_code   1 0 --exit-code
> -check_external_exit_code   1 0 --quiet
> -check_external_exit_code 128 1 --exit-code
> -check_external_exit_code   1 1 --quiet # we don't even call the program
> +check_external_exit_code   1 0 off --exit-code
> +check_external_exit_code   1 0 off --quiet
> +check_external_exit_code 128 1 off --exit-code
> +check_external_exit_code   1 1 off --quiet # we don't even call the program
> +
> +check_external_exit_code   0 0 on --exit-code
> +check_external_exit_code   0 0 on --quiet
> +check_external_exit_code   1 1 on --exit-code
> +check_external_exit_code   1 1 on --quiet
> +check_external_exit_code 128 2 on --exit-code
> +check_external_exit_code 128 2 on --quiet

It would be nice if the tests checked that --quiet does not produce any 
output on stdout.

Best Wishes

Phillip


^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: [PATCH v2 3/3] diff: let external diffs report that changes are uninteresting
  2024-06-06  8:28           ` René Scharfe
@ 2024-06-06 15:49             ` Junio C Hamano
  0 siblings, 0 replies; 32+ messages in thread
From: Junio C Hamano @ 2024-06-06 15:49 UTC (permalink / raw
  To: René Scharfe; +Cc: Johannes Sixt, German Lashevich, Phillip Wood, git

René Scharfe <l.s.r@web.de> writes:

>>> +diff.trustExitCode::
>>> +	If this boolean value is set to true then the `diff.external`
>>> +	command is expected to return exit code 1 if it finds
>>> +	significant changes and 0 if it doesn't, like diff(1).  If it's
>>> +	false then the `diff.external` command is expected to always
>>> +	return exit code 0.  Defaults to false.
>>
>> I find this somewhat unclear. What are the consequences when this value
>> is set to false, but the command exits with code other than 0? Is it
>>
>>     If it's false then any exit code other than 0 of the `diff.external`
>>     command is treated as an error.
>
> Yes, unexpected exit codes are reported as errors.
>
> If trustExitCode is false and --quiet is given then the execution of
> external diffs is skipped, so in that situation there is no exit code to
> expect, though.  Not sure how to express it concisely, though.  This
> attempt looks a bit bloated:
>
> --quiet::
>         Disable all output of the program. Implies `--exit-code`.
>         Disables execution of external diff helpers whose exit code
>         is not trusted, i.e. their respective configuration option
> 	`diff.trustExitCode` or `diff.<driver>.trustExitCode` or
> 	environment variable `GIT_EXTERNAL_DIFF_TRUST_EXIT_CODE` is
> 	false.
>
> Might be worth documenting this original behavior somehow, anyway.  It
> makes sense in hindsight, but surprised me a bit when I wrote the tests.

Yes.  The explanation of trustExitCode makes sense as an explanation
of what the variable means (i.e. if set, we pay attention to the
exit code of the external diff driver, otherwise a non-zero exit is
an error), but I suspect that readers are _more_ interested in how
the external diff driver contributes to the answer to the "has this
path been changed?" question when the variable is on and off.  And
the above description of "--quiet" does help answer that question
somewhat.


^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: [PATCH v2 3/3] diff: let external diffs report that changes are uninteresting
  2024-06-06  9:48         ` Phillip Wood
@ 2024-06-07  8:19           ` René Scharfe
  0 siblings, 0 replies; 32+ messages in thread
From: René Scharfe @ 2024-06-07  8:19 UTC (permalink / raw
  To: phillip.wood, git; +Cc: Junio C Hamano, German Lashevich, Johannes Sixt

Am 06.06.24 um 11:48 schrieb Phillip Wood:
> On 05/06/2024 09:38, René Scharfe wrote:
>> +diff.trustExitCode::
>> +    If this boolean value is set to true then the `diff.external`
>> +    command is expected to return exit code 1 if it finds
>> +    significant changes and 0 if it doesn't, like diff(1).  If it's
>> +    false then the `diff.external` command is expected to always
>> +    return exit code 0.  Defaults to false.
>
> I wonder if "significant changes" is a bit ambiguous and as Johannes
> said it would be good to mention that other exit codes are errors.
> Perhaps
>
>     If this boolean value is set to true then the `diff.external`
>     command is expected to return exit code 0 if it considers the
>     input files to be equal and 1 if they are not, like diff(1).
>     If it is false then the `diff.external` command is expected to
>     always return exit code 0. In both cases any other exit code
>     is considered to be an error. Defaults to false.

The first part looks like an obvious improvement.  Stating that
unexpected exit codes are errors looks tautological to me, though.
Perhaps mentioning that git diff stops at that point adds would be
useful?  Or perhaps a tad of redundancy is just what's needed here?

>
>
>>       strvec_push(&cmd.args, pgm->cmd);
>>       strvec_push(&cmd.args, name);
>> @@ -4406,7 +4424,10 @@ static void run_external_diff(const struct external_diff *pgm,
>>       diff_free_filespec_data(one);
>>       diff_free_filespec_data(two);
>>       cmd.use_shell = 1;
>
> Should we be redirecting stdout to /dev/null here when the user
> passes --quiet?

Oh, yes.  We didn't even start the program before, but with the
patch it becomes necessary.  Good find!

>
>> -    if (run_command(&cmd))
>> +    rc = run_command(&cmd);
>> +    if ((!pgm->trust_exit_code && !rc) || (pgm->trust_exit_code && rc == 1))
>> +        o->found_changes = 1;
>> +    else if (!pgm->trust_exit_code || rc)
>>           die(_("external diff died, stopping at %s"), name);
>
> This is a bit fiddly because we may, or may not trust the exit code
> but the logic here looks good.

Yeah, it's not as clear as it could be.  One reason is that it avoids
redundancy at the action side and the other is adherence to the rule of
not explicitly comparing to zero.  We could enumerate all cases:

	if (!pgm->trust_exit_code && rc == 0)
		o->found_changes = 1;
	else if (pgm->trust_exit_code && rc == 0)
		; /* nothing */
	else if (pgm->trust_exit_code && rc == 1)
		o->found_changes = 1;
	else
		die(_("external diff died, stopping at %s"), name);

Or avoid redundancy in the conditions:

	if (pgm->trust_exit_code) {
		if (rc == 1)
			o->found_changes = 1;
		else if (rc != 0)
			die(_("external diff died, stopping at %s"), name);
	} else {
		if (rc == 0)
			o->found_changes = 1;
		else
			die(_("external diff died, stopping at %s"), name);
	}

We should not get into bit twiddling, though:

	o->found_changes |= rc == pgm->trust_exit_code;
	if ((unsigned)rc > pgm->trust_exit_code)
		die(_("external diff died, stopping at %s"), name);

>
>> -check_external_exit_code   1 0 --exit-code
>> -check_external_exit_code   1 0 --quiet
>> -check_external_exit_code 128 1 --exit-code
>> -check_external_exit_code   1 1 --quiet # we don't even call the program
>> +check_external_exit_code   1 0 off --exit-code
>> +check_external_exit_code   1 0 off --quiet
>> +check_external_exit_code 128 1 off --exit-code
>> +check_external_exit_code   1 1 off --quiet # we don't even call the program
>> +
>> +check_external_exit_code   0 0 on --exit-code
>> +check_external_exit_code   0 0 on --quiet
>> +check_external_exit_code   1 1 on --exit-code
>> +check_external_exit_code   1 1 on --quiet
>> +check_external_exit_code 128 2 on --exit-code
>> +check_external_exit_code 128 2 on --quiet
>
> It would be nice if the tests checked that --quiet does not produce
> any output on stdout.

Right, we need this after unlocking external diff execution with
--quiet.

René

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [PATCH v3 0/3] diff: fix --exit-code with external diff
  2024-05-05 10:20   ` [PATCH 2/2] diff: fix --exit-code with external diff René Scharfe
  2024-05-05 15:25     ` Phillip Wood
  2024-06-05  8:31     ` [PATCH v2 0/3] " René Scharfe
@ 2024-06-09  7:35     ` René Scharfe
  2024-06-09  7:38       ` [PATCH v3 1/3] t4020: test exit code with external diffs René Scharfe
                         ` (3 more replies)
  2 siblings, 4 replies; 32+ messages in thread
From: René Scharfe @ 2024-06-09  7:35 UTC (permalink / raw
  To: git; +Cc: Junio C Hamano, German Lashevich, Phillip Wood, Johannes Sixt

Changes since v2:
- rebase; config strings are no longer const
- document the handling of unexpected exit codes
- document that external diffs are skipped with --quiet and trustExitCode=off
- silence external diff output with --quiet
- check output in tests
- test diff runs without --exit-code and --quiet as well
- slightly untangle the exit code handling code to make it easier to read
- fix copy/paste error in documentation of diff.<driver>.trustExitCode

  t4020: test exit code with external diffs
  userdiff: add and use struct external_diff
  diff: let external diffs report that changes are uninteresting

 Documentation/config/diff.txt   | 18 +++++++++
 Documentation/diff-options.txt  |  5 +++
 Documentation/git.txt           | 10 +++++
 Documentation/gitattributes.txt |  5 +++
 diff.c                          | 68 +++++++++++++++++++++++++--------
 t/t4020-diff-external.sh        | 66 ++++++++++++++++++++++++++++++++
 userdiff.c                      |  8 +++-
 userdiff.h                      |  7 +++-
 8 files changed, 168 insertions(+), 19 deletions(-)

Range-Diff gegen v2:
1:  118aba667b < -:  ---------- t4020: test exit code with external diffs
-:  ---------- > 1:  d59f0c6fdf t4020: test exit code with external diffs
2:  0b4dabebe1 ! 2:  4ad160ab1f userdiff: add and use struct external_diff
    @@ diff.c
     @@ diff.c: static int diff_color_moved_ws_default;
      static int diff_context_default = 3;
      static int diff_interhunk_context_default;
    - static const char *diff_word_regex_cfg;
    --static const char *external_diff_cmd_cfg;
    + static char *diff_word_regex_cfg;
    +-static char *external_diff_cmd_cfg;
     +static struct external_diff external_diff_cfg;
    - static const char *diff_order_file_cfg;
    + static char *diff_order_file_cfg;
      int diff_auto_refresh_index = 1;
      static int diff_mnemonic_prefix;
     @@ diff.c: int git_diff_ui_config(const char *var, const char *value,
    @@ userdiff.h: struct userdiff_funcname {
      };

     +struct external_diff {
    -+	const char *cmd;
    ++	char *cmd;
     +};
     +
      struct userdiff_driver {
      	const char *name;
    --	const char *external;
    +-	char *external;
     +	struct external_diff external;
    - 	const char *algorithm;
    + 	char *algorithm;
      	int binary;
      	struct userdiff_funcname funcname;
3:  4d54ca8281 ! 3:  29c8d3b61a diff: let external diffs report that changes are uninteresting
    @@ Commit message
         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 11be65cfa4 (diff: fix --exit-code with
         external diff, 2024-05-05) and quickly reverted, as it does not work
    @@ Documentation/config/diff.txt: diff.external::
      	your files, you might want to use linkgit:gitattributes[5] instead.

     +diff.trustExitCode::
    -+	If this boolean value is set to true then the `diff.external`
    -+	command is expected to return exit code 1 if it finds
    -+	significant changes and 0 if it doesn't, like diff(1).  If it's
    -+	false then the `diff.external` command is expected to always
    -+	return exit code 0.  Defaults to false.
    ++	If this boolean value is set to true then the
    ++	`diff.external` command is expected to return exit code
    ++	0 if it considers the input files to be equal or 1 if it
    ++	considers them to be different, like `diff(1)`.
    ++	If it is set to false, which is the default, then the command
    ++	is expected to return exit code 0 regardless of equality.
    ++	Any other exit code causes Git to report a fatal error.
     +
      diff.ignoreSubmodules::
      	Sets the default value of --ignore-submodules. Note that this
    @@ Documentation/config/diff.txt: diff.<driver>.command::
     +diff.<driver>.trustExitCode::
     +	If this boolean value is set to true then the
     +	`diff.<driver>.command` command is expected to return exit code
    -+	1 if it finds significant changes and 0 if it doesn't, like
    -+	diff(1).  If it's false then the `diff.external` command is
    -+	expected to always return exit code 0.  Defaults to false.
    ++	0 if it considers the input files to be equal or 1 if it
    ++	considers them to be different, like `diff(1)`.
    ++	If it is set to false, which is the default, then the command
    ++	is expected to return exit code 0 regardless of equality.
    ++	Any other exit code causes Git to report a fatal error.
     +
      diff.<driver>.xfuncname::
      	The regular expression that the diff driver should use to
      	recognize the hunk header.  A built-in pattern may also be used.

    + ## Documentation/diff-options.txt ##
    +@@ Documentation/diff-options.txt: ifndef::git-log[]
    +
    + --quiet::
    + 	Disable all output of the program. Implies `--exit-code`.
    +-	Disables execution of external diff helpers.
    ++	Disables execution of external diff helpers whose exit code
    ++	is not trusted, i.e. their respective configuration option
    ++	`diff.trustExitCode` or `diff.<driver>.trustExitCode` or
    ++	environment variable `GIT_EXTERNAL_DIFF_TRUST_EXIT_CODE` is
    ++	false.
    + endif::git-log[]
    + endif::git-format-patch[]
    +
    +
      ## Documentation/git.txt ##
     @@ Documentation/git.txt: parameter, <path>.
      For each path `GIT_EXTERNAL_DIFF` is called, two environment variables,
      `GIT_DIFF_PATH_COUNTER` and `GIT_DIFF_PATH_TOTAL` are set.

     +`GIT_EXTERNAL_DIFF_TRUST_EXIT_CODE`::
    -+	Setting this environment variable indicates the the program
    -+	specified with `GIT_EXTERNAL_DIFF` returns exit code 1 if it
    -+	finds significant changes and 0 if it doesn't, like diff(1).
    -+	If it's not set, the program is expected to always return
    -+	exit code 0.
    ++	If this Boolean environment variable is set to true then the
    ++	`GIT_EXTERNAL_DIFF` command is expected to return exit code
    ++	0 if it considers the input files to be equal or 1 if it
    ++	considers them to be different, like `diff(1)`.
    ++	If it is set to false, which is the default, then the command
    ++	is expected to return exit code 0 regardless of equality.
    ++	Any other exit code causes Git to report a fatal error.
    ++
     +
      `GIT_DIFF_PATH_COUNTER`::
      	A 1-based counter incremented by one for every path.
    @@ diff.c: static void run_external_diff(const struct external_diff *pgm,
      {
      	struct child_process cmd = CHILD_PROCESS_INIT;
      	struct diff_queue_struct *q = &diff_queued_diff;
    ++	int quiet = !(o->output_format & DIFF_FORMAT_PATCH);
     +	int rc;
     +
     +	/*
    @@ diff.c: static void run_external_diff(const struct external_diff *pgm,
     +	 * external diff program lacks the ability to tell us whether
     +	 * it's empty then we consider it non-empty without even asking.
     +	 */
    -+	if (!(o->output_format & DIFF_FORMAT_PATCH) && !pgm->trust_exit_code) {
    ++	if (!pgm->trust_exit_code && quiet) {
     +		o->found_changes = 1;
     +		return;
     +	}
    @@ diff.c: static void run_external_diff(const struct external_diff *pgm,
      	diff_free_filespec_data(two);
      	cmd.use_shell = 1;
     -	if (run_command(&cmd))
    ++	cmd.no_stdout = quiet;
     +	rc = run_command(&cmd);
    -+	if ((!pgm->trust_exit_code && !rc) || (pgm->trust_exit_code && rc == 1))
    ++	if (!pgm->trust_exit_code && rc == 0)
    ++		o->found_changes = 1;
    ++	else if (pgm->trust_exit_code && rc == 0)
    ++		; /* nothing */
    ++	else if (pgm->trust_exit_code && rc == 1)
     +		o->found_changes = 1;
    -+	else if (!pgm->trust_exit_code || rc)
    ++	else
      		die(_("external diff died, stopping at %s"), name);

      	remove_tempfile();
    @@ diff.c: void diff_setup_done(struct diff_options *options)
      	if (options->flags.follow_renames)

      ## t/t4020-diff-external.sh ##
    -@@ t/t4020-diff-external.sh: test_expect_success 'no diff with -diff' '
    - check_external_exit_code () {
    - 	expect_code=$1
    - 	command_code=$2
    --	option=$3
    -+	trust_exit_code=$3
    -+	option=$4
    -
    - 	command="exit $command_code;"
    +@@ t/t4020-diff-external.sh: check_external_diff () {
    + 	expect_out=$2
    + 	expect_err=$3
    + 	command_code=$4
    +-	shift 4
    ++	trust_exit_code=$5
    ++	shift 5
    + 	options="$@"
    +
    + 	command="echo output; exit $command_code;"
     -	desc="external diff '$command'"
     +	desc="external diff '$command' with trustExitCode=$trust_exit_code"
    + 	with_options="${options:+ with }$options"

    - 	test_expect_success "$desc via attribute with $option" "
    + 	test_expect_success "$desc via attribute$with_options" "
      		test_config diff.foo.command \"$command\" &&
     +		test_config diff.foo.trustExitCode $trust_exit_code &&
      		echo \"file diff=foo\" >.gitattributes &&
    - 		test_expect_code $expect_code git diff $option
    - 	"
    + 		test_expect_code $expect_code git diff $options >out 2>err &&
    + 		test_cmp $expect_out out &&
    +@@ t/t4020-diff-external.sh: check_external_diff () {

    - 	test_expect_success "$desc via diff.external with $option" "
    + 	test_expect_success "$desc via diff.external$with_options" "
      		test_config diff.external \"$command\" &&
     +		test_config diff.trustExitCode $trust_exit_code &&
      		>.gitattributes &&
    - 		test_expect_code $expect_code git diff $option
    - 	"
    -@@ t/t4020-diff-external.sh: check_external_exit_code () {
    + 		test_expect_code $expect_code git diff $options >out 2>err &&
    + 		test_cmp $expect_out out &&
    +@@ t/t4020-diff-external.sh: check_external_diff () {
      		>.gitattributes &&
      		test_expect_code $expect_code env \
      			GIT_EXTERNAL_DIFF=\"$command\" \
     +			GIT_EXTERNAL_DIFF_TRUST_EXIT_CODE=$trust_exit_code \
    - 			git diff $option
    - 	"
    - }
    -
    --check_external_exit_code   1 0 --exit-code
    --check_external_exit_code   1 0 --quiet
    --check_external_exit_code 128 1 --exit-code
    --check_external_exit_code   1 1 --quiet # we don't even call the program
    -+check_external_exit_code   1 0 off --exit-code
    -+check_external_exit_code   1 0 off --quiet
    -+check_external_exit_code 128 1 off --exit-code
    -+check_external_exit_code   1 1 off --quiet # we don't even call the program
    + 			git diff $options >out 2>err &&
    + 		test_cmp $expect_out out &&
    + 		test_cmp $expect_err err
    +@@ t/t4020-diff-external.sh: test_expect_success 'setup output files' '
    + 	echo "fatal: external diff died, stopping at file" >error
    + '
    +
    +-check_external_diff   0 output empty 0
    +-check_external_diff 128 output error 1
    +-
    +-check_external_diff   1 output empty 0 --exit-code
    +-check_external_diff 128 output error 1 --exit-code
    +-
    +-check_external_diff   1 empty  empty 0 --quiet
    +-check_external_diff   1 empty  empty 1 --quiet # we don't even call the program
    ++check_external_diff   0 output empty 0 off
    ++check_external_diff 128 output error 1 off
    ++check_external_diff   0 output empty 0 on
    ++check_external_diff   0 output empty 1 on
    ++check_external_diff 128 output error 2 on
    ++
    ++check_external_diff   1 output empty 0 off --exit-code
    ++check_external_diff 128 output error 1 off --exit-code
    ++check_external_diff   0 output empty 0 on  --exit-code
    ++check_external_diff   1 output empty 1 on  --exit-code
    ++check_external_diff 128 output error 2 on  --exit-code
     +
    -+check_external_exit_code   0 0 on --exit-code
    -+check_external_exit_code   0 0 on --quiet
    -+check_external_exit_code   1 1 on --exit-code
    -+check_external_exit_code   1 1 on --quiet
    -+check_external_exit_code 128 2 on --exit-code
    -+check_external_exit_code 128 2 on --quiet
    ++check_external_diff   1 empty  empty 0 off --quiet
    ++check_external_diff   1 empty  empty 1 off --quiet # we don't even call the program
    ++check_external_diff   0 empty  empty 0 on  --quiet
    ++check_external_diff   1 empty  empty 1 on  --quiet
    ++check_external_diff 128 empty  error 2 on  --quiet

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

    @@ userdiff.h
     @@ userdiff.h: struct userdiff_funcname {

      struct external_diff {
    - 	const char *cmd;
    + 	char *cmd;
     +	unsigned trust_exit_code:1;
      };

--
2.45.2

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [PATCH v3 1/3] t4020: test exit code with external diffs
  2024-06-09  7:35     ` [PATCH v3 " René Scharfe
@ 2024-06-09  7:38       ` René Scharfe
  2024-06-10 16:33         ` Junio C Hamano
  2024-06-09  7:39       ` [PATCH v3 2/3] userdiff: add and use struct external_diff René Scharfe
                         ` (2 subsequent siblings)
  3 siblings, 1 reply; 32+ messages in thread
From: René Scharfe @ 2024-06-09  7:38 UTC (permalink / raw
  To: git; +Cc: Junio C Hamano, German Lashevich, Phillip Wood, Johannes Sixt

Add tests to check the exit code of git diff with its options --quiet
and --exit-code when using an external diff program.  Currently we
cannot tell whether it found significant changes or not.

While at it, document briefly that --quiet turns off execution of
external diff programs because that behavior surprised me for a moment
while writing the tests.

Signed-off-by: René Scharfe <l.s.r@web.de>
---
 Documentation/diff-options.txt |  1 +
 t/t4020-diff-external.sh       | 53 ++++++++++++++++++++++++++++++++++
 2 files changed, 54 insertions(+)

diff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt
index c7df20e571..6b73daf540 100644
--- a/Documentation/diff-options.txt
+++ b/Documentation/diff-options.txt
@@ -820,6 +820,7 @@ ifndef::git-log[]

 --quiet::
 	Disable all output of the program. Implies `--exit-code`.
+	Disables execution of external diff helpers.
 endif::git-log[]
 endif::git-format-patch[]

diff --git a/t/t4020-diff-external.sh b/t/t4020-diff-external.sh
index fdd865f7c3..4070523cdb 100755
--- a/t/t4020-diff-external.sh
+++ b/t/t4020-diff-external.sh
@@ -172,6 +172,59 @@ test_expect_success 'no diff with -diff' '
 	grep Binary out
 '

+check_external_diff () {
+	expect_code=$1
+	expect_out=$2
+	expect_err=$3
+	command_code=$4
+	shift 4
+	options="$@"
+
+	command="echo output; exit $command_code;"
+	desc="external diff '$command'"
+	with_options="${options:+ with }$options"
+
+	test_expect_success "$desc via attribute$with_options" "
+		test_config diff.foo.command \"$command\" &&
+		echo \"file diff=foo\" >.gitattributes &&
+		test_expect_code $expect_code git diff $options >out 2>err &&
+		test_cmp $expect_out out &&
+		test_cmp $expect_err err
+	"
+
+	test_expect_success "$desc via diff.external$with_options" "
+		test_config diff.external \"$command\" &&
+		>.gitattributes &&
+		test_expect_code $expect_code git diff $options >out 2>err &&
+		test_cmp $expect_out out &&
+		test_cmp $expect_err err
+	"
+
+	test_expect_success "$desc via GIT_EXTERNAL_DIFF$with_options" "
+		>.gitattributes &&
+		test_expect_code $expect_code env \
+			GIT_EXTERNAL_DIFF=\"$command\" \
+			git diff $options >out 2>err &&
+		test_cmp $expect_out out &&
+		test_cmp $expect_err err
+	"
+}
+
+test_expect_success 'setup output files' '
+	: >empty &&
+	echo output >output &&
+	echo "fatal: external diff died, stopping at file" >error
+'
+
+check_external_diff   0 output empty 0
+check_external_diff 128 output error 1
+
+check_external_diff   1 output empty 0 --exit-code
+check_external_diff 128 output error 1 --exit-code
+
+check_external_diff   1 empty  empty 0 --quiet
+check_external_diff   1 empty  empty 1 --quiet # we don't even call the program
+
 echo NULZbetweenZwords | perl -pe 'y/Z/\000/' > file

 test_expect_success 'force diff with "diff"' '
--
2.45.2

^ permalink raw reply related	[flat|nested] 32+ messages in thread

* [PATCH v3 2/3] userdiff: add and use struct external_diff
  2024-06-09  7:35     ` [PATCH v3 " René Scharfe
  2024-06-09  7:38       ` [PATCH v3 1/3] t4020: test exit code with external diffs René Scharfe
@ 2024-06-09  7:39       ` René Scharfe
  2024-06-09  7:41       ` [PATCH v3 3/3] diff: let external diffs report that changes are uninteresting René Scharfe
  2024-06-10 13:48       ` [PATCH v3 0/3] diff: fix --exit-code with external diff phillip.wood123
  3 siblings, 0 replies; 32+ messages in thread
From: René Scharfe @ 2024-06-09  7:39 UTC (permalink / raw
  To: git; +Cc: Junio C Hamano, German Lashevich, Phillip Wood, Johannes Sixt

Wrap the string specifying the external diff command in a new struct to
simplify adding attributes, which the next patch will do.

Make sure external_diff() still returns NULL if neither the environment
variable GIT_EXTERNAL_DIFF nor the configuration option diff.external is
set, to continue allowing its use in a boolean context.

Use a designated initializer for the default builtin userdiff driver to
adjust to the type change of the second struct member.  Spelling out
only the non-zero members improves readability as a nice side-effect.

No functional change intended.

Signed-off-by: René Scharfe <l.s.r@web.de>
---
 diff.c     | 32 +++++++++++++++++---------------
 userdiff.c |  4 ++--
 userdiff.h |  6 +++++-
 3 files changed, 24 insertions(+), 18 deletions(-)

diff --git a/diff.c b/diff.c
index e70301df76..4b86c61631 100644
--- a/diff.c
+++ b/diff.c
@@ -57,7 +57,7 @@ static int diff_color_moved_ws_default;
 static int diff_context_default = 3;
 static int diff_interhunk_context_default;
 static char *diff_word_regex_cfg;
-static char *external_diff_cmd_cfg;
+static struct external_diff external_diff_cfg;
 static char *diff_order_file_cfg;
 int diff_auto_refresh_index = 1;
 static int diff_mnemonic_prefix;
@@ -431,7 +431,7 @@ int git_diff_ui_config(const char *var, const char *value,
 		return 0;
 	}
 	if (!strcmp(var, "diff.external"))
-		return git_config_string(&external_diff_cmd_cfg, var, value);
+		return git_config_string(&external_diff_cfg.cmd, var, value);
 	if (!strcmp(var, "diff.wordregex"))
 		return git_config_string(&diff_word_regex_cfg, var, value);
 	if (!strcmp(var, "diff.orderfile"))
@@ -548,18 +548,20 @@ static char *quote_two(const char *one, const char *two)
 	return strbuf_detach(&res, NULL);
 }

-static const char *external_diff(void)
+static const struct external_diff *external_diff(void)
 {
-	static const char *external_diff_cmd = NULL;
+	static struct external_diff external_diff_env, *external_diff_ptr;
 	static int done_preparing = 0;

 	if (done_preparing)
-		return external_diff_cmd;
-	external_diff_cmd = xstrdup_or_null(getenv("GIT_EXTERNAL_DIFF"));
-	if (!external_diff_cmd)
-		external_diff_cmd = external_diff_cmd_cfg;
+		return external_diff_ptr;
+	external_diff_env.cmd = xstrdup_or_null(getenv("GIT_EXTERNAL_DIFF"));
+	if (external_diff_env.cmd)
+		external_diff_ptr = &external_diff_env;
+	else if (external_diff_cfg.cmd)
+		external_diff_ptr = &external_diff_cfg;
 	done_preparing = 1;
-	return external_diff_cmd;
+	return external_diff_ptr;
 }

 /*
@@ -4375,7 +4377,7 @@ static void add_external_diff_name(struct repository *r,
  *               infile2 infile2-sha1 infile2-mode [ rename-to ]
  *
  */
-static void run_external_diff(const char *pgm,
+static void run_external_diff(const struct external_diff *pgm,
 			      const char *name,
 			      const char *other,
 			      struct diff_filespec *one,
@@ -4386,7 +4388,7 @@ static void run_external_diff(const char *pgm,
 	struct child_process cmd = CHILD_PROCESS_INIT;
 	struct diff_queue_struct *q = &diff_queued_diff;

-	strvec_push(&cmd.args, pgm);
+	strvec_push(&cmd.args, pgm->cmd);
 	strvec_push(&cmd.args, name);

 	if (one && two) {
@@ -4512,7 +4514,7 @@ static void fill_metainfo(struct strbuf *msg,
 	}
 }

-static void run_diff_cmd(const char *pgm,
+static void run_diff_cmd(const struct external_diff *pgm,
 			 const char *name,
 			 const char *other,
 			 const char *attr_path,
@@ -4530,8 +4532,8 @@ static void run_diff_cmd(const char *pgm,
 	if (o->flags.allow_external || !o->ignore_driver_algorithm)
 		drv = userdiff_find_by_path(o->repo->index, attr_path);

-	if (o->flags.allow_external && drv && drv->external)
-		pgm = drv->external;
+	if (o->flags.allow_external && drv && drv->external.cmd)
+		pgm = &drv->external;

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

 static void run_diff(struct diff_filepair *p, struct diff_options *o)
 {
-	const char *pgm = external_diff();
+	const struct external_diff *pgm = external_diff();
 	struct strbuf msg;
 	struct diff_filespec *one = p->one;
 	struct diff_filespec *two = p->two;
diff --git a/userdiff.c b/userdiff.c
index 82bc76b910..f47e2d9f36 100644
--- a/userdiff.c
+++ b/userdiff.c
@@ -333,7 +333,7 @@ PATTERNS("scheme",
 	 "|([^][)(}{[ \t])+"),
 PATTERNS("tex", "^(\\\\((sub)*section|chapter|part)\\*{0,1}\\{.*)$",
 	 "\\\\[a-zA-Z@]+|\\\\.|([a-zA-Z0-9]|[^\x01-\x7f])+"),
-{ "default", NULL, NULL, -1, { NULL, 0 } },
+{ .name = "default", .binary = -1 },
 };
 #undef PATTERNS
 #undef IPATTERN
@@ -445,7 +445,7 @@ int userdiff_config(const char *k, const char *v)
 	if (!strcmp(type, "binary"))
 		return parse_tristate(&drv->binary, k, v);
 	if (!strcmp(type, "command"))
-		return git_config_string(&drv->external, k, v);
+		return git_config_string(&drv->external.cmd, k, v);
 	if (!strcmp(type, "textconv"))
 		return git_config_string(&drv->textconv, k, v);
 	if (!strcmp(type, "cachetextconv"))
diff --git a/userdiff.h b/userdiff.h
index cc8e5abfef..2d59a8fc56 100644
--- a/userdiff.h
+++ b/userdiff.h
@@ -11,9 +11,13 @@ struct userdiff_funcname {
 	int cflags;
 };

+struct external_diff {
+	char *cmd;
+};
+
 struct userdiff_driver {
 	const char *name;
-	char *external;
+	struct external_diff external;
 	char *algorithm;
 	int binary;
 	struct userdiff_funcname funcname;
--
2.45.2

^ permalink raw reply related	[flat|nested] 32+ messages in thread

* [PATCH v3 3/3] diff: let external diffs report that changes are uninteresting
  2024-06-09  7:35     ` [PATCH v3 " René Scharfe
  2024-06-09  7:38       ` [PATCH v3 1/3] t4020: test exit code with external diffs René Scharfe
  2024-06-09  7:39       ` [PATCH v3 2/3] userdiff: add and use struct external_diff René Scharfe
@ 2024-06-09  7:41       ` René Scharfe
  2024-06-10 13:48       ` [PATCH v3 0/3] diff: fix --exit-code with external diff phillip.wood123
  3 siblings, 0 replies; 32+ messages in thread
From: René Scharfe @ 2024-06-09  7:41 UTC (permalink / raw
  To: git; +Cc: Junio C Hamano, German Lashevich, Phillip Wood, Johannes Sixt

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 11be65cfa4 (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 <phillip.wood123@gmail.com>
Signed-off-by: René Scharfe <l.s.r@web.de>
---
 Documentation/config/diff.txt   | 18 +++++++++++++++++
 Documentation/diff-options.txt  |  6 +++++-
 Documentation/git.txt           | 10 +++++++++
 Documentation/gitattributes.txt |  5 +++++
 diff.c                          | 36 ++++++++++++++++++++++++++++++++-
 t/t4020-diff-external.sh        | 33 +++++++++++++++++++++---------
 userdiff.c                      |  4 ++++
 userdiff.h                      |  1 +
 8 files changed, 101 insertions(+), 12 deletions(-)

diff --git a/Documentation/config/diff.txt b/Documentation/config/diff.txt
index 5ce7b91f1d..190bda17e5 100644
--- a/Documentation/config/diff.txt
+++ b/Documentation/config/diff.txt
@@ -79,6 +79,15 @@ diff.external::
 	you want to use an external diff program only on a subset of
 	your files, you might want to use linkgit:gitattributes[5] instead.

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

+diff.<driver>.trustExitCode::
+	If this boolean value is set to true then the
+	`diff.<driver>.command` command is expected to return exit code
+	0 if it considers the input files to be equal or 1 if it
+	considers them to be different, like `diff(1)`.
+	If it is set to false, which is the default, then the command
+	is expected to return exit code 0 regardless of equality.
+	Any other exit code causes Git to report a fatal error.
+
 diff.<driver>.xfuncname::
 	The regular expression that the diff driver should use to
 	recognize the hunk header.  A built-in pattern may also be used.
diff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt
index 6b73daf540..cd0b81adbb 100644
--- a/Documentation/diff-options.txt
+++ b/Documentation/diff-options.txt
@@ -820,7 +820,11 @@ ifndef::git-log[]

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

diff --git a/Documentation/git.txt b/Documentation/git.txt
index a31a70acca..4489e2297a 100644
--- a/Documentation/git.txt
+++ b/Documentation/git.txt
@@ -644,6 +644,16 @@ parameter, <path>.
 For each path `GIT_EXTERNAL_DIFF` is called, two environment variables,
 `GIT_DIFF_PATH_COUNTER` and `GIT_DIFF_PATH_TOTAL` are set.

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

diff --git a/Documentation/gitattributes.txt b/Documentation/gitattributes.txt
index 4338d023d9..80cae17f37 100644
--- a/Documentation/gitattributes.txt
+++ b/Documentation/gitattributes.txt
@@ -776,6 +776,11 @@ with the above configuration, i.e. `j-c-diff`, with 7
 parameters, just like `GIT_EXTERNAL_DIFF` program is called.
 See linkgit:git[1] for details.

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

diff --git a/diff.c b/diff.c
index 4b86c61631..39ba842049 100644
--- a/diff.c
+++ b/diff.c
@@ -432,6 +432,10 @@ int git_diff_ui_config(const char *var, const char *value,
 	}
 	if (!strcmp(var, "diff.external"))
 		return git_config_string(&external_diff_cfg.cmd, var, value);
+	if (!strcmp(var, "diff.trustexitcode")) {
+		external_diff_cfg.trust_exit_code = git_config_bool(var, value);
+		return 0;
+	}
 	if (!strcmp(var, "diff.wordregex"))
 		return git_config_string(&diff_word_regex_cfg, var, value);
 	if (!strcmp(var, "diff.orderfile"))
@@ -556,6 +560,8 @@ static const struct external_diff *external_diff(void)
 	if (done_preparing)
 		return external_diff_ptr;
 	external_diff_env.cmd = xstrdup_or_null(getenv("GIT_EXTERNAL_DIFF"));
+	if (git_env_bool("GIT_EXTERNAL_DIFF_TRUST_EXIT_CODE", 0))
+		external_diff_env.trust_exit_code = 1;
 	if (external_diff_env.cmd)
 		external_diff_ptr = &external_diff_env;
 	else if (external_diff_cfg.cmd)
@@ -4387,6 +4393,19 @@ static void run_external_diff(const struct external_diff *pgm,
 {
 	struct child_process cmd = CHILD_PROCESS_INIT;
 	struct diff_queue_struct *q = &diff_queued_diff;
+	int quiet = !(o->output_format & DIFF_FORMAT_PATCH);
+	int rc;
+
+	/*
+	 * Trivial equality is handled by diff_unmodified_pair() before
+	 * we get here.  If we don't need to show the diff and the
+	 * external diff program lacks the ability to tell us whether
+	 * it's empty then we consider it non-empty without even asking.
+	 */
+	if (!pgm->trust_exit_code && quiet) {
+		o->found_changes = 1;
+		return;
+	}

 	strvec_push(&cmd.args, pgm->cmd);
 	strvec_push(&cmd.args, name);
@@ -4408,7 +4427,15 @@ static void run_external_diff(const struct external_diff *pgm,
 	diff_free_filespec_data(one);
 	diff_free_filespec_data(two);
 	cmd.use_shell = 1;
-	if (run_command(&cmd))
+	cmd.no_stdout = quiet;
+	rc = run_command(&cmd);
+	if (!pgm->trust_exit_code && rc == 0)
+		o->found_changes = 1;
+	else if (pgm->trust_exit_code && rc == 0)
+		; /* nothing */
+	else if (pgm->trust_exit_code && rc == 1)
+		o->found_changes = 1;
+	else
 		die(_("external diff died, stopping at %s"), name);

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

+	/*
+	 * External diffs could declare non-identical contents equal
+	 * (think diff --ignore-space-change).
+	 */
+	if (options->flags.allow_external && options->flags.exit_with_status)
+		options->flags.diff_from_contents = 1;
+
 	options->diff_path_counter = 0;

 	if (options->flags.follow_renames)
diff --git a/t/t4020-diff-external.sh b/t/t4020-diff-external.sh
index 4070523cdb..3baa52a9bf 100755
--- a/t/t4020-diff-external.sh
+++ b/t/t4020-diff-external.sh
@@ -177,15 +177,17 @@ check_external_diff () {
 	expect_out=$2
 	expect_err=$3
 	command_code=$4
-	shift 4
+	trust_exit_code=$5
+	shift 5
 	options="$@"

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

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

 	test_expect_success "$desc via diff.external$with_options" "
 		test_config diff.external \"$command\" &&
+		test_config diff.trustExitCode $trust_exit_code &&
 		>.gitattributes &&
 		test_expect_code $expect_code git diff $options >out 2>err &&
 		test_cmp $expect_out out &&
@@ -204,6 +207,7 @@ check_external_diff () {
 		>.gitattributes &&
 		test_expect_code $expect_code env \
 			GIT_EXTERNAL_DIFF=\"$command\" \
+			GIT_EXTERNAL_DIFF_TRUST_EXIT_CODE=$trust_exit_code \
 			git diff $options >out 2>err &&
 		test_cmp $expect_out out &&
 		test_cmp $expect_err err
@@ -216,14 +220,23 @@ test_expect_success 'setup output files' '
 	echo "fatal: external diff died, stopping at file" >error
 '

-check_external_diff   0 output empty 0
-check_external_diff 128 output error 1
-
-check_external_diff   1 output empty 0 --exit-code
-check_external_diff 128 output error 1 --exit-code
-
-check_external_diff   1 empty  empty 0 --quiet
-check_external_diff   1 empty  empty 1 --quiet # we don't even call the program
+check_external_diff   0 output empty 0 off
+check_external_diff 128 output error 1 off
+check_external_diff   0 output empty 0 on
+check_external_diff   0 output empty 1 on
+check_external_diff 128 output error 2 on
+
+check_external_diff   1 output empty 0 off --exit-code
+check_external_diff 128 output error 1 off --exit-code
+check_external_diff   0 output empty 0 on  --exit-code
+check_external_diff   1 output empty 1 on  --exit-code
+check_external_diff 128 output error 2 on  --exit-code
+
+check_external_diff   1 empty  empty 0 off --quiet
+check_external_diff   1 empty  empty 1 off --quiet # we don't even call the program
+check_external_diff   0 empty  empty 0 on  --quiet
+check_external_diff   1 empty  empty 1 on  --quiet
+check_external_diff 128 empty  error 2 on  --quiet

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

diff --git a/userdiff.c b/userdiff.c
index f47e2d9f36..6cdfb147c3 100644
--- a/userdiff.c
+++ b/userdiff.c
@@ -446,6 +446,10 @@ int userdiff_config(const char *k, const char *v)
 		return parse_tristate(&drv->binary, k, v);
 	if (!strcmp(type, "command"))
 		return git_config_string(&drv->external.cmd, k, v);
+	if (!strcmp(type, "trustexitcode")) {
+		drv->external.trust_exit_code = git_config_bool(k, v);
+		return 0;
+	}
 	if (!strcmp(type, "textconv"))
 		return git_config_string(&drv->textconv, k, v);
 	if (!strcmp(type, "cachetextconv"))
diff --git a/userdiff.h b/userdiff.h
index 2d59a8fc56..f5cb53d0d4 100644
--- a/userdiff.h
+++ b/userdiff.h
@@ -13,6 +13,7 @@ struct userdiff_funcname {

 struct external_diff {
 	char *cmd;
+	unsigned trust_exit_code:1;
 };

 struct userdiff_driver {
--
2.45.2

^ permalink raw reply related	[flat|nested] 32+ messages in thread

* Re: [PATCH v3 0/3] diff: fix --exit-code with external diff
  2024-06-09  7:35     ` [PATCH v3 " René Scharfe
                         ` (2 preceding siblings ...)
  2024-06-09  7:41       ` [PATCH v3 3/3] diff: let external diffs report that changes are uninteresting René Scharfe
@ 2024-06-10 13:48       ` phillip.wood123
  3 siblings, 0 replies; 32+ messages in thread
From: phillip.wood123 @ 2024-06-10 13:48 UTC (permalink / raw
  To: René Scharfe, git
  Cc: Junio C Hamano, German Lashevich, Phillip Wood, Johannes Sixt

Hi René

On 09/06/2024 08:35, René Scharfe wrote:
> Changes since v2:
> - rebase; config strings are no longer const
> - document the handling of unexpected exit codes
> - document that external diffs are skipped with --quiet and trustExitCode=off
> - silence external diff output with --quiet
> - check output in tests
> - test diff runs without --exit-code and --quiet as well
> - slightly untangle the exit code handling code to make it easier to read
> - fix copy/paste error in documentation of diff.<driver>.trustExitCode

The changes in this version all look good. I've re-read the patches and 
didn't spot any other issues so this is ready as far as I'm concerned.

Thanks

Phillip

>    t4020: test exit code with external diffs
>    userdiff: add and use struct external_diff
>    diff: let external diffs report that changes are uninteresting
> 
>   Documentation/config/diff.txt   | 18 +++++++++
>   Documentation/diff-options.txt  |  5 +++
>   Documentation/git.txt           | 10 +++++
>   Documentation/gitattributes.txt |  5 +++
>   diff.c                          | 68 +++++++++++++++++++++++++--------
>   t/t4020-diff-external.sh        | 66 ++++++++++++++++++++++++++++++++
>   userdiff.c                      |  8 +++-
>   userdiff.h                      |  7 +++-
>   8 files changed, 168 insertions(+), 19 deletions(-)
> 
> Range-Diff gegen v2:
> 1:  118aba667b < -:  ---------- t4020: test exit code with external diffs
> -:  ---------- > 1:  d59f0c6fdf t4020: test exit code with external diffs
> 2:  0b4dabebe1 ! 2:  4ad160ab1f userdiff: add and use struct external_diff
>      @@ diff.c
>       @@ diff.c: static int diff_color_moved_ws_default;
>        static int diff_context_default = 3;
>        static int diff_interhunk_context_default;
>      - static const char *diff_word_regex_cfg;
>      --static const char *external_diff_cmd_cfg;
>      + static char *diff_word_regex_cfg;
>      +-static char *external_diff_cmd_cfg;
>       +static struct external_diff external_diff_cfg;
>      - static const char *diff_order_file_cfg;
>      + static char *diff_order_file_cfg;
>        int diff_auto_refresh_index = 1;
>        static int diff_mnemonic_prefix;
>       @@ diff.c: int git_diff_ui_config(const char *var, const char *value,
>      @@ userdiff.h: struct userdiff_funcname {
>        };
> 
>       +struct external_diff {
>      -+	const char *cmd;
>      ++	char *cmd;
>       +};
>       +
>        struct userdiff_driver {
>        	const char *name;
>      --	const char *external;
>      +-	char *external;
>       +	struct external_diff external;
>      - 	const char *algorithm;
>      + 	char *algorithm;
>        	int binary;
>        	struct userdiff_funcname funcname;
> 3:  4d54ca8281 ! 3:  29c8d3b61a diff: let external diffs report that changes are uninteresting
>      @@ Commit message
>           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 11be65cfa4 (diff: fix --exit-code with
>           external diff, 2024-05-05) and quickly reverted, as it does not work
>      @@ Documentation/config/diff.txt: diff.external::
>        	your files, you might want to use linkgit:gitattributes[5] instead.
> 
>       +diff.trustExitCode::
>      -+	If this boolean value is set to true then the `diff.external`
>      -+	command is expected to return exit code 1 if it finds
>      -+	significant changes and 0 if it doesn't, like diff(1).  If it's
>      -+	false then the `diff.external` command is expected to always
>      -+	return exit code 0.  Defaults to false.
>      ++	If this boolean value is set to true then the
>      ++	`diff.external` command is expected to return exit code
>      ++	0 if it considers the input files to be equal or 1 if it
>      ++	considers them to be different, like `diff(1)`.
>      ++	If it is set to false, which is the default, then the command
>      ++	is expected to return exit code 0 regardless of equality.
>      ++	Any other exit code causes Git to report a fatal error.
>       +
>        diff.ignoreSubmodules::
>        	Sets the default value of --ignore-submodules. Note that this
>      @@ Documentation/config/diff.txt: diff.<driver>.command::
>       +diff.<driver>.trustExitCode::
>       +	If this boolean value is set to true then the
>       +	`diff.<driver>.command` command is expected to return exit code
>      -+	1 if it finds significant changes and 0 if it doesn't, like
>      -+	diff(1).  If it's false then the `diff.external` command is
>      -+	expected to always return exit code 0.  Defaults to false.
>      ++	0 if it considers the input files to be equal or 1 if it
>      ++	considers them to be different, like `diff(1)`.
>      ++	If it is set to false, which is the default, then the command
>      ++	is expected to return exit code 0 regardless of equality.
>      ++	Any other exit code causes Git to report a fatal error.
>       +
>        diff.<driver>.xfuncname::
>        	The regular expression that the diff driver should use to
>        	recognize the hunk header.  A built-in pattern may also be used.
> 
>      + ## Documentation/diff-options.txt ##
>      +@@ Documentation/diff-options.txt: ifndef::git-log[]
>      +
>      + --quiet::
>      + 	Disable all output of the program. Implies `--exit-code`.
>      +-	Disables execution of external diff helpers.
>      ++	Disables execution of external diff helpers whose exit code
>      ++	is not trusted, i.e. their respective configuration option
>      ++	`diff.trustExitCode` or `diff.<driver>.trustExitCode` or
>      ++	environment variable `GIT_EXTERNAL_DIFF_TRUST_EXIT_CODE` is
>      ++	false.
>      + endif::git-log[]
>      + endif::git-format-patch[]
>      +
>      +
>        ## Documentation/git.txt ##
>       @@ Documentation/git.txt: parameter, <path>.
>        For each path `GIT_EXTERNAL_DIFF` is called, two environment variables,
>        `GIT_DIFF_PATH_COUNTER` and `GIT_DIFF_PATH_TOTAL` are set.
> 
>       +`GIT_EXTERNAL_DIFF_TRUST_EXIT_CODE`::
>      -+	Setting this environment variable indicates the the program
>      -+	specified with `GIT_EXTERNAL_DIFF` returns exit code 1 if it
>      -+	finds significant changes and 0 if it doesn't, like diff(1).
>      -+	If it's not set, the program is expected to always return
>      -+	exit code 0.
>      ++	If this Boolean environment variable is set to true then the
>      ++	`GIT_EXTERNAL_DIFF` command is expected to return exit code
>      ++	0 if it considers the input files to be equal or 1 if it
>      ++	considers them to be different, like `diff(1)`.
>      ++	If it is set to false, which is the default, then the command
>      ++	is expected to return exit code 0 regardless of equality.
>      ++	Any other exit code causes Git to report a fatal error.
>      ++
>       +
>        `GIT_DIFF_PATH_COUNTER`::
>        	A 1-based counter incremented by one for every path.
>      @@ diff.c: static void run_external_diff(const struct external_diff *pgm,
>        {
>        	struct child_process cmd = CHILD_PROCESS_INIT;
>        	struct diff_queue_struct *q = &diff_queued_diff;
>      ++	int quiet = !(o->output_format & DIFF_FORMAT_PATCH);
>       +	int rc;
>       +
>       +	/*
>      @@ diff.c: static void run_external_diff(const struct external_diff *pgm,
>       +	 * external diff program lacks the ability to tell us whether
>       +	 * it's empty then we consider it non-empty without even asking.
>       +	 */
>      -+	if (!(o->output_format & DIFF_FORMAT_PATCH) && !pgm->trust_exit_code) {
>      ++	if (!pgm->trust_exit_code && quiet) {
>       +		o->found_changes = 1;
>       +		return;
>       +	}
>      @@ diff.c: static void run_external_diff(const struct external_diff *pgm,
>        	diff_free_filespec_data(two);
>        	cmd.use_shell = 1;
>       -	if (run_command(&cmd))
>      ++	cmd.no_stdout = quiet;
>       +	rc = run_command(&cmd);
>      -+	if ((!pgm->trust_exit_code && !rc) || (pgm->trust_exit_code && rc == 1))
>      ++	if (!pgm->trust_exit_code && rc == 0)
>      ++		o->found_changes = 1;
>      ++	else if (pgm->trust_exit_code && rc == 0)
>      ++		; /* nothing */
>      ++	else if (pgm->trust_exit_code && rc == 1)
>       +		o->found_changes = 1;
>      -+	else if (!pgm->trust_exit_code || rc)
>      ++	else
>        		die(_("external diff died, stopping at %s"), name);
> 
>        	remove_tempfile();
>      @@ diff.c: void diff_setup_done(struct diff_options *options)
>        	if (options->flags.follow_renames)
> 
>        ## t/t4020-diff-external.sh ##
>      -@@ t/t4020-diff-external.sh: test_expect_success 'no diff with -diff' '
>      - check_external_exit_code () {
>      - 	expect_code=$1
>      - 	command_code=$2
>      --	option=$3
>      -+	trust_exit_code=$3
>      -+	option=$4
>      -
>      - 	command="exit $command_code;"
>      +@@ t/t4020-diff-external.sh: check_external_diff () {
>      + 	expect_out=$2
>      + 	expect_err=$3
>      + 	command_code=$4
>      +-	shift 4
>      ++	trust_exit_code=$5
>      ++	shift 5
>      + 	options="$@"
>      +
>      + 	command="echo output; exit $command_code;"
>       -	desc="external diff '$command'"
>       +	desc="external diff '$command' with trustExitCode=$trust_exit_code"
>      + 	with_options="${options:+ with }$options"
> 
>      - 	test_expect_success "$desc via attribute with $option" "
>      + 	test_expect_success "$desc via attribute$with_options" "
>        		test_config diff.foo.command \"$command\" &&
>       +		test_config diff.foo.trustExitCode $trust_exit_code &&
>        		echo \"file diff=foo\" >.gitattributes &&
>      - 		test_expect_code $expect_code git diff $option
>      - 	"
>      + 		test_expect_code $expect_code git diff $options >out 2>err &&
>      + 		test_cmp $expect_out out &&
>      +@@ t/t4020-diff-external.sh: check_external_diff () {
> 
>      - 	test_expect_success "$desc via diff.external with $option" "
>      + 	test_expect_success "$desc via diff.external$with_options" "
>        		test_config diff.external \"$command\" &&
>       +		test_config diff.trustExitCode $trust_exit_code &&
>        		>.gitattributes &&
>      - 		test_expect_code $expect_code git diff $option
>      - 	"
>      -@@ t/t4020-diff-external.sh: check_external_exit_code () {
>      + 		test_expect_code $expect_code git diff $options >out 2>err &&
>      + 		test_cmp $expect_out out &&
>      +@@ t/t4020-diff-external.sh: check_external_diff () {
>        		>.gitattributes &&
>        		test_expect_code $expect_code env \
>        			GIT_EXTERNAL_DIFF=\"$command\" \
>       +			GIT_EXTERNAL_DIFF_TRUST_EXIT_CODE=$trust_exit_code \
>      - 			git diff $option
>      - 	"
>      - }
>      -
>      --check_external_exit_code   1 0 --exit-code
>      --check_external_exit_code   1 0 --quiet
>      --check_external_exit_code 128 1 --exit-code
>      --check_external_exit_code   1 1 --quiet # we don't even call the program
>      -+check_external_exit_code   1 0 off --exit-code
>      -+check_external_exit_code   1 0 off --quiet
>      -+check_external_exit_code 128 1 off --exit-code
>      -+check_external_exit_code   1 1 off --quiet # we don't even call the program
>      + 			git diff $options >out 2>err &&
>      + 		test_cmp $expect_out out &&
>      + 		test_cmp $expect_err err
>      +@@ t/t4020-diff-external.sh: test_expect_success 'setup output files' '
>      + 	echo "fatal: external diff died, stopping at file" >error
>      + '
>      +
>      +-check_external_diff   0 output empty 0
>      +-check_external_diff 128 output error 1
>      +-
>      +-check_external_diff   1 output empty 0 --exit-code
>      +-check_external_diff 128 output error 1 --exit-code
>      +-
>      +-check_external_diff   1 empty  empty 0 --quiet
>      +-check_external_diff   1 empty  empty 1 --quiet # we don't even call the program
>      ++check_external_diff   0 output empty 0 off
>      ++check_external_diff 128 output error 1 off
>      ++check_external_diff   0 output empty 0 on
>      ++check_external_diff   0 output empty 1 on
>      ++check_external_diff 128 output error 2 on
>      ++
>      ++check_external_diff   1 output empty 0 off --exit-code
>      ++check_external_diff 128 output error 1 off --exit-code
>      ++check_external_diff   0 output empty 0 on  --exit-code
>      ++check_external_diff   1 output empty 1 on  --exit-code
>      ++check_external_diff 128 output error 2 on  --exit-code
>       +
>      -+check_external_exit_code   0 0 on --exit-code
>      -+check_external_exit_code   0 0 on --quiet
>      -+check_external_exit_code   1 1 on --exit-code
>      -+check_external_exit_code   1 1 on --quiet
>      -+check_external_exit_code 128 2 on --exit-code
>      -+check_external_exit_code 128 2 on --quiet
>      ++check_external_diff   1 empty  empty 0 off --quiet
>      ++check_external_diff   1 empty  empty 1 off --quiet # we don't even call the program
>      ++check_external_diff   0 empty  empty 0 on  --quiet
>      ++check_external_diff   1 empty  empty 1 on  --quiet
>      ++check_external_diff 128 empty  error 2 on  --quiet
> 
>        echo NULZbetweenZwords | perl -pe 'y/Z/\000/' > file
> 
>      @@ userdiff.h
>       @@ userdiff.h: struct userdiff_funcname {
> 
>        struct external_diff {
>      - 	const char *cmd;
>      + 	char *cmd;
>       +	unsigned trust_exit_code:1;
>        };
> 
> --
> 2.45.2

^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: [PATCH v3 1/3] t4020: test exit code with external diffs
  2024-06-09  7:38       ` [PATCH v3 1/3] t4020: test exit code with external diffs René Scharfe
@ 2024-06-10 16:33         ` Junio C Hamano
  0 siblings, 0 replies; 32+ messages in thread
From: Junio C Hamano @ 2024-06-10 16:33 UTC (permalink / raw
  To: René Scharfe; +Cc: git, German Lashevich, Phillip Wood, Johannes Sixt

René Scharfe <l.s.r@web.de> writes:

> +check_external_diff () {
> +	expect_code=$1
> +	expect_out=$2
> +	expect_err=$3
> +	command_code=$4
> +	shift 4
> +	options="$@"

Tiny nit, but I'd prefer to see "$@" reserved for its magic and all
other times where it is equivalent to "$*", see the latter used.

Other than that, all three patches looked good to me.  Thanks.

^ permalink raw reply	[flat|nested] 32+ messages in thread

end of thread, other threads:[~2024-06-10 16:33 UTC | newest]

Thread overview: 32+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2024-04-20  1:13 Possible git-diff bug when using exit-code with diff filters German Lashevich
2024-04-21 10:42 ` René Scharfe
2024-04-21 18:17   ` Junio C Hamano
2024-04-21 18:32     ` rsbecker
2024-04-21 19:09       ` Junio C Hamano
2024-04-21 20:18         ` rsbecker
2024-05-05 10:19     ` René Scharfe
2024-05-06 17:22       ` Junio C Hamano
2024-05-05 10:19   ` [PATCH 1/2] diff: report unmerged paths as changes in run_diff_cmd() René Scharfe
2024-05-05 10:20   ` [PATCH 2/2] diff: fix --exit-code with external diff René Scharfe
2024-05-05 15:25     ` Phillip Wood
2024-05-06 17:31       ` Junio C Hamano
2024-05-06 18:23       ` René Scharfe
2024-05-08 15:25         ` phillip.wood123
2024-05-11 20:32           ` René Scharfe
2024-05-12  9:38             ` René Scharfe
2024-06-05  8:31     ` [PATCH v2 0/3] " René Scharfe
2024-06-05  8:35       ` [PATCH v2 1/3] t4020: test exit code with external diffs René Scharfe
2024-06-05  8:36       ` [PATCH v2 2/3] userdiff: add and use struct external_diff René Scharfe
2024-06-05  8:38       ` [PATCH v2 3/3] diff: let external diffs report that changes are uninteresting René Scharfe
2024-06-06  6:39         ` Johannes Sixt
2024-06-06  8:28           ` René Scharfe
2024-06-06 15:49             ` Junio C Hamano
2024-06-06  9:48         ` Phillip Wood
2024-06-07  8:19           ` René Scharfe
2024-06-05 16:47       ` [PATCH v2 0/3] diff: fix --exit-code with external diff Junio C Hamano
2024-06-09  7:35     ` [PATCH v3 " René Scharfe
2024-06-09  7:38       ` [PATCH v3 1/3] t4020: test exit code with external diffs René Scharfe
2024-06-10 16:33         ` Junio C Hamano
2024-06-09  7:39       ` [PATCH v3 2/3] userdiff: add and use struct external_diff René Scharfe
2024-06-09  7:41       ` [PATCH v3 3/3] diff: let external diffs report that changes are uninteresting René Scharfe
2024-06-10 13:48       ` [PATCH v3 0/3] diff: fix --exit-code with external diff phillip.wood123

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).