Merge branch 'jc/merge-base-reflog'

Code the logic in "pull --rebase" that figures out a fork point
from reflog entries in C.

* jc/merge-base-reflog:
  merge-base: teach "--fork-point" mode
  merge-base: use OPT_CMDMODE and clarify the command line parsing
This commit is contained in:
Junio C Hamano 2013-12-05 12:58:27 -08:00
commit 07d406b742
3 changed files with 196 additions and 19 deletions

View File

@ -13,6 +13,7 @@ SYNOPSIS
'git merge-base' [-a|--all] --octopus <commit>... 'git merge-base' [-a|--all] --octopus <commit>...
'git merge-base' --is-ancestor <commit> <commit> 'git merge-base' --is-ancestor <commit> <commit>
'git merge-base' --independent <commit>... 'git merge-base' --independent <commit>...
'git merge-base' --fork-point <ref> [<commit>]
DESCRIPTION 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 ancestor', i.e. a 'merge base'. Note that there can be more than one
merge base for a pair of commits. merge base for a pair of commits.
OPERATION MODE OPERATION MODES
-------------- ---------------
As the most common special case, specifying only two commits on the As the most common special case, specifying only two commits on the
command line means computing the merge base between the given two commits. 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. 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. 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 <commit>) forked from another branch (or any reference)
<ref>. This does not just look for the common ancestor of
the two commits, but also takes into account the reflog of
<ref> to see if the history leading to <commit> forked from
an earlier incarnation of the branch <ref> (see discussion
on this mode below).
OPTIONS OPTIONS
------- -------
@ -137,6 +146,31 @@ In modern git, you can say this in a more direct way:
instead. 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 See also
-------- --------

View File

@ -1,6 +1,9 @@
#include "builtin.h" #include "builtin.h"
#include "cache.h" #include "cache.h"
#include "commit.h" #include "commit.h"
#include "refs.h"
#include "diff.h"
#include "revision.h"
#include "parse-options.h" #include "parse-options.h"
static int show_merge_base(struct commit **rev, int rev_nr, int show_all) 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 <commit>..."), N_("git merge-base [-a|--all] --octopus <commit>..."),
N_("git merge-base --independent <commit>..."), N_("git merge-base --independent <commit>..."),
N_("git merge-base --is-ancestor <commit> <commit>"), N_("git merge-base --is-ancestor <commit> <commit>"),
N_("git merge-base --fork-point <ref> [<commit>]"),
NULL NULL
}; };
@ -85,37 +89,148 @@ static int handle_is_ancestor(int argc, const char **argv)
return 1; 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) int cmd_merge_base(int argc, const char **argv, const char *prefix)
{ {
struct commit **rev; struct commit **rev;
int rev_nr = 0; int rev_nr = 0;
int show_all = 0; int show_all = 0;
int octopus = 0; int cmdmode = 0;
int reduce = 0;
int is_ancestor = 0;
struct option options[] = { struct option options[] = {
OPT_BOOL('a', "all", &show_all, N_("output all common ancestors")), 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_CMDMODE(0, "octopus", &cmdmode,
OPT_BOOL(0, "independent", &reduce, N_("list revs not reachable from others")), N_("find ancestors for a single n-way merge"), 'o'),
OPT_BOOL(0, "is-ancestor", &is_ancestor, OPT_CMDMODE(0, "independent", &cmdmode,
N_("is the first one ancestor of the other?")), 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 <commit> forked from reflog of <ref>"), 'f'),
OPT_END() OPT_END()
}; };
git_config(git_default_config, NULL); git_config(git_default_config, NULL);
argc = parse_options(argc, argv, prefix, options, merge_base_usage, 0); 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)
return handle_is_ancestor(argc, argv);
if (reduce && (show_all || octopus))
die("--independent cannot be used with other options");
if (octopus || reduce) if (cmdmode == 'a') {
return handle_octopus(argc, argv, reduce, show_all); 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 (cmdmode == 'r' && show_all)
die("--independent cannot be used with --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)); rev = xmalloc(argc * sizeof(*rev));
while (argc-- > 0) while (argc-- > 0)

View File

@ -230,4 +230,32 @@ test_expect_success 'criss-cross merge-base for octopus-step' '
test_cmp expected.sorted actual.sorted 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 test_done