builtin/history: replace "--ref-action=print" with "--dry-run"

The git-history(1) command has the ability to perform a dry-run that will not end up modifying any references. Instead, we'll only print any ref updates that would happen as a consequence of performing the operation. This mode is somewhat hidden though behind the "--ref-action=print" option. This command line option has its origin in git-replay(1), where it's probably an okayish interface as this command is sitting more on the plumbing side of tools. But git-history(1) is a user-facing tool, and this way of achieving a dry-run is way too technical and thus not very discoverable. Besides usability issues, it also has another issue: the dry-run mode will always operate as if the user wanted to rewrite all branches. But in fact, the user also has the option to only update the HEAD reference, and they might want to perform a dry-run of such an operation, too. We could of course introduce "--ref-action=print-head", but that would become even less ergonomic. Replace "--ref-action=print" with a new "--dry-run" toggle. This new toggle works with both "--ref-action={head,branches}" and is way more discoverable. Add a test to verify that both "--ref-action=" values behave as expected. This patch is best viewed with "--ignore-space-change". Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Feb 16, 2026 at 07:45 UTC 1073fa14e14ae119d6025298eafcb3f1c12bd7df
3 files changed +98 -78
Documentation/git-history.adoc
+9 -5
@@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history
8 SYNOPSIS
9 --------
10 [synopsis]
11 -git history reword <commit> [--ref-action=(branches|head|print)]
11 +git history reword <commit> [--dry-run] [--ref-action=(branches|head)]
12
13 DESCRIPTION
14 -----------
@@ -60,13 +60,17 @@ The following commands are available to rewrite history in different ways:
60 OPTIONS
61 -------
62
63 -`--ref-action=(branches|head|print)`::
63 +`--dry-run`::
64 + Do not update any references, but instead print any ref updates in a
65 + format that can be consumed by linkgit:git-update-ref[1]. Necessary new
66 + objects will be written into the repository, so applying these printed
67 + ref updates is generally safe.
68 +
69 +`--ref-action=(branches|head)`::
70 Control which references will be updated by the command, if any. With
71 `branches`, all local branches that point to commits which are
72 descendants of the original commit will be rewritten. With `head`, only
67 - the current `HEAD` reference will be rewritten. With `print`, all
68 - updates as they would be performed with `branches` are printed in a
69 - format that can be consumed by linkgit:git-update-ref[1].
73 + the current `HEAD` reference will be rewritten.
74
75 GIT
76 ---
builtin/history.c
+79 -71
@@ -18,7 +18,7 @@
18 #include "wt-status.h"
19
20 #define GIT_HISTORY_REWORD_USAGE \
21 - N_("git history reword <commit> [--ref-action=(branches|head|print)]")
21 + N_("git history reword <commit> [--dry-run] [--ref-action=(branches|head)]")
22
23 static void change_data_free(void *util, const char *str UNUSED)
24 {
@@ -155,7 +155,6 @@ enum ref_action {
155 REF_ACTION_DEFAULT,
156 REF_ACTION_BRANCHES,
157 REF_ACTION_HEAD,
158 - REF_ACTION_PRINT,
158 };
159
160 static int parse_ref_action(const struct option *opt, const char *value, int unset)
@@ -167,10 +166,8 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns
166 *action = REF_ACTION_BRANCHES;
167 } else if (!strcmp(value, "head")) {
168 *action = REF_ACTION_HEAD;
170 - } else if (!strcmp(value, "print")) {
171 - *action = REF_ACTION_PRINT;
169 } else {
173 - return error(_("%s expects one of 'branches', 'head' or 'print'"),
170 + return error(_("%s expects one of 'branches' or 'head'"),
171 opt->long_name);
172 }
173
@@ -286,11 +283,29 @@ out:
283 return ret;
284 }
285
286 +static int handle_ref_update(struct ref_transaction *transaction,
287 + const char *refname,
288 + const struct object_id *new_oid,
289 + const struct object_id *old_oid,
290 + const char *reflog_msg,
291 + struct strbuf *err)
292 +{
293 + if (!transaction) {
294 + printf("update %s %s %s\n",
295 + refname, oid_to_hex(new_oid), oid_to_hex(old_oid));
296 + return 0;
297 + }
298 +
299 + return ref_transaction_update(transaction, refname, new_oid, old_oid,
300 + NULL, NULL, 0, reflog_msg, err);
301 +}
302 +
303 static int handle_reference_updates(struct rev_info *revs,
304 enum ref_action action,
305 struct commit *original,
306 struct commit *rewritten,
293 - const char *reflog_msg)
307 + const char *reflog_msg,
308 + int dry_run)
309 {
310 const struct name_decoration *decoration;
311 struct replay_revisions_options opts = { 0 };
@@ -312,82 +327,72 @@ static int handle_reference_updates(struct rev_info *revs,
327 if (ret)
328 goto out;
329
315 - switch (action) {
316 - case REF_ACTION_BRANCHES:
317 - case REF_ACTION_HEAD:
330 + if (action != REF_ACTION_BRANCHES && action != REF_ACTION_HEAD)
331 + BUG("unsupported ref action %d", action);
332 +
333 + if (!dry_run) {
334 transaction = ref_store_transaction_begin(get_main_ref_store(revs->repo), 0, &err);
335 if (!transaction) {
336 ret = error(_("failed to begin ref transaction: %s"), err.buf);
337 goto out;
338 }
339 + }
340
324 - for (size_t i = 0; i < result.updates_nr; i++) {
325 - ret = ref_transaction_update(transaction,
326 - result.updates[i].refname,
327 - &result.updates[i].new_oid,
328 - &result.updates[i].old_oid,
329 - NULL, NULL, 0, reflog_msg, &err);
330 - if (ret) {
331 - ret = error(_("failed to update ref '%s': %s"),
332 - result.updates[i].refname, err.buf);
333 - goto out;
334 - }
341 + for (size_t i = 0; i < result.updates_nr; i++) {
342 + ret = handle_ref_update(transaction,
343 + result.updates[i].refname,
344 + &result.updates[i].new_oid,
345 + &result.updates[i].old_oid,
346 + reflog_msg, &err);
347 + if (ret) {
348 + ret = error(_("failed to update ref '%s': %s"),
349 + result.updates[i].refname, err.buf);
350 + goto out;
351 }
352 + }
353 +
354 + /*
355 + * `replay_revisions()` only updates references that are
356 + * ancestors of `rewritten`, so we need to manually
357 + * handle updating references that point to `original`.
358 + */
359 + for (decoration = get_name_decoration(&original->object);
360 + decoration;
361 + decoration = decoration->next)
362 + {
363 + if (decoration->type != DECORATION_REF_LOCAL &&
364 + decoration->type != DECORATION_REF_HEAD)
365 + continue;
366 +
367 + if (action == REF_ACTION_HEAD &&
368 + decoration->type != DECORATION_REF_HEAD)
369 + continue;
370
371 /*
338 - * `replay_revisions()` only updates references that are
339 - * ancestors of `rewritten`, so we need to manually
340 - * handle updating references that point to `original`.
372 + * We only need to update HEAD separately in case it's
373 + * detached. If it's not we'd already update the branch
374 + * it is pointing to.
375 */
342 - for (decoration = get_name_decoration(&original->object);
343 - decoration;
344 - decoration = decoration->next)
345 - {
346 - if (decoration->type != DECORATION_REF_LOCAL &&
347 - decoration->type != DECORATION_REF_HEAD)
348 - continue;
349 -
350 - if (action == REF_ACTION_HEAD &&
351 - decoration->type != DECORATION_REF_HEAD)
352 - continue;
353 -
354 - /*
355 - * We only need to update HEAD separately in case it's
356 - * detached. If it's not we'd already update the branch
357 - * it is pointing to.
358 - */
359 - if (action == REF_ACTION_BRANCHES &&
360 - decoration->type == DECORATION_REF_HEAD &&
361 - !detached_head)
362 - continue;
363 -
364 - ret = ref_transaction_update(transaction,
365 - decoration->name,
366 - &rewritten->object.oid,
367 - &original->object.oid,
368 - NULL, NULL, 0, reflog_msg, &err);
369 - if (ret) {
370 - ret = error(_("failed to update ref '%s': %s"),
371 - decoration->name, err.buf);
372 - goto out;
373 - }
374 - }
375 -
376 - if (ref_transaction_commit(transaction, &err)) {
377 - ret = error(_("failed to commit ref transaction: %s"), err.buf);
376 + if (action == REF_ACTION_BRANCHES &&
377 + decoration->type == DECORATION_REF_HEAD &&
378 + !detached_head)
379 + continue;
380 +
381 + ret = handle_ref_update(transaction,
382 + decoration->name,
383 + &rewritten->object.oid,
384 + &original->object.oid,
385 + reflog_msg, &err);
386 + if (ret) {
387 + ret = error(_("failed to update ref '%s': %s"),
388 + decoration->name, err.buf);
389 goto out;
390 }
391 + }
392
381 - break;
382 - case REF_ACTION_PRINT:
383 - for (size_t i = 0; i < result.updates_nr; i++)
384 - printf("update %s %s %s\n",
385 - result.updates[i].refname,
386 - oid_to_hex(&result.updates[i].new_oid),
387 - oid_to_hex(&result.updates[i].old_oid));
388 - break;
389 - default:
390 - BUG("unsupported ref action %d", action);
393 + if (transaction && ref_transaction_commit(transaction, &err)) {
394 + ret = error(_("failed to commit ref transaction: %s"), err.buf);
395 + goto out;
396 }
397
398 ret = 0;
@@ -409,10 +414,13 @@ static int cmd_history_reword(int argc,
414 NULL,
415 };
416 enum ref_action action = REF_ACTION_DEFAULT;
417 + int dry_run = 0;
418 struct option options[] = {
419 OPT_CALLBACK_F(0, "ref-action", &action, N_("<action>"),
414 - N_("control ref update behavior (branches|head|print)"),
420 + N_("control ref update behavior (branches|head)"),
421 PARSE_OPT_NONEG, parse_ref_action),
422 + OPT_BOOL('n', "dry-run", &dry_run,
423 + N_("perform a dry-run without updating any refs")),
424 OPT_END(),
425 };
426 struct strbuf reflog_msg = STRBUF_INIT;
@@ -449,7 +457,7 @@ static int cmd_history_reword(int argc,
457 strbuf_addf(&reflog_msg, "reword: updating %s", argv[0]);
458
459 ret = handle_reference_updates(&revs, action, original, rewritten,
452 - reflog_msg.buf);
460 + reflog_msg.buf, dry_run);
461 if (ret < 0) {
462 ret = error(_("failed replaying descendants"));
463 goto out;
t/t3451-history-reword.sh
+10 -2
@@ -221,7 +221,7 @@ test_expect_success 'can reword a merge commit' '
221 )
222 '
223
224 -test_expect_success '--ref-action=print prints ref updates without modifying repo' '
224 +test_expect_success '--dry-run prints ref updates without modifying repo' '
225 test_when_finished "rm -rf repo" &&
226 git init repo --initial-branch=main &&
227 (
@@ -233,7 +233,15 @@ test_expect_success '--ref-action=print prints ref updates without modifying rep
233 test_commit theirs &&
234
235 git refs list >refs-expect &&
236 - reword_with_message --ref-action=print base >updates <<-\EOF &&
236 + reword_with_message --dry-run --ref-action=head base >updates <<-\EOF &&
237 + reworded commit
238 + EOF
239 + git refs list >refs-actual &&
240 + test_cmp refs-expect refs-actual &&
241 + test_grep "update refs/heads/branch" updates &&
242 + test_grep ! "update refs/heads/main" updates &&
243 +
244 + reword_with_message --dry-run base >updates <<-\EOF &&
245 reworded commit
246 EOF
247 git refs list >refs-actual &&