Skip to content

Commit d96855f

Browse files
committed
merge-base: teach "--fork-point" mode
The "git pull --rebase" command computes the fork point of the branch being rebased using the reflog entries of the "base" branch (typically a remote-tracking branch) the branch's work was based on, in order to cope with the case in which the "base" branch has been rewound and rebuilt. For example, if the history looked like this: o---B1 / ---o---o---B2--o---o---o---Base \ B3 \ Derived where the current tip of the "base" branch is at Base, but earlier fetch observed that its tip used to be B3 and then B2 and then B1 before getting to the current commit, and the branch being rebased on top of the latest "base" is based on commit B3, it tries to find B3 by going through the output of "git rev-list --reflog base" (i.e. Base, B1, B2, B3) until it finds a commit that is an ancestor of the current tip "Derived". Internally, we have get_merge_bases_many() that can compute this with one-go. We would want a merge-base between Derived and a fictitious merge commit that would result by merging all the historical tips of "base". When such a commit exist, we should get a single result, which exactly match one of the reflog entries of "base". Teach "git merge-base" a new mode, "--fork-point", to compute exactly that. Helped-by: Martin von Zweigbergk <[email protected]> Helped-by: John Keeping <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 16e57ae commit d96855f

File tree

3 files changed

+173
-2
lines changed

3 files changed

+173
-2
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: 109 additions & 0 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,6 +89,103 @@ 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;
@@ -100,6 +201,8 @@ int cmd_merge_base(int argc, const char **argv, const char *prefix)
100201
N_("list revs not reachable from others"), 'r'),
101202
OPT_CMDMODE(0, "is-ancestor", &cmdmode,
102203
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

@@ -120,6 +223,12 @@ int cmd_merge_base(int argc, const char **argv, const char *prefix)
120223
if (cmdmode == 'r' || cmdmode == 'o')
121224
return handle_octopus(argc, argv, cmdmode == 'r', show_all);
122225

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+
123232
if (argc < 2)
124233
usage_with_options(merge_base_usage, options);
125234

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)