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 <j...@keeping.me.uk>
---
 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 majord...@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html

Reply via email to