Skip to content

Commit 07d406b

Browse files
committed
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
2 parents 219ea0e + d96855f commit 07d406b

File tree

3 files changed

+195
-18
lines changed

3 files changed

+195
-18
lines changed

Documentation/git-merge-base.txt

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ SYNOPSIS
1313
'git merge-base' [-a|--all] --octopus <commit>...
1414
'git merge-base' --is-ancestor <commit> <commit>
1515
'git merge-base' --independent <commit>...
16+
'git merge-base' --fork-point <ref> [<commit>]
1617

1718
DESCRIPTION
1819
-----------
@@ -24,8 +25,8 @@ that does not have any better common ancestor is a 'best common
2425
ancestor', i.e. a 'merge base'. Note that there can be more than one
2526
merge base for a pair of commits.
2627

27-
OPERATION MODE
28-
--------------
28+
OPERATION MODES
29+
---------------
2930

3031
As the most common special case, specifying only two commits on the
3132
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.
5657
and exit with status 0 if true, or with status 1 if not.
5758
Errors are signaled by a non-zero status that is not 1.
5859

60+
--fork-point::
61+
Find the point at which a branch (or any history that leads
62+
to <commit>) forked from another branch (or any reference)
63+
<ref>. This does not just look for the common ancestor of
64+
the two commits, but also takes into account the reflog of
65+
<ref> to see if the history leading to <commit> forked from
66+
an earlier incarnation of the branch <ref> (see discussion
67+
on this mode below).
5968

6069
OPTIONS
6170
-------
@@ -137,6 +146,31 @@ In modern git, you can say this in a more direct way:
137146

138147
instead.
139148

149+
Discussion on fork-point mode
150+
-----------------------------
151+
152+
After working on the `topic` branch created with `git checkout -b
153+
topic origin/master`, the history of remote-tracking branch
154+
`origin/master` may have been rewound and rebuilt, leading to a
155+
history of this shape:
156+
157+
o---B1
158+
/
159+
---o---o---B2--o---o---o---B (origin/master)
160+
\
161+
B3
162+
\
163+
Derived (topic)
164+
165+
where `origin/master` used to point at commits B3, B2, B1 and now it
166+
points at B, and your `topic` branch was started on top of it back
167+
when `origin/master` was at B3. This mode uses the reflog of
168+
`origin/master` to find B3 as the fork point, so that the `topic`
169+
can be rebased on top of the updated `origin/master` by:
170+
171+
$ fork_point=$(git merge-base --fork-point origin/master topic)
172+
$ git rebase --onto origin/master $fork_point topic
173+
140174

141175
See also
142176
--------

builtin/merge-base.c

Lines changed: 131 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
#include "builtin.h"
22
#include "cache.h"
33
#include "commit.h"
4+
#include "refs.h"
5+
#include "diff.h"
6+
#include "revision.h"
47
#include "parse-options.h"
58

69
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[] = {
2730
N_("git merge-base [-a|--all] --octopus <commit>..."),
2831
N_("git merge-base --independent <commit>..."),
2932
N_("git merge-base --is-ancestor <commit> <commit>"),
33+
N_("git merge-base --fork-point <ref> [<commit>]"),
3034
NULL
3135
};
3236

@@ -85,37 +89,148 @@ static int handle_is_ancestor(int argc, const char **argv)
8589
return 1;
8690
}
8791

92+
struct rev_collect {
93+
struct commit **commit;
94+
int nr;
95+
int alloc;
96+
unsigned int initial : 1;
97+
};
98+
99+
static void add_one_commit(unsigned char *sha1, struct rev_collect *revs)
100+
{
101+
struct commit *commit;
102+
103+
if (is_null_sha1(sha1))
104+
return;
105+
106+
commit = lookup_commit(sha1);
107+
if (!commit ||
108+
(commit->object.flags & TMP_MARK) ||
109+
parse_commit(commit))
110+
return;
111+
112+
ALLOC_GROW(revs->commit, revs->nr + 1, revs->alloc);
113+
revs->commit[revs->nr++] = commit;
114+
commit->object.flags |= TMP_MARK;
115+
}
116+
117+
static int collect_one_reflog_ent(unsigned char *osha1, unsigned char *nsha1,
118+
const char *ident, unsigned long timestamp,
119+
int tz, const char *message, void *cbdata)
120+
{
121+
struct rev_collect *revs = cbdata;
122+
123+
if (revs->initial) {
124+
revs->initial = 0;
125+
add_one_commit(osha1, revs);
126+
}
127+
add_one_commit(nsha1, revs);
128+
return 0;
129+
}
130+
131+
static int handle_fork_point(int argc, const char **argv)
132+
{
133+
unsigned char sha1[20];
134+
char *refname;
135+
const char *commitname;
136+
struct rev_collect revs;
137+
struct commit *derived;
138+
struct commit_list *bases;
139+
int i, ret = 0;
140+
141+
switch (dwim_ref(argv[0], strlen(argv[0]), sha1, &refname)) {
142+
case 0:
143+
die("No such ref: '%s'", argv[0]);
144+
case 1:
145+
break; /* good */
146+
default:
147+
die("Ambiguous refname: '%s'", argv[0]);
148+
}
149+
150+
commitname = (argc == 2) ? argv[1] : "HEAD";
151+
if (get_sha1(commitname, sha1))
152+
die("Not a valid object name: '%s'", commitname);
153+
154+
derived = lookup_commit_reference(sha1);
155+
memset(&revs, 0, sizeof(revs));
156+
revs.initial = 1;
157+
for_each_reflog_ent(refname, collect_one_reflog_ent, &revs);
158+
159+
for (i = 0; i < revs.nr; i++)
160+
revs.commit[i]->object.flags &= ~TMP_MARK;
161+
162+
bases = get_merge_bases_many(derived, revs.nr, revs.commit, 0);
163+
164+
/*
165+
* There should be one and only one merge base, when we found
166+
* a common ancestor among reflog entries.
167+
*/
168+
if (!bases || bases->next) {
169+
ret = 1;
170+
goto cleanup_return;
171+
}
172+
173+
/* And the found one must be one of the reflog entries */
174+
for (i = 0; i < revs.nr; i++)
175+
if (&bases->item->object == &revs.commit[i]->object)
176+
break; /* found */
177+
if (revs.nr <= i) {
178+
ret = 1; /* not found */
179+
goto cleanup_return;
180+
}
181+
182+
printf("%s\n", sha1_to_hex(bases->item->object.sha1));
183+
184+
cleanup_return:
185+
free_commit_list(bases);
186+
return ret;
187+
}
188+
88189
int cmd_merge_base(int argc, const char **argv, const char *prefix)
89190
{
90191
struct commit **rev;
91192
int rev_nr = 0;
92193
int show_all = 0;
93-
int octopus = 0;
94-
int reduce = 0;
95-
int is_ancestor = 0;
194+
int cmdmode = 0;
96195

97196
struct option options[] = {
98197
OPT_BOOL('a', "all", &show_all, N_("output all common ancestors")),
99-
OPT_BOOL(0, "octopus", &octopus, N_("find ancestors for a single n-way merge")),
100-
OPT_BOOL(0, "independent", &reduce, N_("list revs not reachable from others")),
101-
OPT_BOOL(0, "is-ancestor", &is_ancestor,
102-
N_("is the first one ancestor of the other?")),
198+
OPT_CMDMODE(0, "octopus", &cmdmode,
199+
N_("find ancestors for a single n-way merge"), 'o'),
200+
OPT_CMDMODE(0, "independent", &cmdmode,
201+
N_("list revs not reachable from others"), 'r'),
202+
OPT_CMDMODE(0, "is-ancestor", &cmdmode,
203+
N_("is the first one ancestor of the other?"), 'a'),
204+
OPT_CMDMODE(0, "fork-point", &cmdmode,
205+
N_("find where <commit> forked from reflog of <ref>"), 'f'),
103206
OPT_END()
104207
};
105208

106209
git_config(git_default_config, NULL);
107210
argc = parse_options(argc, argv, prefix, options, merge_base_usage, 0);
108-
if (!octopus && !reduce && argc < 2)
109-
usage_with_options(merge_base_usage, options);
110-
if (is_ancestor && (show_all || octopus || reduce))
111-
die("--is-ancestor cannot be used with other options");
112-
if (is_ancestor)
211+
212+
if (cmdmode == 'a') {
213+
if (argc < 2)
214+
usage_with_options(merge_base_usage, options);
215+
if (show_all)
216+
die("--is-ancestor cannot be used with --all");
113217
return handle_is_ancestor(argc, argv);
114-
if (reduce && (show_all || octopus))
115-
die("--independent cannot be used with other options");
218+
}
219+
220+
if (cmdmode == 'r' && show_all)
221+
die("--independent cannot be used with --all");
116222

117-
if (octopus || reduce)
118-
return handle_octopus(argc, argv, reduce, show_all);
223+
if (cmdmode == 'r' || cmdmode == 'o')
224+
return handle_octopus(argc, argv, cmdmode == 'r', show_all);
225+
226+
if (cmdmode == 'f') {
227+
if (argc < 1 || 2 < argc)
228+
usage_with_options(merge_base_usage, options);
229+
return handle_fork_point(argc, argv);
230+
}
231+
232+
if (argc < 2)
233+
usage_with_options(merge_base_usage, options);
119234

120235
rev = xmalloc(argc * sizeof(*rev));
121236
while (argc-- > 0)

t/t6010-merge-base.sh

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,4 +230,32 @@ test_expect_success 'criss-cross merge-base for octopus-step' '
230230
test_cmp expected.sorted actual.sorted
231231
'
232232

233+
test_expect_success 'using reflog to find the fork point' '
234+
git reset --hard &&
235+
git checkout -b base $E &&
236+
237+
(
238+
for count in 1 2 3
239+
do
240+
git commit --allow-empty -m "Base commit #$count" &&
241+
git rev-parse HEAD >expect$count &&
242+
git checkout -B derived &&
243+
git commit --allow-empty -m "Derived #$count" &&
244+
git rev-parse HEAD >derived$count &&
245+
git checkout -B base $E || exit 1
246+
done
247+
248+
for count in 1 2 3
249+
do
250+
git merge-base --fork-point base $(cat derived$count) >actual &&
251+
test_cmp expect$count actual || exit 1
252+
done
253+
254+
) &&
255+
# check that we correctly default to HEAD
256+
git checkout derived &&
257+
git merge-base --fork-point base >actual &&
258+
test_cmp expect3 actual
259+
'
260+
233261
test_done

0 commit comments

Comments
 (0)