This is helpful when examining branches with disjoint roots, for example because one is periodically merged into a subtree of the other. With the --merge-child option, "git merge-base" will print a first-parent ancestor of the first revision given, where the commit printed is either a merge-base of the supplied revisions or a merge for which one of its parents (not the first) is a merge-base. For example, given the history: A---C---G \ B-----D---F \ E we have: $ git merge-base F E B $ git merge-base --merge-child F E D $ git merge-base F G C $ git merge-base --merge-child F G C $ git log --left-right F...E < F < D < C < A > E $ git log --left-right F...E --not $(git merge-base --merge-child F E) < F > E The git-log case is useful because it allows us to limit the range of commits that we are examining for patch-identical changes when using --cherry. For example with git-gui in git.git I know that anything before the last merge of git-gui is not interesting: $ time git log --cherry master...git-gui/master >/dev/null real 0m32.731s user 0m31.956s sys 0m0.664s $ time git log --cherry master...git-gui/master --not \ $(git merge-base --merge-child master git-gui/master) \ >/dev/null real 0m2.296s user 0m2.193s sys 0m0.092s Signed-off-by: John Keeping <john@xxxxxxxxxxxxx> --- Documentation/git-merge-base.txt | 6 ++++ builtin/merge-base.c | 61 ++++++++++++++++++++++++++++++++++++++-- t/t6010-merge-base.sh | 25 ++++++++++++++-- 3 files changed, 88 insertions(+), 4 deletions(-) diff --git a/Documentation/git-merge-base.txt b/Documentation/git-merge-base.txt index 87842e3..a1404e1 100644 --- a/Documentation/git-merge-base.txt +++ b/Documentation/git-merge-base.txt @@ -10,6 +10,7 @@ SYNOPSIS -------- [verse] 'git merge-base' [-a|--all] <commit> <commit>... +'git merge-base' [-a|--all] --merge-child <commit>... 'git merge-base' [-a|--all] --octopus <commit>... 'git merge-base' --is-ancestor <commit> <commit> 'git merge-base' --independent <commit>... @@ -39,6 +40,11 @@ As a consequence, the 'merge base' is not necessarily contained in each of the commit arguments if more than two commits are specified. This is different from linkgit:git-show-branch[1] when used with the `--merge-base` option. +--merge-child:: + Find the first-parent ancestor of the first commit that is either + reachable from all of the supplied commits or which has a parent that + is. + --octopus:: Compute the best common ancestors of all supplied commits, in preparation for an n-way merge. This mimics the behavior diff --git a/builtin/merge-base.c b/builtin/merge-base.c index 1bc7991..0bf9f6d 100644 --- a/builtin/merge-base.c +++ b/builtin/merge-base.c @@ -1,7 +1,9 @@ #include "builtin.h" #include "cache.h" #include "commit.h" +#include "diff.h" #include "parse-options.h" +#include "revision.h" static int show_merge_base(struct commit **rev, int rev_nr, int show_all) { @@ -24,12 +26,61 @@ static int show_merge_base(struct commit **rev, int rev_nr, int show_all) static const char * const merge_base_usage[] = { N_("git merge-base [-a|--all] <commit> <commit>..."), + N_("git merge-base [-a|--all] --merge-child <commit>..."), N_("git merge-base [-a|--all] --octopus <commit>..."), N_("git merge-base --independent <commit>..."), N_("git merge-base --is-ancestor <commit> <commit>"), NULL }; +static int handle_merge_child(struct commit **rev, int rev_nr, const char *prefix, int show_all) +{ + struct commit_list *merge_bases; + struct rev_info revs; + struct commit *commit; + + merge_bases = get_merge_bases_many(rev[0], rev_nr - 1, rev + 1, 0); + + if (!merge_bases) + return 1; + + init_revisions(&revs, prefix); + revs.first_parent_only = 1; + + clear_commit_marks(rev[0], SEEN | UNINTERESTING | SHOWN | ADDED); + add_pending_object(&revs, &rev[0]->object, "rev0"); + if (prepare_revision_walk(&revs)) + die(_("revision walk setup failed")); + + while ((commit = get_revision(&revs)) != NULL) { + /* + * If a merge base is a first-parent ancestor of rev[0] then + * we print the commit itself instead of a merge which is its + * child. + */ + if (commit_list_contains(merge_bases, commit)) { + printf("%s\n", sha1_to_hex(commit->object.sha1)); + if (!show_all) + return 0; + } + + struct commit_list *parent; + for (parent = commit->parents; parent; parent = parent->next) { + /* Skip the first parent */ + if (parent == commit->parents) + continue; + + if (commit_list_contains(merge_bases, parent->item)) { + printf("%s\n", sha1_to_hex(commit->object.sha1)); + if (!show_all) + return 0; + } + } + } + + return 0; +} + static struct commit *get_commit_reference(const char *arg) { unsigned char revkey[20]; @@ -93,9 +144,12 @@ int cmd_merge_base(int argc, const char **argv, const char *prefix) int octopus = 0; int reduce = 0; int is_ancestor = 0; + int merge_child = 0; struct option options[] = { OPT_BOOLEAN('a', "all", &show_all, N_("output all common ancestors")), + OPT_BOOLEAN(0, "merge-child", &merge_child, + N_("find a merge with a parent reachable from others")), OPT_BOOLEAN(0, "octopus", &octopus, N_("find ancestors for a single n-way merge")), OPT_BOOLEAN(0, "independent", &reduce, N_("list revs not reachable from others")), OPT_BOOLEAN(0, "is-ancestor", &is_ancestor, @@ -107,11 +161,11 @@ int cmd_merge_base(int argc, const char **argv, const char *prefix) argc = parse_options(argc, argv, prefix, options, merge_base_usage, 0); if (!octopus && !reduce && argc < 2) usage_with_options(merge_base_usage, options); - if (is_ancestor && (show_all | octopus | reduce)) + if (is_ancestor && (show_all | octopus | reduce | merge_child)) die("--is-ancestor cannot be used with other options"); if (is_ancestor) return handle_is_ancestor(argc, argv); - if (reduce && (show_all || octopus)) + if (reduce && (show_all || octopus || merge_child)) die("--independent cannot be used with other options"); if (octopus || reduce) @@ -120,5 +174,8 @@ int cmd_merge_base(int argc, const char **argv, const char *prefix) rev = xmalloc(argc * sizeof(*rev)); while (argc-- > 0) rev[rev_nr++] = get_commit_reference(*argv++); + + if (merge_child) + return handle_merge_child(rev, rev_nr, prefix, show_all); return show_merge_base(rev, rev_nr, show_all); } diff --git a/t/t6010-merge-base.sh b/t/t6010-merge-base.sh index f80bba8..454577e 100755 --- a/t/t6010-merge-base.sh +++ b/t/t6010-merge-base.sh @@ -42,12 +42,16 @@ test_expect_success 'setup' ' T=$(git mktree </dev/null) ' -test_expect_success 'set up G and H' ' +test_expect_success 'set up G, H and L' ' # E---D---C---B---A # \"-_ \ \ # \ `---------G \ # \ \ # F----------------H + # \ + # I---------------------J---K + # \ + # L E=$(doit 5 E) && D=$(doit 4 D $E) && F=$(doit 6 F $E) && @@ -55,7 +59,11 @@ test_expect_success 'set up G and H' ' B=$(doit 2 B $C) && A=$(doit 1 A $B) && G=$(doit 7 G $B $E) && - H=$(doit 8 H $A $F) + H=$(doit 8 H $A $F) && + I=$(doit 9 I) && + J=$(doit 10 J $H $I) && + K=$(doit 11 K $J) && + L=$(doit 12 L $I) ' test_expect_success 'merge-base G H' ' @@ -64,6 +72,9 @@ test_expect_success 'merge-base G H' ' MB=$(git merge-base G H) && git name-rev "$MB" >actual.single && + MB=$(git merge-base --merge-child G H) && + git name-rev "$MB" >actual.merge_child && + MB=$(git merge-base --all G H) && git name-rev "$MB" >actual.all && @@ -71,6 +82,7 @@ test_expect_success 'merge-base G H' ' git name-rev "$MB" >actual.sb && test_cmp expected actual.single && + test_cmp expected actual.merge_child && test_cmp expected actual.all && test_cmp expected actual.sb ' @@ -95,6 +107,15 @@ test_expect_success 'merge-base/show-branch --independent' ' test_cmp expected2 actual2.sb ' +test_expect_success 'merge-base --merge-child K L' ' + git name-rev "$J" >expected && + + MB=$(git merge-base --merge-child K L) && + git name-rev "$MB" >actual && + + test_cmp expected actual +' + test_expect_success 'unsynchronized clocks' ' # This test is to demonstrate that relying on timestamps in a distributed # SCM to provide a _consistent_ partial ordering of commits leads to -- 1.8.3.rc1.289.gcb3647f -- To unsubscribe from this list: send the line "unsubscribe git" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html