diff: teach diff to read algorithm from diff driver

It can be useful to specify diff algorithms per file type. For example, one may want to use the minimal diff algorithm for .json files, another for .c files, etc. The diff machinery already checks attributes for a diff driver. Teach the diff driver parser a new type "algorithm" to look for in the config, which will be used if a driver has been specified through the attributes. Enforce precedence of the diff algorithm by favoring the command line option, then looking at the driver attributes & config combination, then finally the diff.algorithm config. To enforce precedence order, use a new `ignore_driver_algorithm` member during options parsing to indicate the diff algorithm was set via command line args. Signed-off-by: John Cai <johncai86@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

John Cai committed Feb 20, 2023 at 21:04 UTC a4cf900ee734ce9bb73d57c5dfbb1da4a5a88bd3
6 files changed +97 -11
Documentation/gitattributes.txt
+31
@@ -758,6 +758,37 @@ with the above configuration, i.e. `j-c-diff`, with 7
758 parameters, just like `GIT_EXTERNAL_DIFF` program is called.
759 See linkgit:git[1] for details.
760
761 +Setting the internal diff algorithm
762 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
763 +
764 +The diff algorithm can be set through the `diff.algorithm` config key, but
765 +sometimes it may be helpful to set the diff algorithm per path. For example,
766 +one may want to use the `minimal` diff algorithm for .json files, and the
767 +`histogram` for .c files, and so on without having to pass in the algorithm
768 +through the command line each time.
769 +
770 +First, in `.gitattributes`, assign the `diff` attribute for paths.
771 +
772 +------------------------
773 +*.json diff=<name>
774 +------------------------
775 +
776 +Then, define a "diff.<name>.algorithm" configuration to specify the diff
777 +algorithm, choosing from `myers`, `patience`, `minimal`, or `histogram`.
778 +
779 +----------------------------------------------------------------
780 +[diff "<name>"]
781 + algorithm = histogram
782 +----------------------------------------------------------------
783 +
784 +This diff algorithm applies to user facing diff output like git-diff(1),
785 +git-show(1) and is used for the `--stat` output as well. The merge machinery
786 +will not use the diff algorithm set through this method.
787 +
788 +NOTE: If `diff.<name>.command` is defined for path with the
789 +`diff=<name>` attribute, it is executed as an external diff driver
790 +(see above), and adding `diff.<name>.algorithm` has no effect, as the
791 +algorithm is not passed to the external diff driver.
792
793 Defining a custom hunk-header
794 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff.c
+24 -9
@@ -4460,15 +4460,13 @@ static void run_diff_cmd(const char *pgm,
4460 const char *xfrm_msg = NULL;
4461 int complete_rewrite = (p->status == DIFF_STATUS_MODIFIED) && p->score;
4462 int must_show_header = 0;
4463 + struct userdiff_driver *drv = NULL;
4464
4464 -
4465 - if (o->flags.allow_external) {
4466 - struct userdiff_driver *drv;
4467 -
4465 + if (o->flags.allow_external || !o->ignore_driver_algorithm)
4466 drv = userdiff_find_by_path(o->repo->index, attr_path);
4469 - if (drv && drv->external)
4470 - pgm = drv->external;
4471 - }
4467 +
4468 + if (o->flags.allow_external && drv && drv->external)
4469 + pgm = drv->external;
4470
4471 if (msg) {
4472 /*
@@ -4485,12 +4483,16 @@ static void run_diff_cmd(const char *pgm,
4483 run_external_diff(pgm, name, other, one, two, xfrm_msg, o);
4484 return;
4485 }
4488 - if (one && two)
4486 + if (one && two) {
4487 + if (!o->ignore_driver_algorithm && drv && drv->algorithm)
4488 + set_diff_algorithm(o, drv->algorithm);
4489 +
4490 builtin_diff(name, other ? other : name,
4491 one, two, xfrm_msg, must_show_header,
4492 o, complete_rewrite);
4492 - else
4493 + } else {
4494 fprintf(o->file, "* Unmerged path %s\n", name);
4495 + }
4496 }
4497
4498 static void diff_fill_oid_info(struct diff_filespec *one, struct index_state *istate)
@@ -4587,6 +4589,14 @@ static void run_diffstat(struct diff_filepair *p, struct diff_options *o,
4589 const char *name;
4590 const char *other;
4591
4592 + if (!o->ignore_driver_algorithm) {
4593 + struct userdiff_driver *drv = userdiff_find_by_path(o->repo->index,
4594 + p->one->path);
4595 +
4596 + if (drv && drv->algorithm)
4597 + set_diff_algorithm(o, drv->algorithm);
4598 + }
4599 +
4600 if (DIFF_PAIR_UNMERGED(p)) {
4601 /* unmerged */
4602 builtin_diffstat(p->one->path, NULL, NULL, NULL,
@@ -5140,6 +5150,8 @@ static int diff_opt_diff_algorithm(const struct option *opt,
5150 return error(_("option diff-algorithm accepts \"myers\", "
5151 "\"minimal\", \"patience\" and \"histogram\""));
5152
5153 + options->ignore_driver_algorithm = 1;
5154 +
5155 return 0;
5156 }
5157
@@ -5155,6 +5167,8 @@ static int diff_opt_diff_algorithm_no_arg(const struct option *opt,
5167 BUG("available diff algorithms include \"myers\", "
5168 "\"minimal\", \"patience\" and \"histogram\"");
5169
5170 + options->ignore_driver_algorithm = 1;
5171 +
5172 return 0;
5173 }
5174
@@ -5295,6 +5309,7 @@ static int diff_opt_patience(const struct option *opt,
5309 for (i = 0; i < options->anchors_nr; i++)
5310 free(options->anchors[i]);
5311 options->anchors_nr = 0;
5312 + options->ignore_driver_algorithm = 1;
5313
5314 return set_diff_algorithm(options, "patience");
5315 }
diff.h
+1
@@ -333,6 +333,7 @@ struct diff_options {
333 int prefix_length;
334 const char *stat_sep;
335 int xdl_opts;
336 + int ignore_driver_algorithm;
337
338 /* see Documentation/diff-options.txt */
339 char **anchors;
t/lib-diff-alternative.sh
+37 -1
@@ -105,10 +105,46 @@ index $file1..$file2 100644
105 }
106 EOF
107
108 + cat >expect_diffstat <<EOF
109 + file1 => file2 | 21 ++++++++++-----------
110 + 1 file changed, 10 insertions(+), 11 deletions(-)
111 +EOF
112 +
113 STRATEGY=$1
114
115 + test_expect_success "$STRATEGY diff from attributes" '
116 + echo "file* diff=driver" >.gitattributes &&
117 + git config diff.driver.algorithm "$STRATEGY" &&
118 + test_must_fail git diff --no-index file1 file2 > output &&
119 + cat expect &&
120 + cat output &&
121 + test_cmp expect output
122 + '
123 +
124 + test_expect_success "$STRATEGY diff from attributes has valid diffstat" '
125 + echo "file* diff=driver" >.gitattributes &&
126 + git config diff.driver.algorithm "$STRATEGY" &&
127 + test_must_fail git diff --stat --no-index file1 file2 > output &&
128 + test_cmp expect_diffstat output
129 + '
130 +
131 test_expect_success "$STRATEGY diff" '
111 - test_must_fail git diff --no-index "--$STRATEGY" file1 file2 > output &&
132 + test_must_fail git diff --no-index "--diff-algorithm=$STRATEGY" file1 file2 > output &&
133 + test_cmp expect output
134 + '
135 +
136 + test_expect_success "$STRATEGY diff command line precedence before attributes" '
137 + echo "file* diff=driver" >.gitattributes &&
138 + git config diff.driver.algorithm myers &&
139 + test_must_fail git diff --no-index "--diff-algorithm=$STRATEGY" file1 file2 > output &&
140 + test_cmp expect output
141 + '
142 +
143 + test_expect_success "$STRATEGY diff attributes precedence before config" '
144 + git config diff.algorithm default &&
145 + echo "file* diff=driver" >.gitattributes &&
146 + git config diff.driver.algorithm "$STRATEGY" &&
147 + test_must_fail git diff --no-index file1 file2 > output &&
148 test_cmp expect output
149 '
150
userdiff.c
+3 -1
@@ -293,7 +293,7 @@ PATTERNS("scheme",
293 "|([^][)(}{[ \t])+"),
294 PATTERNS("tex", "^(\\\\((sub)*section|chapter|part)\\*{0,1}\\{.*)$",
295 "\\\\[a-zA-Z@]+|\\\\.|[a-zA-Z0-9\x80-\xff]+"),
296 -{ "default", NULL, -1, { NULL, 0 } },
296 +{ "default", NULL, NULL, -1, { NULL, 0 } },
297 };
298 #undef PATTERNS
299 #undef IPATTERN
@@ -393,6 +393,8 @@ int userdiff_config(const char *k, const char *v)
393 return parse_bool(&drv->textconv_want_cache, k, v);
394 if (!strcmp(type, "wordregex"))
395 return git_config_string(&drv->word_regex, k, v);
396 + if (!strcmp(type, "algorithm"))
397 + return git_config_string(&drv->algorithm, k, v);
398
399 return 0;
400 }
userdiff.h
+1
@@ -14,6 +14,7 @@ struct userdiff_funcname {
14 struct userdiff_driver {
15 const char *name;
16 const char *external;
17 + const char *algorithm;
18 int binary;
19 struct userdiff_funcname funcname;
20 const char *word_regex;