Skip to content

Commit 1bc212b

Browse files
committed
Merge branch 'sa/replay-atomic-ref-updates' into seen
* sa/replay-atomic-ref-updates: replay: add replay.defaultAction config option replay: make atomic ref updates the default behavior replay: use die_for_incompatible_opt2() for option validation
2 parents c371994 + a07d37b commit 1bc212b

File tree

4 files changed

+343
-48
lines changed

4 files changed

+343
-48
lines changed

Documentation/config/replay.adoc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
replay.defaultAction::
2+
Control the default behavior of `git replay` for updating references.
3+
Can be set to:
4+
+
5+
--
6+
* `update-refs` (default): Update refs directly using an atomic transaction.
7+
* `show-commands`: Output update-ref commands that can be piped to
8+
`git update-ref --stdin`.
9+
--
10+
+
11+
This can be overridden with the `--update-refs` command-line option.
12+
Note that the command-line option uses slightly different values
13+
(`yes` and `print`) for brevity, but they map to the same behavior
14+
as the config values.

Documentation/git-replay.adoc

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,17 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
99
SYNOPSIS
1010
--------
1111
[verse]
12-
(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
12+
(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>)
13+
[--update-refs[=<mode>]] <revision-range>...
1314

1415
DESCRIPTION
1516
-----------
1617

1718
Takes ranges of commits and replays them onto a new location. Leaves
18-
the working tree and the index untouched, and updates no references.
19-
The output of this command is meant to be used as input to
20-
`git update-ref --stdin`, which would update the relevant branches
19+
the working tree and the index untouched. By default, updates the
20+
relevant references using an atomic transaction (all refs update or
21+
none). Use `--update-refs=print` to avoid automatic ref updates and
22+
instead get update commands that can be piped to `git update-ref --stdin`
2123
(see the OUTPUT section below).
2224

2325
THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
@@ -29,18 +31,28 @@ OPTIONS
2931
Starting point at which to create the new commits. May be any
3032
valid commit, and not just an existing branch name.
3133
+
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.
34+
When `--onto` is specified, the branch(es) in the revision range will be
35+
updated to point at the new commits (or update commands will be printed
36+
if `--update-refs=print` is used), similar to the way how
37+
`git rebase --update-refs` updates multiple branches in the affected range.
3638

3739
--advance <branch>::
3840
Starting point at which to create the new commits; must be a
3941
branch name.
4042
+
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).
43+
When `--advance` is specified, the branch passed as an argument will be
44+
updated to point at the new commits (or an update command will be printed
45+
if `--update-refs=print` is used). This mimics a cherry-pick operation.
46+
47+
--update-refs[=<mode>]::
48+
Control how references are updated. The mode can be:
49+
+
50+
--
51+
* `yes` (default): Update refs directly using an atomic transaction.
52+
All ref updates succeed or all fail.
53+
* `print`: Output update-ref commands instead of updating refs.
54+
The output can be piped as-is to `git update-ref --stdin`.
55+
--
4456

4557
<revision-range>::
4658
Range of commits to replay. More than one <revision-range> can
@@ -54,15 +66,19 @@ include::rev-list-options.adoc[]
5466
OUTPUT
5567
------
5668

57-
When there are no conflicts, the output of this command is usable as
58-
input to `git update-ref --stdin`. It is of the form:
69+
By default, when there are no conflicts, this command updates the relevant
70+
references using an atomic transaction and produces no output. All ref
71+
updates succeed or all fail.
72+
73+
When `--update-refs=print` is used, the output is usable as input to
74+
`git update-ref --stdin`. It is of the form:
5975

6076
update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
6177
update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
6278
update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
6379

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

@@ -77,44 +93,45 @@ is something other than 0 or 1.
7793
EXAMPLES
7894
--------
7995

80-
To simply rebase `mybranch` onto `target`:
96+
To simply rebase `mybranch` onto `target` (default behavior):
8197

8298
------------
8399
$ git replay --onto target origin/main..mybranch
84-
update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
85100
------------
86101

87102
To cherry-pick the commits from mybranch onto target:
88103

89104
------------
90105
$ git replay --advance target origin/main..mybranch
91-
update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}
92106
------------
93107

94108
Note that the first two examples replay the exact same commits and on
95109
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.
110+
updates mybranch to point at the new commits and the second updates
111+
target to point at them.
112+
113+
To get the traditional pipeline output:
114+
115+
------------
116+
$ git replay --update-refs=print --onto target origin/main..mybranch
117+
update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
118+
------------
98119

99120
What if you have a stack of branches, one depending upon another, and
100121
you'd really like to rebase the whole set?
101122

102123
------------
103124
$ git replay --contained --onto origin/main origin/main..tipbranch
104-
update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
105-
update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
106-
update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
107125
------------
108126

127+
This automatically finds and rebases all branches contained within the
128+
`origin/main..tipbranch` range.
129+
109130
When calling `git replay`, one does not need to specify a range of
110-
commits to replay using the syntax `A..B`; any range expression will
111-
do:
131+
commits to replay using the syntax `A..B`; any range expression will do:
112132

113133
------------
114134
$ git replay --onto origin/main ^base branch1 branch2 branch3
115-
update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
116-
update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
117-
update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
118135
------------
119136

120137
This will simultaneously rebase `branch1`, `branch2`, and `branch3`,

builtin/replay.c

Lines changed: 95 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "git-compat-util.h"
99

1010
#include "builtin.h"
11+
#include "config.h"
1112
#include "environment.h"
1213
#include "hex.h"
1314
#include "lockfile.h"
@@ -284,6 +285,26 @@ static struct commit *pick_regular_commit(struct repository *repo,
284285
return create_commit(repo, result->tree, pickme, replayed_base);
285286
}
286287

288+
static int handle_ref_update(const char *mode,
289+
struct ref_transaction *transaction,
290+
const char *refname,
291+
const struct object_id *new_oid,
292+
const struct object_id *old_oid,
293+
struct strbuf *err)
294+
{
295+
if (!strcmp(mode, "print")) {
296+
printf("update %s %s %s\n",
297+
refname,
298+
oid_to_hex(new_oid),
299+
oid_to_hex(old_oid));
300+
return 0;
301+
}
302+
303+
/* mode == "yes" - update refs directly */
304+
return ref_transaction_update(transaction, refname, new_oid, old_oid,
305+
NULL, NULL, 0, "git replay", err);
306+
}
307+
287308
int cmd_replay(int argc,
288309
const char **argv,
289310
const char *prefix,
@@ -294,6 +315,7 @@ int cmd_replay(int argc,
294315
struct commit *onto = NULL;
295316
const char *onto_name = NULL;
296317
int contained = 0;
318+
const char *update_refs_mode = NULL;
297319

298320
struct rev_info revs;
299321
struct commit *last_commit = NULL;
@@ -302,12 +324,14 @@ int cmd_replay(int argc,
302324
struct merge_result result;
303325
struct strset *update_refs = NULL;
304326
kh_oid_map_t *replayed_commits;
327+
struct ref_transaction *transaction = NULL;
328+
struct strbuf transaction_err = STRBUF_INIT;
305329
int ret = 0;
306330

307-
const char * const replay_usage[] = {
331+
const char *const replay_usage[] = {
308332
N_("(EXPERIMENTAL!) git replay "
309333
"([--contained] --onto <newbase> | --advance <branch>) "
310-
"<revision-range>..."),
334+
"[--update-refs[=<mode>]] <revision-range>..."),
311335
NULL
312336
};
313337
struct option replay_options[] = {
@@ -319,6 +343,9 @@ int cmd_replay(int argc,
319343
N_("replay onto given commit")),
320344
OPT_BOOL(0, "contained", &contained,
321345
N_("advance all branches contained in revision-range")),
346+
OPT_STRING(0, "update-refs", &update_refs_mode,
347+
N_("mode"),
348+
N_("control ref update behavior (yes|print)")),
322349
OPT_END()
323350
};
324351

@@ -330,9 +357,31 @@ int cmd_replay(int argc,
330357
usage_with_options(replay_usage, replay_options);
331358
}
332359

333-
if (advance_name_opt && contained)
334-
die(_("options '%s' and '%s' cannot be used together"),
335-
"--advance", "--contained");
360+
die_for_incompatible_opt2(!!advance_name_opt, "--advance",
361+
contained, "--contained");
362+
363+
/* Set default mode from config if not specified on command line */
364+
if (!update_refs_mode) {
365+
const char *config_value = NULL;
366+
if (!repo_config_get_string_tmp(repo, "replay.defaultaction", &config_value)) {
367+
if (!strcmp(config_value, "update-refs"))
368+
update_refs_mode = "yes";
369+
else if (!strcmp(config_value, "show-commands"))
370+
update_refs_mode = "print";
371+
else
372+
die(_("invalid value for replay.defaultAction: '%s' "
373+
"(expected 'update-refs' or 'show-commands')"),
374+
config_value);
375+
} else {
376+
update_refs_mode = "yes";
377+
}
378+
}
379+
380+
/* Validate update-refs mode */
381+
if (strcmp(update_refs_mode, "yes") && strcmp(update_refs_mode, "print"))
382+
die(_("invalid value for --update-refs: '%s' (expected 'yes' or 'print')"),
383+
update_refs_mode);
384+
336385
advance_name = xstrdup_or_null(advance_name_opt);
337386

338387
repo_init_revisions(repo, &revs, prefix);
@@ -389,6 +438,17 @@ int cmd_replay(int argc,
389438
determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
390439
&onto, &update_refs);
391440

441+
/* Initialize ref transaction if we're updating refs directly */
442+
if (!strcmp(update_refs_mode, "yes")) {
443+
transaction = ref_store_transaction_begin(get_main_ref_store(repo),
444+
0, &transaction_err);
445+
if (!transaction) {
446+
ret = error(_("failed to begin ref transaction: %s"),
447+
transaction_err.buf);
448+
goto cleanup;
449+
}
450+
}
451+
392452
if (!onto) /* FIXME: Should handle replaying down to root commit */
393453
die("Replaying down to root commit is not supported yet!");
394454

@@ -434,21 +494,40 @@ int cmd_replay(int argc,
434494
if (decoration->type == DECORATION_REF_LOCAL &&
435495
(contained || strset_contains(update_refs,
436496
decoration->name))) {
437-
printf("update %s %s %s\n",
438-
decoration->name,
439-
oid_to_hex(&last_commit->object.oid),
440-
oid_to_hex(&commit->object.oid));
497+
if (handle_ref_update(update_refs_mode, transaction,
498+
decoration->name,
499+
&last_commit->object.oid,
500+
&commit->object.oid,
501+
&transaction_err) < 0) {
502+
ret = error(_("failed to update ref '%s': %s"),
503+
decoration->name, transaction_err.buf);
504+
goto cleanup;
505+
}
441506
}
442507
decoration = decoration->next;
443508
}
444509
}
445510

446511
/* In --advance mode, advance the target ref */
447512
if (result.clean == 1 && advance_name) {
448-
printf("update %s %s %s\n",
449-
advance_name,
450-
oid_to_hex(&last_commit->object.oid),
451-
oid_to_hex(&onto->object.oid));
513+
if (handle_ref_update(update_refs_mode, transaction,
514+
advance_name,
515+
&last_commit->object.oid,
516+
&onto->object.oid,
517+
&transaction_err) < 0) {
518+
ret = error(_("failed to update ref '%s': %s"),
519+
advance_name, transaction_err.buf);
520+
goto cleanup;
521+
}
522+
}
523+
524+
/* Commit the ref transaction if we have one */
525+
if (transaction && result.clean == 1) {
526+
if (ref_transaction_commit(transaction, &transaction_err)) {
527+
ret = error(_("failed to commit ref transaction: %s"),
528+
transaction_err.buf);
529+
goto cleanup;
530+
}
452531
}
453532

454533
merge_finalize(&merge_opt, &result);
@@ -460,6 +539,9 @@ int cmd_replay(int argc,
460539
ret = result.clean;
461540

462541
cleanup:
542+
if (transaction)
543+
ref_transaction_free(transaction);
544+
strbuf_release(&transaction_err);
463545
release_revisions(&revs);
464546
free(advance_name);
465547

0 commit comments

Comments
 (0)