Skip to content

Commit 1cbe82c

Browse files
edith007gitster
authored andcommitted
replay: make atomic ref updates the default behavior
The git replay command currently outputs update commands that can be piped to update-ref to achieve a rebase, e.g. git replay --onto main topic1..topic2 | git update-ref --stdin This separation had advantages for three special cases: * it made testing easy (when state isn't modified from one step to the next, you don't need to make temporary branches or have undo commands, or try to track the changes) * it provided a natural can-it-rebase-cleanly (and what would it rebase to) capability without automatically updating refs, similar to a --dry-run * it provided a natural low-level tool for the suite of hash-object, mktree, commit-tree, mktag, merge-tree, and update-ref, allowing users to have another building block for experimentation and making new tools However, it should be noted that all three of these are somewhat special cases; users, whether on the client or server side, would almost certainly find it more ergonomical to simply have the updating of refs be the default. For server-side operations in particular, the pipeline architecture creates process coordination overhead. Server implementations that need to perform rebases atomically must maintain additional code to: 1. Spawn and manage a pipeline between git-replay and git-update-ref 2. Coordinate stdout/stderr streams across the pipe boundary 3. Handle partial failure states if the pipeline breaks mid-execution 4. Parse and validate the update-ref command output Change the default behavior to update refs directly, and atomically (at least to the extent supported by the refs backend in use). This eliminates the process coordination overhead for the common case. For users needing the traditional pipeline workflow, add a new `--update-refs=<mode>` option that preserves the original behavior: git replay --update-refs=print --onto main topic1..topic2 | git update-ref --stdin The mode can be: * `yes` (default): Update refs directly using an atomic transaction * `print`: Output update-ref commands for pipeline use Implementation details: The atomic ref updates are implemented using Git's ref transaction API. In cmd_replay(), when not in 'print' mode, we initialize a transaction using ref_store_transaction_begin() with the default atomic behavior. As commits are replayed, ref updates are staged into the transaction using ref_transaction_update(). Finally, ref_transaction_commit() applies all updates atomically—either all updates succeed or none do. To avoid code duplication between the 'print' and 'yes' modes, this commit extracts a handle_ref_update() helper function. This function takes the mode and either prints the update command or stages it into the transaction. This keeps both code paths consistent and makes future maintenance easier. The helper function signature: static int handle_ref_update(const char *mode, struct ref_transaction *transaction, const char *refname, const struct object_id *new_oid, const struct object_id *old_oid, struct strbuf *err) When mode is 'print', it prints the update-ref command. When mode is 'yes', it calls ref_transaction_update() to stage the update. This eliminates the duplication that would otherwise exist at each ref update call site. Test suite changes: All existing tests that expected command output now use `--update-refs=print` to preserve their original behavior. This keeps the tests valid while allowing them to verify that the pipeline workflow still works correctly. New tests were added to verify: - Default atomic behavior (no output, refs updated directly) - Bare repository support (server-side use case) - Equivalence between traditional pipeline and atomic updates - Real atomicity using a lock file to verify all-or-nothing guarantee - Test isolation using test_when_finished to clean up state The bare repository tests were fixed to rebuild their expectations independently rather than comparing to previous test output, improving test reliability and isolation. A following commit will add a `replay.defaultAction` configuration option for users who prefer the traditional pipeline output as their default behavior. Helped-by: Elijah Newren <newren@gmail.com> Helped-by: Patrick Steinhardt <ps@pks.im> Helped-by: Christian Couder <christian.couder@gmail.com> Helped-by: Phillip Wood <phillip.wood123@gmail.com> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent 1901e97 commit 1cbe82c

3 files changed

Lines changed: 267 additions & 45 deletions

File tree

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: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,26 @@ static struct commit *pick_regular_commit(struct repository *repo,
284284
return create_commit(repo, result->tree, pickme, replayed_base);
285285
}
286286

287+
static int handle_ref_update(const char *mode,
288+
struct ref_transaction *transaction,
289+
const char *refname,
290+
const struct object_id *new_oid,
291+
const struct object_id *old_oid,
292+
struct strbuf *err)
293+
{
294+
if (!strcmp(mode, "print")) {
295+
printf("update %s %s %s\n",
296+
refname,
297+
oid_to_hex(new_oid),
298+
oid_to_hex(old_oid));
299+
return 0;
300+
}
301+
302+
/* mode == "yes" - update refs directly */
303+
return ref_transaction_update(transaction, refname, new_oid, old_oid,
304+
NULL, NULL, 0, "git replay", err);
305+
}
306+
287307
int cmd_replay(int argc,
288308
const char **argv,
289309
const char *prefix,
@@ -294,6 +314,7 @@ int cmd_replay(int argc,
294314
struct commit *onto = NULL;
295315
const char *onto_name = NULL;
296316
int contained = 0;
317+
const char *update_refs_mode = NULL;
297318

298319
struct rev_info revs;
299320
struct commit *last_commit = NULL;
@@ -302,12 +323,14 @@ int cmd_replay(int argc,
302323
struct merge_result result;
303324
struct strset *update_refs = NULL;
304325
kh_oid_map_t *replayed_commits;
326+
struct ref_transaction *transaction = NULL;
327+
struct strbuf transaction_err = STRBUF_INIT;
305328
int ret = 0;
306329

307-
const char * const replay_usage[] = {
330+
const char *const replay_usage[] = {
308331
N_("(EXPERIMENTAL!) git replay "
309332
"([--contained] --onto <newbase> | --advance <branch>) "
310-
"<revision-range>..."),
333+
"[--update-refs[=<mode>]] <revision-range>..."),
311334
NULL
312335
};
313336
struct option replay_options[] = {
@@ -319,6 +342,9 @@ int cmd_replay(int argc,
319342
N_("replay onto given commit")),
320343
OPT_BOOL(0, "contained", &contained,
321344
N_("advance all branches contained in revision-range")),
345+
OPT_STRING(0, "update-refs", &update_refs_mode,
346+
N_("mode"),
347+
N_("control ref update behavior (yes|print)")),
322348
OPT_END()
323349
};
324350

@@ -333,6 +359,15 @@ int cmd_replay(int argc,
333359
die_for_incompatible_opt2(!!advance_name_opt, "--advance",
334360
contained, "--contained");
335361

362+
/* Set default mode if not specified */
363+
if (!update_refs_mode)
364+
update_refs_mode = "yes";
365+
366+
/* Validate update-refs mode */
367+
if (strcmp(update_refs_mode, "yes") && strcmp(update_refs_mode, "print"))
368+
die(_("invalid value for --update-refs: '%s' (expected 'yes' or 'print')"),
369+
update_refs_mode);
370+
336371
advance_name = xstrdup_or_null(advance_name_opt);
337372

338373
repo_init_revisions(repo, &revs, prefix);
@@ -389,6 +424,17 @@ int cmd_replay(int argc,
389424
determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
390425
&onto, &update_refs);
391426

427+
/* Initialize ref transaction if we're updating refs directly */
428+
if (!strcmp(update_refs_mode, "yes")) {
429+
transaction = ref_store_transaction_begin(get_main_ref_store(repo),
430+
0, &transaction_err);
431+
if (!transaction) {
432+
ret = error(_("failed to begin ref transaction: %s"),
433+
transaction_err.buf);
434+
goto cleanup;
435+
}
436+
}
437+
392438
if (!onto) /* FIXME: Should handle replaying down to root commit */
393439
die("Replaying down to root commit is not supported yet!");
394440

@@ -434,21 +480,40 @@ int cmd_replay(int argc,
434480
if (decoration->type == DECORATION_REF_LOCAL &&
435481
(contained || strset_contains(update_refs,
436482
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));
483+
if (handle_ref_update(update_refs_mode, transaction,
484+
decoration->name,
485+
&last_commit->object.oid,
486+
&commit->object.oid,
487+
&transaction_err) < 0) {
488+
ret = error(_("failed to update ref '%s': %s"),
489+
decoration->name, transaction_err.buf);
490+
goto cleanup;
491+
}
441492
}
442493
decoration = decoration->next;
443494
}
444495
}
445496

446497
/* In --advance mode, advance the target ref */
447498
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));
499+
if (handle_ref_update(update_refs_mode, transaction,
500+
advance_name,
501+
&last_commit->object.oid,
502+
&onto->object.oid,
503+
&transaction_err) < 0) {
504+
ret = error(_("failed to update ref '%s': %s"),
505+
advance_name, transaction_err.buf);
506+
goto cleanup;
507+
}
508+
}
509+
510+
/* Commit the ref transaction if we have one */
511+
if (transaction && result.clean == 1) {
512+
if (ref_transaction_commit(transaction, &transaction_err)) {
513+
ret = error(_("failed to commit ref transaction: %s"),
514+
transaction_err.buf);
515+
goto cleanup;
516+
}
452517
}
453518

454519
merge_finalize(&merge_opt, &result);
@@ -460,6 +525,9 @@ int cmd_replay(int argc,
460525
ret = result.clean;
461526

462527
cleanup:
528+
if (transaction)
529+
ref_transaction_free(transaction);
530+
strbuf_release(&transaction_err);
463531
release_revisions(&revs);
464532
free(advance_name);
465533

0 commit comments

Comments
 (0)