mirror of
https://github.com/git/git.git
synced 2024-12-12 19:34:35 +08:00
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:
commit
07d406b742
@ -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
|
||||
--------
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user