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 <martinvonz@gmail.com> Helped-by: John Keeping <john@keeping.me.uk> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Junio C Hamano committed Oct 23, 2013 at 16:47 UTC d96855ff517560650c62eca51a8bb78263f6a3ef
3 files changed +173 -2
Documentation/git-merge-base.txt
+36 -2
@@ -13,6 +13,7 @@ SYNOPSIS
13 'git merge-base' [-a|--all] --octopus <commit>...
14 'git merge-base' --is-ancestor <commit> <commit>
15 'git merge-base' --independent <commit>...
16 +'git merge-base' --fork-point <ref> [<commit>]
17
18 DESCRIPTION
19 -----------
@@ -24,8 +25,8 @@ that does not have any better common ancestor is a 'best common
25 ancestor', i.e. a 'merge base'. Note that there can be more than one
26 merge base for a pair of commits.
27
27 -OPERATION MODE
28 ---------------
28 +OPERATION MODES
29 +---------------
30
31 As the most common special case, specifying only two commits on the
32 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.
57 and exit with status 0 if true, or with status 1 if not.
58 Errors are signaled by a non-zero status that is not 1.
59
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).
68
69 OPTIONS
70 -------
@@ -137,6 +146,31 @@ In modern git, you can say this in a more direct way:
146
147 instead.
148
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 +
174
175 See also
176 --------
builtin/merge-base.c
+109
@@ -1,6 +1,9 @@
1 #include "builtin.h"
2 #include "cache.h"
3 #include "commit.h"
4 +#include "refs.h"
5 +#include "diff.h"
6 +#include "revision.h"
7 #include "parse-options.h"
8
9 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[] = {
30 N_("git merge-base [-a|--all] --octopus <commit>..."),
31 N_("git merge-base --independent <commit>..."),
32 N_("git merge-base --is-ancestor <commit> <commit>"),
33 + N_("git merge-base --fork-point <ref> [<commit>]"),
34 NULL
35 };
36
@@ -85,6 +89,103 @@ static int handle_is_ancestor(int argc, const char **argv)
89 return 1;
90 }
91
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 +
189 int cmd_merge_base(int argc, const char **argv, const char *prefix)
190 {
191 struct commit **rev;
@@ -100,6 +201,8 @@ int cmd_merge_base(int argc, const char **argv, const char *prefix)
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'),
206 OPT_END()
207 };
208
@@ -120,6 +223,12 @@ int cmd_merge_base(int argc, const char **argv, const char *prefix)
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);
234
t/t6010-merge-base.sh
+28
@@ -230,4 +230,32 @@ test_expect_success 'criss-cross merge-base for octopus-step' '
230 test_cmp expected.sorted actual.sorted
231 '
232
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 +
261 test_done