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>
This commit is contained in:
Denton Liu
2020-09-20 04:22:25 -07:00
committed by Junio C Hamano
parent df7dbab881
commit 0f5a1d449b
7 changed files with 92 additions and 6 deletions

View File

@ -9,7 +9,7 @@ git-diff-index - Compare a tree to the working tree or index
SYNOPSIS SYNOPSIS
-------- --------
[verse] [verse]
'git diff-index' [-m] [--cached] [<common diff options>] <tree-ish> [<path>...] 'git diff-index' [-m] [--cached] [--merge-base] [<common diff options>] <tree-ish> [<path>...]
DESCRIPTION DESCRIPTION
----------- -----------
@ -29,6 +29,11 @@ include::diff-options.txt[]
--cached:: --cached::
Do not consider the on-disk file at all. Do not consider the on-disk file at all.
--merge-base::
Instead of comparing <tree-ish> directly, use the merge base
between <tree-ish> and HEAD instead. <tree-ish> must be a
commit.
-m:: -m::
By default, files recorded in the index but not checked By default, files recorded in the index but not checked
out are reported as deleted. This flag makes out are reported as deleted. This flag makes

View File

@ -10,7 +10,7 @@ SYNOPSIS
-------- --------
[verse] [verse]
'git diff' [<options>] [<commit>] [--] [<path>...] 'git diff' [<options>] [<commit>] [--] [<path>...]
'git diff' [<options>] --cached [<commit>] [--] [<path>...] 'git diff' [<options>] --cached [--merge-base] [<commit>] [--] [<path>...]
'git diff' [<options>] <commit> [<commit>...] <commit> [--] [<path>...] 'git diff' [<options>] <commit> [<commit>...] <commit> [--] [<path>...]
'git diff' [<options>] <commit>...<commit> [--] [<path>...] 'git diff' [<options>] <commit>...<commit> [--] [<path>...]
'git diff' [<options>] <blob> <blob> 'git diff' [<options>] <blob> <blob>
@ -40,7 +40,7 @@ files on disk.
or when running the command outside a working tree or when running the command outside a working tree
controlled by Git. This form implies `--exit-code`. controlled by Git. This form implies `--exit-code`.
'git diff' [<options>] --cached [<commit>] [--] [<path>...]:: 'git diff' [<options>] --cached [--merge-base] [<commit>] [--] [<path>...]::
This form is to view the changes you staged for the next This form is to view the changes you staged for the next
commit relative to the named <commit>. Typically you commit relative to the named <commit>. Typically you
@ -49,6 +49,10 @@ files on disk.
If HEAD does not exist (e.g. unborn branches) and If HEAD does not exist (e.g. unborn branches) and
<commit> is not given, it shows all staged changes. <commit> is not given, it shows all staged changes.
--staged is a synonym of --cached. --staged is a synonym of --cached.
+
If --merge-base is given, instead of using <commit>, use the merge base
of <commit> and HEAD. `git diff --merge-base A` is equivalent to
`git diff $(git merge-base A HEAD)`.
'git diff' [<options>] <commit> [--] [<path>...]:: 'git diff' [<options>] <commit> [--] [<path>...]::
@ -89,8 +93,8 @@ files on disk.
Just in case you are doing something exotic, it should be Just in case you are doing something exotic, it should be
noted that all of the <commit> in the above description, except noted that all of the <commit> in the above description, except
in the last two forms that use `..` notations, can be any in the `--merge-base` case and in the last two forms that use `..`
<tree>. notations, can be any <tree>.
For a more complete list of ways to spell <commit>, see For a more complete list of ways to spell <commit>, see
"SPECIFYING REVISIONS" section in linkgit:gitrevisions[7]. "SPECIFYING REVISIONS" section in linkgit:gitrevisions[7].

View File

@ -33,6 +33,8 @@ int cmd_diff_index(int argc, const char **argv, const char *prefix)
if (!strcmp(arg, "--cached")) if (!strcmp(arg, "--cached"))
option |= DIFF_INDEX_CACHED; option |= DIFF_INDEX_CACHED;
else if (!strcmp(arg, "--merge-base"))
option |= DIFF_INDEX_MERGE_BASE;
else else
usage(diff_cache_usage); usage(diff_cache_usage);
} }

View File

@ -139,6 +139,8 @@ static int builtin_diff_index(struct rev_info *revs,
const char *arg = argv[1]; const char *arg = argv[1];
if (!strcmp(arg, "--cached") || !strcmp(arg, "--staged")) if (!strcmp(arg, "--cached") || !strcmp(arg, "--staged"))
option |= DIFF_INDEX_CACHED; option |= DIFF_INDEX_CACHED;
else if (!strcmp(arg, "--merge-base"))
option |= DIFF_INDEX_MERGE_BASE;
else else
usage(builtin_diff_usage); usage(builtin_diff_usage);
argv++; argc--; argv++; argc--;

View File

@ -561,13 +561,26 @@ int run_diff_index(struct rev_info *revs, unsigned int option)
{ {
struct object_array_entry *ent; struct object_array_entry *ent;
int cached = !!(option & DIFF_INDEX_CACHED); int cached = !!(option & DIFF_INDEX_CACHED);
int merge_base = !!(option & DIFF_INDEX_MERGE_BASE);
struct object_id oid;
const char *name;
char merge_base_hex[GIT_MAX_HEXSZ + 1];
if (revs->pending.nr != 1) if (revs->pending.nr != 1)
BUG("run_diff_index must be passed exactly one tree"); BUG("run_diff_index must be passed exactly one tree");
trace_performance_enter(); trace_performance_enter();
ent = revs->pending.objects; ent = revs->pending.objects;
if (diff_cache(revs, &ent->item->oid, ent->name, cached))
if (merge_base) {
diff_get_merge_base(revs, &oid);
name = oid_to_hex_r(merge_base_hex, &oid);
} else {
oidcpy(&oid, &ent->item->oid);
name = ent->name;
}
if (diff_cache(revs, &oid, name, cached))
exit(128); exit(128);
diff_set_mnemonic_prefix(&revs->diffopt, "c/", cached ? "i/" : "w/"); diff_set_mnemonic_prefix(&revs->diffopt, "c/", cached ? "i/" : "w/");

1
diff.h
View File

@ -589,6 +589,7 @@ void diff_get_merge_base(const struct rev_info *revs, struct object_id *mb);
int run_diff_files(struct rev_info *revs, unsigned int option); int run_diff_files(struct rev_info *revs, unsigned int option);
#define DIFF_INDEX_CACHED 01 #define DIFF_INDEX_CACHED 01
#define DIFF_INDEX_MERGE_BASE 02
int run_diff_index(struct rev_info *revs, unsigned int option); int run_diff_index(struct rev_info *revs, unsigned int option);
int do_diff_cache(const struct object_id *, struct diff_options *); int do_diff_cache(const struct object_id *, struct diff_options *);

View File

@ -97,4 +97,63 @@ test_expect_success 'diff --merge-base with three commits' '
test_i18ngrep "usage" err test_i18ngrep "usage" err
' '
for cmd in diff-index diff
do
test_expect_success "$cmd --merge-base with one commit" '
git checkout master &&
git $cmd commit-C >expect &&
git $cmd --merge-base br2 >actual &&
test_cmp expect actual
'
test_expect_success "$cmd --merge-base with one commit and unstaged changes" '
git checkout master &&
test_when_finished git reset --hard &&
echo unstaged >>c &&
git $cmd commit-C >expect &&
git $cmd --merge-base br2 >actual &&
test_cmp expect actual
'
test_expect_success "$cmd --merge-base with one commit and staged and unstaged changes" '
git checkout master &&
test_when_finished git reset --hard &&
echo staged >>c &&
git add c &&
echo unstaged >>c &&
git $cmd commit-C >expect &&
git $cmd --merge-base br2 >actual &&
test_cmp expect actual
'
test_expect_success "$cmd --merge-base --cached with one commit and staged and unstaged changes" '
git checkout master &&
test_when_finished git reset --hard &&
echo staged >>c &&
git add c &&
echo unstaged >>c &&
git $cmd --cached commit-C >expect &&
git $cmd --cached --merge-base br2 >actual &&
test_cmp expect actual
'
test_expect_success "$cmd --merge-base with non-commit" '
git checkout master &&
test_must_fail git $cmd --merge-base master^{tree} 2>err &&
test_i18ngrep "fatal: --merge-base only works with commits" err
'
test_expect_success "$cmd --merge-base with no merge bases and one commit" '
git checkout master &&
test_must_fail git $cmd --merge-base br3 2>err &&
test_i18ngrep "fatal: no merge base found" err
'
test_expect_success "$cmd --merge-base with multiple merge bases and one commit" '
git checkout master &&
test_must_fail git $cmd --merge-base br1 2>err &&
test_i18ngrep "fatal: multiple merge bases found" err
'
done
test_done test_done