Skip to content

Commit

Permalink
builtin/diff-index: learn --merge-base
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
Denton-L authored and gitster committed Sep 21, 2020
1 parent df7dbab commit 0f5a1d4
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 6 deletions.
7 changes: 6 additions & 1 deletion Documentation/git-diff-index.txt
Expand Up @@ -9,7 +9,7 @@ git-diff-index - Compare a tree to the working tree or index
SYNOPSIS
--------
[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
-----------
Expand All @@ -29,6 +29,11 @@ include::diff-options.txt[]
--cached::
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::
By default, files recorded in the index but not checked
out are reported as deleted. This flag makes
Expand Down
12 changes: 8 additions & 4 deletions Documentation/git-diff.txt
Expand Up @@ -10,7 +10,7 @@ SYNOPSIS
--------
[verse]
'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> [--] [<path>...]
'git diff' [<options>] <blob> <blob>
Expand Down Expand Up @@ -40,7 +40,7 @@ files on disk.
or when running the command outside a working tree
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
commit relative to the named <commit>. Typically you
Expand All @@ -49,6 +49,10 @@ files on disk.
If HEAD does not exist (e.g. unborn branches) and
<commit> is not given, it shows all staged changes.
--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>...]::

Expand Down Expand Up @@ -89,8 +93,8 @@ files on disk.

Just in case you are doing something exotic, it should be
noted that all of the <commit> in the above description, except
in the last two forms that use `..` notations, can be any
<tree>.
in the `--merge-base` case and in the last two forms that use `..`
notations, can be any <tree>.

For a more complete list of ways to spell <commit>, see
"SPECIFYING REVISIONS" section in linkgit:gitrevisions[7].
Expand Down
2 changes: 2 additions & 0 deletions builtin/diff-index.c
Expand Up @@ -33,6 +33,8 @@ int cmd_diff_index(int argc, const char **argv, const char *prefix)

if (!strcmp(arg, "--cached"))
option |= DIFF_INDEX_CACHED;
else if (!strcmp(arg, "--merge-base"))
option |= DIFF_INDEX_MERGE_BASE;
else
usage(diff_cache_usage);
}
Expand Down
2 changes: 2 additions & 0 deletions builtin/diff.c
Expand Up @@ -139,6 +139,8 @@ static int builtin_diff_index(struct rev_info *revs,
const char *arg = argv[1];
if (!strcmp(arg, "--cached") || !strcmp(arg, "--staged"))
option |= DIFF_INDEX_CACHED;
else if (!strcmp(arg, "--merge-base"))
option |= DIFF_INDEX_MERGE_BASE;
else
usage(builtin_diff_usage);
argv++; argc--;
Expand Down
15 changes: 14 additions & 1 deletion diff-lib.c
Expand Up @@ -561,13 +561,26 @@ int run_diff_index(struct rev_info *revs, unsigned int option)
{
struct object_array_entry *ent;
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)
BUG("run_diff_index must be passed exactly one tree");

trace_performance_enter();
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);

diff_set_mnemonic_prefix(&revs->diffopt, "c/", cached ? "i/" : "w/");
Expand Down
1 change: 1 addition & 0 deletions diff.h
Expand Up @@ -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);

#define DIFF_INDEX_CACHED 01
#define DIFF_INDEX_MERGE_BASE 02
int run_diff_index(struct rev_info *revs, unsigned int option);

int do_diff_cache(const struct object_id *, struct diff_options *);
Expand Down
59 changes: 59 additions & 0 deletions t/t4068-diff-symmetric-merge-base.sh
Expand Up @@ -97,4 +97,63 @@ test_expect_success 'diff --merge-base with three commits' '
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

0 comments on commit 0f5a1d4

Please sign in to comment.