diff --git a/Documentation/git-merge-base.txt b/Documentation/git-merge-base.txt index 87842e33f8aebc7d27fa732af1de15f0e4175c07..808426faac5b9b607810777a718113e77b836715 100644 --- a/Documentation/git-merge-base.txt +++ b/Documentation/git-merge-base.txt @@ -13,6 +13,7 @@ SYNOPSIS 'git merge-base' [-a|--all] --octopus ... 'git merge-base' --is-ancestor 'git merge-base' --independent ... +'git merge-base' --fork-point [] DESCRIPTION ----------- @@ -24,8 +25,8 @@ that does not have any better common ancestor is a 'best common ancestor', i.e. a 'merge base'. Note that there can be more than one merge base for a pair of commits. -OPERATION MODE --------------- +OPERATION MODES +--------------- As the most common special case, specifying only two commits on the command line means computing the merge base between the given two commits. @@ -56,6 +57,14 @@ from linkgit:git-show-branch[1] when used with the `--merge-base` option. and exit with status 0 if true, or with status 1 if not. Errors are signaled by a non-zero status that is not 1. +--fork-point:: + Find the point at which a branch (or any history that leads + to ) forked from another branch (or any reference) + . This does not just look for the common ancestor of + the two commits, but also takes into account the reflog of + to see if the history leading to forked from + an earlier incarnation of the branch (see discussion + on this mode below). OPTIONS ------- @@ -137,6 +146,31 @@ In modern git, you can say this in a more direct way: instead. +Discussion on fork-point mode +----------------------------- + +After working on the `topic` branch created with `git checkout -b +topic origin/master`, the history of remote-tracking branch +`origin/master` may have been rewound and rebuilt, leading to a +history of this shape: + + o---B1 + / + ---o---o---B2--o---o---o---B (origin/master) + \ + B3 + \ + Derived (topic) + +where `origin/master` used to point at commits B3, B2, B1 and now it +points at B, and your `topic` branch was started on top of it back +when `origin/master` was at B3. This mode uses the reflog of +`origin/master` to find B3 as the fork point, so that the `topic` +can be rebased on top of the updated `origin/master` by: + + $ fork_point=$(git merge-base --fork-point origin/master topic) + $ git rebase --onto origin/master $fork_point topic + See also -------- diff --git a/builtin/merge-base.c b/builtin/merge-base.c index e88eb93f143ef5883b3e5f8f069fd13f70ff7abf..a2923235e1024e554b0dc937c8222282d1855065 100644 --- a/builtin/merge-base.c +++ b/builtin/merge-base.c @@ -1,6 +1,9 @@ #include "builtin.h" #include "cache.h" #include "commit.h" +#include "refs.h" +#include "diff.h" +#include "revision.h" #include "parse-options.h" static int show_merge_base(struct commit **rev, int rev_nr, int show_all) @@ -27,6 +30,7 @@ static const char * const merge_base_usage[] = { N_("git merge-base [-a|--all] --octopus ..."), N_("git merge-base --independent ..."), N_("git merge-base --is-ancestor "), + N_("git merge-base --fork-point []"), NULL }; @@ -85,37 +89,148 @@ static int handle_is_ancestor(int argc, const char **argv) return 1; } +struct rev_collect { + struct commit **commit; + int nr; + int alloc; + unsigned int initial : 1; +}; + +static void add_one_commit(unsigned char *sha1, struct rev_collect *revs) +{ + struct commit *commit; + + if (is_null_sha1(sha1)) + return; + + commit = lookup_commit(sha1); + if (!commit || + (commit->object.flags & TMP_MARK) || + parse_commit(commit)) + return; + + ALLOC_GROW(revs->commit, revs->nr + 1, revs->alloc); + revs->commit[revs->nr++] = commit; + commit->object.flags |= TMP_MARK; +} + +static int collect_one_reflog_ent(unsigned char *osha1, unsigned char *nsha1, + const char *ident, unsigned long timestamp, + int tz, const char *message, void *cbdata) +{ + struct rev_collect *revs = cbdata; + + if (revs->initial) { + revs->initial = 0; + add_one_commit(osha1, revs); + } + add_one_commit(nsha1, revs); + return 0; +} + +static int handle_fork_point(int argc, const char **argv) +{ + unsigned char sha1[20]; + char *refname; + const char *commitname; + struct rev_collect revs; + struct commit *derived; + struct commit_list *bases; + int i, ret = 0; + + switch (dwim_ref(argv[0], strlen(argv[0]), sha1, &refname)) { + case 0: + die("No such ref: '%s'", argv[0]); + case 1: + break; /* good */ + default: + die("Ambiguous refname: '%s'", argv[0]); + } + + commitname = (argc == 2) ? argv[1] : "HEAD"; + if (get_sha1(commitname, sha1)) + die("Not a valid object name: '%s'", commitname); + + derived = lookup_commit_reference(sha1); + memset(&revs, 0, sizeof(revs)); + revs.initial = 1; + for_each_reflog_ent(refname, collect_one_reflog_ent, &revs); + + for (i = 0; i < revs.nr; i++) + revs.commit[i]->object.flags &= ~TMP_MARK; + + bases = get_merge_bases_many(derived, revs.nr, revs.commit, 0); + + /* + * There should be one and only one merge base, when we found + * a common ancestor among reflog entries. + */ + if (!bases || bases->next) { + ret = 1; + goto cleanup_return; + } + + /* And the found one must be one of the reflog entries */ + for (i = 0; i < revs.nr; i++) + if (&bases->item->object == &revs.commit[i]->object) + break; /* found */ + if (revs.nr <= i) { + ret = 1; /* not found */ + goto cleanup_return; + } + + printf("%s\n", sha1_to_hex(bases->item->object.sha1)); + +cleanup_return: + free_commit_list(bases); + return ret; +} + int cmd_merge_base(int argc, const char **argv, const char *prefix) { struct commit **rev; int rev_nr = 0; int show_all = 0; - int octopus = 0; - int reduce = 0; - int is_ancestor = 0; + int cmdmode = 0; struct option options[] = { OPT_BOOL('a', "all", &show_all, N_("output all common ancestors")), - OPT_BOOL(0, "octopus", &octopus, N_("find ancestors for a single n-way merge")), - OPT_BOOL(0, "independent", &reduce, N_("list revs not reachable from others")), - OPT_BOOL(0, "is-ancestor", &is_ancestor, - N_("is the first one ancestor of the other?")), + OPT_CMDMODE(0, "octopus", &cmdmode, + N_("find ancestors for a single n-way merge"), 'o'), + OPT_CMDMODE(0, "independent", &cmdmode, + N_("list revs not reachable from others"), 'r'), + OPT_CMDMODE(0, "is-ancestor", &cmdmode, + N_("is the first one ancestor of the other?"), 'a'), + OPT_CMDMODE(0, "fork-point", &cmdmode, + N_("find where forked from reflog of "), 'f'), OPT_END() }; git_config(git_default_config, NULL); 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)) - die("--is-ancestor cannot be used with other options"); - if (is_ancestor) + + if (cmdmode == 'a') { + if (argc < 2) + usage_with_options(merge_base_usage, options); + if (show_all) + die("--is-ancestor cannot be used with --all"); return handle_is_ancestor(argc, argv); - if (reduce && (show_all || octopus)) - die("--independent cannot be used with other options"); + } + + if (cmdmode == 'r' && show_all) + die("--independent cannot be used with --all"); - if (octopus || reduce) - return handle_octopus(argc, argv, reduce, show_all); + if (cmdmode == 'r' || cmdmode == 'o') + return handle_octopus(argc, argv, cmdmode == 'r', show_all); + + if (cmdmode == 'f') { + if (argc < 1 || 2 < argc) + usage_with_options(merge_base_usage, options); + return handle_fork_point(argc, argv); + } + + if (argc < 2) + usage_with_options(merge_base_usage, options); rev = xmalloc(argc * sizeof(*rev)); while (argc-- > 0) diff --git a/t/t6010-merge-base.sh b/t/t6010-merge-base.sh index f80bba871cb45a4afb098c96e7925b6361cf7f95..30a68335b3a69a8189648213606ddeb32b97d99e 100755 --- a/t/t6010-merge-base.sh +++ b/t/t6010-merge-base.sh @@ -230,4 +230,32 @@ test_expect_success 'criss-cross merge-base for octopus-step' ' test_cmp expected.sorted actual.sorted ' +test_expect_success 'using reflog to find the fork point' ' + git reset --hard && + git checkout -b base $E && + + ( + for count in 1 2 3 + do + git commit --allow-empty -m "Base commit #$count" && + git rev-parse HEAD >expect$count && + git checkout -B derived && + git commit --allow-empty -m "Derived #$count" && + git rev-parse HEAD >derived$count && + git checkout -B base $E || exit 1 + done + + for count in 1 2 3 + do + git merge-base --fork-point base $(cat derived$count) >actual && + test_cmp expect$count actual || exit 1 + done + + ) && + # check that we correctly default to HEAD + git checkout derived && + git merge-base --fork-point base >actual && + test_cmp expect3 actual +' + test_done