* [PATCH] update-ref: add --rename option
@ 2026-06-09 21:35 Junio C Hamano
2026-06-10 21:28 ` [PATCH v2] " Junio C Hamano
2026-06-11 21:37 ` [PATCH v3] " Junio C Hamano
0 siblings, 2 replies; 5+ messages 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] 5+ messages in thread
* [PATCH v2] update-ref: add --rename option
2026-06-09 21:35 [PATCH] update-ref: add --rename option Junio C Hamano
@ 2026-06-10 21:28 ` Junio C Hamano
2026-06-11 13:05 ` Patrick Steinhardt
2026-06-11 21:37 ` [PATCH v3] " Junio C Hamano
1 sibling, 1 reply; 5+ messages in thread
From: Junio C Hamano @ 2026-06-10 21:28 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>
(even when used on a local branch ref, the current value and the
reflog of the ref are the only things that are renamed). As the
command is a low-level plumbing command, attempts to rename branches
are not warned, but we document it to draw attention of unsuspecting
users and protect them from burning themselves.
Because the "--stdin" mode wants to operate on its refs in a
reference transaction, and the API function refs_rename_ref() does
not work well as part of a transaction, it is currently not possible
to add a corresponding "rename" verb to the "--stdin" mode before
the underlying API learns to rename refs atomically inside a
transaction. It hence is left for a future refactoring.
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
* The initial draft I sent had a warning when the command is used
to rename local branches, but that is unusual for plumbing
commands that should do one thing it is designed for consistently
well without being chatty. This version only has words of warning
in the documentation.
Documentation/git-update-ref.adoc | 9 +++++++++
builtin/update-ref.c | 32 +++++++++++++++++++++++++++++--
t/t1400-update-ref.sh | 24 +++++++++++++++++++++++
3 files changed, 63 insertions(+), 2 deletions(-)
diff --git a/Documentation/git-update-ref.adoc b/Documentation/git-update-ref.adoc
index 37a5019a8b..0c27efaa52 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>. The command fails if <old-refname> does not exist, or
+if <new-refname> already exists. Because `git update-ref` does not
+update active worktree `HEAD` symbolic references or `.git/config`
+tracking settings when you rename a local branch in the `refs/heads/`
+hierarchy, think twice before using this command to rename a local
+branch (use `git branch -m` instead).
+
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..65ee8af08c 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,32 @@ 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 (!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..4330cad282 100755
--- a/t/t1400-update-ref.sh
+++ b/t/t1400-update-ref.sh
@@ -2455,4 +2455,28 @@ 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 moves old-refname and its reflog to new-refname' '
+ test_config core.logallrefupdates always &&
+ git update-ref -m "old tag" refs/tags/old-tag HEAD &&
+ git update-ref -m "to new" --rename refs/tags/old-tag refs/tags/new-tag 2>err &&
+ test_must_be_empty err &&
+ git show-ref --exists refs/tags/new-tag &&
+ test_must_fail git show-ref --exists refs/tags/old-tag &&
+ git log -g refs/tags/new-tag >output &&
+ test_grep "old tag" output &&
+ test_grep "to new" output
+'
+
test_done
Range-diff against v1:
1: 79af0ef7d1 ! 1: 00cd13fda7 update-ref: add --rename option
@@ Commit message
$ git update-ref --rename <old-refname> <new-refname>
- It renames <old-refname> together with its reflog to <new-refname>.
+ It renames <old-refname> together with its reflog to <new-refname>
+ (even when used on a local branch ref, the current value and the
+ reflog of the ref are the only things that are renamed). As the
+ command is a low-level plumbing command, attempts to rename branches
+ are not warned, but we document it to draw attention of unsuspecting
+ users and protect them from burning themselves.
- 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.
+ Because the "--stdin" mode wants to operate on its refs in a
+ reference transaction, and the API function refs_rename_ref() does
+ not work well as part of a transaction, it is currently not possible
+ to add a corresponding "rename" verb to the "--stdin" mode before
+ the underlying API learns to rename refs atomically inside a
+ transaction. It hence is left for a future refactoring.
Signed-off-by: Junio C Hamano <gitster@pobox.com>
@@ Documentation/git-update-ref.adoc: the result of following the symbolic pointers
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.
++<new-refname>. The command fails if <old-refname> does not exist, or
++if <new-refname> already exists. Because `git update-ref` does not
++update active worktree `HEAD` symbolic references or `.git/config`
++tracking settings when you rename a local branch in the `refs/heads/`
++hierarchy, think twice before using this command to rename a local
++branch (use `git branch -m` instead).
+
With `--stdin`, update-ref reads instructions from standard input and
performs all modifications together. Specify commands of the form:
@@ builtin/update-ref.c: int cmd_update_ref(int argc,
+ 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);
+
@@ t/t1400-update-ref.sh: test_expect_success 'dangling symref overwritten without
+ 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_config core.logallrefupdates always &&
++ git update-ref -m "old tag" refs/tags/old-tag HEAD &&
++ git update-ref -m "to new" --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
++ git show-ref --exists refs/tags/new-tag &&
++ test_must_fail git show-ref --exists refs/tags/old-tag &&
++ git log -g refs/tags/new-tag >output &&
++ test_grep "old tag" output &&
++ test_grep "to new" output
+'
+
test_done
--
2.54.0-615-g639a4a7340
^ permalink raw reply related [flat|nested] 5+ messages in thread
* Re: [PATCH v2] update-ref: add --rename option
2026-06-10 21:28 ` [PATCH v2] " Junio C Hamano
@ 2026-06-11 13:05 ` Patrick Steinhardt
2026-06-11 18:47 ` Junio C Hamano
0 siblings, 1 reply; 5+ messages in thread
From: Patrick Steinhardt @ 2026-06-11 13:05 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git
On Wed, Jun 10, 2026 at 02:28:00PM -0700, Junio C Hamano wrote:
> 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>
> (even when used on a local branch ref, the current value and the
> reflog of the ref are the only things that are renamed). As the
> command is a low-level plumbing command, attempts to rename branches
> are not warned, but we document it to draw attention of unsuspecting
> users and protect them from burning themselves.
This sentence reads a tiny bit awkward now and is probably an artifact
of the change between v1 and v2.
> Because the "--stdin" mode wants to operate on its refs in a
> reference transaction, and the API function refs_rename_ref() does
> not work well as part of a transaction, it is currently not possible
> to add a corresponding "rename" verb to the "--stdin" mode before
> the underlying API learns to rename refs atomically inside a
> transaction. It hence is left for a future refactoring.
Fair. I thought at times about extending reference transactions to also
fully support renames, but I never had a good use case where it would
really matter.
> * The initial draft I sent had a warning when the command is used
> to rename local branches, but that is unusual for plumbing
> commands that should do one thing it is designed for consistently
> well without being chatty. This version only has words of warning
> in the documentation.
Good, I just wanted to complain about that warning. I think it's good to
just accept this as-is. I really doubt that folks would go out of their
way to use git-update-ref(1) if all they want was to rename their
branch.
One thing that I'm missing from the commit message: what's the
motivation for this new mode?
> diff --git a/Documentation/git-update-ref.adoc b/Documentation/git-update-ref.adoc
> index 37a5019a8b..0c27efaa52 100644
> --- a/Documentation/git-update-ref.adoc
> +++ b/Documentation/git-update-ref.adoc
> @@ -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>. The command fails if <old-refname> does not exist, or
> +if <new-refname> already exists. Because `git update-ref` does not
> +update active worktree `HEAD` symbolic references or `.git/config`
> +tracking settings when you rename a local branch in the `refs/heads/`
> +hierarchy, think twice before using this command to rename a local
> +branch (use `git branch -m` instead).
I'd rephrase this slightly to first document behaviour and then draw the
conclusion that it shouldn't be used in many cases separately. For
example:
This command does not update any symbolic references pointing to
the renamed reference, and neither does it update `.git/config`
tracking settings. It is thus not recommended to use it for renaming
local branches. Use `git branch -m` instead.
> diff --git a/builtin/update-ref.c b/builtin/update-ref.c
> index 2d68c40ecb..65ee8af08c 100644
> --- a/builtin/update-ref.c
> +++ b/builtin/update-ref.c
> @@ -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';
Okay, so here we make it mutually exclusive with "--delete".
> @@ -800,6 +802,32 @@ 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);
And here with "-d". Good.
> + 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;
> + }
Hm. I think we're not using "--deref" / "--no-deref" at all, but we
document this flag as accepted in the synopsis.
Thanks!
Patrick
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v2] update-ref: add --rename option
2026-06-11 13:05 ` Patrick Steinhardt
@ 2026-06-11 18:47 ` Junio C Hamano
0 siblings, 0 replies; 5+ messages in thread
From: Junio C Hamano @ 2026-06-11 18:47 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
Patrick Steinhardt <ps@pks.im> writes:
> One thing that I'm missing from the commit message: what's the
> motivation for this new mode?
Maintenance of merge-fix database, a kludgy way to manage evil
merges that are needed to deal with inter-topic semantic crashes.
If you are really interested, see the appendix.
>> diff --git a/Documentation/git-update-ref.adoc b/Documentation/git-update-ref.adoc
>> index 37a5019a8b..0c27efaa52 100644
>> --- a/Documentation/git-update-ref.adoc
>> +++ b/Documentation/git-update-ref.adoc
>> @@ -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>. The command fails if <old-refname> does not exist, or
>> +if <new-refname> already exists. Because `git update-ref` does not
>> +update active worktree `HEAD` symbolic references or `.git/config`
>> +tracking settings when you rename a local branch in the `refs/heads/`
>> +hierarchy, think twice before using this command to rename a local
>> +branch (use `git branch -m` instead).
>
> I'd rephrase this slightly to first document behaviour and then draw the
> conclusion that it shouldn't be used in many cases separately. For
> example:
>
> This command does not update any symbolic references pointing to
> the renamed reference, and neither does it update `.git/config`
> tracking settings. It is thus not recommended to use it for renaming
> local branches. Use `git branch -m` instead.
Thanks, that is much better.
>> + 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;
>> + }
>
> Hm. I think we're not using "--deref" / "--no-deref" at all, but we
> document this flag as accepted in the synopsis.
Good point. refs_rename_ref() never derefs, right? We should drop
these two from the synopsis section.
[Appendix]
Often there are two topics, A and B, in flight that merging A into B
(or vice versa) requires changes more than the mechanical merge
needs. If this is a one-shot merge of A into B (or B into A), then
we can just record the evil merge and be done with it, but the same
issue arises if you are merging A into 'seen' first and then later
(possibly after merging other topics on top) B into 'seen'. The
merge of 'B' needs the same evil merge to resolve semantic
conflicts.
As those familiar with how 'seen' works in my tree, reapplying such
evil merges MUST BE automated, or the project will not work at all,
as 'seen' is rebuilt at least twice during the day, or even more
often.
So, what I do is, when I merge 'B' into 'seen' after merging 'A' and
possibly some other topics, I let the rerere database to record the
resolution of textual conflicts and make a commit. The tree
recorded in this commit will not work, due to semantic conflicts. I
create another commit on top of this merge to resolve the semantic
conflict to make the tree work.
Let's take ps/history-drop (A) and ps/setup-drop-global-state (B) as
an easy-to-understand example.
$ git checkout --detach ps/history-drop
$ git merge ps/setup-drop-global-state
This textually merges cleanly, but the result would not compile.
The history-drop added a new call to "is_bare_repository()", while
setup-drop-global-state added an extra parameter to the function.
So a merge-fix prepared on top of this "textually clean but does
not work" merge is created and looks something like this:
diff --git a/builtin/history.c b/builtin/history.c
index 65845e7359..eece221e63 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -1150,7 +1150,7 @@ static int cmd_history_drop(int argc,
* inconsistent repository state. So we first perform a dry-run merge
* here before updating refs.
*/
- if (!is_bare_repository()) {
+ if (!is_bare_repository(repo)) {
ret = find_head_tree_change(repo, &result, &old_head,
&new_head, &head_moves);
if (ret < 0)
And this commit (i.e. a commit on top of the mechanical/textual
merge result that adjusts the non-working merge result into workable
form) is pointed at by refs/merge-fix/ps/setup-drop-global-state.
Rebuilding 'seen' is driven by a script that takes a moral
equivalent of 'git log --first-parent --oneline --reverse
master..seen' and replays each merge on top of what is checked out
(to bootstrap, you would "git checkout -B seen master" and start
there). For each topic branch found in the input, the script
(1) skips if the topic has been merged and move on to the next
topic.
(2) runs "git merge" of the topic, taking resolution by the rerere
database. If this step leaves mechanical/textual conflicts,
the script stops and I'll hand resolve to update my rerere
database, and rerun the script (which will succeed the next
time).
(3) runs "git cherry-pick --no-commit merge-fix/$topic" if such a
ref exists, and if successfull, runs "git commit --amend".
That is how merging ps/setup-drop-global-state into 'seen' that has
already merged ps/history-drop would automatically get the right
evil merge to resolve semantic conflicts.
The renaming of update-ref becomes needed when the order of merging
topics into 'seen' changes. Ideally, these cherry-pickable commits
that are stored under refs/merge-fix hierarchies SHOULD be indexable
by a pair of topic (i.e. "when topic A and topic B first meets, apply
this evil merge"), but this computation is cumbersome to write, so
the above scheme has baked-in assumption that we know which topic
comes later. Once we start merging ps/setup-drop-global-state first
and then ps/history-drop next, we would need
$ git update-ref --rename \
refs/merge-fix/ps/setup-drop-global-state \
refs/merge-fix/ps/history-drop
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH v3] update-ref: add --rename option
2026-06-09 21:35 [PATCH] update-ref: add --rename option Junio C Hamano
2026-06-10 21:28 ` [PATCH v2] " Junio C Hamano
@ 2026-06-11 21:37 ` Junio C Hamano
1 sibling, 0 replies; 5+ messages in thread
From: Junio C Hamano @ 2026-06-11 21:37 UTC (permalink / raw)
To: git; +Cc: Patrick Steinhardt
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>;
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.
Because the "--stdin" mode wants to operate on its refs in a
reference transaction, and the API function refs_rename_ref() does
not work well as part of a transaction, it is currently not possible
to add a corresponding "rename" verb to the "--stdin" mode before
the underlying API learns to rename refs atomically inside a
transaction. It hence is left for a future refactoring.
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
* As a single patch topic, the range-diff relative to v2 is at the
end of the message.
- Simplified the proposed commit log message a bit.
- Dropped mention of --[no-]deref from the synopsis section.
- Reworded documentation with help from Patrick.
Documentation/git-update-ref.adoc | 9 +++++++++
builtin/update-ref.c | 32 +++++++++++++++++++++++++++++--
t/t1400-update-ref.sh | 24 +++++++++++++++++++++++
3 files changed, 63 insertions(+), 2 deletions(-)
diff --git a/Documentation/git-update-ref.adoc b/Documentation/git-update-ref.adoc
index 37a5019a8b..3b4df23a86 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>] --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>. The command fails if <old-refname> does not exist, or
+if <new-refname> already exists. The command does not update any
+symbolic references pointing to the renamed reference, and neither
+does it update `.git/config` tracking settings. It is thus not
+recommended to use it for renaming local branches. Use `git branch -m`
+instead.
+
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..65ee8af08c 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,32 @@ 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 (!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..4330cad282 100755
--- a/t/t1400-update-ref.sh
+++ b/t/t1400-update-ref.sh
@@ -2455,4 +2455,28 @@ 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 moves old-refname and its reflog to new-refname' '
+ test_config core.logallrefupdates always &&
+ git update-ref -m "old tag" refs/tags/old-tag HEAD &&
+ git update-ref -m "to new" --rename refs/tags/old-tag refs/tags/new-tag 2>err &&
+ test_must_be_empty err &&
+ git show-ref --exists refs/tags/new-tag &&
+ test_must_fail git show-ref --exists refs/tags/old-tag &&
+ git log -g refs/tags/new-tag >output &&
+ test_grep "old tag" output &&
+ test_grep "to new" output
+'
+
test_done
Range-diff against v2:
1: 00cd13fda7 ! 1: a54c2d4d68 update-ref: add --rename option
@@ Commit message
$ git update-ref --rename <old-refname> <new-refname>
- It renames <old-refname> together with its reflog to <new-refname>
- (even when used on a local branch ref, the current value and the
- reflog of the ref are the only things that are renamed). As the
- command is a low-level plumbing command, attempts to rename branches
- are not warned, but we document it to draw attention of unsuspecting
- users and protect them from burning themselves.
+ It renames <old-refname> together with its reflog to <new-refname>;
+ 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.
Because the "--stdin" mode wants to operate on its refs in a
reference transaction, and the API function refs_rename_ref() does
@@ Documentation/git-update-ref.adoc: 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>] --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]
@@ Documentation/git-update-ref.adoc: the result of following the symbolic pointers
+With `--rename`, it renames <old-refname> together with its reflog to
+<new-refname>. The command fails if <old-refname> does not exist, or
-+if <new-refname> already exists. Because `git update-ref` does not
-+update active worktree `HEAD` symbolic references or `.git/config`
-+tracking settings when you rename a local branch in the `refs/heads/`
-+hierarchy, think twice before using this command to rename a local
-+branch (use `git branch -m` instead).
++if <new-refname> already exists. The command does not update any
++symbolic references pointing to the renamed reference, and neither
++does it update `.git/config` tracking settings. It is thus not
++recommended to use it for renaming local branches. Use `git branch -m`
++instead.
+
With `--stdin`, update-ref reads instructions from standard input and
performs all modifications together. Specify commands of the form:
--
2.55.0-rc0-119-ga57a595f62
^ permalink raw reply related [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-06-11 21:37 UTC | newest]
Thread overview: 5+ messages (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
2026-06-10 21:28 ` [PATCH v2] " Junio C Hamano
2026-06-11 13:05 ` Patrick Steinhardt
2026-06-11 18:47 ` Junio C Hamano
2026-06-11 21:37 ` [PATCH v3] " 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