Git development
 help / color / mirror / Atom feed
* [PATCH] update-ref: add --rename option
@ 2026-06-09 21:35 Junio C Hamano
  0 siblings, 0 replies; only message in thread
From: Junio C Hamano @ 2026-06-09 21:35 UTC (permalink / raw)
  To: git

Add a "--rename" option to "git update-ref" with the syntax:

 $ git update-ref --rename <old-refname> <new-refname>

It renames <old-refname> together with its reflog to <new-refname>.

The command warns to use "git branch --rename" instead if old or new
refname falls inside refs/heads/. We issue this warning (but perform
the rename anyway) because "git update-ref" acts as a low-level
plumbing utility: unlike "git branch", it does not update active
worktree "HEAD" symbolic references or ".git/config" branch tracking
configuration.

Note that we do not add a corresponding "rename" verb to the "--stdin"
mode in this commit. While adding a "rename SP <oldref> SP <newref> LF"
command to the batch stream would be useful, operations in "--stdin"
mode are executed within an atomic "ref_transaction". Reference renaming
is currently implemented at the backend level via standalone, non-transactional
calls (e.g. "refs_rename_ref()"). Supporting renames in a batch transaction
would require extending "struct ref_transaction" and the reference storage
backend APIs to coordinate ref and reflog moves atomically, which is
left for a future refactoring.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---

 Documentation/git-update-ref.adoc |  9 ++++++++
 builtin/update-ref.c              | 36 +++++++++++++++++++++++++++++--
 t/t1400-update-ref.sh             | 29 +++++++++++++++++++++++++
 3 files changed, 72 insertions(+), 2 deletions(-)

diff --git a/Documentation/git-update-ref.adoc b/Documentation/git-update-ref.adoc
index 37a5019a8b..a622f2512b 100644
--- a/Documentation/git-update-ref.adoc
+++ b/Documentation/git-update-ref.adoc
@@ -9,6 +9,7 @@ SYNOPSIS
 --------
 [synopsis]
 git update-ref [-m <reason>] [--no-deref] -d <ref> [<old-oid>]
+git update-ref [-m <reason>] [--no-deref] --rename <old-refname> <new-refname>
 git update-ref [-m <reason>] [--no-deref] [--create-reflog] <ref> <new-oid> [<old-oid>]
 git update-ref [-m <reason>] [--no-deref] --stdin [-z] [--batch-updates]
 
@@ -39,6 +40,14 @@ the result of following the symbolic pointers.
 With `-d`, it deletes the named <ref> after verifying that it
 still contains <old-oid>.
 
+With `--rename`, it renames <old-refname> together with its reflog to
+<new-refname>. If either <old-refname> or <new-refname> falls inside
+`refs/heads/`, a warning will be issued to use `git branch --rename`
+instead, because `git update-ref` does not update active worktree
+`HEAD` symbolic references or `.git/config` tracking settings. It
+fails if <old-refname> does not exist, or if <new-refname> already
+exists.
+
 With `--stdin`, update-ref reads instructions from standard input and
 performs all modifications together.  Specify commands of the form:
 
diff --git a/builtin/update-ref.c b/builtin/update-ref.c
index 2d68c40ecb..1ae2dac6c5 100644
--- a/builtin/update-ref.c
+++ b/builtin/update-ref.c
@@ -15,6 +15,7 @@
 static const char * const git_update_ref_usage[] = {
 	N_("git update-ref [<options>] -d <refname> [<old-oid>]"),
 	N_("git update-ref [<options>]    <refname> <new-oid> [<old-oid>]"),
+	N_("git update-ref [<options>] --rename <old-refname> <new-refname>"),
 	N_("git update-ref [<options>] --stdin [-z] [--batch-updates]"),
 	NULL
 };
@@ -756,13 +757,14 @@ int cmd_update_ref(int argc,
 {
 	const char *refname, *oldval;
 	struct object_id oid, oldoid;
-	int delete = 0, no_deref = 0, read_stdin = 0, end_null = 0;
+	int delete = 0, rename = 0, no_deref = 0, read_stdin = 0, end_null = 0;
 	int create_reflog = 0;
 	unsigned int flags = 0;
 
 	struct option options[] = {
 		OPT_STRING( 'm', NULL, &msg, N_("reason"), N_("reason of the update")),
 		OPT_BOOL('d', NULL, &delete, N_("delete the reference")),
+		OPT_BOOL( 0 , "rename", &rename, N_("rename the reference")),
 		OPT_BOOL( 0 , "no-deref", &no_deref,
 					N_("update <refname> not the one it points to")),
 		OPT_BOOL('z', NULL, &end_null, N_("stdin has NUL-terminated arguments")),
@@ -787,7 +789,7 @@ int cmd_update_ref(int argc,
 	}
 
 	if (read_stdin) {
-		if (delete || argc > 0)
+		if (delete || rename || argc > 0)
 			usage_with_options(git_update_ref_usage, options);
 		if (end_null)
 			line_termination = '\0';
@@ -800,6 +802,36 @@ int cmd_update_ref(int argc,
 	if (end_null)
 		usage_with_options(git_update_ref_usage, options);
 
+	if (rename) {
+		const char *oldref, *newref;
+
+		if (delete || argc != 2)
+			usage_with_options(git_update_ref_usage, options);
+
+		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 (starts_with(oldref, "refs/heads/") ||
+		    starts_with(newref, "refs/heads/"))
+			warning(_("You may want 'git branch --rename' instead?"));
+
+		if (!refs_ref_exists(get_main_ref_store(the_repository), oldref))
+			die("no ref named '%s'", oldref);
+
+		if (refs_ref_exists(get_main_ref_store(the_repository), newref))
+			die("ref '%s' already exists", newref);
+
+		if (refs_rename_ref(get_main_ref_store(the_repository),
+				    oldref, newref, msg))
+			die("rename failed");
+		return 0;
+	}
+
 	if (delete) {
 		if (argc < 1 || argc > 2)
 			usage_with_options(git_update_ref_usage, options);
diff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh
index b2858a9061..fcb986d485 100755
--- a/t/t1400-update-ref.sh
+++ b/t/t1400-update-ref.sh
@@ -2455,4 +2455,33 @@ test_expect_success 'dangling symref overwritten without old oid' '
 	test_must_fail git rev-parse --verify refs/heads/does-not-exist
 '
 
+test_expect_success '--rename fails if old-refname does not exist' '
+	test_must_fail git update-ref --rename refs/tags/no-such-ref refs/tags/new-ref 2>err &&
+	test_grep "no ref named .refs/tags/no-such-ref." err
+'
+
+test_expect_success '--rename fails if new-refname does exist' '
+	git update-ref refs/tags/existing HEAD &&
+	git update-ref refs/tags/old-ref HEAD &&
+	test_must_fail git update-ref --rename refs/tags/old-ref refs/tags/existing 2>err &&
+	test_grep "ref .refs/tags/existing. already exists" err
+'
+
+test_expect_success '--rename warns if old or new refname falls inside refs/heads/' '
+	git update-ref refs/heads/old-branch HEAD &&
+	git update-ref --rename refs/heads/old-branch refs/heads/new-branch 2>err &&
+	test_grep "branch --rename. instead" err &&
+	git rev-parse --verify refs/heads/new-branch &&
+	test_must_fail git rev-parse --verify refs/heads/old-branch
+'
+
+test_expect_success '--rename moves old-refname and its reflog to new-refname' '
+	git update-ref -m "initial tag" refs/tags/old-tag HEAD &&
+	git update-ref --rename refs/tags/old-tag refs/tags/new-tag 2>err &&
+	test_must_be_empty err &&
+	git rev-parse --verify refs/tags/new-tag &&
+	test_must_fail git rev-parse --verify refs/tags/old-tag &&
+	git log -g refs/tags/new-tag
+'
+
 test_done
-- 
2.54.0-618-gdfae347457


^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2026-06-09 21:35 UTC | newest]

Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-09 21:35 [PATCH] update-ref: add --rename option Junio C Hamano

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox