[PATCH v3 0/3] diff: fix --exit-code with external diff
- From
René Scharfe <l.s.r@web.de>
- Date
- Jun 9, 2024, 07:35 UTC
- Message-ID
- <168fecaa-2ebd-4897-b0ba-3bd2a37c01e7@web.de>
- In-Reply-To
- <e2e4a4e9-55db-403c-902d-fd8af3aea05c@web.de>
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 --quietecho 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