git/list[1] front-page[2] threads[3] people[4] search[5] about
 

Re: [PATCH 3/4] builtin/history: replace "--ref-action=print" with "--dry-run"

From
D. Ben Knoble <ben.knoble@gmail.com>
Date
Feb 12, 2026, 22:20 UTC
Message-ID
<CALnO6CBterXWjecpRG7jTc4PaHDJLuY_FyrA0URC0TXBWSXgHQ@mail.gmail.com>
In-Reply-To
<20260212-b4-pks-history-dry-run-v1-3-1ce03d631c1b@pks.im>
On Thu, Feb 12, 2026 at 7:45 AM Patrick Steinhardt <ps@pks.im> wrote:
Show 64 quoted lines
>
> 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-actoin=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.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
>  Documentation/git-history.adoc |  12 ++--
>  builtin/history.c              | 150 ++++++++++++++++++++++-------------------
>  t/t3451-history-reword.sh      |  12 +++-
>  3 files changed, 96 insertions(+), 78 deletions(-)
>
> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> index 154e262b76..33353815cb 100644
> --- a/Documentation/git-history.adoc
> +++ b/Documentation/git-history.adoc
> @@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history
>  SYNOPSIS
>  --------
>  [synopsis]
> -git history reword <commit> [--ref-action=(branches|head|print)]
> +git history reword <commit> [--dry-run] [--ref-action=(branches|head)]
>
>  DESCRIPTION
>  -----------
> @@ -60,13 +60,15 @@ The following commands are available to rewrite history in different ways:
>  OPTIONS
>  -------
>
> -`--ref-action=(branches|head|print)`::
> +`--dry-run`::
> +       Do not update any references, but instead print any ref updates in a
> +       format that can be consumed by linkgit:git-update-ref[1].
> +
> +`--ref-action=(branches|head)`::
>         Control which references will be updated by the command, if any. With
>         `branches`, all local branches that point to commits which are
>         descendants of the original commit will be rewritten. With `head`, only
> -       the current `HEAD` reference will be rewritten. With `print`, all
> -       updates as they would be performed with `branches` are printed in a
> -       format that can be consumed by linkgit:git-update-ref[1].
> +       the current `HEAD` reference will be rewritten.

Unrelated to this series, I've just realized I can't tell from these docs what the default --ref-action is. The code says "branches."

Show 84 quoted lines
>
>  GIT
>  ---
> diff --git a/builtin/history.c b/builtin/history.c
> index ca0cdb6a58..40e2925cea 100644
> --- a/builtin/history.c
> +++ b/builtin/history.c
> @@ -18,7 +18,7 @@
>  #include "wt-status.h"
>
>  #define GIT_HISTORY_REWORD_USAGE \
> -       N_("git history reword <commit> [--ref-action=(branches|head|print)]")
> +       N_("git history reword <commit> [--dry-run] [--ref-action=(branches|head)]")
>
>  static void change_data_free(void *util, const char *str UNUSED)
>  {
> @@ -155,7 +155,6 @@ enum ref_action {
>         REF_ACTION_DEFAULT,
>         REF_ACTION_BRANCHES,
>         REF_ACTION_HEAD,
> -       REF_ACTION_PRINT,
>  };
>
>  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
>                 *action = REF_ACTION_BRANCHES;
>         } else if (!strcmp(value, "head")) {
>                 *action = REF_ACTION_HEAD;
> -       } else if (!strcmp(value, "print")) {
> -               *action = REF_ACTION_PRINT;
>         } else {
> -               return error(_("%s expects one of 'branches', 'head' or 'print'"),
> +               return error(_("%s expects one of 'branches' or 'head'"),
>                              opt->long_name);
>         }
>
> @@ -287,11 +284,29 @@ static int setup_revwalk(struct repository *repo,
>         return ret;
>  }
>
> +static int handle_ref_update(struct ref_transaction *transaction,
> +                            const char *refname,
> +                            const struct object_id *new_oid,
> +                            const struct object_id *old_oid,
> +                            const char *reflog_msg,
> +                            struct strbuf *err)
> +{
> +       if (!transaction) {
> +               printf("update %s %s %s\n",
> +                      refname, oid_to_hex(new_oid), oid_to_hex(old_oid));
> +               return 0;
> +       }
> +
> +       return ref_transaction_update(transaction, refname, new_oid, old_oid,
> +                                     NULL, NULL, 0, reflog_msg, err);
> +}
> +
>  static int handle_reference_updates(struct rev_info *revs,
>                                     enum ref_action action,
>                                     struct commit *original,
>                                     struct commit *rewritten,
> -                                   const char *reflog_msg)
> +                                   const char *reflog_msg,
> +                                   int dry_run)
>  {
>         const struct name_decoration *decoration;
>         struct replay_revisions_options opts = { 0 };
> @@ -313,82 +328,72 @@ static int handle_reference_updates(struct rev_info *revs,
>         if (ret)
>                 goto out;
>
> -       switch (action) {
> -       case REF_ACTION_BRANCHES:
> -       case REF_ACTION_HEAD:
> +       if (action != REF_ACTION_BRANCHES && action != REF_ACTION_HEAD)
> +               BUG("unsupported ref action %d", action);
> +
> +       if (!dry_run) {
>                 transaction = ref_store_transaction_begin(get_main_ref_store(revs->repo), 0, &err);
>                 if (!transaction) {
>                         ret = error(_("failed to begin ref transaction: %s"), err.buf);
>                         goto out;
>                 }
> +       }

It took me longer than I'd like to admit to realize that only initializing the transaction this way is safe, because we change the handler below:

Show 24 quoted lines
>
> -               for (size_t i = 0; i < result.updates_nr; i++) {
> -                       ret = ref_transaction_update(transaction,
> -                                                    result.updates[i].refname,
> -                                                    &result.updates[i].new_oid,
> -                                                    &result.updates[i].old_oid,
> -                                                    NULL, NULL, 0, reflog_msg, &err);
> -                       if (ret) {
> -                               ret = error(_("failed to update ref '%s': %s"),
> -                                           result.updates[i].refname, err.buf);
> -                               goto out;
> -                       }
> +       for (size_t i = 0; i < result.updates_nr; i++) {
> +               ret = handle_ref_update(transaction,
> +                                       result.updates[i].refname,
> +                                       &result.updates[i].new_oid,
> +                                       &result.updates[i].old_oid,
> +                                       reflog_msg, &err);
> +               if (ret) {
> +                       ret = error(_("failed to update ref '%s': %s"),
> +                                   result.updates[i].refname, err.buf);
> +                       goto out;
>                 }
> +       }

To use our new "NULL-safe transaction update" function. Phew. Using "-b" with git-diff/show/etc. helps quite a bit.

Show 93 quoted lines
> +
> +       /*
> +        * `replay_revisions()` only updates references that are
> +        * ancestors of `rewritten`, so we need to manually
> +        * handle updating references that point to `original`.
> +        */
> +       for (decoration = get_name_decoration(&original->object);
> +            decoration;
> +            decoration = decoration->next)
> +       {
> +               if (decoration->type != DECORATION_REF_LOCAL &&
> +                   decoration->type != DECORATION_REF_HEAD)
> +                       continue;
> +
> +               if (action == REF_ACTION_HEAD &&
> +                   decoration->type != DECORATION_REF_HEAD)
> +                       continue;
>
>                 /*
> -                * `replay_revisions()` only updates references that are
> -                * ancestors of `rewritten`, so we need to manually
> -                * handle updating references that point to `original`.
> +                * We only need to update HEAD separately in case it's
> +                * detached. If it's not we'd already update the branch
> +                * it is pointing to.
>                  */
> -               for (decoration = get_name_decoration(&original->object);
> -                    decoration;
> -                    decoration = decoration->next)
> -               {
> -                       if (decoration->type != DECORATION_REF_LOCAL &&
> -                           decoration->type != DECORATION_REF_HEAD)
> -                               continue;
> -
> -                       if (action == REF_ACTION_HEAD &&
> -                           decoration->type != DECORATION_REF_HEAD)
> -                               continue;
> -
> -                       /*
> -                        * We only need to update HEAD separately in case it's
> -                        * detached. If it's not we'd already update the branch
> -                        * it is pointing to.
> -                        */
> -                       if (action == REF_ACTION_BRANCHES &&
> -                           decoration->type == DECORATION_REF_HEAD &&
> -                           !detached_head)
> -                               continue;
> -
> -                       ret = ref_transaction_update(transaction,
> -                                                    decoration->name,
> -                                                    &rewritten->object.oid,
> -                                                    &original->object.oid,
> -                                                    NULL, NULL, 0, reflog_msg, &err);
> -                       if (ret) {
> -                               ret = error(_("failed to update ref '%s': %s"),
> -                                           decoration->name, err.buf);
> -                               goto out;
> -                       }
> -               }
> -
> -               if (ref_transaction_commit(transaction, &err)) {
> -                       ret = error(_("failed to commit ref transaction: %s"), err.buf);
> +               if (action == REF_ACTION_BRANCHES &&
> +                   decoration->type == DECORATION_REF_HEAD &&
> +                   !detached_head)
> +                       continue;
> +
> +               ret = handle_ref_update(transaction,
> +                                       decoration->name,
> +                                       &rewritten->object.oid,
> +                                       &original->object.oid,
> +                                       reflog_msg, &err);
> +               if (ret) {
> +                       ret = error(_("failed to update ref '%s': %s"),
> +                                   decoration->name, err.buf);
>                         goto out;
>                 }
> +       }
>
> -               break;
> -       case REF_ACTION_PRINT:
> -               for (size_t i = 0; i < result.updates_nr; i++)
> -                       printf("update %s %s %s\n",
> -                              result.updates[i].refname,
> -                              oid_to_hex(&result.updates[i].new_oid),
> -                              oid_to_hex(&result.updates[i].old_oid));
> -               break;
> -       default:
> -               BUG("unsupported ref action %d", action);
> +       if (transaction && ref_transaction_commit(transaction, &err)) {
> +               ret = error(_("failed to commit ref transaction: %s"), err.buf);
> +               goto out;
>         }

All effectively whitespace change, except for the new transaction check, which is sensible.

Show 61 quoted lines
>
>         ret = 0;
> @@ -410,10 +415,13 @@ static int cmd_history_reword(int argc,
>                 NULL,
>         };
>         enum ref_action action = REF_ACTION_DEFAULT;
> +       int dry_run = 0;
>         struct option options[] = {
>                 OPT_CALLBACK_F(0, "ref-action", &action, N_("<action>"),
> -                              N_("control ref update behavior (branches|head|print)"),
> +                              N_("control ref update behavior (branches|head)"),
>                                PARSE_OPT_NONEG, parse_ref_action),
> +               OPT_BOOL('n', "dry-run", &dry_run,
> +                        N_("perform a dry-run without updating any refs")),
>                 OPT_END(),
>         };
>         struct strbuf reflog_msg = STRBUF_INIT;
> @@ -450,7 +458,7 @@ static int cmd_history_reword(int argc,
>         strbuf_addf(&reflog_msg, "reword: updating %s", argv[0]);
>
>         ret = handle_reference_updates(&revs, action, original, rewritten,
> -                                      reflog_msg.buf);
> +                                      reflog_msg.buf, dry_run);
>         if (ret < 0) {
>                 ret = error(_("failed replaying descendants"));
>                 goto out;
> diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
> index 12a9a7d051..702d40dc06 100755
> --- a/t/t3451-history-reword.sh
> +++ b/t/t3451-history-reword.sh
> @@ -221,7 +221,7 @@ test_expect_success 'can reword a merge commit' '
>         )
>  '
>
> -test_expect_success '--ref-action=print prints ref updates without modifying repo' '
> +test_expect_success '--dry-run prints ref updates without modifying repo' '
>         test_when_finished "rm -rf repo" &&
>         git init repo --initial-branch=main &&
>         (
> @@ -233,7 +233,15 @@ test_expect_success '--ref-action=print prints ref updates without modifying rep
>                 test_commit theirs &&
>
>                 git refs list >refs-expect &&
> -               reword_with_message --ref-action=print base >updates <<-\EOF &&
> +               reword_with_message --dry-run --ref-action=head base >updates <<-\EOF &&
> +               reworded commit
> +               EOF
> +               git refs list >refs-actual &&
> +               test_cmp refs-expect refs-actual &&
> +               test_grep "update refs/heads/branch" updates &&
> +               test_grep ! "update refs/heads/main" updates &&
> +
> +               reword_with_message --dry-run base >updates <<-\EOF &&
>                 reworded commit
>                 EOF
>                 git refs list >refs-actual &&
>
> --
> 2.53.0.295.g64333814d3.dirty
>
>

-- D. Ben Knoble

Previous: Patrick SteinhardtNext: Patrick Steinhardt
Message 15 of 34 in “builtin/history: some smaller UI improvements”
  1. 0/4 builtin/history: some smaller UI improvementsPatrick Steinhardt, Feb 12, 2026
  2. 1/4 builtin/history: perform revwalk checks before asking for user inputPatrick Steinhardt, Feb 12, 2026
  3. Junio C HamanoFeb 12, 2026
  4. Patrick SteinhardtFeb 13, 2026
  5. Junio C HamanoFeb 13, 2026
  6. 2/4 builtin/history: check for merges before asking for user inputPatrick Steinhardt, Feb 12, 2026
  7. D. Ben KnobleFeb 12, 2026
  8. Junio C HamanoFeb 12, 2026
  9. Patrick SteinhardtFeb 13, 2026
  10. Patrick SteinhardtFeb 13, 2026
  11. Ben KnobleFeb 13, 2026
  12. 3/4 builtin/history: replace "--ref-action=print" with "--dry-run"Patrick Steinhardt, Feb 12, 2026
  13. Junio C HamanoFeb 12, 2026
  14. Patrick SteinhardtFeb 13, 2026
  15. D. Ben KnobleFeb 12, 2026
  16. Patrick SteinhardtFeb 13, 2026
  17. 4/4 builtin/history: rename "--ref-action=" to "--update-refs="Patrick Steinhardt, Feb 12, 2026
  18. 0/5 builtin/history: some smaller UI improvementsPatrick Steinhardt, Feb 13, 2026
  19. 1/5 builtin/history: perform revwalk checks before asking for user inputPatrick Steinhardt, Feb 13, 2026
  20. Junio C HamanoFeb 13, 2026
  21. 2/5 builtin/history: check for merges before asking for user inputPatrick Steinhardt, Feb 13, 2026
  22. 3/5 builtin/history: replace "--ref-action=print" with "--dry-run"Patrick Steinhardt, Feb 13, 2026
  23. Kristoffer HaugsbakkFeb 13, 2026
  24. Patrick SteinhardtFeb 16, 2026
  25. Kristoffer HaugsbakkFeb 18, 2026
  26. 4/5 builtin/history: rename "--ref-action=" to "--update-refs="Patrick Steinhardt, Feb 13, 2026
  27. 5/5 Documentation/git-history: document default for "--update-refs="Patrick Steinhardt, Feb 13, 2026
  28. Junio C HamanoFeb 13, 2026
  29. 0/5 builtin/history: some smaller UI improvementsPatrick Steinhardt, Feb 16, 2026
  30. 1/5 builtin/history: perform revwalk checks before asking for user inputPatrick Steinhardt, Feb 16, 2026
  31. 2/5 builtin/history: check for merges before asking for user inputPatrick Steinhardt, Feb 16, 2026
  32. 3/5 builtin/history: replace "--ref-action=print" with "--dry-run"Patrick Steinhardt, Feb 16, 2026
  33. 4/5 builtin/history: rename "--ref-action=" to "--update-refs="Patrick Steinhardt, Feb 16, 2026
  34. 5/5 Documentation/git-history: document default for "--update-refs="Patrick Steinhardt, Feb 16, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.