builtin/diff-tree: learn --merge-base
The previous commit introduced ---merge-base a way to take the diff between the working tree or index and the merge base between an arbitrary commit and HEAD. It makes sense to extend this option to support the case where two commits are given too and behave in a manner identical to `git diff A...B`. Introduce the --merge-base flag as an alternative to triple-dot notation. Thus, we would be able to write the above as `git diff --merge-base A B`. Signed-off-by: Denton Liu <liu.denton@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
Denton Liu committed
Sep 14, 2020 at 11:36 UTC
3d09c22869a7bd47f1683031937af0ed93b2fa3b
5 files changed
+89
-16
Documentation/git-diff-tree.txt
+6
-1
@@ -10,7 +10,7 @@ SYNOPSIS
10
--------
11
[verse]
12
'git diff-tree' [--stdin] [-m] [-s] [-v] [--no-commit-id] [--pretty]
13
- [-t] [-r] [-c | --cc] [--combined-all-paths] [--root]
13
+ [-t] [-r] [-c | --cc] [--combined-all-paths] [--root] [--merge-base]
14
[<common diff options>] <tree-ish> [<tree-ish>] [<path>...]
15
16
DESCRIPTION
@@ -43,6 +43,11 @@ include::diff-options.txt[]
43
When `--root` is specified the initial commit will be shown as a big
44
creation event. This is equivalent to a diff against the NULL tree.
45
46
+--merge-base::
47
+ Instead of comparing the <tree-ish>s directly, use the merge
48
+ base between the two <tree-ish>s as the "before" side. There
49
+ must be two <tree-ish>s given and they must both be commits.
50
+
51
--stdin::
52
When `--stdin` is specified, the command does not take
53
<tree-ish> arguments from the command line. Instead, it
Documentation/git-diff.txt
+6
-2
@@ -11,7 +11,7 @@ SYNOPSIS
11
[verse]
12
'git diff' [<options>] [<commit>] [--] [<path>...]
13
'git diff' [<options>] --cached [--merge-base] [<commit>] [--] [<path>...]
14
-'git diff' [<options>] <commit> [<commit>...] <commit> [--] [<path>...]
14
+'git diff' [<options>] [--merge-base] <commit> [<commit>...] <commit> [--] [<path>...]
15
'git diff' [<options>] <commit>...<commit> [--] [<path>...]
16
'git diff' [<options>] <blob> <blob>
17
'git diff' [<options>] --no-index [--] <path> <path>
@@ -62,10 +62,14 @@ of <commit> and HEAD. `git diff --merge-base A` is equivalent to
62
branch name to compare with the tip of a different
63
branch.
64
65
-'git diff' [<options>] <commit> <commit> [--] [<path>...]::
65
+'git diff' [<options>] [--merge-base] <commit> <commit> [--] [<path>...]::
66
67
This is to view the changes between two arbitrary
68
<commit>.
69
++
70
+If --merge-base is given, use the merge base of the two commits for the
71
+"before" side. `git diff --merge-base A B` is equivalent to
72
+`git diff $(git merge-base A B) B`.
73
74
'git diff' [<options>] <commit> <commit>... <commit> [--] [<path>...]::
75
builtin/diff-tree.c
+16
-1
@@ -111,6 +111,7 @@ int cmd_diff_tree(int argc, const char **argv, const char *prefix)
111
struct setup_revision_opt s_r_opt;
112
struct userformat_want w;
113
int read_stdin = 0;
114
+ int merge_base = 0;
115
116
if (argc == 2 && !strcmp(argv[1], "-h"))
117
usage(diff_tree_usage);
@@ -143,9 +144,18 @@ int cmd_diff_tree(int argc, const char **argv, const char *prefix)
144
read_stdin = 1;
145
continue;
146
}
147
+ if (!strcmp(arg, "--merge-base")) {
148
+ merge_base = 1;
149
+ continue;
150
+ }
151
usage(diff_tree_usage);
152
}
153
154
+ if (read_stdin && merge_base)
155
+ die(_("--stdin and --merge-base are mutually exclusive"));
156
+ if (merge_base && opt->pending.nr != 2)
157
+ die(_("--merge-base only works with two commits"));
158
+
159
/*
160
* NOTE! We expect "a..b" to expand to "^a b" but it is
161
* perfectly valid for revision range parser to yield "b ^a",
@@ -165,7 +175,12 @@ int cmd_diff_tree(int argc, const char **argv, const char *prefix)
175
case 2:
176
tree1 = opt->pending.objects[0].item;
177
tree2 = opt->pending.objects[1].item;
168
- if (tree2->flags & UNINTERESTING) {
178
+ if (merge_base) {
179
+ struct object_id oid;
180
+
181
+ diff_get_merge_base(opt, &oid);
182
+ tree1 = lookup_object(the_repository, &oid);
183
+ } else if (tree2->flags & UNINTERESTING) {
184
SWAP(tree2, tree1);
185
}
186
diff_tree_oid(&tree1->oid, &tree2->oid, "", &opt->diffopt);
builtin/diff.c
+27
-12
@@ -26,7 +26,7 @@
26
static const char builtin_diff_usage[] =
27
"git diff [<options>] [<commit>] [--] [<path>...]\n"
28
" or: git diff [<options>] --cached [<commit>] [--] [<path>...]\n"
29
-" or: git diff [<options>] <commit> [<commit>...] <commit> [--] [<path>...]\n"
29
+" or: git diff [<options>] <commit> [--merge-base] [<commit>...] <commit> [--] [<path>...]\n"
30
" or: git diff [<options>] <commit>...<commit>] [--] [<path>...]\n"
31
" or: git diff [<options>] <blob> <blob>]\n"
32
" or: git diff [<options>] --no-index [--] <path> <path>]\n"
@@ -172,19 +172,34 @@ static int builtin_diff_tree(struct rev_info *revs,
172
struct object_array_entry *ent1)
173
{
174
const struct object_id *(oid[2]);
175
- int swap = 0;
175
+ struct object_id mb_oid;
176
+ int merge_base = 0;
177
177
- if (argc > 1)
178
- usage(builtin_diff_usage);
178
+ while (1 < argc) {
179
+ const char *arg = argv[1];
180
+ if (!strcmp(arg, "--merge-base"))
181
+ merge_base = 1;
182
+ else
183
+ usage(builtin_diff_usage);
184
+ argv++; argc--;
185
+ }
186
180
- /*
181
- * We saw two trees, ent0 and ent1. If ent1 is uninteresting,
182
- * swap them.
183
- */
184
- if (ent1->item->flags & UNINTERESTING)
185
- swap = 1;
186
- oid[swap] = &ent0->item->oid;
187
- oid[1 - swap] = &ent1->item->oid;
187
+ if (merge_base) {
188
+ diff_get_merge_base(revs, &mb_oid);
189
+ oid[0] = &mb_oid;
190
+ oid[1] = &revs->pending.objects[1].item->oid;
191
+ } else {
192
+ int swap = 0;
193
+
194
+ /*
195
+ * We saw two trees, ent0 and ent1. If ent1 is uninteresting,
196
+ * swap them.
197
+ */
198
+ if (ent1->item->flags & UNINTERESTING)
199
+ swap = 1;
200
+ oid[swap] = &ent0->item->oid;
201
+ oid[1 - swap] = &ent1->item->oid;
202
+ }
203
diff_tree_oid(oid[0], oid[1], "", &revs->diffopt);
204
log_tree_diff_flush(revs);
205
return 0;
t/t4068-diff-symmetric-merge-base.sh
+34
@@ -156,4 +156,38 @@ do
156
'
157
done
158
159
+for cmd in diff-tree diff
160
+do
161
+ test_expect_success "$cmd --merge-base with two commits" '
162
+ git $cmd commit-C master >expect &&
163
+ git $cmd --merge-base br2 master >actual &&
164
+ test_cmp expect actual
165
+ '
166
+
167
+ test_expect_success "$cmd --merge-base commit and non-commit" '
168
+ test_must_fail git $cmd --merge-base br2 master^{tree} 2>err &&
169
+ test_i18ngrep "fatal: --merge-base only works with commits" err
170
+ '
171
+
172
+ test_expect_success "$cmd --merge-base with no merge bases and two commits" '
173
+ test_must_fail git $cmd --merge-base br2 br3 2>err &&
174
+ test_i18ngrep "fatal: no merge base found" err
175
+ '
176
+
177
+ test_expect_success "$cmd --merge-base with multiple merge bases and two commits" '
178
+ test_must_fail git $cmd --merge-base master br1 2>err &&
179
+ test_i18ngrep "fatal: multiple merge bases found" err
180
+ '
181
+done
182
+
183
+test_expect_success 'diff-tree --merge-base with one commit' '
184
+ test_must_fail git diff-tree --merge-base master 2>err &&
185
+ test_i18ngrep "fatal: --merge-base only works with two commits" err
186
+'
187
+
188
+test_expect_success 'diff --merge-base with range' '
189
+ test_must_fail git diff --merge-base br2..br3 2>err &&
190
+ test_i18ngrep "fatal: --merge-base does not work with ranges" err
191
+'
192
+
193
test_done