[PATCH 4/4] builtin/refs: add "rename" subcommand
From: Patrick Steinhardt <hidden>
Date: 2026-06-16 08:44:28
Subsystem:
documentation, the rest · Maintainers:
Jonathan Corbet, Linus Torvalds
Add a "rename" subcommand to git-refs(1) with the syntax: $ git refs rename <oldref> <newref> It renames <oldref> together with its reflog to <newref>; even when used on a local branch ref, the current value and the reflog of the ref are the only things that are renamed. Document it and redirect casual users to "git branch -m" if that is what they wanted to do. Co-authored-by: Junio C Hamano [off-list ref] Signed-off-by: Patrick Steinhardt <redacted> --- Documentation/git-refs.adoc | 10 ++++ builtin/refs.c | 42 ++++++++++++++ t/meson.build | 1 + t/t1466-refs-rename.sh | 131 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+)
diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc
index 0a887cf5e5..85eb100205 100644
--- a/Documentation/git-refs.adoc
+++ b/Documentation/git-refs.adoc@@ -22,6 +22,7 @@ git refs exists <ref> git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>] git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>] git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>] +git refs rename [--message=<reason>] <oldref> <newref> DESCRIPTION -----------
@@ -65,6 +66,11 @@ update:: `<old-value>` is given, the reference is only updated after verifying that it currently contains `<old-value>`. +rename:: + Rename the reference `<oldref>` to `<newref>`. The old reference must + exist and the new reference must not yet exist, and both must have a + well-formed name (see linkgit:git-check-ref-format[1]). + OPTIONS -------
@@ -106,6 +112,10 @@ include::pack-refs-options.adoc[] The following options are specific to commands which write references: +`--create-reflog`:: + Create a reflog for the reference even if one would not ordinarily be + created. + `--message=<reason>`:: Use the given <reason> string for the reflog entry associated with the update. An empty message is rejected.
diff --git a/builtin/refs.c b/builtin/refs.c
index 3238ddf3f0..b90baf5633 100644
--- a/builtin/refs.c
+++ b/builtin/refs.c@@ -27,6 +27,9 @@ #define REFS_UPDATE_USAGE \ N_("git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]") +#define REFS_RENAME_USAGE \ + N_("git refs rename [--message=<reason>] <oldref> <newref>") + static int cmd_refs_migrate(int argc, const char **argv, const char *prefix, struct repository *repo) {
@@ -267,6 +270,43 @@ static int cmd_refs_update(int argc, const char **argv, const char *prefix, UPDATE_REFS_DIE_ON_ERR); } +static int cmd_refs_rename(int argc, const char **argv, const char *prefix, + struct repository *repo) +{ + static char const * const refs_rename_usage[] = { + REFS_RENAME_USAGE, + NULL + }; + const char *message = NULL; + struct option opts[] = { + OPT_STRING(0, "message", &message, N_("reason"), + N_("reason of the update")), + OPT_END(), + }; + const char *oldref, *newref; + + argc = parse_options(argc, argv, prefix, opts, refs_rename_usage, 0); + if (argc != 2) + usage(_("rename requires old and new reference name")); + if (message && !*message) + die(_("refusing to perform update with empty message")); + + oldref = argv[0]; + newref = argv[1]; + + if (check_refname_format(oldref, 0)) + die(_("invalid ref format: %s"), oldref); + if (check_refname_format(newref, 0)) + die(_("invalid ref format: %s"), newref); + + if (!refs_ref_exists(get_main_ref_store(repo), oldref)) + die(_("reference does not exist: '%s'"), oldref); + if (refs_ref_exists(get_main_ref_store(repo), newref)) + die(_("reference already exists: '%s'"), newref); + + return refs_rename_ref(get_main_ref_store(repo), oldref, newref, message); +} + int cmd_refs(int argc, const char **argv, const char *prefix,
@@ -280,6 +320,7 @@ int cmd_refs(int argc, REFS_OPTIMIZE_USAGE, REFS_DELETE_USAGE, REFS_UPDATE_USAGE, + REFS_RENAME_USAGE, NULL, }; parse_opt_subcommand_fn *fn = NULL;
@@ -291,6 +332,7 @@ int cmd_refs(int argc, OPT_SUBCOMMAND("optimize", &fn, cmd_refs_optimize), OPT_SUBCOMMAND("delete", &fn, cmd_refs_delete), OPT_SUBCOMMAND("update", &fn, cmd_refs_update), + OPT_SUBCOMMAND("rename", &fn, cmd_refs_rename), OPT_END(), };
diff --git a/t/meson.build b/t/meson.build
index 2063962dab..a1a6880fe6 100644
--- a/t/meson.build
+++ b/t/meson.build@@ -225,6 +225,7 @@ integration_tests = [ 't1463-refs-optimize.sh', 't1464-refs-delete.sh', 't1465-refs-update.sh', + 't1466-refs-rename.sh', 't1500-rev-parse.sh', 't1501-work-tree.sh', 't1502-rev-parse-parseopt.sh',
diff --git a/t/t1466-refs-rename.sh b/t/t1466-refs-rename.sh
new file mode 100755
index 0000000000..f80d58e0f4
--- /dev/null
+++ b/t/t1466-refs-rename.sh@@ -0,0 +1,131 @@ +#!/bin/sh + +test_description='git refs rename' + +. ./test-lib.sh + +setup_repo () { + git init "$1" && + test_commit -C "$1" A && + test_commit -C "$1" B +} + +test_ref_matches () { + git rev-parse "$1" >expect && + echo "$2" >actual && + test_cmp expect actual +} + +test_expect_success 'rename an existing reference' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git refs update refs/heads/foo $A && + git refs rename refs/heads/foo refs/heads/bar && + test_must_fail git refs exists refs/heads/foo && + test_ref_matches refs/heads/bar $A + ) +' + +test_expect_success 'rename moves the reflog along with the reference' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git refs update --message="rename me" refs/heads/foo $A && + git refs rename refs/heads/foo refs/heads/bar && + git reflog show refs/heads/bar >reflog && + test_grep "rename me" reflog && + test_must_fail git reflog exists refs/heads/foo + ) +' + +test_expect_success 'rename with message records reason in reflog' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git refs update refs/heads/foo $A && + git refs rename --message="rename reason" refs/heads/foo refs/heads/bar && + git reflog show refs/heads/bar >actual && + test_grep "rename reason" actual + ) +' + +test_expect_success 'rename a nonexistent reference fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + test_must_fail git refs rename refs/heads/foo refs/heads/bar 2>err && + test_grep "reference does not exist" err + ) +' + +test_expect_success 'rename to an existing reference fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + B=$(git rev-parse B) && + git refs update refs/heads/foo $A && + git refs update refs/heads/bar $B && + test_must_fail git refs rename refs/heads/foo refs/heads/bar 2>err && + test_grep "reference already exists" err + ) +' + +test_expect_success 'rename with empty message fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git refs update refs/heads/foo $A && + test_must_fail git refs rename --message= refs/heads/foo refs/heads/bar 2>err && + test_grep "empty message" err + ) +' + +test_expect_success 'rename with invalid old reference name fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + test_must_fail git refs rename "refs/heads/foo..bar" refs/heads/bar 2>err && + test_grep "invalid ref format" err + ) +' + +test_expect_success 'rename with invalid new reference name fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git refs update refs/heads/foo $A && + test_must_fail git refs rename refs/heads/foo "refs/heads/bar..baz" 2>err && + test_grep "invalid ref format" err + ) +' + +test_expect_success 'rename with too few arguments fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + test_must_fail git -C repo refs rename refs/heads/foo 2>err && + test_grep "requires old and new reference name" err +' + +test_expect_success 'rename with too many arguments fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + test_must_fail git -C repo refs rename refs/heads/foo refs/heads/bar refs/heads/baz 2>err && + test_grep "requires old and new reference name" err +' + +test_done
--
2.55.0.rc0.786.g65d90a0328.dirty