builtin/diff-index: learn --merge-base
There is currently no easy way to take the diff between the working tree or index and the merge base between an arbitrary commit and HEAD. Even diff's `...` notation doesn't allow this because it only works between commits. However, the ability to do this would be desirable to a user who would like to see all the changes they've made on a branch plus uncommitted changes without taking into account changes made in the upstream branch. Teach diff-index and diff (with one commit) the --merge-base option which allows a user to use the merge base of a commit and HEAD as the "before" side. Signed-off-by: Denton Liu <liu.denton@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
Denton Liu committed
Sep 20, 2020 at 04:22 UTC
0f5a1d449b9538c2765de9d6683abbb83a7fb4e2
7 files changed
+92
-6
Documentation/git-diff-index.txt
+6
-1
@@ -9,7 +9,7 @@ git-diff-index - Compare a tree to the working tree or index
9
SYNOPSIS
10
--------
11
[verse]
12
-'git diff-index' [-m] [--cached] [<common diff options>] <tree-ish> [<path>...]
12
+'git diff-index' [-m] [--cached] [--merge-base] [<common diff options>] <tree-ish> [<path>...]
13
14
DESCRIPTION
15
-----------
@@ -29,6 +29,11 @@ include::diff-options.txt[]
29
--cached::
30
Do not consider the on-disk file at all.
31
32
+--merge-base::
33
+ Instead of comparing <tree-ish> directly, use the merge base
34
+ between <tree-ish> and HEAD instead. <tree-ish> must be a
35
+ commit.
36
+
37
-m::
38
By default, files recorded in the index but not checked
39
out are reported as deleted. This flag makes
Documentation/git-diff.txt
+8
-4
@@ -10,7 +10,7 @@ SYNOPSIS
10
--------
11
[verse]
12
'git diff' [<options>] [<commit>] [--] [<path>...]
13
-'git diff' [<options>] --cached [<commit>] [--] [<path>...]
13
+'git diff' [<options>] --cached [--merge-base] [<commit>] [--] [<path>...]
14
'git diff' [<options>] <commit> [<commit>...] <commit> [--] [<path>...]
15
'git diff' [<options>] <commit>...<commit> [--] [<path>...]
16
'git diff' [<options>] <blob> <blob>
@@ -40,7 +40,7 @@ files on disk.
40
or when running the command outside a working tree
41
controlled by Git. This form implies `--exit-code`.
42
43
-'git diff' [<options>] --cached [<commit>] [--] [<path>...]::
43
+'git diff' [<options>] --cached [--merge-base] [<commit>] [--] [<path>...]::
44
45
This form is to view the changes you staged for the next
46
commit relative to the named <commit>. Typically you
@@ -49,6 +49,10 @@ files on disk.
49
If HEAD does not exist (e.g. unborn branches) and
50
<commit> is not given, it shows all staged changes.
51
--staged is a synonym of --cached.
52
++
53
+If --merge-base is given, instead of using <commit>, use the merge base
54
+of <commit> and HEAD. `git diff --merge-base A` is equivalent to
55
+`git diff $(git merge-base A HEAD)`.
56
57
'git diff' [<options>] <commit> [--] [<path>...]::
58
@@ -89,8 +93,8 @@ files on disk.
93
94
Just in case you are doing something exotic, it should be
95
noted that all of the <commit> in the above description, except
92
-in the last two forms that use `..` notations, can be any
93
-<tree>.
96
+in the `--merge-base` case and in the last two forms that use `..`
97
+notations, can be any <tree>.
98
99
For a more complete list of ways to spell <commit>, see
100
"SPECIFYING REVISIONS" section in linkgit:gitrevisions[7].
builtin/diff-index.c
+2
@@ -33,6 +33,8 @@ int cmd_diff_index(int argc, const char **argv, const char *prefix)
33
34
if (!strcmp(arg, "--cached"))
35
option |= DIFF_INDEX_CACHED;
36
+ else if (!strcmp(arg, "--merge-base"))
37
+ option |= DIFF_INDEX_MERGE_BASE;
38
else
39
usage(diff_cache_usage);
40
}
builtin/diff.c
+2
@@ -139,6 +139,8 @@ static int builtin_diff_index(struct rev_info *revs,
139
const char *arg = argv[1];
140
if (!strcmp(arg, "--cached") || !strcmp(arg, "--staged"))
141
option |= DIFF_INDEX_CACHED;
142
+ else if (!strcmp(arg, "--merge-base"))
143
+ option |= DIFF_INDEX_MERGE_BASE;
144
else
145
usage(builtin_diff_usage);
146
argv++; argc--;
diff-lib.c
+14
-1
@@ -561,13 +561,26 @@ int run_diff_index(struct rev_info *revs, unsigned int option)
561
{
562
struct object_array_entry *ent;
563
int cached = !!(option & DIFF_INDEX_CACHED);
564
+ int merge_base = !!(option & DIFF_INDEX_MERGE_BASE);
565
+ struct object_id oid;
566
+ const char *name;
567
+ char merge_base_hex[GIT_MAX_HEXSZ + 1];
568
569
if (revs->pending.nr != 1)
570
BUG("run_diff_index must be passed exactly one tree");
571
572
trace_performance_enter();
573
ent = revs->pending.objects;
570
- if (diff_cache(revs, &ent->item->oid, ent->name, cached))
574
+
575
+ if (merge_base) {
576
+ diff_get_merge_base(revs, &oid);
577
+ name = oid_to_hex_r(merge_base_hex, &oid);
578
+ } else {
579
+ oidcpy(&oid, &ent->item->oid);
580
+ name = ent->name;
581
+ }
582
+
583
+ if (diff_cache(revs, &oid, name, cached))
584
exit(128);
585
586
diff_set_mnemonic_prefix(&revs->diffopt, "c/", cached ? "i/" : "w/");
diff.h
+1
@@ -589,6 +589,7 @@ void diff_get_merge_base(const struct rev_info *revs, struct object_id *mb);
589
int run_diff_files(struct rev_info *revs, unsigned int option);
590
591
#define DIFF_INDEX_CACHED 01
592
+#define DIFF_INDEX_MERGE_BASE 02
593
int run_diff_index(struct rev_info *revs, unsigned int option);
594
595
int do_diff_cache(const struct object_id *, struct diff_options *);
t/t4068-diff-symmetric-merge-base.sh
+59
@@ -97,4 +97,63 @@ test_expect_success 'diff --merge-base with three commits' '
97
test_i18ngrep "usage" err
98
'
99
100
+for cmd in diff-index diff
101
+do
102
+ test_expect_success "$cmd --merge-base with one commit" '
103
+ git checkout master &&
104
+ git $cmd commit-C >expect &&
105
+ git $cmd --merge-base br2 >actual &&
106
+ test_cmp expect actual
107
+ '
108
+
109
+ test_expect_success "$cmd --merge-base with one commit and unstaged changes" '
110
+ git checkout master &&
111
+ test_when_finished git reset --hard &&
112
+ echo unstaged >>c &&
113
+ git $cmd commit-C >expect &&
114
+ git $cmd --merge-base br2 >actual &&
115
+ test_cmp expect actual
116
+ '
117
+
118
+ test_expect_success "$cmd --merge-base with one commit and staged and unstaged changes" '
119
+ git checkout master &&
120
+ test_when_finished git reset --hard &&
121
+ echo staged >>c &&
122
+ git add c &&
123
+ echo unstaged >>c &&
124
+ git $cmd commit-C >expect &&
125
+ git $cmd --merge-base br2 >actual &&
126
+ test_cmp expect actual
127
+ '
128
+
129
+ test_expect_success "$cmd --merge-base --cached with one commit and staged and unstaged changes" '
130
+ git checkout master &&
131
+ test_when_finished git reset --hard &&
132
+ echo staged >>c &&
133
+ git add c &&
134
+ echo unstaged >>c &&
135
+ git $cmd --cached commit-C >expect &&
136
+ git $cmd --cached --merge-base br2 >actual &&
137
+ test_cmp expect actual
138
+ '
139
+
140
+ test_expect_success "$cmd --merge-base with non-commit" '
141
+ git checkout master &&
142
+ test_must_fail git $cmd --merge-base master^{tree} 2>err &&
143
+ test_i18ngrep "fatal: --merge-base only works with commits" err
144
+ '
145
+
146
+ test_expect_success "$cmd --merge-base with no merge bases and one commit" '
147
+ git checkout master &&
148
+ test_must_fail git $cmd --merge-base br3 2>err &&
149
+ test_i18ngrep "fatal: no merge base found" err
150
+ '
151
+
152
+ test_expect_success "$cmd --merge-base with multiple merge bases and one commit" '
153
+ git checkout master &&
154
+ test_must_fail git $cmd --merge-base br1 2>err &&
155
+ test_i18ngrep "fatal: multiple merge bases found" err
156
+ '
157
+done
158
+
159
test_done