log,diff-tree: add --combined-all-paths option

The combined diff format for merges will only list one filename, even if rename or copy detection is active. For example, with raw format one might see: ::100644 100644 100644 fabadb8 cc95eb0 4866510 MM describe.c ::100755 100755 100755 52b7a2d 6d1ac04 d2ac7d7 RM bar.sh ::100644 100644 100644 e07d6c5 9042e82 ee91881 RR phooey.c This doesn't let us know what the original name of bar.sh was in the first parent, and doesn't let us know what either of the original names of phooey.c were in either of the parents. In contrast, for non-merge commits, raw format does provide original filenames (and a rename score to boot). In order to also provide original filenames for merge commits, add a --combined-all-paths option (which must be used with either -c or --cc, and is likely only useful with rename or copy detection active) so that we can print tab-separated filenames when renames are involved. This transforms the above output to: ::100644 100644 100644 fabadb8 cc95eb0 4866510 MM desc.c desc.c desc.c ::100755 100755 100755 52b7a2d 6d1ac04 d2ac7d7 RM foo.sh bar.sh bar.sh ::100644 100644 100644 e07d6c5 9042e82 ee91881 RR fooey.c fuey.c phooey.c Further, in patch format, this changes the from/to headers so that instead of just having one "from" header, we get one for each parent. For example, instead of having --- a/phooey.c +++ b/phooey.c we would see --- a/fooey.c --- a/fuey.c +++ b/phooey.c Signed-off-by: Elijah Newren <newren@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Elijah Newren committed Feb 7, 2019 at 17:12 UTC d76ce4f734634f47b467b7f6eea11d6bf8c81f22
10 files changed +212 -17
Documentation/diff-format.txt
+17 -3
@@ -95,12 +95,26 @@ from the format described above in the following way:
95 . there are more "src" modes and "src" sha1
96 . status is concatenated status characters for each parent
97 . no optional "score" number
98 -. single path, only for "dst"
98 +. tab-separated pathname(s) of the file
99
100 -Example:
100 +For `-c` and `--cc`, only the destination or final path is shown even
101 +if the file was renamed on any side of history. With
102 +`--combined-all-paths`, the name of the path in each parent is shown
103 +followed by the name of the path in the merge commit.
104 +
105 +Examples for `-c` and `--cc` without `--combined-all-paths`:
106 +------------------------------------------------
107 +::100644 100644 100644 fabadb8 cc95eb0 4866510 MM desc.c
108 +::100755 100755 100755 52b7a2d 6d1ac04 d2ac7d7 RM bar.sh
109 +::100644 100644 100644 e07d6c5 9042e82 ee91881 RR phooey.c
110 +------------------------------------------------
111 +
112 +Examples when `--combined-all-paths` added to either `-c` or `--cc`:
113
114 ------------------------------------------------
103 -::100644 100644 100644 fabadb8 cc95eb0 4866510 MM describe.c
115 +::100644 100644 100644 fabadb8 cc95eb0 4866510 MM desc.c desc.c desc.c
116 +::100755 100755 100755 52b7a2d 6d1ac04 d2ac7d7 RM foo.sh bar.sh bar.sh
117 +::100644 100644 100644 e07d6c5 9042e82 ee91881 RR fooey.c fuey.c phooey.c
118 ------------------------------------------------
119
120 Note that 'combined diff' lists only files which were modified from
Documentation/diff-generate-patch.txt
+13
@@ -143,6 +143,19 @@ copying detection) are designed to work with diff of two
143 Similar to two-line header for traditional 'unified' diff
144 format, `/dev/null` is used to signal created or deleted
145 files.
146 ++
147 +However, if the --combined-all-paths option is provided, instead of a
148 +two-line from-file/to-file you get a N+1 line from-file/to-file header,
149 +where N is the number of parents in the merge commit
150 +
151 + --- a/file
152 + --- a/file
153 + --- a/file
154 + +++ b/file
155 ++
156 +This extended format can be useful if rename or copy detection is
157 +active, to allow you to see the original name of the file in different
158 +parents.
159
160 4. Chunk header format is modified to prevent people from
161 accidentally feeding it to `patch -p1`. Combined diff format
Documentation/git-diff-tree.txt
+9 -2
@@ -10,8 +10,8 @@ SYNOPSIS
10 --------
11 [verse]
12 'git diff-tree' [--stdin] [-m] [-s] [-v] [--no-commit-id] [--pretty]
13 - [-t] [-r] [-c | --cc] [--root] [<common diff options>]
14 - <tree-ish> [<tree-ish>] [<path>...]
13 + [-t] [-r] [-c | --cc] [--combined-all-paths] [--root]
14 + [<common diff options>] <tree-ish> [<tree-ish>] [<path>...]
15
16 DESCRIPTION
17 -----------
@@ -108,6 +108,13 @@ include::pretty-options.txt[]
108 itself and the commit log message is not shown, just like in any other
109 "empty diff" case.
110
111 +--combined-all-paths::
112 + This flag causes combined diffs (used for merge commits) to
113 + list the name of the file from all parents. It thus only has
114 + effect when -c or --cc are specified, and is likely only
115 + useful if filename changes are detected (i.e. when either
116 + rename or copy detection have been requested).
117 +
118 --always::
119 Show the commit itself and the commit log message even
120 if the diff itself is empty.
Documentation/rev-list-options.txt
+7
@@ -948,6 +948,13 @@ options may be given. See linkgit:git-diff-files[1] for more options.
948 the parents have only two variants and the merge result picks
949 one of them without modification.
950
951 +--combined-all-paths::
952 + This flag causes combined diffs (used for merge commits) to
953 + list the name of the file from all parents. It thus only has
954 + effect when -c or --cc are specified, and is likely only
955 + useful if filename changes are detected (i.e. when either
956 + rename or copy detection have been requested).
957 +
958 -m::
959 This flag makes the merge commits show the full diff like
960 regular commits; for each merge parent, a separate log entry
builtin/diff-tree.c
+5 -1
@@ -82,9 +82,13 @@ static int diff_tree_stdin(char *line)
82 }
83
84 static const char diff_tree_usage[] =
85 -"git diff-tree [--stdin] [-m] [-c] [--cc] [-s] [-v] [--pretty] [-t] [-r] [--root] "
85 +"git diff-tree [--stdin] [-m] [-c | --cc] [-s] [-v] [--pretty] [-t] [-r] [--root] "
86 "[<common-diff-options>] <tree-ish> [<tree-ish>] [<path>...]\n"
87 " -r diff recursively\n"
88 +" -c show combined diff for merge commits\n"
89 +" --cc show combined diff for merge commits removing uninteresting hunks\n"
90 +" --combined-all-paths\n"
91 +" show name of file in all parents for combined diffs\n"
92 " --root include the initial commit as diff against /dev/null\n"
93 COMMON_DIFF_OPTIONS_HELP;
94
combine-diff.c
+65 -11
@@ -23,11 +23,20 @@ static int compare_paths(const struct combine_diff_path *one,
23 two->path, strlen(two->path), two->mode);
24 }
25
26 -static struct combine_diff_path *intersect_paths(struct combine_diff_path *curr, int n, int num_parent)
26 +static int filename_changed(char status)
27 +{
28 + return status == 'R' || status == 'C';
29 +}
30 +
31 +static struct combine_diff_path *intersect_paths(
32 + struct combine_diff_path *curr,
33 + int n,
34 + int num_parent,
35 + int combined_all_paths)
36 {
37 struct diff_queue_struct *q = &diff_queued_diff;
38 struct combine_diff_path *p, **tail = &curr;
30 - int i, cmp;
39 + int i, j, cmp;
40
41 if (!n) {
42 for (i = 0; i < q->nr; i++) {
@@ -50,6 +59,13 @@ static struct combine_diff_path *intersect_paths(struct combine_diff_path *curr,
59 oidcpy(&p->parent[n].oid, &q->queue[i]->one->oid);
60 p->parent[n].mode = q->queue[i]->one->mode;
61 p->parent[n].status = q->queue[i]->status;
62 +
63 + if (combined_all_paths &&
64 + filename_changed(p->parent[n].status)) {
65 + strbuf_init(&p->parent[n].path, 0);
66 + strbuf_addstr(&p->parent[n].path,
67 + q->queue[i]->one->path);
68 + }
69 *tail = p;
70 tail = &p->next;
71 }
@@ -68,6 +84,10 @@ static struct combine_diff_path *intersect_paths(struct combine_diff_path *curr,
84 if (cmp < 0) {
85 /* p->path not in q->queue[]; drop it */
86 *tail = p->next;
87 + for (j = 0; j < num_parent; j++)
88 + if (combined_all_paths &&
89 + filename_changed(p->parent[j].status))
90 + strbuf_release(&p->parent[j].path);
91 free(p);
92 continue;
93 }
@@ -81,6 +101,10 @@ static struct combine_diff_path *intersect_paths(struct combine_diff_path *curr,
101 oidcpy(&p->parent[n].oid, &q->queue[i]->one->oid);
102 p->parent[n].mode = q->queue[i]->one->mode;
103 p->parent[n].status = q->queue[i]->status;
104 + if (combined_all_paths &&
105 + filename_changed(p->parent[n].status))
106 + strbuf_addstr(&p->parent[n].path,
107 + q->queue[i]->one->path);
108
109 tail = &p->next;
110 i++;
@@ -960,12 +984,25 @@ static void show_combined_header(struct combine_diff_path *elem,
984 if (!show_file_header)
985 return;
986
963 - if (added)
964 - dump_quoted_path("--- ", "", "/dev/null",
965 - line_prefix, c_meta, c_reset);
966 - else
967 - dump_quoted_path("--- ", a_prefix, elem->path,
968 - line_prefix, c_meta, c_reset);
987 + if (rev->combined_all_paths) {
988 + for (i = 0; i < num_parent; i++) {
989 + char *path = filename_changed(elem->parent[i].status)
990 + ? elem->parent[i].path.buf : elem->path;
991 + if (elem->parent[i].status == DIFF_STATUS_ADDED)
992 + dump_quoted_path("--- ", "", "/dev/null",
993 + line_prefix, c_meta, c_reset);
994 + else
995 + dump_quoted_path("--- ", a_prefix, path,
996 + line_prefix, c_meta, c_reset);
997 + }
998 + } else {
999 + if (added)
1000 + dump_quoted_path("--- ", "", "/dev/null",
1001 + line_prefix, c_meta, c_reset);
1002 + else
1003 + dump_quoted_path("--- ", a_prefix, elem->path,
1004 + line_prefix, c_meta, c_reset);
1005 + }
1006 if (deleted)
1007 dump_quoted_path("+++ ", "", "/dev/null",
1008 line_prefix, c_meta, c_reset);
@@ -1227,6 +1264,15 @@ static void show_raw_diff(struct combine_diff_path *p, int num_parent, struct re
1264 putchar(inter_name_termination);
1265 }
1266
1267 + for (i = 0; i < num_parent; i++)
1268 + if (rev->combined_all_paths) {
1269 + if (filename_changed(p->parent[i].status))
1270 + write_name_quoted(p->parent[i].path.buf, stdout,
1271 + inter_name_termination);
1272 + else
1273 + write_name_quoted(p->path, stdout,
1274 + inter_name_termination);
1275 + }
1276 write_name_quoted(p->path, stdout, line_termination);
1277 }
1278
@@ -1324,7 +1370,9 @@ static const char *path_path(void *obj)
1370
1371 /* find set of paths that every parent touches */
1372 static struct combine_diff_path *find_paths_generic(const struct object_id *oid,
1327 - const struct oid_array *parents, struct diff_options *opt)
1373 + const struct oid_array *parents,
1374 + struct diff_options *opt,
1375 + int combined_all_paths)
1376 {
1377 struct combine_diff_path *paths = NULL;
1378 int i, num_parent = parents->nr;
@@ -1350,7 +1398,8 @@ static struct combine_diff_path *find_paths_generic(const struct object_id *oid,
1398 opt->output_format = DIFF_FORMAT_NO_OUTPUT;
1399 diff_tree_oid(&parents->oid[i], oid, "", opt);
1400 diffcore_std(opt);
1353 - paths = intersect_paths(paths, i, num_parent);
1401 + paths = intersect_paths(paths, i, num_parent,
1402 + combined_all_paths);
1403
1404 /* if showing diff, show it in requested order */
1405 if (opt->output_format != DIFF_FORMAT_NO_OUTPUT &&
@@ -1460,7 +1509,8 @@ void diff_tree_combined(const struct object_id *oid,
1509 * diff(sha1,parent_i) for all i to do the job, specifically
1510 * for parent0.
1511 */
1463 - paths = find_paths_generic(oid, parents, &diffopts);
1512 + paths = find_paths_generic(oid, parents, &diffopts,
1513 + rev->combined_all_paths);
1514 }
1515 else {
1516 int stat_opt;
@@ -1535,6 +1585,10 @@ void diff_tree_combined(const struct object_id *oid,
1585 while (paths) {
1586 struct combine_diff_path *tmp = paths;
1587 paths = paths->next;
1588 + for (i = 0; i < num_parent; i++)
1589 + if (rev->combined_all_paths &&
1590 + filename_changed(tmp->parent[i].status))
1591 + strbuf_release(&tmp->parent[i].path);
1592 free(tmp);
1593 }
1594
diff.h
+1
@@ -294,6 +294,7 @@ struct combine_diff_path {
294 char status;
295 unsigned int mode;
296 struct object_id oid;
297 + struct strbuf path;
298 } parent[FLEX_ARRAY];
299 };
300 #define combine_diff_path_size(n, l) \
revision.c
+6
@@ -1995,6 +1995,9 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
1995 revs->diff = 1;
1996 revs->dense_combined_merges = 0;
1997 revs->combine_merges = 1;
1998 + } else if (!strcmp(arg, "--combined-all-paths")) {
1999 + revs->diff = 1;
2000 + revs->combined_all_paths = 1;
2001 } else if (!strcmp(arg, "--cc")) {
2002 revs->diff = 1;
2003 revs->dense_combined_merges = 1;
@@ -2491,6 +2494,9 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s
2494 }
2495 if (revs->combine_merges)
2496 revs->ignore_merges = 0;
2497 + if (revs->combined_all_paths && !revs->combine_merges)
2498 + die("--combined-all-paths makes no sense without -c or --cc");
2499 +
2500 revs->diffopt.abbrev = revs->abbrev;
2501
2502 if (revs->line_level_traverse) {
revision.h
+1
@@ -171,6 +171,7 @@ struct rev_info {
171 verbose_header:1,
172 ignore_merges:1,
173 combine_merges:1,
174 + combined_all_paths:1,
175 dense_combined_merges:1,
176 always_show_header:1;
177
t/t4038-diff-combined.sh
+88
@@ -435,4 +435,92 @@ test_expect_success 'combine diff gets tree sorting right' '
435 test_cmp expect actual
436 '
437
438 +test_expect_success 'setup for --combined-all-paths' '
439 + git branch side1c &&
440 + git branch side2c &&
441 + git checkout side1c &&
442 + test_seq 1 10 >filename-side1c &&
443 + git add filename-side1c &&
444 + git commit -m with &&
445 + git checkout side2c &&
446 + test_seq 1 9 >filename-side2c &&
447 + echo ten >>filename-side2c &&
448 + git add filename-side2c &&
449 + git commit -m iam &&
450 + git checkout -b mergery side1c &&
451 + git merge --no-commit side2c &&
452 + git rm filename-side1c &&
453 + echo eleven >>filename-side2c &&
454 + git mv filename-side2c filename-merged &&
455 + git add filename-merged &&
456 + git commit
457 +'
458 +
459 +test_expect_success '--combined-all-paths and --raw' '
460 + cat <<-\EOF >expect &&
461 + ::100644 100644 100644 f00c965d8307308469e537302baa73048488f162 088bd5d92c2a8e0203ca8e7e4c2a5c692f6ae3f7 333b9c62519f285e1854830ade0fe1ef1d40ee1b RR filename-side1c filename-side2c filename-merged
462 + EOF
463 + git diff-tree -c -M --raw --combined-all-paths HEAD >actual.tmp &&
464 + sed 1d <actual.tmp >actual &&
465 + test_cmp expect actual
466 +'
467 +
468 +test_expect_success '--combined-all-paths and --cc' '
469 + cat <<-\EOF >expect &&
470 + --- a/filename-side1c
471 + --- a/filename-side2c
472 + +++ b/filename-merged
473 + EOF
474 + git diff-tree --cc -M --combined-all-paths HEAD >actual.tmp &&
475 + grep ^[-+][-+][-+] <actual.tmp >actual &&
476 + test_cmp expect actual
477 +'
478 +
479 +test_expect_success FUNNYNAMES 'setup for --combined-all-paths with funny names' '
480 + git branch side1d &&
481 + git branch side2d &&
482 + git checkout side1d &&
483 + test_seq 1 10 >$(printf "file\twith\ttabs") &&
484 + git add file* &&
485 + git commit -m with &&
486 + git checkout side2d &&
487 + test_seq 1 9 >$(printf "i\tam\ttabbed") &&
488 + echo ten >>$(printf "i\tam\ttabbed") &&
489 + git add *tabbed &&
490 + git commit -m iam &&
491 + git checkout -b funny-names-mergery side1d &&
492 + git merge --no-commit side2d &&
493 + git rm *tabs &&
494 + echo eleven >>$(printf "i\tam\ttabbed") &&
495 + git mv "$(printf "i\tam\ttabbed")" "$(printf "fickle\tnaming")" &&
496 + git add fickle* &&
497 + git commit
498 +'
499 +
500 +test_expect_success FUNNYNAMES '--combined-all-paths and --raw and funny names' '
501 + cat <<-\EOF >expect &&
502 + ::100644 100644 100644 f00c965d8307308469e537302baa73048488f162 088bd5d92c2a8e0203ca8e7e4c2a5c692f6ae3f7 333b9c62519f285e1854830ade0fe1ef1d40ee1b RR "file\twith\ttabs" "i\tam\ttabbed" "fickle\tnaming"
503 + EOF
504 + git diff-tree -c -M --raw --combined-all-paths HEAD >actual.tmp &&
505 + sed 1d <actual.tmp >actual &&
506 + test_cmp expect actual
507 +'
508 +
509 +test_expect_success FUNNYNAMES '--combined-all-paths and --raw -and -z and funny names' '
510 + printf "aaf8087c3cbd4db8e185a2d074cf27c53cfb75d7\0::100644 100644 100644 f00c965d8307308469e537302baa73048488f162 088bd5d92c2a8e0203ca8e7e4c2a5c692f6ae3f7 333b9c62519f285e1854830ade0fe1ef1d40ee1b RR\0file\twith\ttabs\0i\tam\ttabbed\0fickle\tnaming\0" >expect &&
511 + git diff-tree -c -M --raw --combined-all-paths -z HEAD >actual &&
512 + test_cmp -a expect actual
513 +'
514 +
515 +test_expect_success FUNNYNAMES '--combined-all-paths and --cc and funny names' '
516 + cat <<-\EOF >expect &&
517 + --- "a/file\twith\ttabs"
518 + --- "a/i\tam\ttabbed"
519 + +++ "b/fickle\tnaming"
520 + EOF
521 + git diff-tree --cc -M --combined-all-paths HEAD >actual.tmp &&
522 + grep ^[-+][-+][-+] <actual.tmp >actual &&
523 + test_cmp expect actual
524 +'
525 +
526 test_done