Skip to content

Commit fb260a5

Browse files
committed
Merge branch 'sa/replay-atomic-ref-updates' into jch
"git replay" (experimental) learned to perform ref updates itself in a transaction by default, instead of emitting where each refs should point at and leaving the actual update to another command. * sa/replay-atomic-ref-updates: replay: add replay.refAction config option replay: make atomic ref updates the default behavior replay: use die_for_incompatible_opt2() for option validation
2 parents 327bab9 + 336ac90 commit fb260a5

File tree

4 files changed

+277
-43
lines changed

4 files changed

+277
-43
lines changed

Documentation/config/replay.adoc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
replay.refAction::
2+
Specifies the default mode for handling reference updates in
3+
`git replay`. The value can be:
4+
+
5+
--
6+
* `update`: Update refs directly using an atomic transaction (default behavior).
7+
* `print`: Output update-ref commands for pipeline use.
8+
--
9+
+
10+
This setting can be overridden with the `--ref-action` command-line option.
11+
When not configured, `git replay` defaults to `update` mode.

Documentation/git-replay.adoc

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@ 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>) [--ref-action[=<mode>]] <revision-range>...
1313

1414
DESCRIPTION
1515
-----------
1616

1717
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
18+
the working tree and the index untouched. By default, updates the
19+
relevant references using an atomic transaction (all refs update or
20+
none). Use `--ref-action=print` to avoid automatic ref updates and
21+
instead get update commands that can be piped to `git update-ref --stdin`
2122
(see the OUTPUT section below).
2223

2324
THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
@@ -29,18 +30,29 @@ OPTIONS
2930
Starting point at which to create the new commits. May be any
3031
valid commit, and not just an existing branch name.
3132
+
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.
33+
When `--onto` is specified, the branch(es) in the revision range will be
34+
updated to point at the new commits, similar to the way `git rebase --update-refs`
35+
updates multiple branches in the affected range.
3636

3737
--advance <branch>::
3838
Starting point at which to create the new commits; must be a
3939
branch name.
4040
+
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).
41+
The history is replayed on top of the <branch> and <branch> is updated to
42+
point at the tip of the resulting history. This is different from `--onto`,
43+
which uses the target only as a starting point without updating it.
44+
45+
--ref-action[=<mode>]::
46+
Control how references are updated. The mode can be:
47+
+
48+
--
49+
* `update` (default): Update refs directly using an atomic transaction.
50+
All refs are updated or none are (all-or-nothing behavior).
51+
* `print`: Output update-ref commands for pipeline use. This is the
52+
traditional behavior where output can be piped to `git update-ref --stdin`.
53+
--
54+
+
55+
The default mode can be configured via the `replay.refAction` configuration variable.
4456

4557
<revision-range>::
4658
Range of commits to replay. More than one <revision-range> can
@@ -54,8 +66,11 @@ 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, or with `--ref-action=update`, this command produces no output on
70+
success, as refs are updated directly using an atomic transaction.
71+
72+
When using `--ref-action=print`, the output is usable as input to
73+
`git update-ref --stdin`. It is of the form:
5974

6075
update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
6176
update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
@@ -81,40 +96,44 @@ To simply rebase `mybranch` onto `target`:
8196

8297
------------
8398
$ git replay --onto target origin/main..mybranch
99+
------------
100+
101+
The refs are updated atomically and no output is produced on success.
102+
103+
To see what would be updated without actually updating:
104+
105+
------------
106+
$ git replay --ref-action=print --onto target origin/main..mybranch
84107
update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
85108
------------
86109

87110
To cherry-pick the commits from mybranch onto target:
88111

89112
------------
90113
$ git replay --advance target origin/main..mybranch
91-
update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}
92114
------------
93115

94116
Note that the first two examples replay the exact same commits and on
95117
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.
118+
updates mybranch to point at the new commits and the second updates
119+
target to point at them.
98120

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

102124
------------
103125
$ 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}
107126
------------
108127

128+
All three branches (`branch1`, `branch2`, and `tipbranch`) are updated
129+
atomically.
130+
109131
When calling `git replay`, one does not need to specify a range of
110132
commits to replay using the syntax `A..B`; any range expression will
111133
do:
112134

113135
------------
114136
$ 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}
118137
------------
119138

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

builtin/replay.c

Lines changed: 120 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"
@@ -20,6 +21,11 @@
2021
#include <oidset.h>
2122
#include <tree.h>
2223

24+
enum ref_action_mode {
25+
REF_ACTION_UPDATE,
26+
REF_ACTION_PRINT,
27+
};
28+
2329
static const char *short_commit_name(struct repository *repo,
2430
struct commit *commit)
2531
{
@@ -284,6 +290,54 @@ static struct commit *pick_regular_commit(struct repository *repo,
284290
return create_commit(repo, result->tree, pickme, replayed_base);
285291
}
286292

293+
static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
294+
{
295+
if (!ref_action || !strcmp(ref_action, "update"))
296+
return REF_ACTION_UPDATE;
297+
if (!strcmp(ref_action, "print"))
298+
return REF_ACTION_PRINT;
299+
die(_("invalid %s value: '%s'"), source, ref_action);
300+
}
301+
302+
static enum ref_action_mode get_ref_action_mode(struct repository *repo, const char *ref_action)
303+
{
304+
const char *config_value = NULL;
305+
306+
/* Command line option takes precedence */
307+
if (ref_action)
308+
return parse_ref_action_mode(ref_action, "--ref-action");
309+
310+
/* Check config value */
311+
if (!repo_config_get_string_tmp(repo, "replay.refAction", &config_value))
312+
return parse_ref_action_mode(config_value, "replay.refAction");
313+
314+
/* Default to update mode */
315+
return REF_ACTION_UPDATE;
316+
}
317+
318+
static int handle_ref_update(enum ref_action_mode mode,
319+
struct ref_transaction *transaction,
320+
const char *refname,
321+
const struct object_id *new_oid,
322+
const struct object_id *old_oid,
323+
const char *reflog_msg,
324+
struct strbuf *err)
325+
{
326+
switch (mode) {
327+
case REF_ACTION_PRINT:
328+
printf("update %s %s %s\n",
329+
refname,
330+
oid_to_hex(new_oid),
331+
oid_to_hex(old_oid));
332+
return 0;
333+
case REF_ACTION_UPDATE:
334+
return ref_transaction_update(transaction, refname, new_oid, old_oid,
335+
NULL, NULL, 0, reflog_msg, err);
336+
default:
337+
BUG("unknown ref_action_mode %d", mode);
338+
}
339+
}
340+
287341
int cmd_replay(int argc,
288342
const char **argv,
289343
const char *prefix,
@@ -294,6 +348,8 @@ int cmd_replay(int argc,
294348
struct commit *onto = NULL;
295349
const char *onto_name = NULL;
296350
int contained = 0;
351+
const char *ref_action = NULL;
352+
enum ref_action_mode ref_mode;
297353

298354
struct rev_info revs;
299355
struct commit *last_commit = NULL;
@@ -302,12 +358,15 @@ int cmd_replay(int argc,
302358
struct merge_result result;
303359
struct strset *update_refs = NULL;
304360
kh_oid_map_t *replayed_commits;
361+
struct ref_transaction *transaction = NULL;
362+
struct strbuf transaction_err = STRBUF_INIT;
363+
struct strbuf reflog_msg = STRBUF_INIT;
305364
int ret = 0;
306365

307-
const char * const replay_usage[] = {
366+
const char *const replay_usage[] = {
308367
N_("(EXPERIMENTAL!) git replay "
309368
"([--contained] --onto <newbase> | --advance <branch>) "
310-
"<revision-range>..."),
369+
"[--ref-action[=<mode>]] <revision-range>..."),
311370
NULL
312371
};
313372
struct option replay_options[] = {
@@ -319,6 +378,9 @@ int cmd_replay(int argc,
319378
N_("replay onto given commit")),
320379
OPT_BOOL(0, "contained", &contained,
321380
N_("advance all branches contained in revision-range")),
381+
OPT_STRING(0, "ref-action", &ref_action,
382+
N_("mode"),
383+
N_("control ref update behavior (update|print)")),
322384
OPT_END()
323385
};
324386

@@ -330,9 +392,12 @@ int cmd_replay(int argc,
330392
usage_with_options(replay_usage, replay_options);
331393
}
332394

333-
if (advance_name_opt && contained)
334-
die(_("options '%s' and '%s' cannot be used together"),
335-
"--advance", "--contained");
395+
die_for_incompatible_opt2(!!advance_name_opt, "--advance",
396+
contained, "--contained");
397+
398+
/* Parse ref action mode from command line or config */
399+
ref_mode = get_ref_action_mode(repo, ref_action);
400+
336401
advance_name = xstrdup_or_null(advance_name_opt);
337402

338403
repo_init_revisions(repo, &revs, prefix);
@@ -389,6 +454,24 @@ int cmd_replay(int argc,
389454
determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
390455
&onto, &update_refs);
391456

457+
/* Build reflog message */
458+
if (advance_name_opt)
459+
strbuf_addf(&reflog_msg, "replay --advance %s", advance_name_opt);
460+
else
461+
strbuf_addf(&reflog_msg, "replay --onto %s",
462+
oid_to_hex(&onto->object.oid));
463+
464+
/* Initialize ref transaction if using update mode */
465+
if (ref_mode == REF_ACTION_UPDATE) {
466+
transaction = ref_store_transaction_begin(get_main_ref_store(repo),
467+
0, &transaction_err);
468+
if (!transaction) {
469+
ret = error(_("failed to begin ref transaction: %s"),
470+
transaction_err.buf);
471+
goto cleanup;
472+
}
473+
}
474+
392475
if (!onto) /* FIXME: Should handle replaying down to root commit */
393476
die("Replaying down to root commit is not supported yet!");
394477

@@ -434,21 +517,41 @@ int cmd_replay(int argc,
434517
if (decoration->type == DECORATION_REF_LOCAL &&
435518
(contained || strset_contains(update_refs,
436519
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));
520+
if (handle_ref_update(ref_mode, transaction,
521+
decoration->name,
522+
&last_commit->object.oid,
523+
&commit->object.oid,
524+
reflog_msg.buf,
525+
&transaction_err) < 0) {
526+
ret = error(_("failed to update ref '%s': %s"),
527+
decoration->name, transaction_err.buf);
528+
goto cleanup;
529+
}
441530
}
442531
decoration = decoration->next;
443532
}
444533
}
445534

446535
/* In --advance mode, advance the target ref */
447536
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));
537+
if (handle_ref_update(ref_mode, transaction, advance_name,
538+
&last_commit->object.oid,
539+
&onto->object.oid,
540+
reflog_msg.buf,
541+
&transaction_err) < 0) {
542+
ret = error(_("failed to update ref '%s': %s"),
543+
advance_name, transaction_err.buf);
544+
goto cleanup;
545+
}
546+
}
547+
548+
/* Commit the ref transaction if we have one */
549+
if (transaction && result.clean == 1) {
550+
if (ref_transaction_commit(transaction, &transaction_err)) {
551+
ret = error(_("failed to commit ref transaction: %s"),
552+
transaction_err.buf);
553+
goto cleanup;
554+
}
452555
}
453556

454557
merge_finalize(&merge_opt, &result);
@@ -460,6 +563,10 @@ int cmd_replay(int argc,
460563
ret = result.clean;
461564

462565
cleanup:
566+
if (transaction)
567+
ref_transaction_free(transaction);
568+
strbuf_release(&transaction_err);
569+
strbuf_release(&reflog_msg);
463570
release_revisions(&revs);
464571
free(advance_name);
465572

0 commit comments

Comments
 (0)