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' --is-ancestor <commit> <commit>
'git merge-base' --independent <commit>...
'git merge-base' --fork-point <ref> [<commit>]
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 <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
-------
@ -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
--------

View File

@ -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 <commit>..."),
N_("git merge-base --independent <commit>..."),
N_("git merge-base --is-ancestor <commit> <commit>"),
N_("git merge-base --fork-point <ref> [<commit>]"),
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 <commit> forked from reflog of <ref>"), '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)
return handle_is_ancestor(argc, argv);
if (reduce && (show_all || octopus))
die("--independent cannot be used with other options");
if (octopus || reduce)
return handle_octopus(argc, argv, reduce, show_all);
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 (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));
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_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