Skip to content

Commit 22d99f0

Browse files
newrenchriscool
authored andcommitted
replay: add --advance or 'cherry-pick' mode
There is already a 'rebase' mode with `--onto`. Let's add an 'advance' or 'cherry-pick' mode with `--advance`. This new mode will make the target branch advance as we replay commits onto it. The replayed commits should have a single tip, so that it's clear where the target branch should be advanced. If they have more than one tip, this new mode will error out. Co-authored-by: Christian Couder <[email protected]> Signed-off-by: Elijah Newren <[email protected]> Signed-off-by: Christian Couder <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 3916ec3 commit 22d99f0

File tree

3 files changed

+243
-17
lines changed

3 files changed

+243
-17
lines changed

Documentation/git-replay.txt

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
99
SYNOPSIS
1010
--------
1111
[verse]
12-
(EXPERIMENTAL!) 'git replay' --onto <newbase> <revision-range>...
12+
(EXPERIMENTAL!) 'git replay' (--onto <newbase> | --advance <branch>) <revision-range>...
1313

1414
DESCRIPTION
1515
-----------
@@ -29,14 +29,25 @@ OPTIONS
2929
Starting point at which to create the new commits. May be any
3030
valid commit, and not just an existing branch name.
3131
+
32-
The update-ref command(s) in the output will update the branch(es) in
33-
the revision range to point at the new commits, similar to the way how
34-
`git rebase --update-refs` updates multiple branches in the affected
35-
range.
32+
When `--onto` is specified, the update-ref command(s) in the output will
33+
update the branch(es) in the revision range to point at the new
34+
commits, similar to the way how `git rebase --update-refs` updates
35+
multiple branches in the affected range.
36+
37+
--advance <branch>::
38+
Starting point at which to create the new commits; must be a
39+
branch name.
40+
+
41+
When `--advance` is specified, the update-ref command(s) in the output
42+
will update the branch passed as an argument to `--advance` to point at
43+
the new commits (in other words, this mimics a cherry-pick operation).
3644

3745
<revision-range>::
38-
Range of commits to replay; see "Specifying Ranges" in
39-
linkgit:git-rev-parse and the "Commit Limiting" options below.
46+
Range of commits to replay. More than one <revision-range> can
47+
be passed, but in `--advance <branch>` mode, they should have
48+
a single tip, so that it's clear where <branch> should point
49+
to. See "Specifying Ranges" in linkgit:git-rev-parse and the
50+
"Commit Limiting" options below.
4051

4152
include::rev-list-options.txt[]
4253

@@ -51,7 +62,9 @@ input to `git update-ref --stdin`. It is of the form:
5162
update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
5263

5364
where the number of refs updated depends on the arguments passed and
54-
the shape of the history being replayed.
65+
the shape of the history being replayed. When using `--advance`, the
66+
number of refs updated is always one, but for `--onto`, it can be one
67+
or more (rebasing multiple branches simultaneously is supported).
5568

5669
EXIT STATUS
5770
-----------
@@ -71,6 +84,18 @@ $ git replay --onto target origin/main..mybranch
7184
update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
7285
------------
7386

87+
To cherry-pick the commits from mybranch onto target:
88+
89+
------------
90+
$ git replay --advance target origin/main..mybranch
91+
update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}
92+
------------
93+
94+
Note that the first two examples replay the exact same commits and on
95+
top of the exact same new base, they only differ in that the first
96+
provides instructions to make mybranch point at the new commits and
97+
the second provides instructions to make target point at them.
98+
7499
When calling `git replay`, one does not need to specify a range of
75100
commits to replay using the syntax `A..B`; any range expression will
76101
do:

builtin/replay.c

Lines changed: 176 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include "parse-options.h"
1515
#include "refs.h"
1616
#include "revision.h"
17+
#include "strmap.h"
1718
#include <oidset.h>
1819
#include <tree.h>
1920

@@ -82,6 +83,146 @@ static struct commit *create_commit(struct tree *tree,
8283
return (struct commit *)obj;
8384
}
8485

86+
struct ref_info {
87+
struct commit *onto;
88+
struct strset positive_refs;
89+
struct strset negative_refs;
90+
int positive_refexprs;
91+
int negative_refexprs;
92+
};
93+
94+
static void get_ref_information(struct rev_cmdline_info *cmd_info,
95+
struct ref_info *ref_info)
96+
{
97+
int i;
98+
99+
ref_info->onto = NULL;
100+
strset_init(&ref_info->positive_refs);
101+
strset_init(&ref_info->negative_refs);
102+
ref_info->positive_refexprs = 0;
103+
ref_info->negative_refexprs = 0;
104+
105+
/*
106+
* When the user specifies e.g.
107+
* git replay origin/main..mybranch
108+
* git replay ^origin/next mybranch1 mybranch2
109+
* we want to be able to determine where to replay the commits. In
110+
* these examples, the branches are probably based on an old version
111+
* of either origin/main or origin/next, so we want to replay on the
112+
* newest version of that branch. In contrast we would want to error
113+
* out if they ran
114+
* git replay ^origin/master ^origin/next mybranch
115+
* git replay mybranch~2..mybranch
116+
* the first of those because there's no unique base to choose, and
117+
* the second because they'd likely just be replaying commits on top
118+
* of the same commit and not making any difference.
119+
*/
120+
for (i = 0; i < cmd_info->nr; i++) {
121+
struct rev_cmdline_entry *e = cmd_info->rev + i;
122+
struct object_id oid;
123+
const char *refexpr = e->name;
124+
char *fullname = NULL;
125+
int can_uniquely_dwim = 1;
126+
127+
if (*refexpr == '^')
128+
refexpr++;
129+
if (repo_dwim_ref(the_repository, refexpr, strlen(refexpr), &oid, &fullname, 0) != 1)
130+
can_uniquely_dwim = 0;
131+
132+
if (e->flags & BOTTOM) {
133+
if (can_uniquely_dwim)
134+
strset_add(&ref_info->negative_refs, fullname);
135+
if (!ref_info->negative_refexprs)
136+
ref_info->onto = lookup_commit_reference_gently(the_repository,
137+
&e->item->oid, 1);
138+
ref_info->negative_refexprs++;
139+
} else {
140+
if (can_uniquely_dwim)
141+
strset_add(&ref_info->positive_refs, fullname);
142+
ref_info->positive_refexprs++;
143+
}
144+
145+
free(fullname);
146+
}
147+
}
148+
149+
static void determine_replay_mode(struct rev_cmdline_info *cmd_info,
150+
const char *onto_name,
151+
const char **advance_name,
152+
struct commit **onto,
153+
struct strset **update_refs)
154+
{
155+
struct ref_info rinfo;
156+
157+
get_ref_information(cmd_info, &rinfo);
158+
if (!rinfo.positive_refexprs)
159+
die(_("need some commits to replay"));
160+
if (onto_name && *advance_name)
161+
die(_("--onto and --advance are incompatible"));
162+
else if (onto_name) {
163+
*onto = peel_committish(onto_name);
164+
if (rinfo.positive_refexprs <
165+
strset_get_size(&rinfo.positive_refs))
166+
die(_("all positive revisions given must be references"));
167+
} else if (*advance_name) {
168+
struct object_id oid;
169+
char *fullname = NULL;
170+
171+
*onto = peel_committish(*advance_name);
172+
if (repo_dwim_ref(the_repository, *advance_name, strlen(*advance_name),
173+
&oid, &fullname, 0) == 1) {
174+
*advance_name = fullname;
175+
} else {
176+
die(_("argument to --advance must be a reference"));
177+
}
178+
if (rinfo.positive_refexprs > 1)
179+
die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
180+
} else {
181+
int positive_refs_complete = (
182+
rinfo.positive_refexprs ==
183+
strset_get_size(&rinfo.positive_refs));
184+
int negative_refs_complete = (
185+
rinfo.negative_refexprs ==
186+
strset_get_size(&rinfo.negative_refs));
187+
/*
188+
* We need either positive_refs_complete or
189+
* negative_refs_complete, but not both.
190+
*/
191+
if (rinfo.negative_refexprs > 0 &&
192+
positive_refs_complete == negative_refs_complete)
193+
die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
194+
if (negative_refs_complete) {
195+
struct hashmap_iter iter;
196+
struct strmap_entry *entry;
197+
198+
if (rinfo.negative_refexprs == 0)
199+
die(_("all positive revisions given must be references"));
200+
else if (rinfo.negative_refexprs > 1)
201+
die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
202+
else if (rinfo.positive_refexprs > 1)
203+
die(_("cannot advance target with multiple source branches because ordering would be ill-defined"));
204+
205+
/* Only one entry, but we have to loop to get it */
206+
strset_for_each_entry(&rinfo.negative_refs,
207+
&iter, entry) {
208+
*advance_name = entry->key;
209+
}
210+
} else { /* positive_refs_complete */
211+
if (rinfo.negative_refexprs > 1)
212+
die(_("cannot implicitly determine correct base for --onto"));
213+
if (rinfo.negative_refexprs == 1)
214+
*onto = rinfo.onto;
215+
}
216+
}
217+
if (!*advance_name) {
218+
*update_refs = xcalloc(1, sizeof(**update_refs));
219+
**update_refs = rinfo.positive_refs;
220+
memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
221+
}
222+
strset_clear(&rinfo.negative_refs);
223+
strset_clear(&rinfo.positive_refs);
224+
}
225+
85226
static struct commit *pick_regular_commit(struct commit *pickme,
86227
struct commit *last_commit,
87228
struct merge_options *merge_opt,
@@ -114,20 +255,26 @@ static struct commit *pick_regular_commit(struct commit *pickme,
114255

115256
int cmd_replay(int argc, const char **argv, const char *prefix)
116257
{
117-
struct commit *onto;
258+
const char *advance_name = NULL;
259+
struct commit *onto = NULL;
118260
const char *onto_name = NULL;
119-
struct commit *last_commit = NULL;
261+
120262
struct rev_info revs;
263+
struct commit *last_commit = NULL;
121264
struct commit *commit;
122265
struct merge_options merge_opt;
123266
struct merge_result result;
267+
struct strset *update_refs = NULL;
124268
int ret = 0;
125269

126270
const char * const replay_usage[] = {
127-
N_("(EXPERIMENTAL!) git replay --onto <newbase> <revision-range>..."),
271+
N_("(EXPERIMENTAL!) git replay (--onto <newbase> | --advance <branch>) <revision-range>..."),
128272
NULL
129273
};
130274
struct option replay_options[] = {
275+
OPT_STRING(0, "advance", &advance_name,
276+
N_("branch"),
277+
N_("make replay advance given branch")),
131278
OPT_STRING(0, "onto", &onto_name,
132279
N_("revision"),
133280
N_("replay onto given commit")),
@@ -137,13 +284,11 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
137284
argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
138285
PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
139286

140-
if (!onto_name) {
141-
error(_("option --onto is mandatory"));
287+
if (!onto_name && !advance_name) {
288+
error(_("option --onto or --advance is mandatory"));
142289
usage_with_options(replay_usage, replay_options);
143290
}
144291

145-
onto = peel_committish(onto_name);
146-
147292
repo_init_revisions(the_repository, &revs, prefix);
148293

149294
/*
@@ -195,6 +340,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
195340
revs.simplify_history = 0;
196341
}
197342

343+
determine_replay_mode(&revs.cmdline, onto_name, &advance_name,
344+
&onto, &update_refs);
345+
346+
if (!onto) /* FIXME: Should handle replaying down to root commit */
347+
die("Replaying down to root commit is not supported yet!");
348+
198349
if (prepare_revision_walk(&revs) < 0) {
199350
ret = error(_("error preparing revisions"));
200351
goto cleanup;
@@ -203,6 +354,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
203354
init_merge_options(&merge_opt, the_repository);
204355
memset(&result, 0, sizeof(result));
205356
merge_opt.show_rename_progress = 0;
357+
206358
result.tree = repo_get_commit_tree(the_repository, onto);
207359
last_commit = onto;
208360
while ((commit = get_revision(&revs))) {
@@ -217,12 +369,15 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
217369
if (!last_commit)
218370
break;
219371

372+
/* Update any necessary branches */
373+
if (advance_name)
374+
continue;
220375
decoration = get_name_decoration(&commit->object);
221376
if (!decoration)
222377
continue;
223-
224378
while (decoration) {
225-
if (decoration->type == DECORATION_REF_LOCAL) {
379+
if (decoration->type == DECORATION_REF_LOCAL &&
380+
strset_contains(update_refs, decoration->name)) {
226381
printf("update %s %s %s\n",
227382
decoration->name,
228383
oid_to_hex(&last_commit->object.oid),
@@ -232,10 +387,22 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
232387
}
233388
}
234389

390+
/* In --advance mode, advance the target ref */
391+
if (result.clean == 1 && advance_name) {
392+
printf("update %s %s %s\n",
393+
advance_name,
394+
oid_to_hex(&last_commit->object.oid),
395+
oid_to_hex(&onto->object.oid));
396+
}
397+
235398
merge_finalize(&merge_opt, &result);
236399
ret = result.clean;
237400

238401
cleanup:
402+
if (update_refs) {
403+
strset_clear(update_refs);
404+
free(update_refs);
405+
}
239406
release_revisions(&revs);
240407

241408
/* Return */

t/t3650-replay-basics.sh

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,38 @@ test_expect_success 'using replay on bare repo to rebase with a conflict' '
8080
test_expect_code 1 git -C bare replay --onto topic1 B..conflict
8181
'
8282

83+
test_expect_success 'using replay to perform basic cherry-pick' '
84+
# The differences between this test and previous ones are:
85+
# --advance vs --onto
86+
# 2nd field of result is refs/heads/main vs. refs/heads/topic2
87+
# 4th field of result is hash for main instead of hash for topic2
88+
89+
git replay --advance main topic1..topic2 >result &&
90+
91+
test_line_count = 1 result &&
92+
93+
git log --format=%s $(cut -f 3 -d " " result) >actual &&
94+
test_write_lines E D M L B A >expect &&
95+
test_cmp expect actual &&
96+
97+
printf "update refs/heads/main " >expect &&
98+
printf "%s " $(cut -f 3 -d " " result) >>expect &&
99+
git rev-parse main >>expect &&
100+
101+
test_cmp expect result
102+
'
103+
104+
test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
105+
git -C bare replay --advance main topic1..topic2 >result-bare &&
106+
test_cmp expect result-bare
107+
'
108+
109+
test_expect_success 'replay on bare repo fails with both --advance and --onto' '
110+
test_must_fail git -C bare replay --advance main --onto main topic1..topic2 >result-bare
111+
'
112+
113+
test_expect_success 'replay fails when both --advance and --onto are omitted' '
114+
test_must_fail git replay topic1..topic2 >result
115+
'
116+
83117
test_done

0 commit comments

Comments
 (0)