Skip to content

Commit

Permalink
builtin/diff-tree: learn --merge-base
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
Denton-L authored and gitster committed Sep 21, 2020
1 parent 0f5a1d4 commit 3d09c22
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 16 deletions.
7 changes: 6 additions & 1 deletion Documentation/git-diff-tree.txt
Expand Up @@ -10,7 +10,7 @@ SYNOPSIS
--------
[verse]
'git diff-tree' [--stdin] [-m] [-s] [-v] [--no-commit-id] [--pretty]
[-t] [-r] [-c | --cc] [--combined-all-paths] [--root]
[-t] [-r] [-c | --cc] [--combined-all-paths] [--root] [--merge-base]
[<common diff options>] <tree-ish> [<tree-ish>] [<path>...]

DESCRIPTION
Expand Down Expand Up @@ -43,6 +43,11 @@ include::diff-options.txt[]
When `--root` is specified the initial commit will be shown as a big
creation event. This is equivalent to a diff against the NULL tree.

--merge-base::
Instead of comparing the <tree-ish>s directly, use the merge
base between the two <tree-ish>s as the "before" side. There
must be two <tree-ish>s given and they must both be commits.

--stdin::
When `--stdin` is specified, the command does not take
<tree-ish> arguments from the command line. Instead, it
Expand Down
8 changes: 6 additions & 2 deletions Documentation/git-diff.txt
Expand Up @@ -11,7 +11,7 @@ SYNOPSIS
[verse]
'git diff' [<options>] [<commit>] [--] [<path>...]
'git diff' [<options>] --cached [--merge-base] [<commit>] [--] [<path>...]
'git diff' [<options>] <commit> [<commit>...] <commit> [--] [<path>...]
'git diff' [<options>] [--merge-base] <commit> [<commit>...] <commit> [--] [<path>...]
'git diff' [<options>] <commit>...<commit> [--] [<path>...]
'git diff' [<options>] <blob> <blob>
'git diff' [<options>] --no-index [--] <path> <path>
Expand Down Expand Up @@ -62,10 +62,14 @@ of <commit> and HEAD. `git diff --merge-base A` is equivalent to
branch name to compare with the tip of a different
branch.

'git diff' [<options>] <commit> <commit> [--] [<path>...]::
'git diff' [<options>] [--merge-base] <commit> <commit> [--] [<path>...]::

This is to view the changes between two arbitrary
<commit>.
+
If --merge-base is given, use the merge base of the two commits for the
"before" side. `git diff --merge-base A B` is equivalent to
`git diff $(git merge-base A B) B`.

'git diff' [<options>] <commit> <commit>... <commit> [--] [<path>...]::

Expand Down
17 changes: 16 additions & 1 deletion builtin/diff-tree.c
Expand Up @@ -111,6 +111,7 @@ int cmd_diff_tree(int argc, const char **argv, const char *prefix)
struct setup_revision_opt s_r_opt;
struct userformat_want w;
int read_stdin = 0;
int merge_base = 0;

if (argc == 2 && !strcmp(argv[1], "-h"))
usage(diff_tree_usage);
Expand Down Expand Up @@ -143,9 +144,18 @@ int cmd_diff_tree(int argc, const char **argv, const char *prefix)
read_stdin = 1;
continue;
}
if (!strcmp(arg, "--merge-base")) {
merge_base = 1;
continue;
}
usage(diff_tree_usage);
}

if (read_stdin && merge_base)
die(_("--stdin and --merge-base are mutually exclusive"));
if (merge_base && opt->pending.nr != 2)
die(_("--merge-base only works with two commits"));

/*
* NOTE! We expect "a..b" to expand to "^a b" but it is
* perfectly valid for revision range parser to yield "b ^a",
Expand All @@ -165,7 +175,12 @@ int cmd_diff_tree(int argc, const char **argv, const char *prefix)
case 2:
tree1 = opt->pending.objects[0].item;
tree2 = opt->pending.objects[1].item;
if (tree2->flags & UNINTERESTING) {
if (merge_base) {
struct object_id oid;

diff_get_merge_base(opt, &oid);
tree1 = lookup_object(the_repository, &oid);
} else if (tree2->flags & UNINTERESTING) {
SWAP(tree2, tree1);
}
diff_tree_oid(&tree1->oid, &tree2->oid, "", &opt->diffopt);
Expand Down
39 changes: 27 additions & 12 deletions builtin/diff.c
Expand Up @@ -26,7 +26,7 @@
static const char builtin_diff_usage[] =
"git diff [<options>] [<commit>] [--] [<path>...]\n"
" or: git diff [<options>] --cached [<commit>] [--] [<path>...]\n"
" or: git diff [<options>] <commit> [<commit>...] <commit> [--] [<path>...]\n"
" or: git diff [<options>] <commit> [--merge-base] [<commit>...] <commit> [--] [<path>...]\n"
" or: git diff [<options>] <commit>...<commit>] [--] [<path>...]\n"
" or: git diff [<options>] <blob> <blob>]\n"
" or: git diff [<options>] --no-index [--] <path> <path>]\n"
Expand Down Expand Up @@ -172,19 +172,34 @@ static int builtin_diff_tree(struct rev_info *revs,
struct object_array_entry *ent1)
{
const struct object_id *(oid[2]);
int swap = 0;
struct object_id mb_oid;
int merge_base = 0;

if (argc > 1)
usage(builtin_diff_usage);
while (1 < argc) {
const char *arg = argv[1];
if (!strcmp(arg, "--merge-base"))
merge_base = 1;
else
usage(builtin_diff_usage);
argv++; argc--;
}

/*
* We saw two trees, ent0 and ent1. If ent1 is uninteresting,
* swap them.
*/
if (ent1->item->flags & UNINTERESTING)
swap = 1;
oid[swap] = &ent0->item->oid;
oid[1 - swap] = &ent1->item->oid;
if (merge_base) {
diff_get_merge_base(revs, &mb_oid);
oid[0] = &mb_oid;
oid[1] = &revs->pending.objects[1].item->oid;
} else {
int swap = 0;

/*
* We saw two trees, ent0 and ent1. If ent1 is uninteresting,
* swap them.
*/
if (ent1->item->flags & UNINTERESTING)
swap = 1;
oid[swap] = &ent0->item->oid;
oid[1 - swap] = &ent1->item->oid;
}
diff_tree_oid(oid[0], oid[1], "", &revs->diffopt);
log_tree_diff_flush(revs);
return 0;
Expand Down
34 changes: 34 additions & 0 deletions t/t4068-diff-symmetric-merge-base.sh
Expand Up @@ -156,4 +156,38 @@ do
'
done

for cmd in diff-tree diff
do
test_expect_success "$cmd --merge-base with two commits" '
git $cmd commit-C master >expect &&
git $cmd --merge-base br2 master >actual &&
test_cmp expect actual
'

test_expect_success "$cmd --merge-base commit and non-commit" '
test_must_fail git $cmd --merge-base br2 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 two commits" '
test_must_fail git $cmd --merge-base br2 br3 2>err &&
test_i18ngrep "fatal: no merge base found" err
'

test_expect_success "$cmd --merge-base with multiple merge bases and two commits" '
test_must_fail git $cmd --merge-base master br1 2>err &&
test_i18ngrep "fatal: multiple merge bases found" err
'
done

test_expect_success 'diff-tree --merge-base with one commit' '
test_must_fail git diff-tree --merge-base master 2>err &&
test_i18ngrep "fatal: --merge-base only works with two commits" err
'

test_expect_success 'diff --merge-base with range' '
test_must_fail git diff --merge-base br2..br3 2>err &&
test_i18ngrep "fatal: --merge-base does not work with ranges" err
'

test_done

0 comments on commit 3d09c22

Please sign in to comment.