* [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; 9+ 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] 9+ 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; 9+ 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] 9+ 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; 9+ 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] 9+ 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 2026-06-12 6:04 ` Patrick Steinhardt 2026-06-12 16:31 ` Junio C Hamano 0 siblings, 2 replies; 9+ 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] 9+ messages in thread
* Re: [PATCH v2] update-ref: add --rename option 2026-06-11 18:47 ` Junio C Hamano @ 2026-06-12 6:04 ` Patrick Steinhardt 2026-06-12 16:31 ` Junio C Hamano 1 sibling, 0 replies; 9+ messages in thread From: Patrick Steinhardt @ 2026-06-12 6:04 UTC (permalink / raw) To: Junio C Hamano; +Cc: git On Thu, Jun 11, 2026 at 11:47:16AM -0700, Junio C Hamano wrote: > 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. Thanks for the explanation! Patrick ^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2] update-ref: add --rename option 2026-06-11 18:47 ` Junio C Hamano 2026-06-12 6:04 ` Patrick Steinhardt @ 2026-06-12 16:31 ` Junio C Hamano 1 sibling, 0 replies; 9+ messages in thread From: Junio C Hamano @ 2026-06-12 16:31 UTC (permalink / raw) To: Patrick Steinhardt; +Cc: git Junio C Hamano <gitster@pobox.com> writes: > [Appendix] > ... 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. One scheme might be to use "refs/merge-fix/$A--$B" to store the interaction between topic $A and topic $B, with the convention that no topic is named with double-dash in its name. We sequencially merge these in-flight topic into the integration branch (e.g., 'seen'). When merging topic X, we roughly would need to do the following. (0) Skip if X is already in the integration branch. (1) See what topic Y that would also be merged for the first time to the integration branch. This is because a complex topic X often is done by merging in-flight Y into then-current master and applyng patches on top, and depending on the state of the integration branch, such topic Y may or may not have already been mergeed there. Enumerate all these topic Ys that would be pulled into the integration branch as a side effect of merging X. (2) Enumerate all merge-fix refs that has any of the topic Ys or X. For each such refs/merge-fix/$A--$B (where either $A or $B is X or one of Ys), call the other side of "--" Z. If Z is already in the integration branch, then we found the merge-fix we need to apply. The "topic X might pull other topics that haven't been merged together with it when it gets merged" is what makes it cumbersome to write. If we do not have to worry about it, it would be fairly straight forward, but then the bulk-merge driver probably needs to learn a safety check to make sure at the point of merging each topic that the merge is not pulling another topic (base) into as a side effect. It would mean that you prepare a new topic X on top of a merge of Y into 'master', and X can never be merged into 'seen' before Y is. Which may or may not be what we really want, and I need to think about it a bit. ^ permalink raw reply [flat|nested] 9+ 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 2026-06-12 6:00 ` Patrick Steinhardt 1 sibling, 1 reply; 9+ 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] 9+ messages in thread
* Re: [PATCH v3] update-ref: add --rename option 2026-06-11 21:37 ` [PATCH v3] " Junio C Hamano @ 2026-06-12 6:00 ` Patrick Steinhardt 2026-06-12 15:41 ` Junio C Hamano 0 siblings, 1 reply; 9+ messages in thread From: Patrick Steinhardt @ 2026-06-12 6:00 UTC (permalink / raw) To: Junio C Hamano; +Cc: git On Thu, Jun 11, 2026 at 02:37:53PM -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. Document it > and redirect casual users to "git branch -m" if that is what they > wanted to do. This reads much better, thanks. > 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] This slightly triggers my OCD, but oh, well. No need to change this. > 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 > @@ -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); Arguably, we should also complain when either "--no-deref" or "--deref" were given, as they don't have any effect. A slight tangent: this is part of why I really don't like commands that determine their mode via flags: you now have to worry about every combination of flags and whether they even make sense. With subcommands we at least only have to worry about the set of flags that directly apply to that given subcommand. Makes me wonder whether I should have a look at extending git-refs(1) further: git refs delete <ref> [<oldvalue>] git refs update <ref> <newvalue> [<oldvalue>] git refs rename <ref> <oldname> <newname> I always wanted to do this eventually so that we have one top-level command that knows how to do "everything refs". Anyway, except for this nit the patch looks good to me, thanks! Patrick ^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v3] update-ref: add --rename option 2026-06-12 6:00 ` Patrick Steinhardt @ 2026-06-12 15:41 ` Junio C Hamano 0 siblings, 0 replies; 9+ messages in thread From: Junio C Hamano @ 2026-06-12 15:41 UTC (permalink / raw) To: Patrick Steinhardt; +Cc: git Patrick Steinhardt <ps@pks.im> writes: > A slight tangent: this is part of why I really don't like commands that > determine their mode via flags: you now have to worry about every > combination of flags and whether they even make sense. With subcommands > we at least only have to worry about the set of flags that directly > apply to that given subcommand. > > Makes me wonder whether I should have a look at extending git-refs(1) > further: > > git refs delete <ref> [<oldvalue>] > git refs update <ref> <newvalue> [<oldvalue>] > git refs rename <ref> <oldname> <newname> > > I always wanted to do this eventually so that we have one top-level > command that knows how to do "everything refs". That may indeed be a better direction to go, but isn't update-ref the "everything refs" command already? ^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-06-12 16:31 UTC | newest] Thread overview: 9+ 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-12 6:04 ` Patrick Steinhardt 2026-06-12 16:31 ` Junio C Hamano 2026-06-11 21:37 ` [PATCH v3] " Junio C Hamano 2026-06-12 6:00 ` Patrick Steinhardt 2026-06-12 15:41 ` 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