{"thread":{"id":"65819","subject":"[PATCH 0/4] builtin/refs: add ability to write references","startedAt":"2026-06-16T08:44:19Z","lastAt":"2026-07-09T10:05:58Z","messageCount":47,"participants":["Patrick Steinhardt","Junio C Hamano","Toon Claes"],"isPatch":true,"patchVersion":1,"patchTotal":4},"messages":[{"id":"545633","messageId":"20260616-pks-refs-writing-subcommands-v1-0-9f5219b6109d@pks.im","threadId":"65819","inReplyTo":null,"subject":"[PATCH 0/4] builtin/refs: add ability to write references","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-16T08:44:05Z","receivedAt":"2026-06-16T08:44:19Z","isPatch":true,"body":"Hi,\n\nReference-related functionality in Git is currently spread across many\ndifferent commands: git-update-ref(1), git-for-each-ref(1),\ngit-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it\nhard for users to discover what functionality we have available to work\nwith references.\n\nWe have thus started to consolidate this functionality into git-refs(1),\nwhich is a toolbox of everything related to references. Until now, the\ncommand doesn't handle functionality of git-update-ref(1).\n\nThis patch series backfills most of the functionality by introducing\nthree new commands:\n\n  - `git refs delete` to delete references. This is the equivalent of\n    `git update-ref -d`.\n\n  - `git refs update` to update references. This is the equivalent of\n    `git update-ref <refname> <oldvalue> <newvalue>`.\n\n  - `git refs rename` to rename a reference, including its reflog. This\n    does not have an equivalent in git-update-ref(1), but is inspired by\n    and supersedes [1].\n\nThanks!\n\nPatrick\n\n[1]: <xmqqv7brz9ba.fsf@gitster.g>\n\n---\nPatrick Steinhardt (4):\n      builtin/refs: drop `the_repository`\n      builtin/refs: add \"delete\" subcommand\n      builtin/refs: add \"update\" subcommand\n      builtin/refs: add \"rename\" subcommand\n\n Documentation/git-refs.adoc |  34 +++++++++\n builtin/refs.c              | 153 +++++++++++++++++++++++++++++++++++--\n t/meson.build               |   3 +\n t/t1464-refs-delete.sh      | 133 ++++++++++++++++++++++++++++++++\n t/t1465-refs-update.sh      | 179 ++++++++++++++++++++++++++++++++++++++++++++\n t/t1466-refs-rename.sh      | 131 ++++++++++++++++++++++++++++++++\n 6 files changed, 625 insertions(+), 8 deletions(-)\n\n\n---\nbase-commit: 700432b2ba22603a0bcb71475c9c333d17c9b0d1\nchange-id: 20260616-pks-refs-writing-subcommands-7a77be5bda9b\n\n"},{"id":"545634","messageId":"20260616-pks-refs-writing-subcommands-v1-1-9f5219b6109d@pks.im","threadId":"65819","inReplyTo":"20260616-pks-refs-writing-subcommands-v1-0-9f5219b6109d@pks.im","subject":"[PATCH 1/4] builtin/refs: drop `the_repository`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-16T08:44:06Z","receivedAt":"2026-06-16T08:44:21Z","isPatch":true,"body":"We still have a couple of uses of `the_repository` in \"builtin/refs.c\".\nAll of those are trivial to convert though as the command always\nrequires a repository to exist.\n\nConvert them to use the passed-in repository and drop\n`USE_THE_REPOSITORY_VARIABLE`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/refs.c | 15 +++++++--------\n 1 file changed, 7 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/refs.c b/builtin/refs.c\nindex e3125bc61b..f0faabf45a 100644\n--- a/builtin/refs.c\n+++ b/builtin/refs.c\n@@ -1,4 +1,3 @@\n-#define USE_THE_REPOSITORY_VARIABLE\n #include \"builtin.h\"\n #include \"config.h\"\n #include \"fsck.h\"\n@@ -23,7 +22,7 @@\n \tN_(\"git refs optimize \" PACK_REFS_OPTS)\n \n static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,\n-\t\t\t    struct repository *repo UNUSED)\n+\t\t\t    struct repository *repo)\n {\n \tconst char * const migrate_usage[] = {\n \t\tREFS_MIGRATE_USAGE,\n@@ -59,13 +58,13 @@ static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,\n \t\tgoto out;\n \t}\n \n-\tif (the_repository->ref_storage_format == format) {\n+\tif (repo->ref_storage_format == format) {\n \t\terr = error(_(\"repository already uses '%s' format\"),\n \t\t\t    ref_storage_format_to_name(format));\n \t\tgoto out;\n \t}\n \n-\tif (repo_migrate_ref_storage_format(the_repository, format, flags, &errbuf) < 0) {\n+\tif (repo_migrate_ref_storage_format(repo, format, flags, &errbuf) < 0) {\n \t\terr = error(\"%s\", errbuf.buf);\n \t\tgoto out;\n \t}\n@@ -99,8 +98,8 @@ static int cmd_refs_verify(int argc, const char **argv, const char *prefix,\n \tif (argc)\n \t\tusage(_(\"'git refs verify' takes no arguments\"));\n \n-\trepo_config(the_repository, git_fsck_config, &fsck_refs_options);\n-\tprepare_repo_settings(the_repository);\n+\trepo_config(repo, git_fsck_config, &fsck_refs_options);\n+\tprepare_repo_settings(repo);\n \n \tworktrees = get_worktrees_without_reading_head();\n \tfor (size_t i = 0; worktrees[i]; i++)\n@@ -124,7 +123,7 @@ static int cmd_refs_list(int argc, const char **argv, const char *prefix,\n }\n \n static int cmd_refs_exists(int argc, const char **argv, const char *prefix,\n-\t\t\t   struct repository *repo UNUSED)\n+\t\t\t   struct repository *repo)\n {\n \tstruct strbuf unused_referent = STRBUF_INIT;\n \tstruct object_id unused_oid;\n@@ -145,7 +144,7 @@ static int cmd_refs_exists(int argc, const char **argv, const char *prefix,\n \t\tdie(_(\"'git refs exists' requires a reference\"));\n \n \tref = *argv++;\n-\tif (refs_read_raw_ref(get_main_ref_store(the_repository), ref,\n+\tif (refs_read_raw_ref(get_main_ref_store(repo), ref,\n \t\t\t      &unused_oid, &unused_referent, &unused_type,\n \t\t\t      &failure_errno)) {\n \t\tif (failure_errno == ENOENT || failure_errno == EISDIR) {\n\n-- \n2.55.0.rc0.786.g65d90a0328.dirty\n\n"},{"id":"545635","messageId":"20260616-pks-refs-writing-subcommands-v1-2-9f5219b6109d@pks.im","threadId":"65819","inReplyTo":"20260616-pks-refs-writing-subcommands-v1-0-9f5219b6109d@pks.im","subject":"[PATCH 2/4] builtin/refs: add \"delete\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-16T08:44:07Z","receivedAt":"2026-06-16T08:44:23Z","isPatch":true,"body":"Reference-related functionality in Git is currently spread across many\ndifferent commands: git-update-ref(1), git-for-each-ref(1),\ngit-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it\nhard for users to discover what functionality we have available to work\nwith references.\n\nWe have thus started to consolidate this functionality into git-refs(1),\nwhich is a toolbox of everything related to references. Until now, the\ncommand doesn't handle functionality of git-update-ref(1).\n\nFix this gap by introducing a new \"delete\" subcommand, which is the\nequivalent of `git update-ref -d`.\n\nNote that we're intentionally not using a generic \"write\" subcommand\nwith a \"-d\" flag. This is rather harder to discover, and subcommands\nthat are implmented as flags tend to be hard to reason about in the code\nas we'd have to handle mutually-exclusive flags that stem from the other\nsubcommand-like modes.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-refs.adoc |  17 ++++++\n builtin/refs.c              |  46 +++++++++++++++\n t/meson.build               |   1 +\n t/t1464-refs-delete.sh      | 133 ++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 197 insertions(+)\n\ndiff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc\nindex fa33680cc7..c03e8e6ac3 100644\n--- a/Documentation/git-refs.adoc\n+++ b/Documentation/git-refs.adoc\n@@ -20,6 +20,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]\n \t\t   [ --stdin | (<pattern>...)]\n git refs exists <ref>\n git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]\n+git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]\n \n DESCRIPTION\n -----------\n@@ -51,6 +52,12 @@ optimize::\n \tusage. This subcommand is an alias for linkgit:git-pack-refs[1] and\n \toffers identical functionality.\n \n+delete::\n+\tDelete the given reference. This subcommand mirrors `git update-ref -d`\n+\t(see linkgit:git-update-ref[1]). When `<oldvalue>` is given, the\n+\treference is only deleted after verifying that it currently contains\n+\t`<oldvalue>`.\n+\n OPTIONS\n -------\n \n@@ -90,6 +97,16 @@ The following options are specific to 'git refs optimize':\n \n include::pack-refs-options.adoc[]\n \n+The following options are specific to commands which write references:\n+\n+`--message=<reason>`::\n+\tUse the given <reason> string for the reflog entry associated with the\n+\tupdate. An empty message is rejected.\n+\n+`--no-deref`::\n+\tOperate on <ref> itself rather than the reference it points to via a\n+\tsymbolic ref.\n+\n KNOWN LIMITATIONS\n -----------------\n \ndiff --git a/builtin/refs.c b/builtin/refs.c\nindex f0faabf45a..69eb528522 100644\n--- a/builtin/refs.c\n+++ b/builtin/refs.c\n@@ -21,6 +21,9 @@\n #define REFS_OPTIMIZE_USAGE \\\n \tN_(\"git refs optimize \" PACK_REFS_OPTS)\n \n+#define REFS_DELETE_USAGE \\\n+\tN_(\"git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]\")\n+\n static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,\n \t\t\t    struct repository *repo)\n {\n@@ -175,6 +178,47 @@ static int cmd_refs_optimize(int argc, const char **argv, const char *prefix,\n \treturn pack_refs_core(argc, argv, prefix, repo, refs_optimize_usage);\n }\n \n+static int cmd_refs_delete(int argc, const char **argv, const char *prefix,\n+\t\t\t   struct repository *repo)\n+{\n+\tstatic char const * const refs_delete_usage[] = {\n+\t\tREFS_DELETE_USAGE,\n+\t\tNULL\n+\t};\n+\tconst char *message = NULL;\n+\tunsigned flags = 0;\n+\tstruct option opts[] = {\n+\t\tOPT_STRING(0, \"message\", &message, N_(\"reason\"),\n+\t\t\t   N_(\"reason of the update\")),\n+\t\tOPT_BIT(0 ,\"no-deref\", &flags,\n+\t\t\tN_(\"update <refname> not the one it points to\"),\n+\t\t\tREF_NO_DEREF),\n+\t\tOPT_END(),\n+\t};\n+\tstruct object_id oldoid;\n+\tconst char *refname;\n+\n+\targc = parse_options(argc, argv, prefix, opts, refs_delete_usage, 0);\n+\tif (argc < 1 || argc > 2)\n+\t\tusage(_(\"delete requires reference name and an optional old object ID\"));\n+\n+\tif (message && !*message)\n+\t\tdie(_(\"refusing to perform update with empty message\"));\n+\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\trefname = argv[0];\n+\tif (argc == 2) {\n+\t\tif (repo_get_oid_with_flags(repo, argv[1], &oldoid, GET_OID_SKIP_AMBIGUITY_CHECK))\n+\t\t\tdie(_(\"invalid old object ID: '%s'\"), argv[1]);\n+\t\tif (is_null_oid(&oldoid))\n+\t\t\tdie(_(\"cannot delete object with null old object ID\"));\n+\t}\n+\n+\treturn refs_delete_ref(get_main_ref_store(repo), message, refname,\n+\t\t\t       argc == 2 ? &oldoid : NULL, flags);\n+}\n+\n int cmd_refs(int argc,\n \t     const char **argv,\n \t     const char *prefix,\n@@ -186,6 +230,7 @@ int cmd_refs(int argc,\n \t\t\"git refs list \" COMMON_USAGE_FOR_EACH_REF,\n \t\tREFS_EXISTS_USAGE,\n \t\tREFS_OPTIMIZE_USAGE,\n+\t\tREFS_DELETE_USAGE,\n \t\tNULL,\n \t};\n \tparse_opt_subcommand_fn *fn = NULL;\n@@ -195,6 +240,7 @@ int cmd_refs(int argc,\n \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_refs_list),\n \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_refs_exists),\n \t\tOPT_SUBCOMMAND(\"optimize\", &fn, cmd_refs_optimize),\n+\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_refs_delete),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex c5832fee05..1ccf08a3b5 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -223,6 +223,7 @@ integration_tests = [\n   't1461-refs-list.sh',\n   't1462-refs-exists.sh',\n   't1463-refs-optimize.sh',\n+  't1464-refs-delete.sh',\n   't1500-rev-parse.sh',\n   't1501-work-tree.sh',\n   't1502-rev-parse-parseopt.sh',\ndiff --git a/t/t1464-refs-delete.sh b/t/t1464-refs-delete.sh\nnew file mode 100755\nindex 0000000000..4a36d3866b\n--- /dev/null\n+++ b/t/t1464-refs-delete.sh\n@@ -0,0 +1,133 @@\n+#!/bin/sh\n+\n+test_description='git refs delete'\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+\n+setup_repo () {\n+\tgit init \"$1\" &&\n+\ttest_commit -C \"$1\" A &&\n+\ttest_commit -C \"$1\" B\n+}\n+\n+test_expect_success 'delete without oldvalue verification' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\tA=$(git -C repo rev-parse A) &&\n+\tgit -C repo update-ref refs/heads/foo $A &&\n+\tgit -C repo refs delete refs/heads/foo &&\n+\ttest_must_fail git -C repo show-ref --verify -q refs/heads/foo\n+'\n+\n+test_expect_success 'delete with matching oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\tgit refs delete refs/heads/foo $A &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete with stale oldvalue fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\ttest_must_fail git refs delete refs/heads/foo $B 2>err &&\n+\t\ttest_grep \" but expected \" err &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete with null oldvalue fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\ttest_must_fail git refs delete refs/heads/foo $ZERO_OID 2>err &&\n+\t\ttest_grep \"null old object ID\" err &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete with invalid oldvalue fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\ttest_must_fail git refs delete refs/heads/foo invalid-oid 2>err &&\n+\t\ttest_grep \"invalid old object ID\" err &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete symref with --no-deref leaves target intact' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/foo &&\n+\t\tgit refs delete --no-deref refs/heads/symref &&\n+\t\ttest_must_fail git refs exists refs/heads/symref &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete with message records reason in reflog' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\tgit symbolic-ref HEAD refs/heads/foo &&\n+\t\tgit refs delete --message=delete-reason refs/heads/foo &&\n+\t\ttest_must_fail git refs exists refs/heads/foo &&\n+\t\ttest-tool ref-store main for-each-reflog-ent HEAD >actual &&\n+\t\ttest_grep \"delete-reason$\" actual\n+\t)\n+'\n+\n+test_expect_success 'delete with empty message fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\ttest_must_fail git refs delete --message= refs/heads/foo 2>err &&\n+\t\ttest_grep \"empty message\" err &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete without arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git -C repo refs delete 2>err &&\n+\ttest_grep \"requires reference name\" err\n+'\n+\n+test_expect_success 'delete with too many arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git refs delete one two three 2>err &&\n+\ttest_grep \"requires reference name\" err\n+'\n+\n+test_done\n\n-- \n2.55.0.rc0.786.g65d90a0328.dirty\n\n"},{"id":"545636","messageId":"20260616-pks-refs-writing-subcommands-v1-3-9f5219b6109d@pks.im","threadId":"65819","inReplyTo":"20260616-pks-refs-writing-subcommands-v1-0-9f5219b6109d@pks.im","subject":"[PATCH 3/4] builtin/refs: add \"update\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-16T08:44:08Z","receivedAt":"2026-06-16T08:44:25Z","isPatch":true,"body":"Add a new \"update\" subcommand which mirrors `git update-ref <refname>\n<oldoid> <newoid>`. This follows the same reasoning as the preceding\ncommit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-refs.adoc |   7 ++\n builtin/refs.c              |  50 +++++++++++++\n t/meson.build               |   1 +\n t/t1465-refs-update.sh      | 179 ++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 237 insertions(+)\n\ndiff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc\nindex c03e8e6ac3..0a887cf5e5 100644\n--- a/Documentation/git-refs.adoc\n+++ b/Documentation/git-refs.adoc\n@@ -21,6 +21,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]\n git refs exists <ref>\n git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]\n git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]\n+git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\n \n DESCRIPTION\n -----------\n@@ -58,6 +59,12 @@ delete::\n \treference is only deleted after verifying that it currently contains\n \t`<oldvalue>`.\n \n+update::\n+\tUpdate the given reference to point at `<new-value>`. This subcommand\n+\tmirrors `git update-ref` (see linkgit:git-update-ref[1]). When\n+\t`<old-value>` is given, the reference is only updated after verifying\n+\tthat it currently contains `<old-value>`.\n+\n OPTIONS\n -------\n \ndiff --git a/builtin/refs.c b/builtin/refs.c\nindex 69eb528522..3238ddf3f0 100644\n--- a/builtin/refs.c\n+++ b/builtin/refs.c\n@@ -24,6 +24,9 @@\n #define REFS_DELETE_USAGE \\\n \tN_(\"git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]\")\n \n+#define REFS_UPDATE_USAGE \\\n+\tN_(\"git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\")\n+\n static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,\n \t\t\t    struct repository *repo)\n {\n@@ -219,6 +222,51 @@ static int cmd_refs_delete(int argc, const char **argv, const char *prefix,\n \t\t\t       argc == 2 ? &oldoid : NULL, flags);\n }\n \n+static int cmd_refs_update(int argc, const char **argv, const char *prefix,\n+\t\t\t   struct repository *repo)\n+{\n+\tstatic char const * const refs_update_usage[] = {\n+\t\tREFS_UPDATE_USAGE,\n+\t\tNULL\n+\t};\n+\tconst char *message = NULL;\n+\tunsigned flags = 0;\n+\tstruct option opts[] = {\n+\t\tOPT_STRING(0, \"message\", &message, N_(\"reason\"),\n+\t\t\t   N_(\"reason of the update\")),\n+\t\tOPT_BIT(0 ,\"no-deref\", &flags,\n+\t\t\tN_(\"update <refname> not the one it points to\"),\n+\t\t\tREF_NO_DEREF),\n+\t\tOPT_BIT(0, \"create-reflog\", &flags, N_(\"create a reflog\"),\n+\t\t\tREF_FORCE_CREATE_REFLOG),\n+\t\tOPT_END(),\n+\t};\n+\tstruct object_id newoid, oldoid;\n+\tconst char *refname;\n+\n+\targc = parse_options(argc, argv, prefix, opts, refs_update_usage, 0);\n+\tif (argc < 2 || argc > 3)\n+\t\tusage(_(\"update requires reference name, new value and an optional old value\"));\n+\n+\tif (message && !*message)\n+\t\tdie(_(\"refusing to perform update with empty message\"));\n+\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\trefname = argv[0];\n+\tif (repo_get_oid_with_flags(repo, argv[1], &newoid,\n+\t\t\t\t    GET_OID_SKIP_AMBIGUITY_CHECK))\n+\t\tdie(_(\"invalid new object ID: %s\"), argv[1]);\n+\tif (argc == 3 &&\n+\t    repo_get_oid_with_flags(repo, argv[2], &oldoid,\n+\t\t\t\t    GET_OID_SKIP_AMBIGUITY_CHECK))\n+\t\tdie(_(\"invalid old object ID: %s\"), argv[2]);\n+\n+\treturn refs_update_ref(get_main_ref_store(repo), message, refname,\n+\t\t\t       &newoid, argc == 3 ? &oldoid : NULL, flags,\n+\t\t\t       UPDATE_REFS_DIE_ON_ERR);\n+}\n+\n int cmd_refs(int argc,\n \t     const char **argv,\n \t     const char *prefix,\n@@ -231,6 +279,7 @@ int cmd_refs(int argc,\n \t\tREFS_EXISTS_USAGE,\n \t\tREFS_OPTIMIZE_USAGE,\n \t\tREFS_DELETE_USAGE,\n+\t\tREFS_UPDATE_USAGE,\n \t\tNULL,\n \t};\n \tparse_opt_subcommand_fn *fn = NULL;\n@@ -241,6 +290,7 @@ int cmd_refs(int argc,\n \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_refs_exists),\n \t\tOPT_SUBCOMMAND(\"optimize\", &fn, cmd_refs_optimize),\n \t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_refs_delete),\n+\t\tOPT_SUBCOMMAND(\"update\", &fn, cmd_refs_update),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex 1ccf08a3b5..2063962dab 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -224,6 +224,7 @@ integration_tests = [\n   't1462-refs-exists.sh',\n   't1463-refs-optimize.sh',\n   't1464-refs-delete.sh',\n+  't1465-refs-update.sh',\n   't1500-rev-parse.sh',\n   't1501-work-tree.sh',\n   't1502-rev-parse-parseopt.sh',\ndiff --git a/t/t1465-refs-update.sh b/t/t1465-refs-update.sh\nnew file mode 100755\nindex 0000000000..e7582a6195\n--- /dev/null\n+++ b/t/t1465-refs-update.sh\n@@ -0,0 +1,179 @@\n+#!/bin/sh\n+\n+test_description='git refs update'\n+\n+. ./test-lib.sh\n+\n+setup_repo () {\n+\tgit init \"$1\" &&\n+\ttest_commit -C \"$1\" A &&\n+\ttest_commit -C \"$1\" B\n+}\n+\n+test_ref_matches () {\n+\tgit rev-parse \"$1\" >expect &&\n+\techo \"$2\" >actual &&\n+\ttest_cmp expect actual\n+}\n+\n+test_expect_success 'update creates a new reference' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_ref_matches refs/heads/foo \"$A\"\n+\t)\n+'\n+\n+test_expect_success 'update an existing reference without oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs update refs/heads/foo $B &&\n+\t\ttest_ref_matches refs/heads/foo $B\n+\t)\n+'\n+\n+test_expect_success 'update with matching oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs update refs/heads/foo $B $A &&\n+\t\ttest_ref_matches refs/heads/foo $B\n+\t)\n+'\n+\n+test_expect_success 'update with stale oldvalue fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs update refs/heads/foo $B $B 2>err &&\n+\t\ttest_grep \" but expected \" err &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+test_expect_success 'update with invalid new value fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git refs update refs/heads/foo invalid-oid 2>err &&\n+\t\ttest_grep \"invalid new object ID\" err &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'update with invalid old value fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs update refs/heads/foo $B invalid-oid 2>err &&\n+\t\ttest_grep \"invalid old object ID\" err &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+test_expect_success 'update --no-deref rewrites the symref itself' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/foo &&\n+\t\tgit refs update --no-deref refs/heads/symref $B &&\n+\t\ttest_must_fail git symbolic-ref refs/heads/symref &&\n+\t\ttest_ref_matches refs/heads/symref $B &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+test_expect_success 'update does not create a reflog by default' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/foo $A &&\n+\t\ttest_must_fail git reflog exists refs/foo\n+\t)\n+'\n+\n+test_expect_success 'update creates a reflog with --create-reflog' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update --create-reflog refs/foo $A &&\n+\t\tgit reflog exists refs/foo\n+\t)\n+'\n+\n+test_expect_success 'update with message records reason in reflog' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs update --message=update-reason refs/heads/foo $B &&\n+\t\tgit reflog show refs/heads/foo >actual &&\n+\t\ttest_grep \"update-reason$\" actual\n+\t)\n+'\n+\n+test_expect_success 'update with empty message fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs update --message= refs/heads/foo $B 2>err &&\n+\t\ttest_grep \"empty message\" err\n+\t)\n+'\n+\n+test_expect_success 'update with too few arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git -C repo refs update refs/heads/foo 2>err &&\n+\ttest_grep \"requires reference name, new value\" err\n+'\n+\n+test_expect_success 'update with too many arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\ttest_must_fail git refs update refs/heads/foo $A $B extra 2>err &&\n+\t\ttest_grep \"requires reference name, new value\" err\n+\t)\n+'\n+\n+test_done\n\n-- \n2.55.0.rc0.786.g65d90a0328.dirty\n\n"},{"id":"545637","messageId":"20260616-pks-refs-writing-subcommands-v1-4-9f5219b6109d@pks.im","threadId":"65819","inReplyTo":"20260616-pks-refs-writing-subcommands-v1-0-9f5219b6109d@pks.im","subject":"[PATCH 4/4] builtin/refs: add \"rename\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-16T08:44:09Z","receivedAt":"2026-06-16T08:44:28Z","isPatch":true,"body":"Add a \"rename\" subcommand to git-refs(1) with the syntax:\n\n  $ git refs rename <oldref> <newref>\n\nIt renames <oldref> together with its reflog to <newref>; even when used\non a local branch ref, the current value and the reflog of the ref are\nthe only things that are renamed. Document it and redirect casual users\nto \"git branch -m\" if that is what they wanted to do.\n\nCo-authored-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-refs.adoc |  10 ++++\n builtin/refs.c              |  42 ++++++++++++++\n t/meson.build               |   1 +\n t/t1466-refs-rename.sh      | 131 ++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 184 insertions(+)\n\ndiff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc\nindex 0a887cf5e5..85eb100205 100644\n--- a/Documentation/git-refs.adoc\n+++ b/Documentation/git-refs.adoc\n@@ -22,6 +22,7 @@ git refs exists <ref>\n git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]\n git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]\n git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\n+git refs rename [--message=<reason>] <oldref> <newref>\n \n DESCRIPTION\n -----------\n@@ -65,6 +66,11 @@ update::\n \t`<old-value>` is given, the reference is only updated after verifying\n \tthat it currently contains `<old-value>`.\n \n+rename::\n+\tRename the reference `<oldref>` to `<newref>`. The old reference must\n+\texist and the new reference must not yet exist, and both must have a\n+\twell-formed name (see linkgit:git-check-ref-format[1]).\n+\n OPTIONS\n -------\n \n@@ -106,6 +112,10 @@ include::pack-refs-options.adoc[]\n \n The following options are specific to commands which write references:\n \n+`--create-reflog`::\n+\tCreate a reflog for the reference even if one would not ordinarily be\n+\tcreated.\n+\n `--message=<reason>`::\n \tUse the given <reason> string for the reflog entry associated with the\n \tupdate. An empty message is rejected.\ndiff --git a/builtin/refs.c b/builtin/refs.c\nindex 3238ddf3f0..b90baf5633 100644\n--- a/builtin/refs.c\n+++ b/builtin/refs.c\n@@ -27,6 +27,9 @@\n #define REFS_UPDATE_USAGE \\\n \tN_(\"git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\")\n \n+#define REFS_RENAME_USAGE \\\n+\tN_(\"git refs rename [--message=<reason>] <oldref> <newref>\")\n+\n static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,\n \t\t\t    struct repository *repo)\n {\n@@ -267,6 +270,43 @@ static int cmd_refs_update(int argc, const char **argv, const char *prefix,\n \t\t\t       UPDATE_REFS_DIE_ON_ERR);\n }\n \n+static int cmd_refs_rename(int argc, const char **argv, const char *prefix,\n+\t\t\t   struct repository *repo)\n+{\n+\tstatic char const * const refs_rename_usage[] = {\n+\t\tREFS_RENAME_USAGE,\n+\t\tNULL\n+\t};\n+\tconst char *message = NULL;\n+\tstruct option opts[] = {\n+\t\tOPT_STRING(0, \"message\", &message, N_(\"reason\"),\n+\t\t\t   N_(\"reason of the update\")),\n+\t\tOPT_END(),\n+\t};\n+\tconst char *oldref, *newref;\n+\n+\targc = parse_options(argc, argv, prefix, opts, refs_rename_usage, 0);\n+\tif (argc != 2)\n+\t\tusage(_(\"rename requires old and new reference name\"));\n+\tif (message && !*message)\n+\t\tdie(_(\"refusing to perform update with empty message\"));\n+\n+\toldref = argv[0];\n+\tnewref = argv[1];\n+\n+\tif (check_refname_format(oldref, 0))\n+\t\tdie(_(\"invalid ref format: %s\"), oldref);\n+\tif (check_refname_format(newref, 0))\n+\t\tdie(_(\"invalid ref format: %s\"), newref);\n+\n+\tif (!refs_ref_exists(get_main_ref_store(repo), oldref))\n+\t\tdie(_(\"reference does not exist: '%s'\"), oldref);\n+\tif (refs_ref_exists(get_main_ref_store(repo), newref))\n+\t\tdie(_(\"reference already exists: '%s'\"), newref);\n+\n+\treturn refs_rename_ref(get_main_ref_store(repo), oldref, newref, message);\n+}\n+\n int cmd_refs(int argc,\n \t     const char **argv,\n \t     const char *prefix,\n@@ -280,6 +320,7 @@ int cmd_refs(int argc,\n \t\tREFS_OPTIMIZE_USAGE,\n \t\tREFS_DELETE_USAGE,\n \t\tREFS_UPDATE_USAGE,\n+\t\tREFS_RENAME_USAGE,\n \t\tNULL,\n \t};\n \tparse_opt_subcommand_fn *fn = NULL;\n@@ -291,6 +332,7 @@ int cmd_refs(int argc,\n \t\tOPT_SUBCOMMAND(\"optimize\", &fn, cmd_refs_optimize),\n \t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_refs_delete),\n \t\tOPT_SUBCOMMAND(\"update\", &fn, cmd_refs_update),\n+\t\tOPT_SUBCOMMAND(\"rename\", &fn, cmd_refs_rename),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex 2063962dab..a1a6880fe6 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -225,6 +225,7 @@ integration_tests = [\n   't1463-refs-optimize.sh',\n   't1464-refs-delete.sh',\n   't1465-refs-update.sh',\n+  't1466-refs-rename.sh',\n   't1500-rev-parse.sh',\n   't1501-work-tree.sh',\n   't1502-rev-parse-parseopt.sh',\ndiff --git a/t/t1466-refs-rename.sh b/t/t1466-refs-rename.sh\nnew file mode 100755\nindex 0000000000..f80d58e0f4\n--- /dev/null\n+++ b/t/t1466-refs-rename.sh\n@@ -0,0 +1,131 @@\n+#!/bin/sh\n+\n+test_description='git refs rename'\n+\n+. ./test-lib.sh\n+\n+setup_repo () {\n+\tgit init \"$1\" &&\n+\ttest_commit -C \"$1\" A &&\n+\ttest_commit -C \"$1\" B\n+}\n+\n+test_ref_matches () {\n+\tgit rev-parse \"$1\" >expect &&\n+\techo \"$2\" >actual &&\n+\ttest_cmp expect actual\n+}\n+\n+test_expect_success 'rename an existing reference' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs rename refs/heads/foo refs/heads/bar &&\n+\t\ttest_must_fail git refs exists refs/heads/foo &&\n+\t\ttest_ref_matches refs/heads/bar $A\n+\t)\n+'\n+\n+test_expect_success 'rename moves the reflog along with the reference' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update --message=\"rename me\" refs/heads/foo $A &&\n+\t\tgit refs rename refs/heads/foo refs/heads/bar &&\n+\t\tgit reflog show refs/heads/bar >reflog &&\n+\t\ttest_grep \"rename me\" reflog &&\n+\t\ttest_must_fail git reflog exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'rename with message records reason in reflog' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs rename --message=\"rename reason\" refs/heads/foo refs/heads/bar &&\n+\t\tgit reflog show refs/heads/bar >actual &&\n+\t\ttest_grep \"rename reason\" actual\n+\t)\n+'\n+\n+test_expect_success 'rename a nonexistent reference fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git refs rename refs/heads/foo refs/heads/bar 2>err &&\n+\t\ttest_grep \"reference does not exist\" err\n+\t)\n+'\n+\n+test_expect_success 'rename to an existing reference fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs update refs/heads/bar $B &&\n+\t\ttest_must_fail git refs rename refs/heads/foo refs/heads/bar 2>err &&\n+\t\ttest_grep \"reference already exists\" err\n+\t)\n+'\n+\n+test_expect_success 'rename with empty message fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs rename --message= refs/heads/foo refs/heads/bar 2>err &&\n+\t\ttest_grep \"empty message\" err\n+\t)\n+'\n+\n+test_expect_success 'rename with invalid old reference name fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git refs rename \"refs/heads/foo..bar\" refs/heads/bar 2>err &&\n+\t\ttest_grep \"invalid ref format\" err\n+\t)\n+'\n+\n+test_expect_success 'rename with invalid new reference name fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs rename refs/heads/foo \"refs/heads/bar..baz\" 2>err &&\n+\t\ttest_grep \"invalid ref format\" err\n+\t)\n+'\n+\n+test_expect_success 'rename with too few arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git -C repo refs rename refs/heads/foo 2>err &&\n+\ttest_grep \"requires old and new reference name\" err\n+'\n+\n+test_expect_success 'rename with too many arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git -C repo refs rename refs/heads/foo refs/heads/bar refs/heads/baz 2>err &&\n+\ttest_grep \"requires old and new reference name\" err\n+'\n+\n+test_done\n\n-- \n2.55.0.rc0.786.g65d90a0328.dirty\n\n"},{"id":"545649","messageId":"xmqqeci6bupk.fsf@gitster.g","threadId":"65819","inReplyTo":"20260616-pks-refs-writing-subcommands-v1-3-9f5219b6109d@pks.im","subject":"Re: [PATCH 3/4] builtin/refs: add \"update\" subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-16T11:17:27Z","receivedAt":"2026-06-16T11:17:30Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Add a new \"update\" subcommand which mirrors `git update-ref <refname>\n> <oldoid> <newoid>`. This follows the same reasoning as the preceding\n> commit.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Documentation/git-refs.adoc |   7 ++\n>  builtin/refs.c              |  50 +++++++++++++\n>  t/meson.build               |   1 +\n>  t/t1465-refs-update.sh      | 179 ++++++++++++++++++++++++++++++++++++++++++++\n>  4 files changed, 237 insertions(+)\n\nI do not offhand know (and I am still away by 2 hours from the time\nI wake up and start functioning) if update-ref shares the same\nissue, but with \"delete, update, rename\" combo, lack of \"create\"\nfeels a bit annoying.  Wouldn't we want to offer an option to users\nwho want to ensure that the refs they create are truly new and they\nare not overwriting a ref somebody has created?  Either (1) drop\n\"delete\" and take a special value (e.g. \"\") as <newvalue> to signal\ndeletion and make the same special value used as <oldvalue> signals\ncreation, or (2) add \"create\" and insist that \"update\" takes only an\nexisting ref, would make the annoyance go away, I guess.\n\n> +test_expect_success 'update creates a new reference' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\tA=$(git rev-parse A) &&\n> +\t\tgit refs update refs/heads/foo $A &&\n> +\t\ttest_ref_matches refs/heads/foo \"$A\"\n> +\t)\n> +'\n\nHere we cannot test (and I strongly suspect that \"git refs update\"\nand \"git update-ref\" lack ability to do so) a case where a creation\nis attempted on an existing ref and fails.\n"},{"id":"545668","messageId":"xmqqse6m4jw4.fsf@gitster.g","threadId":"65819","inReplyTo":"20260616-pks-refs-writing-subcommands-v1-3-9f5219b6109d@pks.im","subject":"Re: [PATCH 3/4] builtin/refs: add \"update\" subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-16T14:52:59Z","receivedAt":"2026-06-16T14:53:02Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n>  git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]\n> +git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\n\n\"<old-value> vs <new-value>\" is good, we should update \"delete\" to\nuse \"<old-value>\" to match.\n\n\n>  DESCRIPTION\n>  -----------\n> @@ -58,6 +59,12 @@ delete::\n>  \treference is only deleted after verifying that it currently contains\n>  \t`<oldvalue>`.\n>  \n> +update::\n> +\tUpdate the given reference to point at `<new-value>`. This subcommand\n> +\tmirrors `git update-ref` (see linkgit:git-update-ref[1]). When\n> +\t`<old-value>` is given, the reference is only updated after verifying\n> +\tthat it currently contains `<old-value>`.\n\nAs to the lack of \"create\", among the two potential changes, I have\na slight preference for adding \"create\" and failing \"update\" that\ndoes not refer to an existing ref.  If we go that route, the\n\"--create-reflog\" option should move to \"create\", as \"update\" will\nnever be used to create a new ref.\n\n\n> +\tif (repo_get_oid_with_flags(repo, argv[1], &newoid,\n> +\t\t\t\t    GET_OID_SKIP_AMBIGUITY_CHECK))\n> +\t\tdie(_(\"invalid new object ID: %s\"), argv[1]);\n> +\tif (argc == 3 &&\n> +\t    repo_get_oid_with_flags(repo, argv[2], &oldoid,\n> +\t\t\t\t    GET_OID_SKIP_AMBIGUITY_CHECK))\n> +\t\tdie(_(\"invalid old object ID: %s\"), argv[2]);\n\nOn the \"delete\" side, these messages quote the object name, i.e.,\n\n\t\t\tdie(_(\"invalid old object ID: '%s'\"), argv[1]);\n\nWe should be consistent.\n"},{"id":"545669","messageId":"xmqqo6ha4jw1.fsf@gitster.g","threadId":"65819","inReplyTo":"20260616-pks-refs-writing-subcommands-v1-4-9f5219b6109d@pks.im","subject":"Re: [PATCH 4/4] builtin/refs: add \"rename\" subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-16T14:53:02Z","receivedAt":"2026-06-16T14:53:04Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> +static int cmd_refs_rename(int argc, const char **argv, const char *prefix,\n> +\t\t\t   struct repository *repo)\n> +{\n> +\tstatic char const * const refs_rename_usage[] = {\n> +\t\tREFS_RENAME_USAGE,\n> +\t\tNULL\n> +\t};\n> +\tconst char *message = NULL;\n> +\tstruct option opts[] = {\n> +\t\tOPT_STRING(0, \"message\", &message, N_(\"reason\"),\n> +\t\t\t   N_(\"reason of the update\")),\n> +\t\tOPT_END(),\n> +\t};\n> +\tconst char *oldref, *newref;\n> +\n> +\targc = parse_options(argc, argv, prefix, opts, refs_rename_usage, 0);\n> +\tif (argc != 2)\n> +\t\tusage(_(\"rename requires old and new reference name\"));\n> +\tif (message && !*message)\n> +\t\tdie(_(\"refusing to perform update with empty message\"));\n> +\n> +\toldref = argv[0];\n> +\tnewref = argv[1];\n> +\n> +\tif (check_refname_format(oldref, 0))\n> +\t\tdie(_(\"invalid ref format: %s\"), oldref);\n> +\tif (check_refname_format(newref, 0))\n> +\t\tdie(_(\"invalid ref format: %s\"), newref);\n\nDo we want to quote the value?  What other subcommands do in \"git refs\"?\n\n> +\tif (!refs_ref_exists(get_main_ref_store(repo), oldref))\n> +\t\tdie(_(\"reference does not exist: '%s'\"), oldref);\n> +\tif (refs_ref_exists(get_main_ref_store(repo), newref))\n> +\t\tdie(_(\"reference already exists: '%s'\"), newref);\n> +\n> +\treturn refs_rename_ref(get_main_ref_store(repo), oldref, newref, message);\n> +}\n\nI suspect that my version shared the same issue, but doesn't\nrefs_rename_ref() return -1 for failure, which we may want to turn\nto positive 1 before returning?\n\nThis is a tangent but git.c:handle_builtin() that calls\ngit.c:run_builtin() may want to do the \"negative return? flip the\npolarity\" conversion to make this worry go away.  I dunno what such\na change would break, though.\n\nIf we rename a ref that does not have a reflog, would it leave the\nref under the new name without reflog, or would we get a reflog with\na single entry that marks the fact the old ref was renamed into the\nnew ref?  Should that be controlled via --create-reflog option?\n"},{"id":"545735","messageId":"ajJMnZchqdpiuKTg@pks.im","threadId":"65819","inReplyTo":"xmqqeci6bupk.fsf@gitster.g","subject":"Re: [PATCH 3/4] builtin/refs: add \"update\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-17T07:28:29Z","receivedAt":"2026-06-17T07:28:35Z","isPatch":true,"body":"On Tue, Jun 16, 2026 at 04:17:27AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > Add a new \"update\" subcommand which mirrors `git update-ref <refname>\n> > <oldoid> <newoid>`. This follows the same reasoning as the preceding\n> > commit.\n> >\n> > Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> > ---\n> >  Documentation/git-refs.adoc |   7 ++\n> >  builtin/refs.c              |  50 +++++++++++++\n> >  t/meson.build               |   1 +\n> >  t/t1465-refs-update.sh      | 179 ++++++++++++++++++++++++++++++++++++++++++++\n> >  4 files changed, 237 insertions(+)\n> \n> I do not offhand know (and I am still away by 2 hours from the time\n> I wake up and start functioning) if update-ref shares the same\n> issue, but with \"delete, update, rename\" combo, lack of \"create\"\n> feels a bit annoying.  Wouldn't we want to offer an option to users\n> who want to ensure that the refs they create are truly new and they\n> are not overwriting a ref somebody has created?  Either (1) drop\n> \"delete\" and take a special value (e.g. \"\") as <newvalue> to signal\n> deletion and make the same special value used as <oldvalue> signals\n> creation, or (2) add \"create\" and insist that \"update\" takes only an\n> existing ref, would make the annoyance go away, I guess.\n\nIn theory \"update\" can handle both updating existing references,\ndeleting them and creating them race-free by providing NUL object IDs\nfor either old or new value. But I agree that this is cumbersome, and\nadding another \"create\" subcommand as an easy-to-understand shortcut\nfeels sensible.\n\n> > +test_expect_success 'update creates a new reference' '\n> > +\ttest_when_finished \"rm -rf repo\" &&\n> > +\tsetup_repo repo &&\n> > +\t(\n> > +\t\tcd repo &&\n> > +\t\tA=$(git rev-parse A) &&\n> > +\t\tgit refs update refs/heads/foo $A &&\n> > +\t\ttest_ref_matches refs/heads/foo \"$A\"\n> > +\t)\n> > +'\n> \n> Here we cannot test (and I strongly suspect that \"git refs update\"\n> and \"git update-ref\" lack ability to do so) a case where a creation\n> is attempted on an existing ref and fails.\n\nWe can:\n\n    $ git update-ref $NEW_OID $NULL_OID\n    $ git refs update $NEW_OID $NULL_OID\n\nThis will verify that the reference doesn't exist before actually\nwriting it. Will add a test.\n\nPatrick\n"},{"id":"545736","messageId":"ajJMpH7VQY3Hxm9A@pks.im","threadId":"65819","inReplyTo":"xmqqse6m4jw4.fsf@gitster.g","subject":"Re: [PATCH 3/4] builtin/refs: add \"update\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-17T07:28:36Z","receivedAt":"2026-06-17T07:28:40Z","isPatch":true,"body":"On Tue, Jun 16, 2026 at 07:52:59AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> >  git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]\n> > +git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\n> \n> \"<old-value> vs <new-value>\" is good, we should update \"delete\" to\n> use \"<old-value>\" to match.\n\nGood catch.\n\n> > @@ -58,6 +59,12 @@ delete::\n> >  \treference is only deleted after verifying that it currently contains\n> >  \t`<oldvalue>`.\n> >  \n> > +update::\n> > +\tUpdate the given reference to point at `<new-value>`. This subcommand\n> > +\tmirrors `git update-ref` (see linkgit:git-update-ref[1]). When\n> > +\t`<old-value>` is given, the reference is only updated after verifying\n> > +\tthat it currently contains `<old-value>`.\n> \n> As to the lack of \"create\", among the two potential changes, I have\n> a slight preference for adding \"create\" and failing \"update\" that\n> does not refer to an existing ref.  If we go that route, the\n> \"--create-reflog\" option should move to \"create\", as \"update\" will\n> never be used to create a new ref.\n\nI agree that we should add \"create\", but I also think that we should\nkeep the special syntax of:\n\n  - \"git refs update $NULL_OID [$OLD_OID]\" to delete a branch.\n\n  - \"git refs update $NEW_OID $NULL_OID\" to create a branch.\n\nWhile the other commands are more ergonomic, I don't see a strong reason\nto restrict the \"update\" subcommand. Also, this ensures that it's in\nline with git-update-ref(1), which also allows for these use cases.\n\n> > +\tif (repo_get_oid_with_flags(repo, argv[1], &newoid,\n> > +\t\t\t\t    GET_OID_SKIP_AMBIGUITY_CHECK))\n> > +\t\tdie(_(\"invalid new object ID: %s\"), argv[1]);\n> > +\tif (argc == 3 &&\n> > +\t    repo_get_oid_with_flags(repo, argv[2], &oldoid,\n> > +\t\t\t\t    GET_OID_SKIP_AMBIGUITY_CHECK))\n> > +\t\tdie(_(\"invalid old object ID: %s\"), argv[2]);\n> \n> On the \"delete\" side, these messages quote the object name, i.e.,\n> \n> \t\t\tdie(_(\"invalid old object ID: '%s'\"), argv[1]);\n> \n> We should be consistent.\n\nYup, fixed now.\n\nPatrick\n"},{"id":"545737","messageId":"ajJMqayXuie1FyIW@pks.im","threadId":"65819","inReplyTo":"xmqqo6ha4jw1.fsf@gitster.g","subject":"Re: [PATCH 4/4] builtin/refs: add \"rename\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-17T07:28:41Z","receivedAt":"2026-06-17T07:28:45Z","isPatch":true,"body":"On Tue, Jun 16, 2026 at 07:53:02AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n[snip]\n> > +\tif (!refs_ref_exists(get_main_ref_store(repo), oldref))\n> > +\t\tdie(_(\"reference does not exist: '%s'\"), oldref);\n> > +\tif (refs_ref_exists(get_main_ref_store(repo), newref))\n> > +\t\tdie(_(\"reference already exists: '%s'\"), newref);\n> > +\n> > +\treturn refs_rename_ref(get_main_ref_store(repo), oldref, newref, message);\n> > +}\n> \n> I suspect that my version shared the same issue, but doesn't\n> refs_rename_ref() return -1 for failure, which we may want to turn\n> to positive 1 before returning?\n> \n> This is a tangent but git.c:handle_builtin() that calls\n> git.c:run_builtin() may want to do the \"negative return? flip the\n> polarity\" conversion to make this worry go away.  I dunno what such\n> a change would break, though.\n\nFair. The other subcommands also suffer from the same problem, so I'll\nupdate all of them to return 1 explicitly.\n\n> If we rename a ref that does not have a reflog, would it leave the\n> ref under the new name without reflog, or would we get a reflog with\n> a single entry that marks the fact the old ref was renamed into the\n> new ref?  Should that be controlled via --create-reflog option?\n\nIt would leave it without a reflog. In theory I agree that it might make\nsense to introduce a \"--create-reflog\" option, but that would require\nsome new plumbing in `refs_rename_ref()`. So I'd say that we can add it\nat a later point as needed.\n\nPatrick\n"},{"id":"545746","messageId":"20260617-pks-refs-writing-subcommands-v2-0-07f3d18336f9@pks.im","threadId":"65819","inReplyTo":"20260616-pks-refs-writing-subcommands-v1-0-9f5219b6109d@pks.im","subject":"[PATCH v2 0/5] builtin/refs: add ability to write references","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-17T10:15:57Z","receivedAt":"2026-06-17T10:16:15Z","isPatch":true,"body":"Hi,\n\nReference-related functionality in Git is currently spread across many\ndifferent commands: git-update-ref(1), git-for-each-ref(1),\ngit-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it\nhard for users to discover what functionality we have available to work\nwith references.\n\nWe have thus started to consolidate this functionality into git-refs(1),\nwhich is a toolbox of everything related to references. Until now, the\ncommand doesn't handle functionality of git-update-ref(1).\n\nThis patch series backfills most of the functionality by introducing\nthree new commands:\n\n  - `git refs delete` to delete references. This is the equivalent of\n    `git update-ref -d`.\n\n  - `git refs update` to update references. This is the equivalent of\n    `git update-ref <refname> <oldvalue> <newvalue>`.\n\n  - `git refs rename` to rename a reference, including its reflog. This\n    does not have an equivalent in git-update-ref(1), but is inspired by\n    and supersedes [1].\n\nChanges in v2:\n  - Add a new \"create\" subcommand.\n  - Consistently quote in error messages.\n  - Consistently use `<old-value>` in the synopsis.\n  - Don't return negative exit codes.\n  - Improve documentation of \"update\" subcommand to mention that you can\n    create and delete branches.\n  - Add tests to verify that we can use \"update\" to do this, both in\n    racy and raceless ways.\n  - Add missing calls to `repo_config()`.\n  - Drop useless `GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME` variable.\n  - Link to v1: https://patch.msgid.link/20260616-pks-refs-writing-subcommands-v1-0-9f5219b6109d@pks.im\n\nThanks!\n\nPatrick\n\n[1]: <xmqqv7brz9ba.fsf@gitster.g>\n\n---\nPatrick Steinhardt (5):\n      builtin/refs: drop `the_repository`\n      builtin/refs: add \"delete\" subcommand\n      builtin/refs: add \"update\" subcommand\n      builtin/refs: add \"create\" subcommand\n      builtin/refs: add \"rename\" subcommand\n\n Documentation/git-refs.adoc |  40 +++++++\n builtin/refs.c              | 222 ++++++++++++++++++++++++++++++++++--\n t/meson.build               |   4 +\n t/t1464-refs-delete.sh      | 130 +++++++++++++++++++++\n t/t1465-refs-update.sh      | 268 ++++++++++++++++++++++++++++++++++++++++++++\n t/t1466-refs-create.sh      | 151 +++++++++++++++++++++++++\n t/t1467-refs-rename.sh      | 131 ++++++++++++++++++++++\n 7 files changed, 938 insertions(+), 8 deletions(-)\n\nRange-diff versus v1:\n\n1:  cfbc247e81 = 1:  6d0c5bd06f builtin/refs: drop `the_repository`\n2:  f5f33e5c5b ! 2:  db55d87116 builtin/refs: add \"delete\" subcommand\n    @@ Documentation/git-refs.adoc: git refs list [--count=<count>] [--shell|--perl|--p\n      \t\t   [ --stdin | (<pattern>...)]\n      git refs exists <ref>\n      git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]\n    -+git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]\n    ++git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\n      \n      DESCRIPTION\n      -----------\n    @@ Documentation/git-refs.adoc: optimize::\n      \n     +delete::\n     +\tDelete the given reference. This subcommand mirrors `git update-ref -d`\n    -+\t(see linkgit:git-update-ref[1]). When `<oldvalue>` is given, the\n    ++\t(see linkgit:git-update-ref[1]). When `<old-value>` is given, the\n     +\treference is only deleted after verifying that it currently contains\n    -+\t`<oldvalue>`.\n    ++\t`<old-value>`.\n     +\n      OPTIONS\n      -------\n    @@ builtin/refs.c\n      \tN_(\"git refs optimize \" PACK_REFS_OPTS)\n      \n     +#define REFS_DELETE_USAGE \\\n    -+\tN_(\"git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]\")\n    ++\tN_(\"git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\")\n     +\n      static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,\n      \t\t\t    struct repository *repo)\n    @@ builtin/refs.c: static int cmd_refs_optimize(int argc, const char **argv, const\n     +\t};\n     +\tstruct object_id oldoid;\n     +\tconst char *refname;\n    ++\tint ret;\n     +\n     +\targc = parse_options(argc, argv, prefix, opts, refs_delete_usage, 0);\n     +\tif (argc < 1 || argc > 2)\n    @@ builtin/refs.c: static int cmd_refs_optimize(int argc, const char **argv, const\n     +\t\tif (repo_get_oid_with_flags(repo, argv[1], &oldoid, GET_OID_SKIP_AMBIGUITY_CHECK))\n     +\t\t\tdie(_(\"invalid old object ID: '%s'\"), argv[1]);\n     +\t\tif (is_null_oid(&oldoid))\n    -+\t\t\tdie(_(\"cannot delete object with null old object ID\"));\n    ++\t\t\tdie(_(\"cannot delete reference with null old object ID\"));\n     +\t}\n     +\n    -+\treturn refs_delete_ref(get_main_ref_store(repo), message, refname,\n    -+\t\t\t       argc == 2 ? &oldoid : NULL, flags);\n    ++\tret = refs_delete_ref(get_main_ref_store(repo), message, refname,\n    ++\t\t\t      argc == 2 ? &oldoid : NULL, flags);\n    ++\n    ++\tif (ret < 0)\n    ++\t\tret = 1;\n    ++\treturn ret;\n     +}\n     +\n      int cmd_refs(int argc,\n    @@ t/t1464-refs-delete.sh (new)\n     +\n     +test_description='git refs delete'\n     +\n    -+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n    -+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n    -+\n     +. ./test-lib.sh\n     +\n     +setup_repo () {\n3:  1fc1bed619 ! 3:  85f07a2cb0 builtin/refs: add \"update\" subcommand\n    @@ Documentation/git-refs.adoc\n     @@ Documentation/git-refs.adoc: git refs list [--count=<count>] [--shell|--perl|--python|--tcl]\n      git refs exists <ref>\n      git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]\n    - git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]\n    + git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\n     +git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\n      \n      DESCRIPTION\n      -----------\n     @@ Documentation/git-refs.adoc: delete::\n      \treference is only deleted after verifying that it currently contains\n    - \t`<oldvalue>`.\n    + \t`<old-value>`.\n      \n     +update::\n    -+\tUpdate the given reference to point at `<new-value>`. This subcommand\n    -+\tmirrors `git update-ref` (see linkgit:git-update-ref[1]). When\n    -+\t`<old-value>` is given, the reference is only updated after verifying\n    -+\tthat it currently contains `<old-value>`.\n    ++\tUpdate the given reference to point at `<new-value>`. If `<old-value>`\n    ++\tis given, the reference is only updated after verifying that it\n    ++\tcurrently contains `<old-value>`. As a special case, an all-zeroes\n    ++\t`<new-value>` deletes the branch, whereas an all-zeroes `<old-value>`\n    ++\tensures that the branch does not yet exist.\n     +\n      OPTIONS\n      -------\n      \n    +@@ Documentation/git-refs.adoc: include::pack-refs-options.adoc[]\n    + \n    + The following options are specific to commands which write references:\n    + \n    ++`--create-reflog`::\n    ++\tCreate a reflog for the reference even if one would not ordinarily be\n    ++\tcreated.\n    ++\n    + `--message=<reason>`::\n    + \tUse the given <reason> string for the reflog entry associated with the\n    + \tupdate. An empty message is rejected.\n     \n      ## builtin/refs.c ##\n     @@\n      #define REFS_DELETE_USAGE \\\n    - \tN_(\"git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]\")\n    + \tN_(\"git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\")\n      \n     +#define REFS_UPDATE_USAGE \\\n     +\tN_(\"git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\")\n    @@ builtin/refs.c\n      \t\t\t    struct repository *repo)\n      {\n     @@ builtin/refs.c: static int cmd_refs_delete(int argc, const char **argv, const char *prefix,\n    - \t\t\t       argc == 2 ? &oldoid : NULL, flags);\n    + \treturn ret;\n      }\n      \n     +static int cmd_refs_update(int argc, const char **argv, const char *prefix,\n    @@ builtin/refs.c: static int cmd_refs_delete(int argc, const char **argv, const ch\n     +\t};\n     +\tstruct object_id newoid, oldoid;\n     +\tconst char *refname;\n    ++\tint ret;\n     +\n     +\targc = parse_options(argc, argv, prefix, opts, refs_update_usage, 0);\n     +\tif (argc < 2 || argc > 3)\n    @@ builtin/refs.c: static int cmd_refs_delete(int argc, const char **argv, const ch\n     +\trefname = argv[0];\n     +\tif (repo_get_oid_with_flags(repo, argv[1], &newoid,\n     +\t\t\t\t    GET_OID_SKIP_AMBIGUITY_CHECK))\n    -+\t\tdie(_(\"invalid new object ID: %s\"), argv[1]);\n    ++\t\tdie(_(\"invalid new object ID: '%s'\"), argv[1]);\n     +\tif (argc == 3 &&\n     +\t    repo_get_oid_with_flags(repo, argv[2], &oldoid,\n     +\t\t\t\t    GET_OID_SKIP_AMBIGUITY_CHECK))\n    -+\t\tdie(_(\"invalid old object ID: %s\"), argv[2]);\n    ++\t\tdie(_(\"invalid old object ID: '%s'\"), argv[2]);\n     +\n    -+\treturn refs_update_ref(get_main_ref_store(repo), message, refname,\n    -+\t\t\t       &newoid, argc == 3 ? &oldoid : NULL, flags,\n    -+\t\t\t       UPDATE_REFS_DIE_ON_ERR);\n    ++\tret = refs_update_ref(get_main_ref_store(repo), message, refname,\n    ++\t\t\t      &newoid, argc == 3 ? &oldoid : NULL, flags,\n    ++\t\t\t      UPDATE_REFS_MSG_ON_ERR);\n    ++\n    ++\tif (ret < 0)\n    ++\t\tret = 1;\n    ++\treturn ret;\n     +}\n     +\n      int cmd_refs(int argc,\n    @@ t/t1465-refs-update.sh (new)\n     +\t)\n     +'\n     +\n    ++test_expect_success 'update can create a new branch with oldvalue' '\n    ++\ttest_when_finished \"rm -rf repo\" &&\n    ++\tsetup_repo repo &&\n    ++\t(\n    ++\t\tcd repo &&\n    ++\t\tA=$(git rev-parse A) &&\n    ++\t\tgit refs update refs/heads/foo $A $ZERO_OID 2>err &&\n    ++\t\ttest_ref_matches refs/heads/foo $A\n    ++\t)\n    ++'\n    ++\n    ++test_expect_success 'update can create a new branch without oldvalue' '\n    ++\ttest_when_finished \"rm -rf repo\" &&\n    ++\tsetup_repo repo &&\n    ++\t(\n    ++\t\tcd repo &&\n    ++\t\tA=$(git rev-parse A) &&\n    ++\t\tgit refs update refs/heads/foo $A 2>err &&\n    ++\t\ttest_ref_matches refs/heads/foo $A\n    ++\t)\n    ++'\n    ++\n    ++test_expect_success 'update refuses to create preexisting branch' '\n    ++\ttest_when_finished \"rm -rf repo\" &&\n    ++\tsetup_repo repo &&\n    ++\t(\n    ++\t\tcd repo &&\n    ++\t\tA=$(git rev-parse A) &&\n    ++\t\tB=$(git rev-parse B) &&\n    ++\t\tgit refs update refs/heads/foo $A &&\n    ++\t\ttest_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err &&\n    ++\t\ttest_grep \"reference already exists\" err &&\n    ++\t\ttest_ref_matches refs/heads/foo $A\n    ++\t)\n    ++'\n    ++\n    ++test_expect_success 'update can delete a branch with oldvalue' '\n    ++\ttest_when_finished \"rm -rf repo\" &&\n    ++\tsetup_repo repo &&\n    ++\t(\n    ++\t\tcd repo &&\n    ++\t\tA=$(git rev-parse A) &&\n    ++\t\tgit refs update refs/heads/foo $A 2>err &&\n    ++\t\tgit refs update refs/heads/foo $ZERO_OID $A 2>err &&\n    ++\t\ttest_must_fail git refs exists refs/heads/foo\n    ++\t)\n    ++'\n    ++\n    ++test_expect_success 'update can delete a branch without oldvalue' '\n    ++\ttest_when_finished \"rm -rf repo\" &&\n    ++\tsetup_repo repo &&\n    ++\t(\n    ++\t\tcd repo &&\n    ++\t\tA=$(git rev-parse A) &&\n    ++\t\tgit refs update refs/heads/foo $A 2>err &&\n    ++\t\tgit refs update refs/heads/foo $ZERO_OID 2>err &&\n    ++\t\ttest_must_fail git refs exists refs/heads/foo\n    ++\t)\n    ++'\n    ++\n    ++test_expect_success 'update refuses to delete a branch with mismatching value' '\n    ++\ttest_when_finished \"rm -rf repo\" &&\n    ++\tsetup_repo repo &&\n    ++\t(\n    ++\t\tcd repo &&\n    ++\t\tA=$(git rev-parse A) &&\n    ++\t\tB=$(git rev-parse B) &&\n    ++\t\tgit refs update refs/heads/foo $A 2>err &&\n    ++\t\ttest_must_fail git refs update refs/heads/foo $ZERO_OID $B 2>err &&\n    ++\t\ttest_grep \" but expected \" err &&\n    ++\t\tgit refs exists refs/heads/foo\n    ++\t)\n    ++'\n    ++\n    ++test_expect_success 'update refuses to create preexisting branch' '\n    ++\ttest_when_finished \"rm -rf repo\" &&\n    ++\tsetup_repo repo &&\n    ++\t(\n    ++\t\tcd repo &&\n    ++\t\tA=$(git rev-parse A) &&\n    ++\t\tB=$(git rev-parse B) &&\n    ++\t\tgit refs update refs/heads/foo $A &&\n    ++\t\ttest_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err &&\n    ++\t\ttest_grep \"reference already exists\" err &&\n    ++\t\ttest_ref_matches refs/heads/foo $A\n    ++\t)\n    ++'\n    ++\n    ++\n     +test_expect_success 'update with invalid new value fails' '\n     +\ttest_when_finished \"rm -rf repo\" &&\n     +\tsetup_repo repo &&\n-:  ---------- > 4:  03036ef730 builtin/refs: add \"create\" subcommand\n4:  aadedb14e1 ! 5:  65f0ee4f03 builtin/refs: add \"rename\" subcommand\n    @@ Commit message\n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n      ## Documentation/git-refs.adoc ##\n    -@@ Documentation/git-refs.adoc: git refs exists <ref>\n    - git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]\n    - git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]\n    +@@ Documentation/git-refs.adoc: git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude\n    + git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>\n    + git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\n      git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\n    -+git refs rename [--message=<reason>] <oldref> <newref>\n    ++git refs rename [--message=<reason>] <old-ref> <new-ref>\n      \n      DESCRIPTION\n      -----------\n     @@ Documentation/git-refs.adoc: update::\n    - \t`<old-value>` is given, the reference is only updated after verifying\n    - \tthat it currently contains `<old-value>`.\n    + \t`<new-value>` deletes the branch, whereas an all-zeroes `<old-value>`\n    + \tensures that the branch does not yet exist.\n      \n     +rename::\n     +\tRename the reference `<oldref>` to `<newref>`. The old reference must\n    @@ Documentation/git-refs.adoc: update::\n      OPTIONS\n      -------\n      \n    -@@ Documentation/git-refs.adoc: include::pack-refs-options.adoc[]\n    - \n    - The following options are specific to commands which write references:\n    - \n    -+`--create-reflog`::\n    -+\tCreate a reflog for the reference even if one would not ordinarily be\n    -+\tcreated.\n    -+\n    - `--message=<reason>`::\n    - \tUse the given <reason> string for the reflog entry associated with the\n    - \tupdate. An empty message is rejected.\n     \n      ## builtin/refs.c ##\n     @@\n    @@ builtin/refs.c\n      \tN_(\"git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\")\n      \n     +#define REFS_RENAME_USAGE \\\n    -+\tN_(\"git refs rename [--message=<reason>] <oldref> <newref>\")\n    ++\tN_(\"git refs rename [--message=<reason>] <old-ref> <new-ref>\")\n     +\n      static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,\n      \t\t\t    struct repository *repo)\n      {\n     @@ builtin/refs.c: static int cmd_refs_update(int argc, const char **argv, const char *prefix,\n    - \t\t\t       UPDATE_REFS_DIE_ON_ERR);\n    + \treturn ret;\n      }\n      \n     +static int cmd_refs_rename(int argc, const char **argv, const char *prefix,\n    @@ builtin/refs.c: static int cmd_refs_update(int argc, const char **argv, const ch\n     +\t\tOPT_END(),\n     +\t};\n     +\tconst char *oldref, *newref;\n    ++\tint ret;\n     +\n     +\targc = parse_options(argc, argv, prefix, opts, refs_rename_usage, 0);\n     +\tif (argc != 2)\n    @@ builtin/refs.c: static int cmd_refs_update(int argc, const char **argv, const ch\n     +\tif (message && !*message)\n     +\t\tdie(_(\"refusing to perform update with empty message\"));\n     +\n    ++\trepo_config(repo, git_default_config, NULL);\n    ++\n     +\toldref = argv[0];\n     +\tnewref = argv[1];\n     +\n     +\tif (check_refname_format(oldref, 0))\n    -+\t\tdie(_(\"invalid ref format: %s\"), oldref);\n    ++\t\tdie(_(\"invalid ref format: '%s'\"), oldref);\n     +\tif (check_refname_format(newref, 0))\n    -+\t\tdie(_(\"invalid ref format: %s\"), newref);\n    ++\t\tdie(_(\"invalid ref format: '%s'\"), newref);\n     +\n     +\tif (!refs_ref_exists(get_main_ref_store(repo), oldref))\n     +\t\tdie(_(\"reference does not exist: '%s'\"), oldref);\n     +\tif (refs_ref_exists(get_main_ref_store(repo), newref))\n     +\t\tdie(_(\"reference already exists: '%s'\"), newref);\n     +\n    -+\treturn refs_rename_ref(get_main_ref_store(repo), oldref, newref, message);\n    ++\tret = refs_rename_ref(get_main_ref_store(repo), oldref, newref, message);\n    ++\n    ++\tif (ret < 0)\n    ++\t\tret = 1;\n    ++\treturn ret;\n     +}\n     +\n      int cmd_refs(int argc,\n      \t     const char **argv,\n      \t     const char *prefix,\n     @@ builtin/refs.c: int cmd_refs(int argc,\n    - \t\tREFS_OPTIMIZE_USAGE,\n    + \t\tREFS_CREATE_USAGE,\n      \t\tREFS_DELETE_USAGE,\n      \t\tREFS_UPDATE_USAGE,\n     +\t\tREFS_RENAME_USAGE,\n    @@ builtin/refs.c: int cmd_refs(int argc,\n      \t};\n      \tparse_opt_subcommand_fn *fn = NULL;\n     @@ builtin/refs.c: int cmd_refs(int argc,\n    - \t\tOPT_SUBCOMMAND(\"optimize\", &fn, cmd_refs_optimize),\n    + \t\tOPT_SUBCOMMAND(\"create\", &fn, cmd_refs_create),\n      \t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_refs_delete),\n      \t\tOPT_SUBCOMMAND(\"update\", &fn, cmd_refs_update),\n     +\t\tOPT_SUBCOMMAND(\"rename\", &fn, cmd_refs_rename),\n    @@ builtin/refs.c: int cmd_refs(int argc,\n     \n      ## t/meson.build ##\n     @@ t/meson.build: integration_tests = [\n    -   't1463-refs-optimize.sh',\n        't1464-refs-delete.sh',\n        't1465-refs-update.sh',\n    -+  't1466-refs-rename.sh',\n    +   't1466-refs-create.sh',\n    ++  't1467-refs-rename.sh',\n        't1500-rev-parse.sh',\n        't1501-work-tree.sh',\n        't1502-rev-parse-parseopt.sh',\n     \n    - ## t/t1466-refs-rename.sh (new) ##\n    + ## t/t1467-refs-rename.sh (new) ##\n     @@\n     +#!/bin/sh\n     +\n\n---\nbase-commit: 700432b2ba22603a0bcb71475c9c333d17c9b0d1\nchange-id: 20260616-pks-refs-writing-subcommands-7a77be5bda9b\n\n"},{"id":"545747","messageId":"20260617-pks-refs-writing-subcommands-v2-1-07f3d18336f9@pks.im","threadId":"65819","inReplyTo":"20260617-pks-refs-writing-subcommands-v2-0-07f3d18336f9@pks.im","subject":"[PATCH v2 1/5] builtin/refs: drop `the_repository`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-17T10:15:58Z","receivedAt":"2026-06-17T10:16:16Z","isPatch":true,"body":"We still have a couple of uses of `the_repository` in \"builtin/refs.c\".\nAll of those are trivial to convert though as the command always\nrequires a repository to exist.\n\nConvert them to use the passed-in repository and drop\n`USE_THE_REPOSITORY_VARIABLE`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/refs.c | 15 +++++++--------\n 1 file changed, 7 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/refs.c b/builtin/refs.c\nindex e3125bc61b..f0faabf45a 100644\n--- a/builtin/refs.c\n+++ b/builtin/refs.c\n@@ -1,4 +1,3 @@\n-#define USE_THE_REPOSITORY_VARIABLE\n #include \"builtin.h\"\n #include \"config.h\"\n #include \"fsck.h\"\n@@ -23,7 +22,7 @@\n \tN_(\"git refs optimize \" PACK_REFS_OPTS)\n \n static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,\n-\t\t\t    struct repository *repo UNUSED)\n+\t\t\t    struct repository *repo)\n {\n \tconst char * const migrate_usage[] = {\n \t\tREFS_MIGRATE_USAGE,\n@@ -59,13 +58,13 @@ static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,\n \t\tgoto out;\n \t}\n \n-\tif (the_repository->ref_storage_format == format) {\n+\tif (repo->ref_storage_format == format) {\n \t\terr = error(_(\"repository already uses '%s' format\"),\n \t\t\t    ref_storage_format_to_name(format));\n \t\tgoto out;\n \t}\n \n-\tif (repo_migrate_ref_storage_format(the_repository, format, flags, &errbuf) < 0) {\n+\tif (repo_migrate_ref_storage_format(repo, format, flags, &errbuf) < 0) {\n \t\terr = error(\"%s\", errbuf.buf);\n \t\tgoto out;\n \t}\n@@ -99,8 +98,8 @@ static int cmd_refs_verify(int argc, const char **argv, const char *prefix,\n \tif (argc)\n \t\tusage(_(\"'git refs verify' takes no arguments\"));\n \n-\trepo_config(the_repository, git_fsck_config, &fsck_refs_options);\n-\tprepare_repo_settings(the_repository);\n+\trepo_config(repo, git_fsck_config, &fsck_refs_options);\n+\tprepare_repo_settings(repo);\n \n \tworktrees = get_worktrees_without_reading_head();\n \tfor (size_t i = 0; worktrees[i]; i++)\n@@ -124,7 +123,7 @@ static int cmd_refs_list(int argc, const char **argv, const char *prefix,\n }\n \n static int cmd_refs_exists(int argc, const char **argv, const char *prefix,\n-\t\t\t   struct repository *repo UNUSED)\n+\t\t\t   struct repository *repo)\n {\n \tstruct strbuf unused_referent = STRBUF_INIT;\n \tstruct object_id unused_oid;\n@@ -145,7 +144,7 @@ static int cmd_refs_exists(int argc, const char **argv, const char *prefix,\n \t\tdie(_(\"'git refs exists' requires a reference\"));\n \n \tref = *argv++;\n-\tif (refs_read_raw_ref(get_main_ref_store(the_repository), ref,\n+\tif (refs_read_raw_ref(get_main_ref_store(repo), ref,\n \t\t\t      &unused_oid, &unused_referent, &unused_type,\n \t\t\t      &failure_errno)) {\n \t\tif (failure_errno == ENOENT || failure_errno == EISDIR) {\n\n-- \n2.55.0.rc0.786.g65d90a0328.dirty\n\n"},{"id":"545748","messageId":"20260617-pks-refs-writing-subcommands-v2-2-07f3d18336f9@pks.im","threadId":"65819","inReplyTo":"20260617-pks-refs-writing-subcommands-v2-0-07f3d18336f9@pks.im","subject":"[PATCH v2 2/5] builtin/refs: add \"delete\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-17T10:15:59Z","receivedAt":"2026-06-17T10:16:18Z","isPatch":true,"body":"Reference-related functionality in Git is currently spread across many\ndifferent commands: git-update-ref(1), git-for-each-ref(1),\ngit-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it\nhard for users to discover what functionality we have available to work\nwith references.\n\nWe have thus started to consolidate this functionality into git-refs(1),\nwhich is a toolbox of everything related to references. Until now, the\ncommand doesn't handle functionality of git-update-ref(1).\n\nFix this gap by introducing a new \"delete\" subcommand, which is the\nequivalent of `git update-ref -d`.\n\nNote that we're intentionally not using a generic \"write\" subcommand\nwith a \"-d\" flag. This is rather harder to discover, and subcommands\nthat are implmented as flags tend to be hard to reason about in the code\nas we'd have to handle mutually-exclusive flags that stem from the other\nsubcommand-like modes.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-refs.adoc |  17 ++++++\n builtin/refs.c              |  51 +++++++++++++++++\n t/meson.build               |   1 +\n t/t1464-refs-delete.sh      | 130 ++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 199 insertions(+)\n\ndiff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc\nindex fa33680cc7..2633934463 100644\n--- a/Documentation/git-refs.adoc\n+++ b/Documentation/git-refs.adoc\n@@ -20,6 +20,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]\n \t\t   [ --stdin | (<pattern>...)]\n git refs exists <ref>\n git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]\n+git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\n \n DESCRIPTION\n -----------\n@@ -51,6 +52,12 @@ optimize::\n \tusage. This subcommand is an alias for linkgit:git-pack-refs[1] and\n \toffers identical functionality.\n \n+delete::\n+\tDelete the given reference. This subcommand mirrors `git update-ref -d`\n+\t(see linkgit:git-update-ref[1]). When `<old-value>` is given, the\n+\treference is only deleted after verifying that it currently contains\n+\t`<old-value>`.\n+\n OPTIONS\n -------\n \n@@ -90,6 +97,16 @@ The following options are specific to 'git refs optimize':\n \n include::pack-refs-options.adoc[]\n \n+The following options are specific to commands which write references:\n+\n+`--message=<reason>`::\n+\tUse the given <reason> string for the reflog entry associated with the\n+\tupdate. An empty message is rejected.\n+\n+`--no-deref`::\n+\tOperate on <ref> itself rather than the reference it points to via a\n+\tsymbolic ref.\n+\n KNOWN LIMITATIONS\n -----------------\n \ndiff --git a/builtin/refs.c b/builtin/refs.c\nindex f0faabf45a..edb7d61663 100644\n--- a/builtin/refs.c\n+++ b/builtin/refs.c\n@@ -21,6 +21,9 @@\n #define REFS_OPTIMIZE_USAGE \\\n \tN_(\"git refs optimize \" PACK_REFS_OPTS)\n \n+#define REFS_DELETE_USAGE \\\n+\tN_(\"git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\")\n+\n static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,\n \t\t\t    struct repository *repo)\n {\n@@ -175,6 +178,52 @@ static int cmd_refs_optimize(int argc, const char **argv, const char *prefix,\n \treturn pack_refs_core(argc, argv, prefix, repo, refs_optimize_usage);\n }\n \n+static int cmd_refs_delete(int argc, const char **argv, const char *prefix,\n+\t\t\t   struct repository *repo)\n+{\n+\tstatic char const * const refs_delete_usage[] = {\n+\t\tREFS_DELETE_USAGE,\n+\t\tNULL\n+\t};\n+\tconst char *message = NULL;\n+\tunsigned flags = 0;\n+\tstruct option opts[] = {\n+\t\tOPT_STRING(0, \"message\", &message, N_(\"reason\"),\n+\t\t\t   N_(\"reason of the update\")),\n+\t\tOPT_BIT(0 ,\"no-deref\", &flags,\n+\t\t\tN_(\"update <refname> not the one it points to\"),\n+\t\t\tREF_NO_DEREF),\n+\t\tOPT_END(),\n+\t};\n+\tstruct object_id oldoid;\n+\tconst char *refname;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, refs_delete_usage, 0);\n+\tif (argc < 1 || argc > 2)\n+\t\tusage(_(\"delete requires reference name and an optional old object ID\"));\n+\n+\tif (message && !*message)\n+\t\tdie(_(\"refusing to perform update with empty message\"));\n+\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\trefname = argv[0];\n+\tif (argc == 2) {\n+\t\tif (repo_get_oid_with_flags(repo, argv[1], &oldoid, GET_OID_SKIP_AMBIGUITY_CHECK))\n+\t\t\tdie(_(\"invalid old object ID: '%s'\"), argv[1]);\n+\t\tif (is_null_oid(&oldoid))\n+\t\t\tdie(_(\"cannot delete reference with null old object ID\"));\n+\t}\n+\n+\tret = refs_delete_ref(get_main_ref_store(repo), message, refname,\n+\t\t\t      argc == 2 ? &oldoid : NULL, flags);\n+\n+\tif (ret < 0)\n+\t\tret = 1;\n+\treturn ret;\n+}\n+\n int cmd_refs(int argc,\n \t     const char **argv,\n \t     const char *prefix,\n@@ -186,6 +235,7 @@ int cmd_refs(int argc,\n \t\t\"git refs list \" COMMON_USAGE_FOR_EACH_REF,\n \t\tREFS_EXISTS_USAGE,\n \t\tREFS_OPTIMIZE_USAGE,\n+\t\tREFS_DELETE_USAGE,\n \t\tNULL,\n \t};\n \tparse_opt_subcommand_fn *fn = NULL;\n@@ -195,6 +245,7 @@ int cmd_refs(int argc,\n \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_refs_list),\n \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_refs_exists),\n \t\tOPT_SUBCOMMAND(\"optimize\", &fn, cmd_refs_optimize),\n+\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_refs_delete),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex c5832fee05..1ccf08a3b5 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -223,6 +223,7 @@ integration_tests = [\n   't1461-refs-list.sh',\n   't1462-refs-exists.sh',\n   't1463-refs-optimize.sh',\n+  't1464-refs-delete.sh',\n   't1500-rev-parse.sh',\n   't1501-work-tree.sh',\n   't1502-rev-parse-parseopt.sh',\ndiff --git a/t/t1464-refs-delete.sh b/t/t1464-refs-delete.sh\nnew file mode 100755\nindex 0000000000..efff7d0574\n--- /dev/null\n+++ b/t/t1464-refs-delete.sh\n@@ -0,0 +1,130 @@\n+#!/bin/sh\n+\n+test_description='git refs delete'\n+\n+. ./test-lib.sh\n+\n+setup_repo () {\n+\tgit init \"$1\" &&\n+\ttest_commit -C \"$1\" A &&\n+\ttest_commit -C \"$1\" B\n+}\n+\n+test_expect_success 'delete without oldvalue verification' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\tA=$(git -C repo rev-parse A) &&\n+\tgit -C repo update-ref refs/heads/foo $A &&\n+\tgit -C repo refs delete refs/heads/foo &&\n+\ttest_must_fail git -C repo show-ref --verify -q refs/heads/foo\n+'\n+\n+test_expect_success 'delete with matching oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\tgit refs delete refs/heads/foo $A &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete with stale oldvalue fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\ttest_must_fail git refs delete refs/heads/foo $B 2>err &&\n+\t\ttest_grep \" but expected \" err &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete with null oldvalue fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\ttest_must_fail git refs delete refs/heads/foo $ZERO_OID 2>err &&\n+\t\ttest_grep \"null old object ID\" err &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete with invalid oldvalue fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\ttest_must_fail git refs delete refs/heads/foo invalid-oid 2>err &&\n+\t\ttest_grep \"invalid old object ID\" err &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete symref with --no-deref leaves target intact' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/foo &&\n+\t\tgit refs delete --no-deref refs/heads/symref &&\n+\t\ttest_must_fail git refs exists refs/heads/symref &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete with message records reason in reflog' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\tgit symbolic-ref HEAD refs/heads/foo &&\n+\t\tgit refs delete --message=delete-reason refs/heads/foo &&\n+\t\ttest_must_fail git refs exists refs/heads/foo &&\n+\t\ttest-tool ref-store main for-each-reflog-ent HEAD >actual &&\n+\t\ttest_grep \"delete-reason$\" actual\n+\t)\n+'\n+\n+test_expect_success 'delete with empty message fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\ttest_must_fail git refs delete --message= refs/heads/foo 2>err &&\n+\t\ttest_grep \"empty message\" err &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete without arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git -C repo refs delete 2>err &&\n+\ttest_grep \"requires reference name\" err\n+'\n+\n+test_expect_success 'delete with too many arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git refs delete one two three 2>err &&\n+\ttest_grep \"requires reference name\" err\n+'\n+\n+test_done\n\n-- \n2.55.0.rc0.786.g65d90a0328.dirty\n\n"},{"id":"545749","messageId":"20260617-pks-refs-writing-subcommands-v2-3-07f3d18336f9@pks.im","threadId":"65819","inReplyTo":"20260617-pks-refs-writing-subcommands-v2-0-07f3d18336f9@pks.im","subject":"[PATCH v2 3/5] builtin/refs: add \"update\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-17T10:16:00Z","receivedAt":"2026-06-17T10:16:20Z","isPatch":true,"body":"Add a new \"update\" subcommand which mirrors `git update-ref <refname>\n<oldoid> <newoid>`. This follows the same reasoning as the preceding\ncommit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-refs.adoc |  12 ++\n builtin/refs.c              |  55 +++++++++\n t/meson.build               |   1 +\n t/t1465-refs-update.sh      | 268 ++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 336 insertions(+)\n\ndiff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc\nindex 2633934463..6475bdcc62 100644\n--- a/Documentation/git-refs.adoc\n+++ b/Documentation/git-refs.adoc\n@@ -21,6 +21,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]\n git refs exists <ref>\n git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]\n git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\n+git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\n \n DESCRIPTION\n -----------\n@@ -58,6 +59,13 @@ delete::\n \treference is only deleted after verifying that it currently contains\n \t`<old-value>`.\n \n+update::\n+\tUpdate the given reference to point at `<new-value>`. If `<old-value>`\n+\tis given, the reference is only updated after verifying that it\n+\tcurrently contains `<old-value>`. As a special case, an all-zeroes\n+\t`<new-value>` deletes the branch, whereas an all-zeroes `<old-value>`\n+\tensures that the branch does not yet exist.\n+\n OPTIONS\n -------\n \n@@ -99,6 +107,10 @@ include::pack-refs-options.adoc[]\n \n The following options are specific to commands which write references:\n \n+`--create-reflog`::\n+\tCreate a reflog for the reference even if one would not ordinarily be\n+\tcreated.\n+\n `--message=<reason>`::\n \tUse the given <reason> string for the reflog entry associated with the\n \tupdate. An empty message is rejected.\ndiff --git a/builtin/refs.c b/builtin/refs.c\nindex edb7d61663..08453ae1c8 100644\n--- a/builtin/refs.c\n+++ b/builtin/refs.c\n@@ -24,6 +24,9 @@\n #define REFS_DELETE_USAGE \\\n \tN_(\"git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\")\n \n+#define REFS_UPDATE_USAGE \\\n+\tN_(\"git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\")\n+\n static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,\n \t\t\t    struct repository *repo)\n {\n@@ -224,6 +227,56 @@ static int cmd_refs_delete(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int cmd_refs_update(int argc, const char **argv, const char *prefix,\n+\t\t\t   struct repository *repo)\n+{\n+\tstatic char const * const refs_update_usage[] = {\n+\t\tREFS_UPDATE_USAGE,\n+\t\tNULL\n+\t};\n+\tconst char *message = NULL;\n+\tunsigned flags = 0;\n+\tstruct option opts[] = {\n+\t\tOPT_STRING(0, \"message\", &message, N_(\"reason\"),\n+\t\t\t   N_(\"reason of the update\")),\n+\t\tOPT_BIT(0 ,\"no-deref\", &flags,\n+\t\t\tN_(\"update <refname> not the one it points to\"),\n+\t\t\tREF_NO_DEREF),\n+\t\tOPT_BIT(0, \"create-reflog\", &flags, N_(\"create a reflog\"),\n+\t\t\tREF_FORCE_CREATE_REFLOG),\n+\t\tOPT_END(),\n+\t};\n+\tstruct object_id newoid, oldoid;\n+\tconst char *refname;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, refs_update_usage, 0);\n+\tif (argc < 2 || argc > 3)\n+\t\tusage(_(\"update requires reference name, new value and an optional old value\"));\n+\n+\tif (message && !*message)\n+\t\tdie(_(\"refusing to perform update with empty message\"));\n+\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\trefname = argv[0];\n+\tif (repo_get_oid_with_flags(repo, argv[1], &newoid,\n+\t\t\t\t    GET_OID_SKIP_AMBIGUITY_CHECK))\n+\t\tdie(_(\"invalid new object ID: '%s'\"), argv[1]);\n+\tif (argc == 3 &&\n+\t    repo_get_oid_with_flags(repo, argv[2], &oldoid,\n+\t\t\t\t    GET_OID_SKIP_AMBIGUITY_CHECK))\n+\t\tdie(_(\"invalid old object ID: '%s'\"), argv[2]);\n+\n+\tret = refs_update_ref(get_main_ref_store(repo), message, refname,\n+\t\t\t      &newoid, argc == 3 ? &oldoid : NULL, flags,\n+\t\t\t      UPDATE_REFS_MSG_ON_ERR);\n+\n+\tif (ret < 0)\n+\t\tret = 1;\n+\treturn ret;\n+}\n+\n int cmd_refs(int argc,\n \t     const char **argv,\n \t     const char *prefix,\n@@ -236,6 +289,7 @@ int cmd_refs(int argc,\n \t\tREFS_EXISTS_USAGE,\n \t\tREFS_OPTIMIZE_USAGE,\n \t\tREFS_DELETE_USAGE,\n+\t\tREFS_UPDATE_USAGE,\n \t\tNULL,\n \t};\n \tparse_opt_subcommand_fn *fn = NULL;\n@@ -246,6 +300,7 @@ int cmd_refs(int argc,\n \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_refs_exists),\n \t\tOPT_SUBCOMMAND(\"optimize\", &fn, cmd_refs_optimize),\n \t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_refs_delete),\n+\t\tOPT_SUBCOMMAND(\"update\", &fn, cmd_refs_update),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex 1ccf08a3b5..2063962dab 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -224,6 +224,7 @@ integration_tests = [\n   't1462-refs-exists.sh',\n   't1463-refs-optimize.sh',\n   't1464-refs-delete.sh',\n+  't1465-refs-update.sh',\n   't1500-rev-parse.sh',\n   't1501-work-tree.sh',\n   't1502-rev-parse-parseopt.sh',\ndiff --git a/t/t1465-refs-update.sh b/t/t1465-refs-update.sh\nnew file mode 100755\nindex 0000000000..a9becdda99\n--- /dev/null\n+++ b/t/t1465-refs-update.sh\n@@ -0,0 +1,268 @@\n+#!/bin/sh\n+\n+test_description='git refs update'\n+\n+. ./test-lib.sh\n+\n+setup_repo () {\n+\tgit init \"$1\" &&\n+\ttest_commit -C \"$1\" A &&\n+\ttest_commit -C \"$1\" B\n+}\n+\n+test_ref_matches () {\n+\tgit rev-parse \"$1\" >expect &&\n+\techo \"$2\" >actual &&\n+\ttest_cmp expect actual\n+}\n+\n+test_expect_success 'update creates a new reference' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_ref_matches refs/heads/foo \"$A\"\n+\t)\n+'\n+\n+test_expect_success 'update an existing reference without oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs update refs/heads/foo $B &&\n+\t\ttest_ref_matches refs/heads/foo $B\n+\t)\n+'\n+\n+test_expect_success 'update with matching oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs update refs/heads/foo $B $A &&\n+\t\ttest_ref_matches refs/heads/foo $B\n+\t)\n+'\n+\n+test_expect_success 'update with stale oldvalue fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs update refs/heads/foo $B $B 2>err &&\n+\t\ttest_grep \" but expected \" err &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+test_expect_success 'update can create a new branch with oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A $ZERO_OID 2>err &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+test_expect_success 'update can create a new branch without oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A 2>err &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+test_expect_success 'update refuses to create preexisting branch' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err &&\n+\t\ttest_grep \"reference already exists\" err &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+test_expect_success 'update can delete a branch with oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A 2>err &&\n+\t\tgit refs update refs/heads/foo $ZERO_OID $A 2>err &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'update can delete a branch without oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A 2>err &&\n+\t\tgit refs update refs/heads/foo $ZERO_OID 2>err &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'update refuses to delete a branch with mismatching value' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A 2>err &&\n+\t\ttest_must_fail git refs update refs/heads/foo $ZERO_OID $B 2>err &&\n+\t\ttest_grep \" but expected \" err &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'update refuses to create preexisting branch' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err &&\n+\t\ttest_grep \"reference already exists\" err &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+\n+test_expect_success 'update with invalid new value fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git refs update refs/heads/foo invalid-oid 2>err &&\n+\t\ttest_grep \"invalid new object ID\" err &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'update with invalid old value fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs update refs/heads/foo $B invalid-oid 2>err &&\n+\t\ttest_grep \"invalid old object ID\" err &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+test_expect_success 'update --no-deref rewrites the symref itself' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/foo &&\n+\t\tgit refs update --no-deref refs/heads/symref $B &&\n+\t\ttest_must_fail git symbolic-ref refs/heads/symref &&\n+\t\ttest_ref_matches refs/heads/symref $B &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+test_expect_success 'update does not create a reflog by default' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/foo $A &&\n+\t\ttest_must_fail git reflog exists refs/foo\n+\t)\n+'\n+\n+test_expect_success 'update creates a reflog with --create-reflog' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update --create-reflog refs/foo $A &&\n+\t\tgit reflog exists refs/foo\n+\t)\n+'\n+\n+test_expect_success 'update with message records reason in reflog' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs update --message=update-reason refs/heads/foo $B &&\n+\t\tgit reflog show refs/heads/foo >actual &&\n+\t\ttest_grep \"update-reason$\" actual\n+\t)\n+'\n+\n+test_expect_success 'update with empty message fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs update --message= refs/heads/foo $B 2>err &&\n+\t\ttest_grep \"empty message\" err\n+\t)\n+'\n+\n+test_expect_success 'update with too few arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git -C repo refs update refs/heads/foo 2>err &&\n+\ttest_grep \"requires reference name, new value\" err\n+'\n+\n+test_expect_success 'update with too many arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\ttest_must_fail git refs update refs/heads/foo $A $B extra 2>err &&\n+\t\ttest_grep \"requires reference name, new value\" err\n+\t)\n+'\n+\n+test_done\n\n-- \n2.55.0.rc0.786.g65d90a0328.dirty\n\n"},{"id":"545750","messageId":"20260617-pks-refs-writing-subcommands-v2-4-07f3d18336f9@pks.im","threadId":"65819","inReplyTo":"20260617-pks-refs-writing-subcommands-v2-0-07f3d18336f9@pks.im","subject":"[PATCH v2 4/5] builtin/refs: add \"create\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-17T10:16:01Z","receivedAt":"2026-06-17T10:16:23Z","isPatch":true,"body":"The \"update\" subcommand cannot only update an existing reference, but it\ncan also create new branches and delete existing branches by specifying\nthe all-zeroes object ID as either old or new value. Despite that, we\nalready have the \"delete\" subcommand as a handy shortcut so that a user\ncan easily delete a branch. This relieves them of needing to understand\nthe more arcane uses of the \"update\" command, and of counting the number\nof zeroes they need to pass.\n\nBut while we have a \"delete\" subcommand, we don't have an equivalent\nthat would allow the user to create a new branch, which creates a\ncertain asymmetry.\n\nAdd a new \"create\" subcommand to plug this gap.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-refs.adoc |   5 ++\n builtin/refs.c              |  52 +++++++++++++++\n t/meson.build               |   1 +\n t/t1466-refs-create.sh      | 151 ++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 209 insertions(+)\n\ndiff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc\nindex 6475bdcc62..e6a3528349 100644\n--- a/Documentation/git-refs.adoc\n+++ b/Documentation/git-refs.adoc\n@@ -20,6 +20,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]\n \t\t   [ --stdin | (<pattern>...)]\n git refs exists <ref>\n git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]\n+git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>\n git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\n git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\n \n@@ -53,6 +54,10 @@ optimize::\n \tusage. This subcommand is an alias for linkgit:git-pack-refs[1] and\n \toffers identical functionality.\n \n+create::\n+\tCreate the given reference, which must not already exist, pointing at\n+\t`<new-value>`.\n+\n delete::\n \tDelete the given reference. This subcommand mirrors `git update-ref -d`\n \t(see linkgit:git-update-ref[1]). When `<old-value>` is given, the\ndiff --git a/builtin/refs.c b/builtin/refs.c\nindex 08453ae1c8..92e62fd5df 100644\n--- a/builtin/refs.c\n+++ b/builtin/refs.c\n@@ -21,6 +21,9 @@\n #define REFS_OPTIMIZE_USAGE \\\n \tN_(\"git refs optimize \" PACK_REFS_OPTS)\n \n+#define REFS_CREATE_USAGE \\\n+\tN_(\"git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>\")\n+\n #define REFS_DELETE_USAGE \\\n \tN_(\"git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\")\n \n@@ -181,6 +184,53 @@ static int cmd_refs_optimize(int argc, const char **argv, const char *prefix,\n \treturn pack_refs_core(argc, argv, prefix, repo, refs_optimize_usage);\n }\n \n+static int cmd_refs_create(int argc, const char **argv, const char *prefix,\n+\t\t\t   struct repository *repo)\n+{\n+\tstatic char const * const refs_create_usage[] = {\n+\t\tREFS_CREATE_USAGE,\n+\t\tNULL\n+\t};\n+\tconst char *message = NULL;\n+\tunsigned flags = 0;\n+\tstruct option opts[] = {\n+\t\tOPT_STRING(0, \"message\", &message, N_(\"reason\"),\n+\t\t\t   N_(\"reason of the update\")),\n+\t\tOPT_BIT(0 ,\"no-deref\", &flags,\n+\t\t\tN_(\"update <refname> not the one it points to\"),\n+\t\t\tREF_NO_DEREF),\n+\t\tOPT_BIT(0, \"create-reflog\", &flags, N_(\"create a reflog\"),\n+\t\t\tREF_FORCE_CREATE_REFLOG),\n+\t\tOPT_END(),\n+\t};\n+\tstruct object_id newoid;\n+\tconst char *refname;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, refs_create_usage, 0);\n+\tif (argc != 2)\n+\t\tusage(_(\"create requires reference name and an object ID\"));\n+\n+\tif (message && !*message)\n+\t\tdie(_(\"refusing to perform update with empty message\"));\n+\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\trefname = argv[0];\n+\tif (repo_get_oid_with_flags(repo, argv[1], &newoid, GET_OID_SKIP_AMBIGUITY_CHECK))\n+\t\tdie(_(\"invalid object ID: '%s'\"), argv[1]);\n+\tif (is_null_oid(&newoid))\n+\t\tdie(_(\"cannot create reference with null old object ID\"));\n+\n+\tret = refs_update_ref(get_main_ref_store(repo), message, refname,\n+\t\t\t      &newoid, null_oid(repo->hash_algo), flags,\n+\t\t\t      UPDATE_REFS_MSG_ON_ERR);\n+\n+\tif (ret < 0)\n+\t\tret = 1;\n+\treturn ret;\n+}\n+\n static int cmd_refs_delete(int argc, const char **argv, const char *prefix,\n \t\t\t   struct repository *repo)\n {\n@@ -288,6 +338,7 @@ int cmd_refs(int argc,\n \t\t\"git refs list \" COMMON_USAGE_FOR_EACH_REF,\n \t\tREFS_EXISTS_USAGE,\n \t\tREFS_OPTIMIZE_USAGE,\n+\t\tREFS_CREATE_USAGE,\n \t\tREFS_DELETE_USAGE,\n \t\tREFS_UPDATE_USAGE,\n \t\tNULL,\n@@ -299,6 +350,7 @@ int cmd_refs(int argc,\n \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_refs_list),\n \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_refs_exists),\n \t\tOPT_SUBCOMMAND(\"optimize\", &fn, cmd_refs_optimize),\n+\t\tOPT_SUBCOMMAND(\"create\", &fn, cmd_refs_create),\n \t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_refs_delete),\n \t\tOPT_SUBCOMMAND(\"update\", &fn, cmd_refs_update),\n \t\tOPT_END(),\ndiff --git a/t/meson.build b/t/meson.build\nindex 2063962dab..541e6f919c 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -225,6 +225,7 @@ integration_tests = [\n   't1463-refs-optimize.sh',\n   't1464-refs-delete.sh',\n   't1465-refs-update.sh',\n+  't1466-refs-create.sh',\n   't1500-rev-parse.sh',\n   't1501-work-tree.sh',\n   't1502-rev-parse-parseopt.sh',\ndiff --git a/t/t1466-refs-create.sh b/t/t1466-refs-create.sh\nnew file mode 100755\nindex 0000000000..85c8bd6ea2\n--- /dev/null\n+++ b/t/t1466-refs-create.sh\n@@ -0,0 +1,151 @@\n+#!/bin/sh\n+\n+test_description='git refs create'\n+\n+. ./test-lib.sh\n+\n+setup_repo () {\n+\tgit init \"$1\" &&\n+\ttest_commit -C \"$1\" A &&\n+\ttest_commit -C \"$1\" B\n+}\n+\n+test_ref_matches () {\n+\tgit rev-parse \"$1\" >expect &&\n+\techo \"$2\" >actual &&\n+\ttest_cmp expect actual\n+}\n+\n+test_expect_success 'create a new reference' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs create refs/heads/foo $A &&\n+\t\ttest_ref_matches refs/heads/foo \"$A\"\n+\t)\n+'\n+\n+test_expect_success 'create fails when the reference already exists' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs create refs/heads/foo $A &&\n+\t\ttest_must_fail git refs create refs/heads/foo $B 2>err &&\n+\t\ttest_grep \"reference already exists\" err &&\n+\t\ttest_ref_matches refs/heads/foo \"$A\"\n+\t)\n+'\n+\n+test_expect_success 'create with null new value fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git refs create refs/heads/foo $ZERO_OID 2>err &&\n+\t\ttest_grep \"null old object ID\" err &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'create with invalid new value fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git refs create refs/heads/foo invalid-oid 2>err &&\n+\t\ttest_grep \"invalid object ID\" err &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'create does not create a reflog by default' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs create refs/foo $A &&\n+\t\ttest_must_fail git reflog exists refs/foo\n+\t)\n+'\n+\n+test_expect_success 'create creates a reflog with --create-reflog' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs create --create-reflog refs/foo $A &&\n+\t\tgit reflog exists refs/foo\n+\t)\n+'\n+\n+test_expect_success 'create with message records reason in reflog' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs create --message=\"create reason\" refs/heads/foo $A &&\n+\t\tgit reflog show refs/heads/foo >actual &&\n+\t\ttest_grep \"create reason$\" actual\n+\t)\n+'\n+\n+test_expect_success 'create with symref target creates target reference' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/target &&\n+\t\tgit refs create refs/heads/symref $A &&\n+\t\tgit reflog exists refs/heads/target\n+\t)\n+'\n+\n+test_expect_success 'create with symref target and --no-deref refuses to create reference' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/target &&\n+\t\ttest_must_fail git refs create --no-deref refs/heads/symref $A 2>err &&\n+\t\ttest_grep \"dangling symref already exists\" err &&\n+\t\ttest_must_fail git reflog exists refs/heads/target\n+\t)\n+'\n+\n+test_expect_success 'create with empty message fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\ttest_must_fail git refs create --message= refs/heads/foo $A 2>err &&\n+\t\ttest_grep \"empty message\" err &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'create without arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git -C repo refs create 2>err &&\n+\ttest_grep \"requires reference name\" err\n+'\n+\n+test_expect_success 'create with too many arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git -C repo refs create refs/heads/foo a b 2>err &&\n+\ttest_grep \"requires reference name\" err\n+'\n+\n+test_done\n\n-- \n2.55.0.rc0.786.g65d90a0328.dirty\n\n"},{"id":"545751","messageId":"20260617-pks-refs-writing-subcommands-v2-5-07f3d18336f9@pks.im","threadId":"65819","inReplyTo":"20260617-pks-refs-writing-subcommands-v2-0-07f3d18336f9@pks.im","subject":"[PATCH v2 5/5] builtin/refs: add \"rename\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-17T10:16:02Z","receivedAt":"2026-06-17T10:16:25Z","isPatch":true,"body":"Add a \"rename\" subcommand to git-refs(1) with the syntax:\n\n  $ git refs rename <oldref> <newref>\n\nIt renames <oldref> together with its reflog to <newref>; even when used\non a local branch ref, the current value and the reflog of the ref are\nthe only things that are renamed. Document it and redirect casual users\nto \"git branch -m\" if that is what they wanted to do.\n\nCo-authored-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-refs.adoc |   6 ++\n builtin/refs.c              |  49 +++++++++++++++++\n t/meson.build               |   1 +\n t/t1467-refs-rename.sh      | 131 ++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 187 insertions(+)\n\ndiff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc\nindex e6a3528349..ce278c59bf 100644\n--- a/Documentation/git-refs.adoc\n+++ b/Documentation/git-refs.adoc\n@@ -23,6 +23,7 @@ git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude\n git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>\n git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\n git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\n+git refs rename [--message=<reason>] <old-ref> <new-ref>\n \n DESCRIPTION\n -----------\n@@ -71,6 +72,11 @@ update::\n \t`<new-value>` deletes the branch, whereas an all-zeroes `<old-value>`\n \tensures that the branch does not yet exist.\n \n+rename::\n+\tRename the reference `<oldref>` to `<newref>`. The old reference must\n+\texist and the new reference must not yet exist, and both must have a\n+\twell-formed name (see linkgit:git-check-ref-format[1]).\n+\n OPTIONS\n -------\n \ndiff --git a/builtin/refs.c b/builtin/refs.c\nindex 92e62fd5df..c7aa1a327f 100644\n--- a/builtin/refs.c\n+++ b/builtin/refs.c\n@@ -30,6 +30,9 @@\n #define REFS_UPDATE_USAGE \\\n \tN_(\"git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\")\n \n+#define REFS_RENAME_USAGE \\\n+\tN_(\"git refs rename [--message=<reason>] <old-ref> <new-ref>\")\n+\n static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,\n \t\t\t    struct repository *repo)\n {\n@@ -327,6 +330,50 @@ static int cmd_refs_update(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int cmd_refs_rename(int argc, const char **argv, const char *prefix,\n+\t\t\t   struct repository *repo)\n+{\n+\tstatic char const * const refs_rename_usage[] = {\n+\t\tREFS_RENAME_USAGE,\n+\t\tNULL\n+\t};\n+\tconst char *message = NULL;\n+\tstruct option opts[] = {\n+\t\tOPT_STRING(0, \"message\", &message, N_(\"reason\"),\n+\t\t\t   N_(\"reason of the update\")),\n+\t\tOPT_END(),\n+\t};\n+\tconst char *oldref, *newref;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, refs_rename_usage, 0);\n+\tif (argc != 2)\n+\t\tusage(_(\"rename requires old and new reference name\"));\n+\tif (message && !*message)\n+\t\tdie(_(\"refusing to perform update with empty message\"));\n+\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\toldref = argv[0];\n+\tnewref = argv[1];\n+\n+\tif (check_refname_format(oldref, 0))\n+\t\tdie(_(\"invalid ref format: '%s'\"), oldref);\n+\tif (check_refname_format(newref, 0))\n+\t\tdie(_(\"invalid ref format: '%s'\"), newref);\n+\n+\tif (!refs_ref_exists(get_main_ref_store(repo), oldref))\n+\t\tdie(_(\"reference does not exist: '%s'\"), oldref);\n+\tif (refs_ref_exists(get_main_ref_store(repo), newref))\n+\t\tdie(_(\"reference already exists: '%s'\"), newref);\n+\n+\tret = refs_rename_ref(get_main_ref_store(repo), oldref, newref, message);\n+\n+\tif (ret < 0)\n+\t\tret = 1;\n+\treturn ret;\n+}\n+\n int cmd_refs(int argc,\n \t     const char **argv,\n \t     const char *prefix,\n@@ -341,6 +388,7 @@ int cmd_refs(int argc,\n \t\tREFS_CREATE_USAGE,\n \t\tREFS_DELETE_USAGE,\n \t\tREFS_UPDATE_USAGE,\n+\t\tREFS_RENAME_USAGE,\n \t\tNULL,\n \t};\n \tparse_opt_subcommand_fn *fn = NULL;\n@@ -353,6 +401,7 @@ int cmd_refs(int argc,\n \t\tOPT_SUBCOMMAND(\"create\", &fn, cmd_refs_create),\n \t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_refs_delete),\n \t\tOPT_SUBCOMMAND(\"update\", &fn, cmd_refs_update),\n+\t\tOPT_SUBCOMMAND(\"rename\", &fn, cmd_refs_rename),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex 541e6f919c..a39fd8c4c4 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -226,6 +226,7 @@ integration_tests = [\n   't1464-refs-delete.sh',\n   't1465-refs-update.sh',\n   't1466-refs-create.sh',\n+  't1467-refs-rename.sh',\n   't1500-rev-parse.sh',\n   't1501-work-tree.sh',\n   't1502-rev-parse-parseopt.sh',\ndiff --git a/t/t1467-refs-rename.sh b/t/t1467-refs-rename.sh\nnew file mode 100755\nindex 0000000000..f80d58e0f4\n--- /dev/null\n+++ b/t/t1467-refs-rename.sh\n@@ -0,0 +1,131 @@\n+#!/bin/sh\n+\n+test_description='git refs rename'\n+\n+. ./test-lib.sh\n+\n+setup_repo () {\n+\tgit init \"$1\" &&\n+\ttest_commit -C \"$1\" A &&\n+\ttest_commit -C \"$1\" B\n+}\n+\n+test_ref_matches () {\n+\tgit rev-parse \"$1\" >expect &&\n+\techo \"$2\" >actual &&\n+\ttest_cmp expect actual\n+}\n+\n+test_expect_success 'rename an existing reference' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs rename refs/heads/foo refs/heads/bar &&\n+\t\ttest_must_fail git refs exists refs/heads/foo &&\n+\t\ttest_ref_matches refs/heads/bar $A\n+\t)\n+'\n+\n+test_expect_success 'rename moves the reflog along with the reference' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update --message=\"rename me\" refs/heads/foo $A &&\n+\t\tgit refs rename refs/heads/foo refs/heads/bar &&\n+\t\tgit reflog show refs/heads/bar >reflog &&\n+\t\ttest_grep \"rename me\" reflog &&\n+\t\ttest_must_fail git reflog exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'rename with message records reason in reflog' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs rename --message=\"rename reason\" refs/heads/foo refs/heads/bar &&\n+\t\tgit reflog show refs/heads/bar >actual &&\n+\t\ttest_grep \"rename reason\" actual\n+\t)\n+'\n+\n+test_expect_success 'rename a nonexistent reference fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git refs rename refs/heads/foo refs/heads/bar 2>err &&\n+\t\ttest_grep \"reference does not exist\" err\n+\t)\n+'\n+\n+test_expect_success 'rename to an existing reference fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs update refs/heads/bar $B &&\n+\t\ttest_must_fail git refs rename refs/heads/foo refs/heads/bar 2>err &&\n+\t\ttest_grep \"reference already exists\" err\n+\t)\n+'\n+\n+test_expect_success 'rename with empty message fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs rename --message= refs/heads/foo refs/heads/bar 2>err &&\n+\t\ttest_grep \"empty message\" err\n+\t)\n+'\n+\n+test_expect_success 'rename with invalid old reference name fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git refs rename \"refs/heads/foo..bar\" refs/heads/bar 2>err &&\n+\t\ttest_grep \"invalid ref format\" err\n+\t)\n+'\n+\n+test_expect_success 'rename with invalid new reference name fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs rename refs/heads/foo \"refs/heads/bar..baz\" 2>err &&\n+\t\ttest_grep \"invalid ref format\" err\n+\t)\n+'\n+\n+test_expect_success 'rename with too few arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git -C repo refs rename refs/heads/foo 2>err &&\n+\ttest_grep \"requires old and new reference name\" err\n+'\n+\n+test_expect_success 'rename with too many arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git -C repo refs rename refs/heads/foo refs/heads/bar refs/heads/baz 2>err &&\n+\ttest_grep \"requires old and new reference name\" err\n+'\n+\n+test_done\n\n-- \n2.55.0.rc0.786.g65d90a0328.dirty\n\n"},{"id":"545760","messageId":"xmqqwlvxz7rr.fsf@gitster.g","threadId":"65819","inReplyTo":"ajJMnZchqdpiuKTg@pks.im","subject":"Re: [PATCH 3/4] builtin/refs: add \"update\" subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-17T12:11:20Z","receivedAt":"2026-06-17T12:11:22Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> We can:\n>\n>     $ git update-ref $NEW_OID $NULL_OID\n>     $ git refs update $NEW_OID $NULL_OID\n>\n> This will verify that the reference doesn't exist before actually\n> writing it. Will add a test.\n\nI think refname is missing from the command line, but the above is\ngood.  I forgot we had update-ref already doing that ;-)\n\nThanks.\n"},{"id":"545761","messageId":"xmqqse6lz7nx.fsf@gitster.g","threadId":"65819","inReplyTo":"ajJMqayXuie1FyIW@pks.im","subject":"Re: [PATCH 4/4] builtin/refs: add \"rename\" subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-17T12:13:38Z","receivedAt":"2026-06-17T12:13:41Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n>> If we rename a ref that does not have a reflog, would it leave the\n>> ref under the new name without reflog, or would we get a reflog with\n>> a single entry that marks the fact the old ref was renamed into the\n>> new ref?  Should that be controlled via --create-reflog option?\n>\n> It would leave it without a reflog. In theory I agree that it might make\n> sense to introduce a \"--create-reflog\" option, but that would require\n> some new plumbing in `refs_rename_ref()`. So I'd say that we can add it\n> at a later point as needed.\n\nOK.\n"},{"id":"545763","messageId":"xmqqjyrxz73b.fsf@gitster.g","threadId":"65819","inReplyTo":"20260617-pks-refs-writing-subcommands-v2-0-07f3d18336f9@pks.im","subject":"Re: [PATCH v2 0/5] builtin/refs: add ability to write references","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-17T12:26:00Z","receivedAt":"2026-06-17T12:26:03Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Hi,\n>\n> Reference-related functionality in Git is currently spread across many\n> different commands: git-update-ref(1), git-for-each-ref(1),\n> git-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it\n> hard for users to discover what functionality we have available to work\n> with references.\n>\n> We have thus started to consolidate this functionality into git-refs(1),\n> which is a toolbox of everything related to references. Until now, the\n> command doesn't handle functionality of git-update-ref(1).\n>\n> This patch series backfills most of the functionality by introducing\n> three new commands:\n>\n>   - `git refs delete` to delete references. This is the equivalent of\n>     `git update-ref -d`.\n>\n>   - `git refs update` to update references. This is the equivalent of\n>     `git update-ref <refname> <oldvalue> <newvalue>`.\n>\n>   - `git refs rename` to rename a reference, including its reflog. This\n>     does not have an equivalent in git-update-ref(1), but is inspired by\n>     and supersedes [1].\n\n... and `git refs create`, but we can guess what it would do ;-).\n\nWill queue.  Thanks.\n"},{"id":"546711","messageId":"xmqqik71ul0j.fsf@gitster.g","threadId":"65819","inReplyTo":"20260617-pks-refs-writing-subcommands-v2-0-07f3d18336f9@pks.im","subject":"Re: [PATCH v2 0/5] builtin/refs: add ability to write references","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-29T20:52:44Z","receivedAt":"2026-06-29T20:52:46Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Reference-related functionality in Git is currently spread across many\n> different commands: git-update-ref(1), git-for-each-ref(1),\n> git-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it\n> hard for users to discover what functionality we have available to work\n> with references.\n>\n> We have thus started to consolidate this functionality into git-refs(1),\n> which is a toolbox of everything related to references. Until now, the\n> command doesn't handle functionality of git-update-ref(1).\n\nThis unfortunately hasn't heard any responses since June 17th, so I\ntook a look at it again myself.  All the things we discussed during\nthe review of the initial round has been addressed, it seems.\n\nShall we mark the topic ready for 'next' now?\n"},{"id":"546713","messageId":"xmqq5x31ukqv.fsf@gitster.g","threadId":"65819","inReplyTo":"20260617-pks-refs-writing-subcommands-v2-4-07f3d18336f9@pks.im","subject":"Re: [PATCH v2 4/5] builtin/refs: add \"create\" subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-29T20:58:32Z","receivedAt":"2026-06-29T20:58:34Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> +\tif (repo_get_oid_with_flags(repo, argv[1], &newoid, GET_OID_SKIP_AMBIGUITY_CHECK))\n> +\t\tdie(_(\"invalid object ID: '%s'\"), argv[1]);\n> +\tif (is_null_oid(&newoid))\n> +\t\tdie(_(\"cannot create reference with null old object ID\"));\n\nAn apparent typo here, \"with null old\" -> \"with null new object\nname\".\n\nOther than that, I think this one is good.\n\n\n"},{"id":"546749","messageId":"akObGuYnEwE2Pgxb@pks.im","threadId":"65819","inReplyTo":"xmqqik71ul0j.fsf@gitster.g","subject":"Re: [PATCH v2 0/5] builtin/refs: add ability to write references","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-30T10:31:54Z","receivedAt":"2026-06-30T10:32:00Z","isPatch":true,"body":"On Mon, Jun 29, 2026 at 01:52:44PM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > Reference-related functionality in Git is currently spread across many\n> > different commands: git-update-ref(1), git-for-each-ref(1),\n> > git-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it\n> > hard for users to discover what functionality we have available to work\n> > with references.\n> >\n> > We have thus started to consolidate this functionality into git-refs(1),\n> > which is a toolbox of everything related to references. Until now, the\n> > command doesn't handle functionality of git-update-ref(1).\n> \n> This unfortunately hasn't heard any responses since June 17th, so I\n> took a look at it again myself.  All the things we discussed during\n> the review of the initial round has been addressed, it seems.\n> \n> Shall we mark the topic ready for 'next' now?\n\nLet me send one last reroll to fix the typo you pointed out. But other\nthan that I think this should be ready.\n\nThanks!\n\nPatrick\n"},{"id":"546750","messageId":"akObH7ZVSKcc9n7z@pks.im","threadId":"65819","inReplyTo":"xmqq5x31ukqv.fsf@gitster.g","subject":"Re: [PATCH v2 4/5] builtin/refs: add \"create\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-30T10:31:59Z","receivedAt":"2026-06-30T10:32:10Z","isPatch":true,"body":"On Mon, Jun 29, 2026 at 01:58:32PM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > +\tif (repo_get_oid_with_flags(repo, argv[1], &newoid, GET_OID_SKIP_AMBIGUITY_CHECK))\n> > +\t\tdie(_(\"invalid object ID: '%s'\"), argv[1]);\n> > +\tif (is_null_oid(&newoid))\n> > +\t\tdie(_(\"cannot create reference with null old object ID\"));\n> \n> An apparent typo here, \"with null old\" -> \"with null new object\n> name\".\n> \n> Other than that, I think this one is good.\n\nYes, indeed, good eyes.\n\nPatrick\n"},{"id":"546770","messageId":"20260630-pks-refs-writing-subcommands-v3-0-deb04de1ecef@pks.im","threadId":"65819","inReplyTo":"20260616-pks-refs-writing-subcommands-v1-0-9f5219b6109d@pks.im","subject":"[PATCH v3 0/5] builtin/refs: add ability to write references","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-30T11:49:03Z","receivedAt":"2026-06-30T11:49:13Z","isPatch":true,"body":"Hi,\n\nReference-related functionality in Git is currently spread across many\ndifferent commands: git-update-ref(1), git-for-each-ref(1),\ngit-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it\nhard for users to discover what functionality we have available to work\nwith references.\n\nWe have thus started to consolidate this functionality into git-refs(1),\nwhich is a toolbox of everything related to references. Until now, the\ncommand doesn't handle functionality of git-update-ref(1).\n\nThis patch series backfills most of the functionality by introducing\nthree new commands:\n\n  - `git refs delete` to delete references. This is the equivalent of\n    `git update-ref -d`.\n\n  - `git refs update` to update references. This is the equivalent of\n    `git update-ref <refname> <oldvalue> <newvalue>`.\n\n  - `git refs rename` to rename a reference, including its reflog. This\n    does not have an equivalent in git-update-ref(1), but is inspired by\n    and supersedes [1].\n\nChanges in v3:\n  - Fix confused error message.\n  - Link to v2: https://patch.msgid.link/20260617-pks-refs-writing-subcommands-v2-0-07f3d18336f9@pks.im\n\nChanges in v2:\n  - Add a new \"create\" subcommand.\n  - Consistently quote in error messages.\n  - Consistently use `<old-value>` in the synopsis.\n  - Don't return negative exit codes.\n  - Improve documentation of \"update\" subcommand to mention that you can\n    create and delete branches.\n  - Add tests to verify that we can use \"update\" to do this, both in\n    racy and raceless ways.\n  - Add missing calls to `repo_config()`.\n  - Drop useless `GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME` variable.\n  - Link to v1: https://patch.msgid.link/20260616-pks-refs-writing-subcommands-v1-0-9f5219b6109d@pks.im\n\nThanks!\n\nPatrick\n\n[1]: <xmqqv7brz9ba.fsf@gitster.g>\n\n---\nPatrick Steinhardt (5):\n      builtin/refs: drop `the_repository`\n      builtin/refs: add \"delete\" subcommand\n      builtin/refs: add \"update\" subcommand\n      builtin/refs: add \"create\" subcommand\n      builtin/refs: add \"rename\" subcommand\n\n Documentation/git-refs.adoc |  40 +++++++\n builtin/refs.c              | 222 ++++++++++++++++++++++++++++++++++--\n t/meson.build               |   4 +\n t/t1464-refs-delete.sh      | 130 +++++++++++++++++++++\n t/t1465-refs-update.sh      | 268 ++++++++++++++++++++++++++++++++++++++++++++\n t/t1466-refs-create.sh      | 151 +++++++++++++++++++++++++\n t/t1467-refs-rename.sh      | 131 ++++++++++++++++++++++\n 7 files changed, 938 insertions(+), 8 deletions(-)\n\nRange-diff versus v2:\n\n1:  2341316537 = 1:  9dd7b70df4 builtin/refs: drop `the_repository`\n2:  40efa6887b = 2:  72341f895e builtin/refs: add \"delete\" subcommand\n3:  2f86fb281d = 3:  bcca5fe8ee builtin/refs: add \"update\" subcommand\n4:  fb75fb72cf ! 4:  6fa75a36bd builtin/refs: add \"create\" subcommand\n    @@ builtin/refs.c: static int cmd_refs_optimize(int argc, const char **argv, const\n     +\tif (repo_get_oid_with_flags(repo, argv[1], &newoid, GET_OID_SKIP_AMBIGUITY_CHECK))\n     +\t\tdie(_(\"invalid object ID: '%s'\"), argv[1]);\n     +\tif (is_null_oid(&newoid))\n    -+\t\tdie(_(\"cannot create reference with null old object ID\"));\n    ++\t\tdie(_(\"cannot create reference with null new object ID\"));\n     +\n     +\tret = refs_update_ref(get_main_ref_store(repo), message, refname,\n     +\t\t\t      &newoid, null_oid(repo->hash_algo), flags,\n    @@ t/t1466-refs-create.sh (new)\n     +\t(\n     +\t\tcd repo &&\n     +\t\ttest_must_fail git refs create refs/heads/foo $ZERO_OID 2>err &&\n    -+\t\ttest_grep \"null old object ID\" err &&\n    ++\t\ttest_grep \"null new object ID\" err &&\n     +\t\ttest_must_fail git refs exists refs/heads/foo\n     +\t)\n     +'\n5:  134b161ec7 = 5:  8e12fe028e builtin/refs: add \"rename\" subcommand\n\n---\nbase-commit: 700432b2ba22603a0bcb71475c9c333d17c9b0d1\nchange-id: 20260616-pks-refs-writing-subcommands-7a77be5bda9b\n\n"},{"id":"546771","messageId":"20260630-pks-refs-writing-subcommands-v3-1-deb04de1ecef@pks.im","threadId":"65819","inReplyTo":"20260630-pks-refs-writing-subcommands-v3-0-deb04de1ecef@pks.im","subject":"[PATCH v3 1/5] builtin/refs: drop `the_repository`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-30T11:49:04Z","receivedAt":"2026-06-30T11:49:15Z","isPatch":true,"body":"We still have a couple of uses of `the_repository` in \"builtin/refs.c\".\nAll of those are trivial to convert though as the command always\nrequires a repository to exist.\n\nConvert them to use the passed-in repository and drop\n`USE_THE_REPOSITORY_VARIABLE`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/refs.c | 15 +++++++--------\n 1 file changed, 7 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/refs.c b/builtin/refs.c\nindex e3125bc61b..f0faabf45a 100644\n--- a/builtin/refs.c\n+++ b/builtin/refs.c\n@@ -1,4 +1,3 @@\n-#define USE_THE_REPOSITORY_VARIABLE\n #include \"builtin.h\"\n #include \"config.h\"\n #include \"fsck.h\"\n@@ -23,7 +22,7 @@\n \tN_(\"git refs optimize \" PACK_REFS_OPTS)\n \n static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,\n-\t\t\t    struct repository *repo UNUSED)\n+\t\t\t    struct repository *repo)\n {\n \tconst char * const migrate_usage[] = {\n \t\tREFS_MIGRATE_USAGE,\n@@ -59,13 +58,13 @@ static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,\n \t\tgoto out;\n \t}\n \n-\tif (the_repository->ref_storage_format == format) {\n+\tif (repo->ref_storage_format == format) {\n \t\terr = error(_(\"repository already uses '%s' format\"),\n \t\t\t    ref_storage_format_to_name(format));\n \t\tgoto out;\n \t}\n \n-\tif (repo_migrate_ref_storage_format(the_repository, format, flags, &errbuf) < 0) {\n+\tif (repo_migrate_ref_storage_format(repo, format, flags, &errbuf) < 0) {\n \t\terr = error(\"%s\", errbuf.buf);\n \t\tgoto out;\n \t}\n@@ -99,8 +98,8 @@ static int cmd_refs_verify(int argc, const char **argv, const char *prefix,\n \tif (argc)\n \t\tusage(_(\"'git refs verify' takes no arguments\"));\n \n-\trepo_config(the_repository, git_fsck_config, &fsck_refs_options);\n-\tprepare_repo_settings(the_repository);\n+\trepo_config(repo, git_fsck_config, &fsck_refs_options);\n+\tprepare_repo_settings(repo);\n \n \tworktrees = get_worktrees_without_reading_head();\n \tfor (size_t i = 0; worktrees[i]; i++)\n@@ -124,7 +123,7 @@ static int cmd_refs_list(int argc, const char **argv, const char *prefix,\n }\n \n static int cmd_refs_exists(int argc, const char **argv, const char *prefix,\n-\t\t\t   struct repository *repo UNUSED)\n+\t\t\t   struct repository *repo)\n {\n \tstruct strbuf unused_referent = STRBUF_INIT;\n \tstruct object_id unused_oid;\n@@ -145,7 +144,7 @@ static int cmd_refs_exists(int argc, const char **argv, const char *prefix,\n \t\tdie(_(\"'git refs exists' requires a reference\"));\n \n \tref = *argv++;\n-\tif (refs_read_raw_ref(get_main_ref_store(the_repository), ref,\n+\tif (refs_read_raw_ref(get_main_ref_store(repo), ref,\n \t\t\t      &unused_oid, &unused_referent, &unused_type,\n \t\t\t      &failure_errno)) {\n \t\tif (failure_errno == ENOENT || failure_errno == EISDIR) {\n\n-- \n2.55.0.795.g602f6c329a.dirty\n\n"},{"id":"546772","messageId":"20260630-pks-refs-writing-subcommands-v3-2-deb04de1ecef@pks.im","threadId":"65819","inReplyTo":"20260630-pks-refs-writing-subcommands-v3-0-deb04de1ecef@pks.im","subject":"[PATCH v3 2/5] builtin/refs: add \"delete\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-30T11:49:05Z","receivedAt":"2026-06-30T11:49:17Z","isPatch":true,"body":"Reference-related functionality in Git is currently spread across many\ndifferent commands: git-update-ref(1), git-for-each-ref(1),\ngit-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it\nhard for users to discover what functionality we have available to work\nwith references.\n\nWe have thus started to consolidate this functionality into git-refs(1),\nwhich is a toolbox of everything related to references. Until now, the\ncommand doesn't handle functionality of git-update-ref(1).\n\nFix this gap by introducing a new \"delete\" subcommand, which is the\nequivalent of `git update-ref -d`.\n\nNote that we're intentionally not using a generic \"write\" subcommand\nwith a \"-d\" flag. This is rather harder to discover, and subcommands\nthat are implmented as flags tend to be hard to reason about in the code\nas we'd have to handle mutually-exclusive flags that stem from the other\nsubcommand-like modes.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-refs.adoc |  17 ++++++\n builtin/refs.c              |  51 +++++++++++++++++\n t/meson.build               |   1 +\n t/t1464-refs-delete.sh      | 130 ++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 199 insertions(+)\n\ndiff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc\nindex fa33680cc7..2633934463 100644\n--- a/Documentation/git-refs.adoc\n+++ b/Documentation/git-refs.adoc\n@@ -20,6 +20,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]\n \t\t   [ --stdin | (<pattern>...)]\n git refs exists <ref>\n git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]\n+git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\n \n DESCRIPTION\n -----------\n@@ -51,6 +52,12 @@ optimize::\n \tusage. This subcommand is an alias for linkgit:git-pack-refs[1] and\n \toffers identical functionality.\n \n+delete::\n+\tDelete the given reference. This subcommand mirrors `git update-ref -d`\n+\t(see linkgit:git-update-ref[1]). When `<old-value>` is given, the\n+\treference is only deleted after verifying that it currently contains\n+\t`<old-value>`.\n+\n OPTIONS\n -------\n \n@@ -90,6 +97,16 @@ The following options are specific to 'git refs optimize':\n \n include::pack-refs-options.adoc[]\n \n+The following options are specific to commands which write references:\n+\n+`--message=<reason>`::\n+\tUse the given <reason> string for the reflog entry associated with the\n+\tupdate. An empty message is rejected.\n+\n+`--no-deref`::\n+\tOperate on <ref> itself rather than the reference it points to via a\n+\tsymbolic ref.\n+\n KNOWN LIMITATIONS\n -----------------\n \ndiff --git a/builtin/refs.c b/builtin/refs.c\nindex f0faabf45a..edb7d61663 100644\n--- a/builtin/refs.c\n+++ b/builtin/refs.c\n@@ -21,6 +21,9 @@\n #define REFS_OPTIMIZE_USAGE \\\n \tN_(\"git refs optimize \" PACK_REFS_OPTS)\n \n+#define REFS_DELETE_USAGE \\\n+\tN_(\"git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\")\n+\n static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,\n \t\t\t    struct repository *repo)\n {\n@@ -175,6 +178,52 @@ static int cmd_refs_optimize(int argc, const char **argv, const char *prefix,\n \treturn pack_refs_core(argc, argv, prefix, repo, refs_optimize_usage);\n }\n \n+static int cmd_refs_delete(int argc, const char **argv, const char *prefix,\n+\t\t\t   struct repository *repo)\n+{\n+\tstatic char const * const refs_delete_usage[] = {\n+\t\tREFS_DELETE_USAGE,\n+\t\tNULL\n+\t};\n+\tconst char *message = NULL;\n+\tunsigned flags = 0;\n+\tstruct option opts[] = {\n+\t\tOPT_STRING(0, \"message\", &message, N_(\"reason\"),\n+\t\t\t   N_(\"reason of the update\")),\n+\t\tOPT_BIT(0 ,\"no-deref\", &flags,\n+\t\t\tN_(\"update <refname> not the one it points to\"),\n+\t\t\tREF_NO_DEREF),\n+\t\tOPT_END(),\n+\t};\n+\tstruct object_id oldoid;\n+\tconst char *refname;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, refs_delete_usage, 0);\n+\tif (argc < 1 || argc > 2)\n+\t\tusage(_(\"delete requires reference name and an optional old object ID\"));\n+\n+\tif (message && !*message)\n+\t\tdie(_(\"refusing to perform update with empty message\"));\n+\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\trefname = argv[0];\n+\tif (argc == 2) {\n+\t\tif (repo_get_oid_with_flags(repo, argv[1], &oldoid, GET_OID_SKIP_AMBIGUITY_CHECK))\n+\t\t\tdie(_(\"invalid old object ID: '%s'\"), argv[1]);\n+\t\tif (is_null_oid(&oldoid))\n+\t\t\tdie(_(\"cannot delete reference with null old object ID\"));\n+\t}\n+\n+\tret = refs_delete_ref(get_main_ref_store(repo), message, refname,\n+\t\t\t      argc == 2 ? &oldoid : NULL, flags);\n+\n+\tif (ret < 0)\n+\t\tret = 1;\n+\treturn ret;\n+}\n+\n int cmd_refs(int argc,\n \t     const char **argv,\n \t     const char *prefix,\n@@ -186,6 +235,7 @@ int cmd_refs(int argc,\n \t\t\"git refs list \" COMMON_USAGE_FOR_EACH_REF,\n \t\tREFS_EXISTS_USAGE,\n \t\tREFS_OPTIMIZE_USAGE,\n+\t\tREFS_DELETE_USAGE,\n \t\tNULL,\n \t};\n \tparse_opt_subcommand_fn *fn = NULL;\n@@ -195,6 +245,7 @@ int cmd_refs(int argc,\n \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_refs_list),\n \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_refs_exists),\n \t\tOPT_SUBCOMMAND(\"optimize\", &fn, cmd_refs_optimize),\n+\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_refs_delete),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex c5832fee05..1ccf08a3b5 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -223,6 +223,7 @@ integration_tests = [\n   't1461-refs-list.sh',\n   't1462-refs-exists.sh',\n   't1463-refs-optimize.sh',\n+  't1464-refs-delete.sh',\n   't1500-rev-parse.sh',\n   't1501-work-tree.sh',\n   't1502-rev-parse-parseopt.sh',\ndiff --git a/t/t1464-refs-delete.sh b/t/t1464-refs-delete.sh\nnew file mode 100755\nindex 0000000000..efff7d0574\n--- /dev/null\n+++ b/t/t1464-refs-delete.sh\n@@ -0,0 +1,130 @@\n+#!/bin/sh\n+\n+test_description='git refs delete'\n+\n+. ./test-lib.sh\n+\n+setup_repo () {\n+\tgit init \"$1\" &&\n+\ttest_commit -C \"$1\" A &&\n+\ttest_commit -C \"$1\" B\n+}\n+\n+test_expect_success 'delete without oldvalue verification' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\tA=$(git -C repo rev-parse A) &&\n+\tgit -C repo update-ref refs/heads/foo $A &&\n+\tgit -C repo refs delete refs/heads/foo &&\n+\ttest_must_fail git -C repo show-ref --verify -q refs/heads/foo\n+'\n+\n+test_expect_success 'delete with matching oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\tgit refs delete refs/heads/foo $A &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete with stale oldvalue fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\ttest_must_fail git refs delete refs/heads/foo $B 2>err &&\n+\t\ttest_grep \" but expected \" err &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete with null oldvalue fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\ttest_must_fail git refs delete refs/heads/foo $ZERO_OID 2>err &&\n+\t\ttest_grep \"null old object ID\" err &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete with invalid oldvalue fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\ttest_must_fail git refs delete refs/heads/foo invalid-oid 2>err &&\n+\t\ttest_grep \"invalid old object ID\" err &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete symref with --no-deref leaves target intact' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/foo &&\n+\t\tgit refs delete --no-deref refs/heads/symref &&\n+\t\ttest_must_fail git refs exists refs/heads/symref &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete with message records reason in reflog' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\tgit symbolic-ref HEAD refs/heads/foo &&\n+\t\tgit refs delete --message=delete-reason refs/heads/foo &&\n+\t\ttest_must_fail git refs exists refs/heads/foo &&\n+\t\ttest-tool ref-store main for-each-reflog-ent HEAD >actual &&\n+\t\ttest_grep \"delete-reason$\" actual\n+\t)\n+'\n+\n+test_expect_success 'delete with empty message fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\ttest_must_fail git refs delete --message= refs/heads/foo 2>err &&\n+\t\ttest_grep \"empty message\" err &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete without arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git -C repo refs delete 2>err &&\n+\ttest_grep \"requires reference name\" err\n+'\n+\n+test_expect_success 'delete with too many arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git refs delete one two three 2>err &&\n+\ttest_grep \"requires reference name\" err\n+'\n+\n+test_done\n\n-- \n2.55.0.795.g602f6c329a.dirty\n\n"},{"id":"546773","messageId":"20260630-pks-refs-writing-subcommands-v3-3-deb04de1ecef@pks.im","threadId":"65819","inReplyTo":"20260630-pks-refs-writing-subcommands-v3-0-deb04de1ecef@pks.im","subject":"[PATCH v3 3/5] builtin/refs: add \"update\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-30T11:49:06Z","receivedAt":"2026-06-30T11:49:19Z","isPatch":true,"body":"Add a new \"update\" subcommand which mirrors `git update-ref <refname>\n<oldoid> <newoid>`. This follows the same reasoning as the preceding\ncommit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-refs.adoc |  12 ++\n builtin/refs.c              |  55 +++++++++\n t/meson.build               |   1 +\n t/t1465-refs-update.sh      | 268 ++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 336 insertions(+)\n\ndiff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc\nindex 2633934463..6475bdcc62 100644\n--- a/Documentation/git-refs.adoc\n+++ b/Documentation/git-refs.adoc\n@@ -21,6 +21,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]\n git refs exists <ref>\n git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]\n git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\n+git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\n \n DESCRIPTION\n -----------\n@@ -58,6 +59,13 @@ delete::\n \treference is only deleted after verifying that it currently contains\n \t`<old-value>`.\n \n+update::\n+\tUpdate the given reference to point at `<new-value>`. If `<old-value>`\n+\tis given, the reference is only updated after verifying that it\n+\tcurrently contains `<old-value>`. As a special case, an all-zeroes\n+\t`<new-value>` deletes the branch, whereas an all-zeroes `<old-value>`\n+\tensures that the branch does not yet exist.\n+\n OPTIONS\n -------\n \n@@ -99,6 +107,10 @@ include::pack-refs-options.adoc[]\n \n The following options are specific to commands which write references:\n \n+`--create-reflog`::\n+\tCreate a reflog for the reference even if one would not ordinarily be\n+\tcreated.\n+\n `--message=<reason>`::\n \tUse the given <reason> string for the reflog entry associated with the\n \tupdate. An empty message is rejected.\ndiff --git a/builtin/refs.c b/builtin/refs.c\nindex edb7d61663..08453ae1c8 100644\n--- a/builtin/refs.c\n+++ b/builtin/refs.c\n@@ -24,6 +24,9 @@\n #define REFS_DELETE_USAGE \\\n \tN_(\"git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\")\n \n+#define REFS_UPDATE_USAGE \\\n+\tN_(\"git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\")\n+\n static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,\n \t\t\t    struct repository *repo)\n {\n@@ -224,6 +227,56 @@ static int cmd_refs_delete(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int cmd_refs_update(int argc, const char **argv, const char *prefix,\n+\t\t\t   struct repository *repo)\n+{\n+\tstatic char const * const refs_update_usage[] = {\n+\t\tREFS_UPDATE_USAGE,\n+\t\tNULL\n+\t};\n+\tconst char *message = NULL;\n+\tunsigned flags = 0;\n+\tstruct option opts[] = {\n+\t\tOPT_STRING(0, \"message\", &message, N_(\"reason\"),\n+\t\t\t   N_(\"reason of the update\")),\n+\t\tOPT_BIT(0 ,\"no-deref\", &flags,\n+\t\t\tN_(\"update <refname> not the one it points to\"),\n+\t\t\tREF_NO_DEREF),\n+\t\tOPT_BIT(0, \"create-reflog\", &flags, N_(\"create a reflog\"),\n+\t\t\tREF_FORCE_CREATE_REFLOG),\n+\t\tOPT_END(),\n+\t};\n+\tstruct object_id newoid, oldoid;\n+\tconst char *refname;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, refs_update_usage, 0);\n+\tif (argc < 2 || argc > 3)\n+\t\tusage(_(\"update requires reference name, new value and an optional old value\"));\n+\n+\tif (message && !*message)\n+\t\tdie(_(\"refusing to perform update with empty message\"));\n+\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\trefname = argv[0];\n+\tif (repo_get_oid_with_flags(repo, argv[1], &newoid,\n+\t\t\t\t    GET_OID_SKIP_AMBIGUITY_CHECK))\n+\t\tdie(_(\"invalid new object ID: '%s'\"), argv[1]);\n+\tif (argc == 3 &&\n+\t    repo_get_oid_with_flags(repo, argv[2], &oldoid,\n+\t\t\t\t    GET_OID_SKIP_AMBIGUITY_CHECK))\n+\t\tdie(_(\"invalid old object ID: '%s'\"), argv[2]);\n+\n+\tret = refs_update_ref(get_main_ref_store(repo), message, refname,\n+\t\t\t      &newoid, argc == 3 ? &oldoid : NULL, flags,\n+\t\t\t      UPDATE_REFS_MSG_ON_ERR);\n+\n+\tif (ret < 0)\n+\t\tret = 1;\n+\treturn ret;\n+}\n+\n int cmd_refs(int argc,\n \t     const char **argv,\n \t     const char *prefix,\n@@ -236,6 +289,7 @@ int cmd_refs(int argc,\n \t\tREFS_EXISTS_USAGE,\n \t\tREFS_OPTIMIZE_USAGE,\n \t\tREFS_DELETE_USAGE,\n+\t\tREFS_UPDATE_USAGE,\n \t\tNULL,\n \t};\n \tparse_opt_subcommand_fn *fn = NULL;\n@@ -246,6 +300,7 @@ int cmd_refs(int argc,\n \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_refs_exists),\n \t\tOPT_SUBCOMMAND(\"optimize\", &fn, cmd_refs_optimize),\n \t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_refs_delete),\n+\t\tOPT_SUBCOMMAND(\"update\", &fn, cmd_refs_update),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex 1ccf08a3b5..2063962dab 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -224,6 +224,7 @@ integration_tests = [\n   't1462-refs-exists.sh',\n   't1463-refs-optimize.sh',\n   't1464-refs-delete.sh',\n+  't1465-refs-update.sh',\n   't1500-rev-parse.sh',\n   't1501-work-tree.sh',\n   't1502-rev-parse-parseopt.sh',\ndiff --git a/t/t1465-refs-update.sh b/t/t1465-refs-update.sh\nnew file mode 100755\nindex 0000000000..a9becdda99\n--- /dev/null\n+++ b/t/t1465-refs-update.sh\n@@ -0,0 +1,268 @@\n+#!/bin/sh\n+\n+test_description='git refs update'\n+\n+. ./test-lib.sh\n+\n+setup_repo () {\n+\tgit init \"$1\" &&\n+\ttest_commit -C \"$1\" A &&\n+\ttest_commit -C \"$1\" B\n+}\n+\n+test_ref_matches () {\n+\tgit rev-parse \"$1\" >expect &&\n+\techo \"$2\" >actual &&\n+\ttest_cmp expect actual\n+}\n+\n+test_expect_success 'update creates a new reference' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_ref_matches refs/heads/foo \"$A\"\n+\t)\n+'\n+\n+test_expect_success 'update an existing reference without oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs update refs/heads/foo $B &&\n+\t\ttest_ref_matches refs/heads/foo $B\n+\t)\n+'\n+\n+test_expect_success 'update with matching oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs update refs/heads/foo $B $A &&\n+\t\ttest_ref_matches refs/heads/foo $B\n+\t)\n+'\n+\n+test_expect_success 'update with stale oldvalue fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs update refs/heads/foo $B $B 2>err &&\n+\t\ttest_grep \" but expected \" err &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+test_expect_success 'update can create a new branch with oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A $ZERO_OID 2>err &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+test_expect_success 'update can create a new branch without oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A 2>err &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+test_expect_success 'update refuses to create preexisting branch' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err &&\n+\t\ttest_grep \"reference already exists\" err &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+test_expect_success 'update can delete a branch with oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A 2>err &&\n+\t\tgit refs update refs/heads/foo $ZERO_OID $A 2>err &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'update can delete a branch without oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A 2>err &&\n+\t\tgit refs update refs/heads/foo $ZERO_OID 2>err &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'update refuses to delete a branch with mismatching value' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A 2>err &&\n+\t\ttest_must_fail git refs update refs/heads/foo $ZERO_OID $B 2>err &&\n+\t\ttest_grep \" but expected \" err &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'update refuses to create preexisting branch' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err &&\n+\t\ttest_grep \"reference already exists\" err &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+\n+test_expect_success 'update with invalid new value fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git refs update refs/heads/foo invalid-oid 2>err &&\n+\t\ttest_grep \"invalid new object ID\" err &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'update with invalid old value fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs update refs/heads/foo $B invalid-oid 2>err &&\n+\t\ttest_grep \"invalid old object ID\" err &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+test_expect_success 'update --no-deref rewrites the symref itself' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/foo &&\n+\t\tgit refs update --no-deref refs/heads/symref $B &&\n+\t\ttest_must_fail git symbolic-ref refs/heads/symref &&\n+\t\ttest_ref_matches refs/heads/symref $B &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+test_expect_success 'update does not create a reflog by default' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/foo $A &&\n+\t\ttest_must_fail git reflog exists refs/foo\n+\t)\n+'\n+\n+test_expect_success 'update creates a reflog with --create-reflog' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update --create-reflog refs/foo $A &&\n+\t\tgit reflog exists refs/foo\n+\t)\n+'\n+\n+test_expect_success 'update with message records reason in reflog' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs update --message=update-reason refs/heads/foo $B &&\n+\t\tgit reflog show refs/heads/foo >actual &&\n+\t\ttest_grep \"update-reason$\" actual\n+\t)\n+'\n+\n+test_expect_success 'update with empty message fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs update --message= refs/heads/foo $B 2>err &&\n+\t\ttest_grep \"empty message\" err\n+\t)\n+'\n+\n+test_expect_success 'update with too few arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git -C repo refs update refs/heads/foo 2>err &&\n+\ttest_grep \"requires reference name, new value\" err\n+'\n+\n+test_expect_success 'update with too many arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\ttest_must_fail git refs update refs/heads/foo $A $B extra 2>err &&\n+\t\ttest_grep \"requires reference name, new value\" err\n+\t)\n+'\n+\n+test_done\n\n-- \n2.55.0.795.g602f6c329a.dirty\n\n"},{"id":"546774","messageId":"20260630-pks-refs-writing-subcommands-v3-4-deb04de1ecef@pks.im","threadId":"65819","inReplyTo":"20260630-pks-refs-writing-subcommands-v3-0-deb04de1ecef@pks.im","subject":"[PATCH v3 4/5] builtin/refs: add \"create\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-30T11:49:07Z","receivedAt":"2026-06-30T11:49:22Z","isPatch":true,"body":"The \"update\" subcommand cannot only update an existing reference, but it\ncan also create new branches and delete existing branches by specifying\nthe all-zeroes object ID as either old or new value. Despite that, we\nalready have the \"delete\" subcommand as a handy shortcut so that a user\ncan easily delete a branch. This relieves them of needing to understand\nthe more arcane uses of the \"update\" command, and of counting the number\nof zeroes they need to pass.\n\nBut while we have a \"delete\" subcommand, we don't have an equivalent\nthat would allow the user to create a new branch, which creates a\ncertain asymmetry.\n\nAdd a new \"create\" subcommand to plug this gap.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-refs.adoc |   5 ++\n builtin/refs.c              |  52 +++++++++++++++\n t/meson.build               |   1 +\n t/t1466-refs-create.sh      | 151 ++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 209 insertions(+)\n\ndiff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc\nindex 6475bdcc62..e6a3528349 100644\n--- a/Documentation/git-refs.adoc\n+++ b/Documentation/git-refs.adoc\n@@ -20,6 +20,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]\n \t\t   [ --stdin | (<pattern>...)]\n git refs exists <ref>\n git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]\n+git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>\n git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\n git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\n \n@@ -53,6 +54,10 @@ optimize::\n \tusage. This subcommand is an alias for linkgit:git-pack-refs[1] and\n \toffers identical functionality.\n \n+create::\n+\tCreate the given reference, which must not already exist, pointing at\n+\t`<new-value>`.\n+\n delete::\n \tDelete the given reference. This subcommand mirrors `git update-ref -d`\n \t(see linkgit:git-update-ref[1]). When `<old-value>` is given, the\ndiff --git a/builtin/refs.c b/builtin/refs.c\nindex 08453ae1c8..1ebaf30149 100644\n--- a/builtin/refs.c\n+++ b/builtin/refs.c\n@@ -21,6 +21,9 @@\n #define REFS_OPTIMIZE_USAGE \\\n \tN_(\"git refs optimize \" PACK_REFS_OPTS)\n \n+#define REFS_CREATE_USAGE \\\n+\tN_(\"git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>\")\n+\n #define REFS_DELETE_USAGE \\\n \tN_(\"git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\")\n \n@@ -181,6 +184,53 @@ static int cmd_refs_optimize(int argc, const char **argv, const char *prefix,\n \treturn pack_refs_core(argc, argv, prefix, repo, refs_optimize_usage);\n }\n \n+static int cmd_refs_create(int argc, const char **argv, const char *prefix,\n+\t\t\t   struct repository *repo)\n+{\n+\tstatic char const * const refs_create_usage[] = {\n+\t\tREFS_CREATE_USAGE,\n+\t\tNULL\n+\t};\n+\tconst char *message = NULL;\n+\tunsigned flags = 0;\n+\tstruct option opts[] = {\n+\t\tOPT_STRING(0, \"message\", &message, N_(\"reason\"),\n+\t\t\t   N_(\"reason of the update\")),\n+\t\tOPT_BIT(0 ,\"no-deref\", &flags,\n+\t\t\tN_(\"update <refname> not the one it points to\"),\n+\t\t\tREF_NO_DEREF),\n+\t\tOPT_BIT(0, \"create-reflog\", &flags, N_(\"create a reflog\"),\n+\t\t\tREF_FORCE_CREATE_REFLOG),\n+\t\tOPT_END(),\n+\t};\n+\tstruct object_id newoid;\n+\tconst char *refname;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, refs_create_usage, 0);\n+\tif (argc != 2)\n+\t\tusage(_(\"create requires reference name and an object ID\"));\n+\n+\tif (message && !*message)\n+\t\tdie(_(\"refusing to perform update with empty message\"));\n+\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\trefname = argv[0];\n+\tif (repo_get_oid_with_flags(repo, argv[1], &newoid, GET_OID_SKIP_AMBIGUITY_CHECK))\n+\t\tdie(_(\"invalid object ID: '%s'\"), argv[1]);\n+\tif (is_null_oid(&newoid))\n+\t\tdie(_(\"cannot create reference with null new object ID\"));\n+\n+\tret = refs_update_ref(get_main_ref_store(repo), message, refname,\n+\t\t\t      &newoid, null_oid(repo->hash_algo), flags,\n+\t\t\t      UPDATE_REFS_MSG_ON_ERR);\n+\n+\tif (ret < 0)\n+\t\tret = 1;\n+\treturn ret;\n+}\n+\n static int cmd_refs_delete(int argc, const char **argv, const char *prefix,\n \t\t\t   struct repository *repo)\n {\n@@ -288,6 +338,7 @@ int cmd_refs(int argc,\n \t\t\"git refs list \" COMMON_USAGE_FOR_EACH_REF,\n \t\tREFS_EXISTS_USAGE,\n \t\tREFS_OPTIMIZE_USAGE,\n+\t\tREFS_CREATE_USAGE,\n \t\tREFS_DELETE_USAGE,\n \t\tREFS_UPDATE_USAGE,\n \t\tNULL,\n@@ -299,6 +350,7 @@ int cmd_refs(int argc,\n \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_refs_list),\n \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_refs_exists),\n \t\tOPT_SUBCOMMAND(\"optimize\", &fn, cmd_refs_optimize),\n+\t\tOPT_SUBCOMMAND(\"create\", &fn, cmd_refs_create),\n \t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_refs_delete),\n \t\tOPT_SUBCOMMAND(\"update\", &fn, cmd_refs_update),\n \t\tOPT_END(),\ndiff --git a/t/meson.build b/t/meson.build\nindex 2063962dab..541e6f919c 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -225,6 +225,7 @@ integration_tests = [\n   't1463-refs-optimize.sh',\n   't1464-refs-delete.sh',\n   't1465-refs-update.sh',\n+  't1466-refs-create.sh',\n   't1500-rev-parse.sh',\n   't1501-work-tree.sh',\n   't1502-rev-parse-parseopt.sh',\ndiff --git a/t/t1466-refs-create.sh b/t/t1466-refs-create.sh\nnew file mode 100755\nindex 0000000000..cfb21bf863\n--- /dev/null\n+++ b/t/t1466-refs-create.sh\n@@ -0,0 +1,151 @@\n+#!/bin/sh\n+\n+test_description='git refs create'\n+\n+. ./test-lib.sh\n+\n+setup_repo () {\n+\tgit init \"$1\" &&\n+\ttest_commit -C \"$1\" A &&\n+\ttest_commit -C \"$1\" B\n+}\n+\n+test_ref_matches () {\n+\tgit rev-parse \"$1\" >expect &&\n+\techo \"$2\" >actual &&\n+\ttest_cmp expect actual\n+}\n+\n+test_expect_success 'create a new reference' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs create refs/heads/foo $A &&\n+\t\ttest_ref_matches refs/heads/foo \"$A\"\n+\t)\n+'\n+\n+test_expect_success 'create fails when the reference already exists' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs create refs/heads/foo $A &&\n+\t\ttest_must_fail git refs create refs/heads/foo $B 2>err &&\n+\t\ttest_grep \"reference already exists\" err &&\n+\t\ttest_ref_matches refs/heads/foo \"$A\"\n+\t)\n+'\n+\n+test_expect_success 'create with null new value fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git refs create refs/heads/foo $ZERO_OID 2>err &&\n+\t\ttest_grep \"null new object ID\" err &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'create with invalid new value fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git refs create refs/heads/foo invalid-oid 2>err &&\n+\t\ttest_grep \"invalid object ID\" err &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'create does not create a reflog by default' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs create refs/foo $A &&\n+\t\ttest_must_fail git reflog exists refs/foo\n+\t)\n+'\n+\n+test_expect_success 'create creates a reflog with --create-reflog' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs create --create-reflog refs/foo $A &&\n+\t\tgit reflog exists refs/foo\n+\t)\n+'\n+\n+test_expect_success 'create with message records reason in reflog' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs create --message=\"create reason\" refs/heads/foo $A &&\n+\t\tgit reflog show refs/heads/foo >actual &&\n+\t\ttest_grep \"create reason$\" actual\n+\t)\n+'\n+\n+test_expect_success 'create with symref target creates target reference' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/target &&\n+\t\tgit refs create refs/heads/symref $A &&\n+\t\tgit reflog exists refs/heads/target\n+\t)\n+'\n+\n+test_expect_success 'create with symref target and --no-deref refuses to create reference' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/target &&\n+\t\ttest_must_fail git refs create --no-deref refs/heads/symref $A 2>err &&\n+\t\ttest_grep \"dangling symref already exists\" err &&\n+\t\ttest_must_fail git reflog exists refs/heads/target\n+\t)\n+'\n+\n+test_expect_success 'create with empty message fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\ttest_must_fail git refs create --message= refs/heads/foo $A 2>err &&\n+\t\ttest_grep \"empty message\" err &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'create without arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git -C repo refs create 2>err &&\n+\ttest_grep \"requires reference name\" err\n+'\n+\n+test_expect_success 'create with too many arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git -C repo refs create refs/heads/foo a b 2>err &&\n+\ttest_grep \"requires reference name\" err\n+'\n+\n+test_done\n\n-- \n2.55.0.795.g602f6c329a.dirty\n\n"},{"id":"546775","messageId":"20260630-pks-refs-writing-subcommands-v3-5-deb04de1ecef@pks.im","threadId":"65819","inReplyTo":"20260630-pks-refs-writing-subcommands-v3-0-deb04de1ecef@pks.im","subject":"[PATCH v3 5/5] builtin/refs: add \"rename\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-30T11:49:08Z","receivedAt":"2026-06-30T11:49:24Z","isPatch":true,"body":"Add a \"rename\" subcommand to git-refs(1) with the syntax:\n\n  $ git refs rename <oldref> <newref>\n\nIt renames <oldref> together with its reflog to <newref>; even when used\non a local branch ref, the current value and the reflog of the ref are\nthe only things that are renamed. Document it and redirect casual users\nto \"git branch -m\" if that is what they wanted to do.\n\nCo-authored-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-refs.adoc |   6 ++\n builtin/refs.c              |  49 +++++++++++++++++\n t/meson.build               |   1 +\n t/t1467-refs-rename.sh      | 131 ++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 187 insertions(+)\n\ndiff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc\nindex e6a3528349..ce278c59bf 100644\n--- a/Documentation/git-refs.adoc\n+++ b/Documentation/git-refs.adoc\n@@ -23,6 +23,7 @@ git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude\n git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>\n git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\n git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\n+git refs rename [--message=<reason>] <old-ref> <new-ref>\n \n DESCRIPTION\n -----------\n@@ -71,6 +72,11 @@ update::\n \t`<new-value>` deletes the branch, whereas an all-zeroes `<old-value>`\n \tensures that the branch does not yet exist.\n \n+rename::\n+\tRename the reference `<oldref>` to `<newref>`. The old reference must\n+\texist and the new reference must not yet exist, and both must have a\n+\twell-formed name (see linkgit:git-check-ref-format[1]).\n+\n OPTIONS\n -------\n \ndiff --git a/builtin/refs.c b/builtin/refs.c\nindex 1ebaf30149..a9ca2058ee 100644\n--- a/builtin/refs.c\n+++ b/builtin/refs.c\n@@ -30,6 +30,9 @@\n #define REFS_UPDATE_USAGE \\\n \tN_(\"git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\")\n \n+#define REFS_RENAME_USAGE \\\n+\tN_(\"git refs rename [--message=<reason>] <old-ref> <new-ref>\")\n+\n static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,\n \t\t\t    struct repository *repo)\n {\n@@ -327,6 +330,50 @@ static int cmd_refs_update(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int cmd_refs_rename(int argc, const char **argv, const char *prefix,\n+\t\t\t   struct repository *repo)\n+{\n+\tstatic char const * const refs_rename_usage[] = {\n+\t\tREFS_RENAME_USAGE,\n+\t\tNULL\n+\t};\n+\tconst char *message = NULL;\n+\tstruct option opts[] = {\n+\t\tOPT_STRING(0, \"message\", &message, N_(\"reason\"),\n+\t\t\t   N_(\"reason of the update\")),\n+\t\tOPT_END(),\n+\t};\n+\tconst char *oldref, *newref;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, refs_rename_usage, 0);\n+\tif (argc != 2)\n+\t\tusage(_(\"rename requires old and new reference name\"));\n+\tif (message && !*message)\n+\t\tdie(_(\"refusing to perform update with empty message\"));\n+\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\toldref = argv[0];\n+\tnewref = argv[1];\n+\n+\tif (check_refname_format(oldref, 0))\n+\t\tdie(_(\"invalid ref format: '%s'\"), oldref);\n+\tif (check_refname_format(newref, 0))\n+\t\tdie(_(\"invalid ref format: '%s'\"), newref);\n+\n+\tif (!refs_ref_exists(get_main_ref_store(repo), oldref))\n+\t\tdie(_(\"reference does not exist: '%s'\"), oldref);\n+\tif (refs_ref_exists(get_main_ref_store(repo), newref))\n+\t\tdie(_(\"reference already exists: '%s'\"), newref);\n+\n+\tret = refs_rename_ref(get_main_ref_store(repo), oldref, newref, message);\n+\n+\tif (ret < 0)\n+\t\tret = 1;\n+\treturn ret;\n+}\n+\n int cmd_refs(int argc,\n \t     const char **argv,\n \t     const char *prefix,\n@@ -341,6 +388,7 @@ int cmd_refs(int argc,\n \t\tREFS_CREATE_USAGE,\n \t\tREFS_DELETE_USAGE,\n \t\tREFS_UPDATE_USAGE,\n+\t\tREFS_RENAME_USAGE,\n \t\tNULL,\n \t};\n \tparse_opt_subcommand_fn *fn = NULL;\n@@ -353,6 +401,7 @@ int cmd_refs(int argc,\n \t\tOPT_SUBCOMMAND(\"create\", &fn, cmd_refs_create),\n \t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_refs_delete),\n \t\tOPT_SUBCOMMAND(\"update\", &fn, cmd_refs_update),\n+\t\tOPT_SUBCOMMAND(\"rename\", &fn, cmd_refs_rename),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex 541e6f919c..a39fd8c4c4 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -226,6 +226,7 @@ integration_tests = [\n   't1464-refs-delete.sh',\n   't1465-refs-update.sh',\n   't1466-refs-create.sh',\n+  't1467-refs-rename.sh',\n   't1500-rev-parse.sh',\n   't1501-work-tree.sh',\n   't1502-rev-parse-parseopt.sh',\ndiff --git a/t/t1467-refs-rename.sh b/t/t1467-refs-rename.sh\nnew file mode 100755\nindex 0000000000..f80d58e0f4\n--- /dev/null\n+++ b/t/t1467-refs-rename.sh\n@@ -0,0 +1,131 @@\n+#!/bin/sh\n+\n+test_description='git refs rename'\n+\n+. ./test-lib.sh\n+\n+setup_repo () {\n+\tgit init \"$1\" &&\n+\ttest_commit -C \"$1\" A &&\n+\ttest_commit -C \"$1\" B\n+}\n+\n+test_ref_matches () {\n+\tgit rev-parse \"$1\" >expect &&\n+\techo \"$2\" >actual &&\n+\ttest_cmp expect actual\n+}\n+\n+test_expect_success 'rename an existing reference' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs rename refs/heads/foo refs/heads/bar &&\n+\t\ttest_must_fail git refs exists refs/heads/foo &&\n+\t\ttest_ref_matches refs/heads/bar $A\n+\t)\n+'\n+\n+test_expect_success 'rename moves the reflog along with the reference' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update --message=\"rename me\" refs/heads/foo $A &&\n+\t\tgit refs rename refs/heads/foo refs/heads/bar &&\n+\t\tgit reflog show refs/heads/bar >reflog &&\n+\t\ttest_grep \"rename me\" reflog &&\n+\t\ttest_must_fail git reflog exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'rename with message records reason in reflog' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs rename --message=\"rename reason\" refs/heads/foo refs/heads/bar &&\n+\t\tgit reflog show refs/heads/bar >actual &&\n+\t\ttest_grep \"rename reason\" actual\n+\t)\n+'\n+\n+test_expect_success 'rename a nonexistent reference fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git refs rename refs/heads/foo refs/heads/bar 2>err &&\n+\t\ttest_grep \"reference does not exist\" err\n+\t)\n+'\n+\n+test_expect_success 'rename to an existing reference fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs update refs/heads/bar $B &&\n+\t\ttest_must_fail git refs rename refs/heads/foo refs/heads/bar 2>err &&\n+\t\ttest_grep \"reference already exists\" err\n+\t)\n+'\n+\n+test_expect_success 'rename with empty message fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs rename --message= refs/heads/foo refs/heads/bar 2>err &&\n+\t\ttest_grep \"empty message\" err\n+\t)\n+'\n+\n+test_expect_success 'rename with invalid old reference name fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git refs rename \"refs/heads/foo..bar\" refs/heads/bar 2>err &&\n+\t\ttest_grep \"invalid ref format\" err\n+\t)\n+'\n+\n+test_expect_success 'rename with invalid new reference name fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs rename refs/heads/foo \"refs/heads/bar..baz\" 2>err &&\n+\t\ttest_grep \"invalid ref format\" err\n+\t)\n+'\n+\n+test_expect_success 'rename with too few arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git -C repo refs rename refs/heads/foo 2>err &&\n+\ttest_grep \"requires old and new reference name\" err\n+'\n+\n+test_expect_success 'rename with too many arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git -C repo refs rename refs/heads/foo refs/heads/bar refs/heads/baz 2>err &&\n+\ttest_grep \"requires old and new reference name\" err\n+'\n+\n+test_done\n\n-- \n2.55.0.795.g602f6c329a.dirty\n\n"},{"id":"546804","messageId":"xmqqcxx7susi.fsf@gitster.g","threadId":"65819","inReplyTo":"20260630-pks-refs-writing-subcommands-v3-0-deb04de1ecef@pks.im","subject":"Re: [PATCH v3 0/5] builtin/refs: add ability to write references","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-30T19:16:45Z","receivedAt":"2026-06-30T19:16:57Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n>     @@ builtin/refs.c: static int cmd_refs_optimize(int argc, const char **argv, const\n>      +\tif (repo_get_oid_with_flags(repo, argv[1], &newoid, GET_OID_SKIP_AMBIGUITY_CHECK))\n>      +\t\tdie(_(\"invalid object ID: '%s'\"), argv[1]);\n>      +\tif (is_null_oid(&newoid))\n>     -+\t\tdie(_(\"cannot create reference with null old object ID\"));\n>     ++\t\tdie(_(\"cannot create reference with null new object ID\"));\n>      +\n>      +\tret = refs_update_ref(get_main_ref_store(repo), message, refname,\n>      +\t\t\t      &newoid, null_oid(repo->hash_algo), flags,\n>     @@ t/t1466-refs-create.sh (new)\n>      +\t(\n>      +\t\tcd repo &&\n>      +\t\ttest_must_fail git refs create refs/heads/foo $ZERO_OID 2>err &&\n>     -+\t\ttest_grep \"null old object ID\" err &&\n>     ++\t\ttest_grep \"null new object ID\" err &&\n>      +\t\ttest_must_fail git refs exists refs/heads/foo\n>      +\t)\n>      +'\n\nLooks obviously correct.  Thanks.\n"},{"id":"547067","messageId":"87tsqg2vja.fsf@emacs.iotcl.com","threadId":"65819","inReplyTo":"20260630-pks-refs-writing-subcommands-v3-2-deb04de1ecef@pks.im","subject":"Re: [PATCH v3 2/5] builtin/refs: add \"delete\" subcommand","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-07-03T10:54:17Z","receivedAt":"2026-07-03T10:54:28Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Reference-related functionality in Git is currently spread across many\n> different commands: git-update-ref(1), git-for-each-ref(1),\n> git-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it\n> hard for users to discover what functionality we have available to work\n> with references.\n>\n> We have thus started to consolidate this functionality into git-refs(1),\n> which is a toolbox of everything related to references. Until now, the\n> command doesn't handle functionality of git-update-ref(1).\n>\n> Fix this gap by introducing a new \"delete\" subcommand, which is the\n> equivalent of `git update-ref -d`.\n>\n> Note that we're intentionally not using a generic \"write\" subcommand\n> with a \"-d\" flag. This is rather harder to discover, and subcommands\n> that are implmented as flags tend to be hard to reason about in the code\n> as we'd have to handle mutually-exclusive flags that stem from the other\n> subcommand-like modes.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Documentation/git-refs.adoc |  17 ++++++\n>  builtin/refs.c              |  51 +++++++++++++++++\n>  t/meson.build               |   1 +\n>  t/t1464-refs-delete.sh      | 130 ++++++++++++++++++++++++++++++++++++++++++++\n>  4 files changed, 199 insertions(+)\n>\n> diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc\n> index fa33680cc7..2633934463 100644\n> --- a/Documentation/git-refs.adoc\n> +++ b/Documentation/git-refs.adoc\n> @@ -20,6 +20,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]\n>  \t\t   [ --stdin | (<pattern>...)]\n>  git refs exists <ref>\n>  git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]\n> +git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\n>  \n>  DESCRIPTION\n>  -----------\n> @@ -51,6 +52,12 @@ optimize::\n>  \tusage. This subcommand is an alias for linkgit:git-pack-refs[1] and\n>  \toffers identical functionality.\n>  \n> +delete::\n> +\tDelete the given reference. This subcommand mirrors `git update-ref -d`\n> +\t(see linkgit:git-update-ref[1]). When `<old-value>` is given, the\n> +\treference is only deleted after verifying that it currently contains\n> +\t`<old-value>`.\n> +\n>  OPTIONS\n>  -------\n>  \n> @@ -90,6 +97,16 @@ The following options are specific to 'git refs optimize':\n>  \n>  include::pack-refs-options.adoc[]\n>  \n> +The following options are specific to commands which write references:\n> +\n> +`--message=<reason>`::\n> +\tUse the given <reason> string for the reflog entry associated with the\n> +\tupdate. An empty message is rejected.\n> +\n> +`--no-deref`::\n> +\tOperate on <ref> itself rather than the reference it points to via a\n> +\tsymbolic ref.\n> +\n>  KNOWN LIMITATIONS\n>  -----------------\n>  \n> diff --git a/builtin/refs.c b/builtin/refs.c\n> index f0faabf45a..edb7d61663 100644\n> --- a/builtin/refs.c\n> +++ b/builtin/refs.c\n> @@ -21,6 +21,9 @@\n>  #define REFS_OPTIMIZE_USAGE \\\n>  \tN_(\"git refs optimize \" PACK_REFS_OPTS)\n>  \n> +#define REFS_DELETE_USAGE \\\n> +\tN_(\"git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\")\n> +\n>  static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,\n>  \t\t\t    struct repository *repo)\n>  {\n> @@ -175,6 +178,52 @@ static int cmd_refs_optimize(int argc, const char **argv, const char *prefix,\n>  \treturn pack_refs_core(argc, argv, prefix, repo, refs_optimize_usage);\n>  }\n>  \n> +static int cmd_refs_delete(int argc, const char **argv, const char *prefix,\n> +\t\t\t   struct repository *repo)\n> +{\n> +\tstatic char const * const refs_delete_usage[] = {\n> +\t\tREFS_DELETE_USAGE,\n> +\t\tNULL\n> +\t};\n> +\tconst char *message = NULL;\n> +\tunsigned flags = 0;\n> +\tstruct option opts[] = {\n> +\t\tOPT_STRING(0, \"message\", &message, N_(\"reason\"),\n> +\t\t\t   N_(\"reason of the update\")),\n> +\t\tOPT_BIT(0 ,\"no-deref\", &flags,\n> +\t\t\tN_(\"update <refname> not the one it points to\"),\n> +\t\t\tREF_NO_DEREF),\n\nWould it make sense to allow both --deref and --no-deref? (and --deref\nbeing the default)\n\n> +\t\tOPT_END(),\n> +\t};\n> +\tstruct object_id oldoid;\n> +\tconst char *refname;\n> +\tint ret;\n> +\n> +\targc = parse_options(argc, argv, prefix, opts, refs_delete_usage, 0);\n> +\tif (argc < 1 || argc > 2)\n> +\t\tusage(_(\"delete requires reference name and an optional old object ID\"));\n> +\n> +\tif (message && !*message)\n> +\t\tdie(_(\"refusing to perform update with empty message\"));\n> +\n> +\trepo_config(repo, git_default_config, NULL);\n> +\n> +\trefname = argv[0];\n> +\tif (argc == 2) {\n> +\t\tif (repo_get_oid_with_flags(repo, argv[1], &oldoid, GET_OID_SKIP_AMBIGUITY_CHECK))\n> +\t\t\tdie(_(\"invalid old object ID: '%s'\"), argv[1]);\n> +\t\tif (is_null_oid(&oldoid))\n> +\t\t\tdie(_(\"cannot delete reference with null old object ID\"));\n> +\t}\n> +\n> +\tret = refs_delete_ref(get_main_ref_store(repo), message, refname,\n> +\t\t\t      argc == 2 ? &oldoid : NULL, flags);\n> +\n> +\tif (ret < 0)\n> +\t\tret = 1;\n> +\treturn ret;\n> +}\n> +\n>  int cmd_refs(int argc,\n>  \t     const char **argv,\n>  \t     const char *prefix,\n> @@ -186,6 +235,7 @@ int cmd_refs(int argc,\n>  \t\t\"git refs list \" COMMON_USAGE_FOR_EACH_REF,\n>  \t\tREFS_EXISTS_USAGE,\n>  \t\tREFS_OPTIMIZE_USAGE,\n> +\t\tREFS_DELETE_USAGE,\n>  \t\tNULL,\n>  \t};\n>  \tparse_opt_subcommand_fn *fn = NULL;\n> @@ -195,6 +245,7 @@ int cmd_refs(int argc,\n>  \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_refs_list),\n>  \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_refs_exists),\n>  \t\tOPT_SUBCOMMAND(\"optimize\", &fn, cmd_refs_optimize),\n> +\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_refs_delete),\n>  \t\tOPT_END(),\n>  \t};\n>  \n> diff --git a/t/meson.build b/t/meson.build\n> index c5832fee05..1ccf08a3b5 100644\n> --- a/t/meson.build\n> +++ b/t/meson.build\n> @@ -223,6 +223,7 @@ integration_tests = [\n>    't1461-refs-list.sh',\n>    't1462-refs-exists.sh',\n>    't1463-refs-optimize.sh',\n> +  't1464-refs-delete.sh',\n>    't1500-rev-parse.sh',\n>    't1501-work-tree.sh',\n>    't1502-rev-parse-parseopt.sh',\n> diff --git a/t/t1464-refs-delete.sh b/t/t1464-refs-delete.sh\n> new file mode 100755\n> index 0000000000..efff7d0574\n> --- /dev/null\n> +++ b/t/t1464-refs-delete.sh\n> @@ -0,0 +1,130 @@\n> +#!/bin/sh\n> +\n> +test_description='git refs delete'\n> +\n> +. ./test-lib.sh\n> +\n> +setup_repo () {\n> +\tgit init \"$1\" &&\n> +\ttest_commit -C \"$1\" A &&\n> +\ttest_commit -C \"$1\" B\n> +}\n> +\n> +test_expect_success 'delete without oldvalue verification' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\tA=$(git -C repo rev-parse A) &&\n> +\tgit -C repo update-ref refs/heads/foo $A &&\n> +\tgit -C repo refs delete refs/heads/foo &&\n> +\ttest_must_fail git -C repo show-ref --verify -q refs/heads/foo\n\nWhy not use `git refs exists` here? And why use `git -C repo` in this\ntest, and `cd repo` in the other?\n\n> +'\n> +\n> +test_expect_success 'delete with matching oldvalue' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\tA=$(git rev-parse A) &&\n> +\t\tgit update-ref refs/heads/foo $A &&\n> +\t\tgit refs delete refs/heads/foo $A &&\n> +\t\ttest_must_fail git refs exists refs/heads/foo\n> +\t)\n> +'\n> +\n> +test_expect_success 'delete with stale oldvalue fails' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\tA=$(git rev-parse A) &&\n> +\t\tB=$(git rev-parse B) &&\n> +\t\tgit update-ref refs/heads/foo $A &&\n> +\t\ttest_must_fail git refs delete refs/heads/foo $B 2>err &&\n> +\t\ttest_grep \" but expected \" err &&\n> +\t\tgit refs exists refs/heads/foo\n> +\t)\n> +'\n> +\n> +test_expect_success 'delete with null oldvalue fails' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\tA=$(git rev-parse A) &&\n> +\t\tgit update-ref refs/heads/foo $A &&\n> +\t\ttest_must_fail git refs delete refs/heads/foo $ZERO_OID 2>err &&\n> +\t\ttest_grep \"null old object ID\" err &&\n> +\t\tgit refs exists refs/heads/foo\n> +\t)\n> +'\n> +\n> +test_expect_success 'delete with invalid oldvalue fails' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\tA=$(git rev-parse A) &&\n> +\t\tgit update-ref refs/heads/foo $A &&\n> +\t\ttest_must_fail git refs delete refs/heads/foo invalid-oid 2>err &&\n> +\t\ttest_grep \"invalid old object ID\" err &&\n> +\t\tgit refs exists refs/heads/foo\n> +\t)\n> +'\n> +\n> +test_expect_success 'delete symref with --no-deref leaves target intact' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\tA=$(git rev-parse A) &&\n> +\t\tgit update-ref refs/heads/foo $A &&\n> +\t\tgit symbolic-ref refs/heads/symref refs/heads/foo &&\n> +\t\tgit refs delete --no-deref refs/heads/symref &&\n> +\t\ttest_must_fail git refs exists refs/heads/symref &&\n> +\t\tgit refs exists refs/heads/foo\n> +\t)\n\nWhat happens if you delete a symref and provide an <old-value>?\n\n> +'\n> +\n> +test_expect_success 'delete with message records reason in reflog' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\tA=$(git rev-parse A) &&\n> +\t\tgit update-ref refs/heads/foo $A &&\n> +\t\tgit symbolic-ref HEAD refs/heads/foo &&\n> +\t\tgit refs delete --message=delete-reason refs/heads/foo &&\n> +\t\ttest_must_fail git refs exists refs/heads/foo &&\n> +\t\ttest-tool ref-store main for-each-reflog-ent HEAD >actual &&\n> +\t\ttest_grep \"delete-reason$\" actual\n> +\t)\n> +'\n> +\n> +test_expect_success 'delete with empty message fails' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\tA=$(git rev-parse A) &&\n> +\t\tgit update-ref refs/heads/foo $A &&\n> +\t\ttest_must_fail git refs delete --message= refs/heads/foo 2>err &&\n> +\t\ttest_grep \"empty message\" err &&\n> +\t\tgit refs exists refs/heads/foo\n> +\t)\n> +'\n> +\n> +test_expect_success 'delete without arguments fails' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\ttest_must_fail git -C repo refs delete 2>err &&\n> +\ttest_grep \"requires reference name\" err\n> +'\n> +\n> +test_expect_success 'delete with too many arguments fails' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\ttest_must_fail git refs delete one two three 2>err &&\n> +\ttest_grep \"requires reference name\" err\n> +'\n> +\n> +test_done\n>\n> -- \n> 2.55.0.795.g602f6c329a.dirty\n>\n>\n\n-- \nCheers,\nToon\n"},{"id":"547077","messageId":"aketWqvDbsJ1VyXe@pks.im","threadId":"65819","inReplyTo":"87tsqg2vja.fsf@emacs.iotcl.com","subject":"Re: [PATCH v3 2/5] builtin/refs: add \"delete\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-03T12:38:50Z","receivedAt":"2026-07-03T12:38:56Z","isPatch":true,"body":"On Fri, Jul 03, 2026 at 12:54:17PM +0200, Toon Claes wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> > diff --git a/builtin/refs.c b/builtin/refs.c\n> > index f0faabf45a..edb7d61663 100644\n> > --- a/builtin/refs.c\n> > +++ b/builtin/refs.c\n> > @@ -175,6 +178,52 @@ static int cmd_refs_optimize(int argc, const char **argv, const char *prefix,\n> >  \treturn pack_refs_core(argc, argv, prefix, repo, refs_optimize_usage);\n> >  }\n> >  \n> > +static int cmd_refs_delete(int argc, const char **argv, const char *prefix,\n> > +\t\t\t   struct repository *repo)\n> > +{\n> > +\tstatic char const * const refs_delete_usage[] = {\n> > +\t\tREFS_DELETE_USAGE,\n> > +\t\tNULL\n> > +\t};\n> > +\tconst char *message = NULL;\n> > +\tunsigned flags = 0;\n> > +\tstruct option opts[] = {\n> > +\t\tOPT_STRING(0, \"message\", &message, N_(\"reason\"),\n> > +\t\t\t   N_(\"reason of the update\")),\n> > +\t\tOPT_BIT(0 ,\"no-deref\", &flags,\n> > +\t\t\tN_(\"update <refname> not the one it points to\"),\n> > +\t\t\tREF_NO_DEREF),\n> \n> Would it make sense to allow both --deref and --no-deref? (and --deref\n> being the default)\n\nOur \"parse-options.h\" subsystem is clever enough to make this work\nalready :)\n\n> > diff --git a/t/t1464-refs-delete.sh b/t/t1464-refs-delete.sh\n> > new file mode 100755\n> > index 0000000000..efff7d0574\n> > --- /dev/null\n> > +++ b/t/t1464-refs-delete.sh\n> > @@ -0,0 +1,130 @@\n> > +#!/bin/sh\n> > +\n> > +test_description='git refs delete'\n> > +\n> > +. ./test-lib.sh\n> > +\n> > +setup_repo () {\n> > +\tgit init \"$1\" &&\n> > +\ttest_commit -C \"$1\" A &&\n> > +\ttest_commit -C \"$1\" B\n> > +}\n> > +\n> > +test_expect_success 'delete without oldvalue verification' '\n> > +\ttest_when_finished \"rm -rf repo\" &&\n> > +\tsetup_repo repo &&\n> > +\tA=$(git -C repo rev-parse A) &&\n> > +\tgit -C repo update-ref refs/heads/foo $A &&\n> > +\tgit -C repo refs delete refs/heads/foo &&\n> > +\ttest_must_fail git -C repo show-ref --verify -q refs/heads/foo\n> \n> Why not use `git refs exists` here? And why use `git -C repo` in this\n> test, and `cd repo` in the other?\n\nYeah, there isn't really a good reason.\n\n[snip]\n> > +test_expect_success 'delete symref with --no-deref leaves target intact' '\n> > +\ttest_when_finished \"rm -rf repo\" &&\n> > +\tsetup_repo repo &&\n> > +\t(\n> > +\t\tcd repo &&\n> > +\t\tA=$(git rev-parse A) &&\n> > +\t\tgit update-ref refs/heads/foo $A &&\n> > +\t\tgit symbolic-ref refs/heads/symref refs/heads/foo &&\n> > +\t\tgit refs delete --no-deref refs/heads/symref &&\n> > +\t\ttest_must_fail git refs exists refs/heads/symref &&\n> > +\t\tgit refs exists refs/heads/foo\n> > +\t)\n> \n> What happens if you delete a symref and provide an <old-value>?\n\nThat's a good question indeed. It verifies that the symref target\nresolves to <old-value>. That's the exact same behaviour as `git\nupdate-ref -d`, even though it may be a bit on the funny side.\n\nI've fixed up the above test and added another test for this behaviour,\nbut I'm not sure whether it makes sense to send another reroll for this.\n\nThanks!\n\nPatrick\n"},{"id":"547095","messageId":"87qzlk2m0h.fsf@emacs.iotcl.com","threadId":"65819","inReplyTo":"20260630-pks-refs-writing-subcommands-v3-4-deb04de1ecef@pks.im","subject":"Re: [PATCH v3 4/5] builtin/refs: add \"create\" subcommand","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-07-03T14:19:58Z","receivedAt":"2026-07-03T14:20:06Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> The \"update\" subcommand cannot only update an existing reference, but it\n> can also create new branches and delete existing branches by specifying\n> the all-zeroes object ID as either old or new value. Despite that, we\n> already have the \"delete\" subcommand as a handy shortcut so that a user\n> can easily delete a branch. This relieves them of needing to understand\n> the more arcane uses of the \"update\" command, and of counting the number\n> of zeroes they need to pass.\n>\n> But while we have a \"delete\" subcommand, we don't have an equivalent\n> that would allow the user to create a new branch, which creates a\n> certain asymmetry.\n>\n> Add a new \"create\" subcommand to plug this gap.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Documentation/git-refs.adoc |   5 ++\n>  builtin/refs.c              |  52 +++++++++++++++\n>  t/meson.build               |   1 +\n>  t/t1466-refs-create.sh      | 151 ++++++++++++++++++++++++++++++++++++++++++++\n>  4 files changed, 209 insertions(+)\n>\n> diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc\n> index 6475bdcc62..e6a3528349 100644\n> --- a/Documentation/git-refs.adoc\n> +++ b/Documentation/git-refs.adoc\n> @@ -20,6 +20,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]\n>  \t\t   [ --stdin | (<pattern>...)]\n>  git refs exists <ref>\n>  git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]\n> +git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>\n>  git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\n>  git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\n>  \n> @@ -53,6 +54,10 @@ optimize::\n>  \tusage. This subcommand is an alias for linkgit:git-pack-refs[1] and\n>  \toffers identical functionality.\n>  \n> +create::\n> +\tCreate the given reference, which must not already exist, pointing at\n> +\t`<new-value>`.\n> +\n>  delete::\n>  \tDelete the given reference. This subcommand mirrors `git update-ref -d`\n>  \t(see linkgit:git-update-ref[1]). When `<old-value>` is given, the\n> diff --git a/builtin/refs.c b/builtin/refs.c\n> index 08453ae1c8..1ebaf30149 100644\n> --- a/builtin/refs.c\n> +++ b/builtin/refs.c\n> @@ -21,6 +21,9 @@\n>  #define REFS_OPTIMIZE_USAGE \\\n>  \tN_(\"git refs optimize \" PACK_REFS_OPTS)\n>  \n> +#define REFS_CREATE_USAGE \\\n> +\tN_(\"git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>\")\n> +\n>  #define REFS_DELETE_USAGE \\\n>  \tN_(\"git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\")\n>  \n> @@ -181,6 +184,53 @@ static int cmd_refs_optimize(int argc, const char **argv, const char *prefix,\n>  \treturn pack_refs_core(argc, argv, prefix, repo, refs_optimize_usage);\n>  }\n>  \n> +static int cmd_refs_create(int argc, const char **argv, const char *prefix,\n> +\t\t\t   struct repository *repo)\n> +{\n> +\tstatic char const * const refs_create_usage[] = {\n> +\t\tREFS_CREATE_USAGE,\n> +\t\tNULL\n> +\t};\n> +\tconst char *message = NULL;\n> +\tunsigned flags = 0;\n> +\tstruct option opts[] = {\n> +\t\tOPT_STRING(0, \"message\", &message, N_(\"reason\"),\n> +\t\t\t   N_(\"reason of the update\")),\n> +\t\tOPT_BIT(0 ,\"no-deref\", &flags,\n> +\t\t\tN_(\"update <refname> not the one it points to\"),\n> +\t\t\tREF_NO_DEREF),\n\nCan `git refs create --no-deref` be used to create symrefs? Should we\nadd a test for that? Or can it not\n\nI understand the symmetry, but does it make sense to ask the user to\ncreate symrefs with `--no-deref`? Feels a bit obscure. The docs say:\n\n`--no-deref`::\n\tOperate on <ref> itself rather than the reference it points to via a\n\tsymbolic ref.\n\nThat's far from obvious for a user to realize they need to pass that\noption if they want to create a symref.\n\n> +\t\tOPT_BIT(0, \"create-reflog\", &flags, N_(\"create a reflog\"),\n> +\t\t\tREF_FORCE_CREATE_REFLOG),\n> +\t\tOPT_END(),\n> +\t};\n> +\tstruct object_id newoid;\n> +\tconst char *refname;\n> +\tint ret;\n> +\n> +\targc = parse_options(argc, argv, prefix, opts, refs_create_usage, 0);\n> +\tif (argc != 2)\n> +\t\tusage(_(\"create requires reference name and an object ID\"));\n> +\n> +\tif (message && !*message)\n> +\t\tdie(_(\"refusing to perform update with empty message\"));\n> +\n> +\trepo_config(repo, git_default_config, NULL);\n> +\n> +\trefname = argv[0];\n> +\tif (repo_get_oid_with_flags(repo, argv[1], &newoid, GET_OID_SKIP_AMBIGUITY_CHECK))\n> +\t\tdie(_(\"invalid object ID: '%s'\"), argv[1]);\n> +\tif (is_null_oid(&newoid))\n> +\t\tdie(_(\"cannot create reference with null new object ID\"));\n> +\n> +\tret = refs_update_ref(get_main_ref_store(repo), message, refname,\n> +\t\t\t      &newoid, null_oid(repo->hash_algo), flags,\n> +\t\t\t      UPDATE_REFS_MSG_ON_ERR);\n> +\n> +\tif (ret < 0)\n> +\t\tret = 1;\n> +\treturn ret;\n> +}\n> +\n>  static int cmd_refs_delete(int argc, const char **argv, const char *prefix,\n>  \t\t\t   struct repository *repo)\n>  {\n> @@ -288,6 +338,7 @@ int cmd_refs(int argc,\n>  \t\t\"git refs list \" COMMON_USAGE_FOR_EACH_REF,\n>  \t\tREFS_EXISTS_USAGE,\n>  \t\tREFS_OPTIMIZE_USAGE,\n> +\t\tREFS_CREATE_USAGE,\n>  \t\tREFS_DELETE_USAGE,\n>  \t\tREFS_UPDATE_USAGE,\n>  \t\tNULL,\n> @@ -299,6 +350,7 @@ int cmd_refs(int argc,\n>  \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_refs_list),\n>  \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_refs_exists),\n>  \t\tOPT_SUBCOMMAND(\"optimize\", &fn, cmd_refs_optimize),\n> +\t\tOPT_SUBCOMMAND(\"create\", &fn, cmd_refs_create),\n>  \t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_refs_delete),\n>  \t\tOPT_SUBCOMMAND(\"update\", &fn, cmd_refs_update),\n>  \t\tOPT_END(),\n> diff --git a/t/meson.build b/t/meson.build\n> index 2063962dab..541e6f919c 100644\n> --- a/t/meson.build\n> +++ b/t/meson.build\n> @@ -225,6 +225,7 @@ integration_tests = [\n>    't1463-refs-optimize.sh',\n>    't1464-refs-delete.sh',\n>    't1465-refs-update.sh',\n> +  't1466-refs-create.sh',\n>    't1500-rev-parse.sh',\n>    't1501-work-tree.sh',\n>    't1502-rev-parse-parseopt.sh',\n> diff --git a/t/t1466-refs-create.sh b/t/t1466-refs-create.sh\n> new file mode 100755\n> index 0000000000..cfb21bf863\n> --- /dev/null\n> +++ b/t/t1466-refs-create.sh\n> @@ -0,0 +1,151 @@\n> +#!/bin/sh\n> +\n> +test_description='git refs create'\n> +\n> +. ./test-lib.sh\n> +\n> +setup_repo () {\n> +\tgit init \"$1\" &&\n> +\ttest_commit -C \"$1\" A &&\n> +\ttest_commit -C \"$1\" B\n> +}\n> +\n> +test_ref_matches () {\n> +\tgit rev-parse \"$1\" >expect &&\n> +\techo \"$2\" >actual &&\n> +\ttest_cmp expect actual\n> +}\n> +\n> +test_expect_success 'create a new reference' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\tA=$(git rev-parse A) &&\n> +\t\tgit refs create refs/heads/foo $A &&\n> +\t\ttest_ref_matches refs/heads/foo \"$A\"\n> +\t)\n> +'\n> +\n> +test_expect_success 'create fails when the reference already exists' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\tA=$(git rev-parse A) &&\n> +\t\tB=$(git rev-parse B) &&\n> +\t\tgit refs create refs/heads/foo $A &&\n> +\t\ttest_must_fail git refs create refs/heads/foo $B 2>err &&\n> +\t\ttest_grep \"reference already exists\" err &&\n> +\t\ttest_ref_matches refs/heads/foo \"$A\"\n> +\t)\n> +'\n\nI was curious about this test:\n\n\ttest_expect_success 'create succeed when the reference exists with the same value' '\n\t\ttest_when_finished \"rm -rf repo\" &&\n\t\tsetup_repo repo &&\n\t\t(\n\t\t\tcd repo &&\n\t\t\tA=$(git rev-parse A) &&\n\t\t\tgit refs create refs/heads/foo $A &&\n\t\t\tgit refs create refs/heads/foo $A &&\n\t\t\ttest_ref_matches refs/heads/foo \"$A\"\n\t\t)\n\t'\n\nThat fails. It that intentional?\n\n> +\n> +test_expect_success 'create with null new value fails' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\ttest_must_fail git refs create refs/heads/foo $ZERO_OID 2>err &&\n> +\t\ttest_grep \"null new object ID\" err &&\n> +\t\ttest_must_fail git refs exists refs/heads/foo\n> +\t)\n> +'\n> +\n> +test_expect_success 'create with invalid new value fails' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\ttest_must_fail git refs create refs/heads/foo invalid-oid 2>err &&\n> +\t\ttest_grep \"invalid object ID\" err &&\n> +\t\ttest_must_fail git refs exists refs/heads/foo\n> +\t)\n> +'\n> +\n> +test_expect_success 'create does not create a reflog by default' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\tA=$(git rev-parse A) &&\n> +\t\tgit refs create refs/foo $A &&\n> +\t\ttest_must_fail git reflog exists refs/foo\n> +\t)\n> +'\n> +\n> +test_expect_success 'create creates a reflog with --create-reflog' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\tA=$(git rev-parse A) &&\n> +\t\tgit refs create --create-reflog refs/foo $A &&\n> +\t\tgit reflog exists refs/foo\n> +\t)\n> +'\n> +\n> +test_expect_success 'create with message records reason in reflog' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\tA=$(git rev-parse A) &&\n> +\t\tgit refs create --message=\"create reason\" refs/heads/foo $A &&\n> +\t\tgit reflog show refs/heads/foo >actual &&\n> +\t\ttest_grep \"create reason$\" actual\n> +\t)\n> +'\n> +\n> +test_expect_success 'create with symref target creates target reference' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\tA=$(git rev-parse A) &&\n> +\t\tgit symbolic-ref refs/heads/symref refs/heads/target &&\n> +\t\tgit refs create refs/heads/symref $A &&\n> +\t\tgit reflog exists refs/heads/target\n> +\t)\n> +'\n> +\n> +test_expect_success 'create with symref target and --no-deref refuses to create reference' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\tA=$(git rev-parse A) &&\n> +\t\tgit symbolic-ref refs/heads/symref refs/heads/target &&\n> +\t\ttest_must_fail git refs create --no-deref refs/heads/symref $A 2>err &&\n> +\t\ttest_grep \"dangling symref already exists\" err &&\n> +\t\ttest_must_fail git reflog exists refs/heads/target\n> +\t)\n> +'\n\nWould it make sense to add this test:\n\n\ttest_expect_success 'create with symref target with --no-deref' '\n\t\ttest_when_finished \"rm -rf repo\" &&\n\t\tsetup_repo repo &&\n\t\t(\n\t\t\tcd repo &&\n\t\t\tA=$(git rev-parse A) &&\n\t\t\tgit refs create refs/heads/target $A &&\n\t\t\tgit refs create --no-deref refs/heads/symref refs/heads/target &&\n\t\t\tgit reflog exists refs/heads/symref && false\n\t\t)\n\t'\n\nBut that makes me think, this option `--no-deref` is pretty obscure for\nuse with `git refs create`. There are two situations:\n\n* The symref doesn't exists: so --no-deref basically is forcing the\n  command to create a symref. That's confusing\n* The symref exists already: then the question is, does the user know it\n  exists:\n   - The user knows: so they pass --no-deref because they know it exists\n     and they want to create a symref. But why run `create` then anyway?\n   - The user doesn't know: brings us back to the first asterisk,\n     passing in `--no-deref` to create a symref, making it a weird\n     option name.\n\n\n> +\n> +test_expect_success 'create with empty message fails' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\tA=$(git rev-parse A) &&\n> +\t\ttest_must_fail git refs create --message= refs/heads/foo $A 2>err &&\n> +\t\ttest_grep \"empty message\" err &&\n> +\t\ttest_must_fail git refs exists refs/heads/foo\n> +\t)\n> +'\n> +\n> +test_expect_success 'create without arguments fails' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\ttest_must_fail git -C repo refs create 2>err &&\n> +\ttest_grep \"requires reference name\" err\n> +'\n> +\n> +test_expect_success 'create with too many arguments fails' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\ttest_must_fail git -C repo refs create refs/heads/foo a b 2>err &&\n> +\ttest_grep \"requires reference name\" err\n> +'\n> +\n> +test_done\n>\n> -- \n> 2.55.0.795.g602f6c329a.dirty\n>\n>\n\n-- \nCheers,\nToon\n"},{"id":"547096","messageId":"87o6go2lgt.fsf@emacs.iotcl.com","threadId":"65819","inReplyTo":"20260630-pks-refs-writing-subcommands-v3-5-deb04de1ecef@pks.im","subject":"Re: [PATCH v3 5/5] builtin/refs: add \"rename\" subcommand","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-07-03T14:31:46Z","receivedAt":"2026-07-03T14:31:54Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Add a \"rename\" subcommand to git-refs(1) with the syntax:\n>\n>   $ git refs rename <oldref> <newref>\n>\n> It renames <oldref> together with its reflog to <newref>; even when used\n> on a local branch ref, the current value and the reflog of the ref are\n> the only things that are renamed. Document it and redirect casual users\n> to \"git branch -m\" if that is what they wanted to do.\n>\n> Co-authored-by: Junio C Hamano <gitster@pobox.com>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Documentation/git-refs.adoc |   6 ++\n>  builtin/refs.c              |  49 +++++++++++++++++\n>  t/meson.build               |   1 +\n>  t/t1467-refs-rename.sh      | 131 ++++++++++++++++++++++++++++++++++++++++++++\n>  4 files changed, 187 insertions(+)\n>\n> diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc\n> index e6a3528349..ce278c59bf 100644\n> --- a/Documentation/git-refs.adoc\n> +++ b/Documentation/git-refs.adoc\n> @@ -23,6 +23,7 @@ git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude\n>  git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>\n>  git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\n>  git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\n> +git refs rename [--message=<reason>] <old-ref> <new-ref>\n\nSo symrefs cannot be renamed with this command?\n\n-- \nCheers,\nToon\n"},{"id":"547199","messageId":"aktVdaB2xRk-iI_8@pks.im","threadId":"65819","inReplyTo":"87qzlk2m0h.fsf@emacs.iotcl.com","subject":"Re: [PATCH v3 4/5] builtin/refs: add \"create\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-06T07:12:53Z","receivedAt":"2026-07-06T07:13:03Z","isPatch":true,"body":"On Fri, Jul 03, 2026 at 04:19:58PM +0200, Toon Claes wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> > diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc\n> > index 6475bdcc62..e6a3528349 100644\n> > --- a/Documentation/git-refs.adoc\n> > +++ b/Documentation/git-refs.adoc\n> > @@ -181,6 +184,53 @@ static int cmd_refs_optimize(int argc, const char **argv, const char *prefix,\n> >  \treturn pack_refs_core(argc, argv, prefix, repo, refs_optimize_usage);\n> >  }\n> >  \n> > +static int cmd_refs_create(int argc, const char **argv, const char *prefix,\n> > +\t\t\t   struct repository *repo)\n> > +{\n> > +\tstatic char const * const refs_create_usage[] = {\n> > +\t\tREFS_CREATE_USAGE,\n> > +\t\tNULL\n> > +\t};\n> > +\tconst char *message = NULL;\n> > +\tunsigned flags = 0;\n> > +\tstruct option opts[] = {\n> > +\t\tOPT_STRING(0, \"message\", &message, N_(\"reason\"),\n> > +\t\t\t   N_(\"reason of the update\")),\n> > +\t\tOPT_BIT(0 ,\"no-deref\", &flags,\n> > +\t\t\tN_(\"update <refname> not the one it points to\"),\n> > +\t\t\tREF_NO_DEREF),\n> \n> Can `git refs create --no-deref` be used to create symrefs? Should we\n> add a test for that? Or can it not\n> \n> I understand the symmetry, but does it make sense to ask the user to\n> create symrefs with `--no-deref`? Feels a bit obscure. The docs say:\n> \n> `--no-deref`::\n> \tOperate on <ref> itself rather than the reference it points to via a\n> \tsymbolic ref.\n> \n> That's far from obvious for a user to realize they need to pass that\n> option if they want to create a symref.\n\nIt doesn't cause them to create a symref. What this flag controls is\nwhether the command would fail when the refname exists already as a\nsymbolic ref. That is:\n\n    $ git symbolic-ref refs/heads/symref refs/heads/target\n    $ git refs create refs/heads/symref $OID\n    $ git refs exists refs/heads/target\n\nThe git-refs(1) command would have created \"refs/heads/target\" in this\ncase, and by passing \"--no-deref\" you'd instead make it fail.\n\nThis flag is somewhat weird. Having it is probably a sensible think to\ndo, but now that I think about it I wonder whether the default makes all\nthat much sense in the first place. That being said, _if_ we want to\nchange it then we should change it for all subcommands.\n\n> > diff --git a/t/t1466-refs-create.sh b/t/t1466-refs-create.sh\n> > new file mode 100755\n> > index 0000000000..cfb21bf863\n> > --- /dev/null\n> > +++ b/t/t1466-refs-create.sh\n> > @@ -0,0 +1,151 @@\n[snip]\n> > +test_expect_success 'create fails when the reference already exists' '\n> > +\ttest_when_finished \"rm -rf repo\" &&\n> > +\tsetup_repo repo &&\n> > +\t(\n> > +\t\tcd repo &&\n> > +\t\tA=$(git rev-parse A) &&\n> > +\t\tB=$(git rev-parse B) &&\n> > +\t\tgit refs create refs/heads/foo $A &&\n> > +\t\ttest_must_fail git refs create refs/heads/foo $B 2>err &&\n> > +\t\ttest_grep \"reference already exists\" err &&\n> > +\t\ttest_ref_matches refs/heads/foo \"$A\"\n> > +\t)\n> > +'\n> \n> I was curious about this test:\n> \n> \ttest_expect_success 'create succeed when the reference exists with the same value' '\n> \t\ttest_when_finished \"rm -rf repo\" &&\n> \t\tsetup_repo repo &&\n> \t\t(\n> \t\t\tcd repo &&\n> \t\t\tA=$(git rev-parse A) &&\n> \t\t\tgit refs create refs/heads/foo $A &&\n> \t\t\tgit refs create refs/heads/foo $A &&\n> \t\t\ttest_ref_matches refs/heads/foo \"$A\"\n> \t\t)\n> \t'\n> \n> That fails. It that intentional?\n\nYes, this is intentional. We didn't end up creating the reference, which\nis what the user has asked us to do, and hence we fail.\n\n[snip]\n> > +test_expect_success 'create with symref target and --no-deref refuses to create reference' '\n> > +\ttest_when_finished \"rm -rf repo\" &&\n> > +\tsetup_repo repo &&\n> > +\t(\n> > +\t\tcd repo &&\n> > +\t\tA=$(git rev-parse A) &&\n> > +\t\tgit symbolic-ref refs/heads/symref refs/heads/target &&\n> > +\t\ttest_must_fail git refs create --no-deref refs/heads/symref $A 2>err &&\n> > +\t\ttest_grep \"dangling symref already exists\" err &&\n> > +\t\ttest_must_fail git reflog exists refs/heads/target\n> > +\t)\n> > +'\n> \n> Would it make sense to add this test:\n> \n> \ttest_expect_success 'create with symref target with --no-deref' '\n> \t\ttest_when_finished \"rm -rf repo\" &&\n> \t\tsetup_repo repo &&\n> \t\t(\n> \t\t\tcd repo &&\n> \t\t\tA=$(git rev-parse A) &&\n> \t\t\tgit refs create refs/heads/target $A &&\n> \t\t\tgit refs create --no-deref refs/heads/symref refs/heads/target &&\n> \t\t\tgit reflog exists refs/heads/symref && false\n> \t\t)\n> \t'\n> \n> But that makes me think, this option `--no-deref` is pretty obscure for\n> use with `git refs create`. There are two situations:\n> \n> * The symref doesn't exists: so --no-deref basically is forcing the\n>   command to create a symref. That's confusing\n\nNo, it's not. It tells us that we only want to create the reference if\nit doesn't exist and is not a symref. Otherwise, we'd potentially create\nthe reference that the symref is pointing to.\n\nPatrick\n"},{"id":"547200","messageId":"aktVeqTiHV7c_CIK@pks.im","threadId":"65819","inReplyTo":"87o6go2lgt.fsf@emacs.iotcl.com","subject":"Re: [PATCH v3 5/5] builtin/refs: add \"rename\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-06T07:12:58Z","receivedAt":"2026-07-06T07:13:10Z","isPatch":true,"body":"On Fri, Jul 03, 2026 at 04:31:46PM +0200, Toon Claes wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> > diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc\n> > index e6a3528349..ce278c59bf 100644\n> > --- a/Documentation/git-refs.adoc\n> > +++ b/Documentation/git-refs.adoc\n> > @@ -23,6 +23,7 @@ git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude\n> >  git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>\n> >  git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\n> >  git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\n> > +git refs rename [--message=<reason>] <old-ref> <new-ref>\n> \n> So symrefs cannot be renamed with this command?\n\nIndeed, we don't support renaming symbolic references at all. This is a\nlimitation of our backends, even though it's not necessariliy a sensible\none.\n\nPatrick\n"},{"id":"547220","messageId":"20260706-pks-refs-writing-subcommands-v4-0-d51f6ce7f830@pks.im","threadId":"65819","inReplyTo":"20260616-pks-refs-writing-subcommands-v1-0-9f5219b6109d@pks.im","subject":"[PATCH v4 0/5] builtin/refs: add ability to write references","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-06T13:27:03Z","receivedAt":"2026-07-06T13:27:13Z","isPatch":true,"body":"Hi,\n\nReference-related functionality in Git is currently spread across many\ndifferent commands: git-update-ref(1), git-for-each-ref(1),\ngit-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it\nhard for users to discover what functionality we have available to work\nwith references.\n\nWe have thus started to consolidate this functionality into git-refs(1),\nwhich is a toolbox of everything related to references. Until now, the\ncommand doesn't handle functionality of git-update-ref(1).\n\nThis patch series backfills most of the functionality by introducing\nthree new commands:\n\n  - `git refs delete` to delete references. This is the equivalent of\n    `git update-ref -d`.\n\n  - `git refs update` to update references. This is the equivalent of\n    `git update-ref <refname> <oldvalue> <newvalue>`.\n\n  - `git refs rename` to rename a reference, including its reflog. This\n    does not have an equivalent in git-update-ref(1), but is inspired by\n    and supersedes [1].\n\nChanges in v4:\n  - Add a couple more tests around symrefs.\n  - Use a subshell in one of the tests for consistency.\n  - Link to v3: https://patch.msgid.link/20260630-pks-refs-writing-subcommands-v3-0-deb04de1ecef@pks.im\n\nChanges in v3:\n  - Fix confused error message.\n  - Link to v2: https://patch.msgid.link/20260617-pks-refs-writing-subcommands-v2-0-07f3d18336f9@pks.im\n\nChanges in v2:\n  - Add a new \"create\" subcommand.\n  - Consistently quote in error messages.\n  - Consistently use `<old-value>` in the synopsis.\n  - Don't return negative exit codes.\n  - Improve documentation of \"update\" subcommand to mention that you can\n    create and delete branches.\n  - Add tests to verify that we can use \"update\" to do this, both in\n    racy and raceless ways.\n  - Add missing calls to `repo_config()`.\n  - Drop useless `GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME` variable.\n  - Link to v1: https://patch.msgid.link/20260616-pks-refs-writing-subcommands-v1-0-9f5219b6109d@pks.im\n\nThanks!\n\nPatrick\n\n[1]: <xmqqv7brz9ba.fsf@gitster.g>\n\n---\nPatrick Steinhardt (5):\n      builtin/refs: drop `the_repository`\n      builtin/refs: add \"delete\" subcommand\n      builtin/refs: add \"update\" subcommand\n      builtin/refs: add \"create\" subcommand\n      builtin/refs: add \"rename\" subcommand\n\n Documentation/git-refs.adoc |  40 +++++++\n builtin/refs.c              | 222 ++++++++++++++++++++++++++++++++++--\n t/meson.build               |   4 +\n t/t1464-refs-delete.sh      | 152 +++++++++++++++++++++++++\n t/t1465-refs-update.sh      | 268 ++++++++++++++++++++++++++++++++++++++++++++\n t/t1466-refs-create.sh      | 151 +++++++++++++++++++++++++\n t/t1467-refs-rename.sh      | 144 ++++++++++++++++++++++++\n 7 files changed, 973 insertions(+), 8 deletions(-)\n\nRange-diff versus v3:\n\n1:  dc87ed0ebc = 1:  3b40441317 builtin/refs: drop `the_repository`\n2:  cec7d978f1 ! 2:  8089847912 builtin/refs: add \"delete\" subcommand\n    @@ t/t1464-refs-delete.sh (new)\n     +test_expect_success 'delete without oldvalue verification' '\n     +\ttest_when_finished \"rm -rf repo\" &&\n     +\tsetup_repo repo &&\n    -+\tA=$(git -C repo rev-parse A) &&\n    -+\tgit -C repo update-ref refs/heads/foo $A &&\n    -+\tgit -C repo refs delete refs/heads/foo &&\n    -+\ttest_must_fail git -C repo show-ref --verify -q refs/heads/foo\n    ++\t(\n    ++\t\tcd repo &&\n    ++\t\tA=$(git rev-parse A) &&\n    ++\t\tgit update-ref refs/heads/foo $A &&\n    ++\t\tgit refs delete refs/heads/foo &&\n    ++\t\ttest_must_fail git refs exists refs/heads/foo\n    ++\t)\n     +'\n     +\n     +test_expect_success 'delete with matching oldvalue' '\n    @@ t/t1464-refs-delete.sh (new)\n     +\t)\n     +'\n     +\n    ++test_expect_success 'delete symref with --no-deref verifies target OID' '\n    ++\ttest_when_finished \"rm -rf repo\" &&\n    ++\tsetup_repo repo &&\n    ++\t(\n    ++\t\tcd repo &&\n    ++\t\tA=$(git rev-parse A) &&\n    ++\t\tB=$(git rev-parse B) &&\n    ++\t\tgit update-ref refs/heads/foo $A &&\n    ++\t\tgit symbolic-ref refs/heads/symref refs/heads/foo &&\n    ++\n    ++\t\ttest_must_fail git refs delete --no-deref refs/heads/symref $B &&\n    ++\t\tgit refs exists refs/heads/symref &&\n    ++\n    ++\t\tgit refs delete --no-deref refs/heads/symref $A &&\n    ++\t\ttest_must_fail git refs exists refs/heads/symref &&\n    ++\t\tgit refs exists refs/heads/foo\n    ++\t)\n    ++'\n    ++\n     +test_expect_success 'delete with message records reason in reflog' '\n     +\ttest_when_finished \"rm -rf repo\" &&\n     +\tsetup_repo repo &&\n3:  8e73b0f711 = 3:  fb830f8f9e builtin/refs: add \"update\" subcommand\n4:  f5ad0c9b18 = 4:  f3c17471c1 builtin/refs: add \"create\" subcommand\n5:  5c01f2e828 ! 5:  82e4efb2a9 builtin/refs: add \"rename\" subcommand\n    @@ t/t1467-refs-rename.sh (new)\n     +\t)\n     +'\n     +\n    ++test_expect_success 'rename with symbolic ref fails' '\n    ++\ttest_when_finished \"rm -rf repo\" &&\n    ++\tsetup_repo repo &&\n    ++\t(\n    ++\t\tcd repo &&\n    ++\t\tA=$(git rev-parse A) &&\n    ++\t\tgit refs create refs/heads/target $A &&\n    ++\t\tgit symbolic-ref refs/heads/symref refs/heads/target &&\n    ++\t\t! git refs rename refs/heads/symref refs/heads/renamed 2>err &&\n    ++\t\ttest_grep \"is a symbolic ref, .* not supported\" err\n    ++\t)\n    ++'\n    ++\n     +test_expect_success 'rename with empty message fails' '\n     +\ttest_when_finished \"rm -rf repo\" &&\n     +\tsetup_repo repo &&\n\n---\nbase-commit: 700432b2ba22603a0bcb71475c9c333d17c9b0d1\nchange-id: 20260616-pks-refs-writing-subcommands-7a77be5bda9b\n\n"},{"id":"547221","messageId":"20260706-pks-refs-writing-subcommands-v4-1-d51f6ce7f830@pks.im","threadId":"65819","inReplyTo":"20260706-pks-refs-writing-subcommands-v4-0-d51f6ce7f830@pks.im","subject":"[PATCH v4 1/5] builtin/refs: drop `the_repository`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-06T13:27:04Z","receivedAt":"2026-07-06T13:27:14Z","isPatch":true,"body":"We still have a couple of uses of `the_repository` in \"builtin/refs.c\".\nAll of those are trivial to convert though as the command always\nrequires a repository to exist.\n\nConvert them to use the passed-in repository and drop\n`USE_THE_REPOSITORY_VARIABLE`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/refs.c | 15 +++++++--------\n 1 file changed, 7 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/refs.c b/builtin/refs.c\nindex e3125bc61b..f0faabf45a 100644\n--- a/builtin/refs.c\n+++ b/builtin/refs.c\n@@ -1,4 +1,3 @@\n-#define USE_THE_REPOSITORY_VARIABLE\n #include \"builtin.h\"\n #include \"config.h\"\n #include \"fsck.h\"\n@@ -23,7 +22,7 @@\n \tN_(\"git refs optimize \" PACK_REFS_OPTS)\n \n static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,\n-\t\t\t    struct repository *repo UNUSED)\n+\t\t\t    struct repository *repo)\n {\n \tconst char * const migrate_usage[] = {\n \t\tREFS_MIGRATE_USAGE,\n@@ -59,13 +58,13 @@ static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,\n \t\tgoto out;\n \t}\n \n-\tif (the_repository->ref_storage_format == format) {\n+\tif (repo->ref_storage_format == format) {\n \t\terr = error(_(\"repository already uses '%s' format\"),\n \t\t\t    ref_storage_format_to_name(format));\n \t\tgoto out;\n \t}\n \n-\tif (repo_migrate_ref_storage_format(the_repository, format, flags, &errbuf) < 0) {\n+\tif (repo_migrate_ref_storage_format(repo, format, flags, &errbuf) < 0) {\n \t\terr = error(\"%s\", errbuf.buf);\n \t\tgoto out;\n \t}\n@@ -99,8 +98,8 @@ static int cmd_refs_verify(int argc, const char **argv, const char *prefix,\n \tif (argc)\n \t\tusage(_(\"'git refs verify' takes no arguments\"));\n \n-\trepo_config(the_repository, git_fsck_config, &fsck_refs_options);\n-\tprepare_repo_settings(the_repository);\n+\trepo_config(repo, git_fsck_config, &fsck_refs_options);\n+\tprepare_repo_settings(repo);\n \n \tworktrees = get_worktrees_without_reading_head();\n \tfor (size_t i = 0; worktrees[i]; i++)\n@@ -124,7 +123,7 @@ static int cmd_refs_list(int argc, const char **argv, const char *prefix,\n }\n \n static int cmd_refs_exists(int argc, const char **argv, const char *prefix,\n-\t\t\t   struct repository *repo UNUSED)\n+\t\t\t   struct repository *repo)\n {\n \tstruct strbuf unused_referent = STRBUF_INIT;\n \tstruct object_id unused_oid;\n@@ -145,7 +144,7 @@ static int cmd_refs_exists(int argc, const char **argv, const char *prefix,\n \t\tdie(_(\"'git refs exists' requires a reference\"));\n \n \tref = *argv++;\n-\tif (refs_read_raw_ref(get_main_ref_store(the_repository), ref,\n+\tif (refs_read_raw_ref(get_main_ref_store(repo), ref,\n \t\t\t      &unused_oid, &unused_referent, &unused_type,\n \t\t\t      &failure_errno)) {\n \t\tif (failure_errno == ENOENT || failure_errno == EISDIR) {\n\n-- \n2.55.0.795.g602f6c329a.dirty\n\n"},{"id":"547222","messageId":"20260706-pks-refs-writing-subcommands-v4-2-d51f6ce7f830@pks.im","threadId":"65819","inReplyTo":"20260706-pks-refs-writing-subcommands-v4-0-d51f6ce7f830@pks.im","subject":"[PATCH v4 2/5] builtin/refs: add \"delete\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-06T13:27:05Z","receivedAt":"2026-07-06T13:27:16Z","isPatch":true,"body":"Reference-related functionality in Git is currently spread across many\ndifferent commands: git-update-ref(1), git-for-each-ref(1),\ngit-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it\nhard for users to discover what functionality we have available to work\nwith references.\n\nWe have thus started to consolidate this functionality into git-refs(1),\nwhich is a toolbox of everything related to references. Until now, the\ncommand doesn't handle functionality of git-update-ref(1).\n\nFix this gap by introducing a new \"delete\" subcommand, which is the\nequivalent of `git update-ref -d`.\n\nNote that we're intentionally not using a generic \"write\" subcommand\nwith a \"-d\" flag. This is rather harder to discover, and subcommands\nthat are implmented as flags tend to be hard to reason about in the code\nas we'd have to handle mutually-exclusive flags that stem from the other\nsubcommand-like modes.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-refs.adoc |  17 +++++\n builtin/refs.c              |  51 +++++++++++++++\n t/meson.build               |   1 +\n t/t1464-refs-delete.sh      | 152 ++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 221 insertions(+)\n\ndiff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc\nindex fa33680cc7..2633934463 100644\n--- a/Documentation/git-refs.adoc\n+++ b/Documentation/git-refs.adoc\n@@ -20,6 +20,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]\n \t\t   [ --stdin | (<pattern>...)]\n git refs exists <ref>\n git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]\n+git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\n \n DESCRIPTION\n -----------\n@@ -51,6 +52,12 @@ optimize::\n \tusage. This subcommand is an alias for linkgit:git-pack-refs[1] and\n \toffers identical functionality.\n \n+delete::\n+\tDelete the given reference. This subcommand mirrors `git update-ref -d`\n+\t(see linkgit:git-update-ref[1]). When `<old-value>` is given, the\n+\treference is only deleted after verifying that it currently contains\n+\t`<old-value>`.\n+\n OPTIONS\n -------\n \n@@ -90,6 +97,16 @@ The following options are specific to 'git refs optimize':\n \n include::pack-refs-options.adoc[]\n \n+The following options are specific to commands which write references:\n+\n+`--message=<reason>`::\n+\tUse the given <reason> string for the reflog entry associated with the\n+\tupdate. An empty message is rejected.\n+\n+`--no-deref`::\n+\tOperate on <ref> itself rather than the reference it points to via a\n+\tsymbolic ref.\n+\n KNOWN LIMITATIONS\n -----------------\n \ndiff --git a/builtin/refs.c b/builtin/refs.c\nindex f0faabf45a..edb7d61663 100644\n--- a/builtin/refs.c\n+++ b/builtin/refs.c\n@@ -21,6 +21,9 @@\n #define REFS_OPTIMIZE_USAGE \\\n \tN_(\"git refs optimize \" PACK_REFS_OPTS)\n \n+#define REFS_DELETE_USAGE \\\n+\tN_(\"git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\")\n+\n static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,\n \t\t\t    struct repository *repo)\n {\n@@ -175,6 +178,52 @@ static int cmd_refs_optimize(int argc, const char **argv, const char *prefix,\n \treturn pack_refs_core(argc, argv, prefix, repo, refs_optimize_usage);\n }\n \n+static int cmd_refs_delete(int argc, const char **argv, const char *prefix,\n+\t\t\t   struct repository *repo)\n+{\n+\tstatic char const * const refs_delete_usage[] = {\n+\t\tREFS_DELETE_USAGE,\n+\t\tNULL\n+\t};\n+\tconst char *message = NULL;\n+\tunsigned flags = 0;\n+\tstruct option opts[] = {\n+\t\tOPT_STRING(0, \"message\", &message, N_(\"reason\"),\n+\t\t\t   N_(\"reason of the update\")),\n+\t\tOPT_BIT(0 ,\"no-deref\", &flags,\n+\t\t\tN_(\"update <refname> not the one it points to\"),\n+\t\t\tREF_NO_DEREF),\n+\t\tOPT_END(),\n+\t};\n+\tstruct object_id oldoid;\n+\tconst char *refname;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, refs_delete_usage, 0);\n+\tif (argc < 1 || argc > 2)\n+\t\tusage(_(\"delete requires reference name and an optional old object ID\"));\n+\n+\tif (message && !*message)\n+\t\tdie(_(\"refusing to perform update with empty message\"));\n+\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\trefname = argv[0];\n+\tif (argc == 2) {\n+\t\tif (repo_get_oid_with_flags(repo, argv[1], &oldoid, GET_OID_SKIP_AMBIGUITY_CHECK))\n+\t\t\tdie(_(\"invalid old object ID: '%s'\"), argv[1]);\n+\t\tif (is_null_oid(&oldoid))\n+\t\t\tdie(_(\"cannot delete reference with null old object ID\"));\n+\t}\n+\n+\tret = refs_delete_ref(get_main_ref_store(repo), message, refname,\n+\t\t\t      argc == 2 ? &oldoid : NULL, flags);\n+\n+\tif (ret < 0)\n+\t\tret = 1;\n+\treturn ret;\n+}\n+\n int cmd_refs(int argc,\n \t     const char **argv,\n \t     const char *prefix,\n@@ -186,6 +235,7 @@ int cmd_refs(int argc,\n \t\t\"git refs list \" COMMON_USAGE_FOR_EACH_REF,\n \t\tREFS_EXISTS_USAGE,\n \t\tREFS_OPTIMIZE_USAGE,\n+\t\tREFS_DELETE_USAGE,\n \t\tNULL,\n \t};\n \tparse_opt_subcommand_fn *fn = NULL;\n@@ -195,6 +245,7 @@ int cmd_refs(int argc,\n \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_refs_list),\n \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_refs_exists),\n \t\tOPT_SUBCOMMAND(\"optimize\", &fn, cmd_refs_optimize),\n+\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_refs_delete),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex c5832fee05..1ccf08a3b5 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -223,6 +223,7 @@ integration_tests = [\n   't1461-refs-list.sh',\n   't1462-refs-exists.sh',\n   't1463-refs-optimize.sh',\n+  't1464-refs-delete.sh',\n   't1500-rev-parse.sh',\n   't1501-work-tree.sh',\n   't1502-rev-parse-parseopt.sh',\ndiff --git a/t/t1464-refs-delete.sh b/t/t1464-refs-delete.sh\nnew file mode 100755\nindex 0000000000..c88063e494\n--- /dev/null\n+++ b/t/t1464-refs-delete.sh\n@@ -0,0 +1,152 @@\n+#!/bin/sh\n+\n+test_description='git refs delete'\n+\n+. ./test-lib.sh\n+\n+setup_repo () {\n+\tgit init \"$1\" &&\n+\ttest_commit -C \"$1\" A &&\n+\ttest_commit -C \"$1\" B\n+}\n+\n+test_expect_success 'delete without oldvalue verification' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\tgit refs delete refs/heads/foo &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete with matching oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\tgit refs delete refs/heads/foo $A &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete with stale oldvalue fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\ttest_must_fail git refs delete refs/heads/foo $B 2>err &&\n+\t\ttest_grep \" but expected \" err &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete with null oldvalue fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\ttest_must_fail git refs delete refs/heads/foo $ZERO_OID 2>err &&\n+\t\ttest_grep \"null old object ID\" err &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete with invalid oldvalue fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\ttest_must_fail git refs delete refs/heads/foo invalid-oid 2>err &&\n+\t\ttest_grep \"invalid old object ID\" err &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete symref with --no-deref leaves target intact' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/foo &&\n+\t\tgit refs delete --no-deref refs/heads/symref &&\n+\t\ttest_must_fail git refs exists refs/heads/symref &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete symref with --no-deref verifies target OID' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/foo &&\n+\n+\t\ttest_must_fail git refs delete --no-deref refs/heads/symref $B &&\n+\t\tgit refs exists refs/heads/symref &&\n+\n+\t\tgit refs delete --no-deref refs/heads/symref $A &&\n+\t\ttest_must_fail git refs exists refs/heads/symref &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete with message records reason in reflog' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\tgit symbolic-ref HEAD refs/heads/foo &&\n+\t\tgit refs delete --message=delete-reason refs/heads/foo &&\n+\t\ttest_must_fail git refs exists refs/heads/foo &&\n+\t\ttest-tool ref-store main for-each-reflog-ent HEAD >actual &&\n+\t\ttest_grep \"delete-reason$\" actual\n+\t)\n+'\n+\n+test_expect_success 'delete with empty message fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit update-ref refs/heads/foo $A &&\n+\t\ttest_must_fail git refs delete --message= refs/heads/foo 2>err &&\n+\t\ttest_grep \"empty message\" err &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'delete without arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git -C repo refs delete 2>err &&\n+\ttest_grep \"requires reference name\" err\n+'\n+\n+test_expect_success 'delete with too many arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git refs delete one two three 2>err &&\n+\ttest_grep \"requires reference name\" err\n+'\n+\n+test_done\n\n-- \n2.55.0.795.g602f6c329a.dirty\n\n"},{"id":"547224","messageId":"20260706-pks-refs-writing-subcommands-v4-3-d51f6ce7f830@pks.im","threadId":"65819","inReplyTo":"20260706-pks-refs-writing-subcommands-v4-0-d51f6ce7f830@pks.im","subject":"[PATCH v4 3/5] builtin/refs: add \"update\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-06T13:27:06Z","receivedAt":"2026-07-06T13:27:19Z","isPatch":true,"body":"Add a new \"update\" subcommand which mirrors `git update-ref <refname>\n<oldoid> <newoid>`. This follows the same reasoning as the preceding\ncommit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-refs.adoc |  12 ++\n builtin/refs.c              |  55 +++++++++\n t/meson.build               |   1 +\n t/t1465-refs-update.sh      | 268 ++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 336 insertions(+)\n\ndiff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc\nindex 2633934463..6475bdcc62 100644\n--- a/Documentation/git-refs.adoc\n+++ b/Documentation/git-refs.adoc\n@@ -21,6 +21,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]\n git refs exists <ref>\n git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]\n git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\n+git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\n \n DESCRIPTION\n -----------\n@@ -58,6 +59,13 @@ delete::\n \treference is only deleted after verifying that it currently contains\n \t`<old-value>`.\n \n+update::\n+\tUpdate the given reference to point at `<new-value>`. If `<old-value>`\n+\tis given, the reference is only updated after verifying that it\n+\tcurrently contains `<old-value>`. As a special case, an all-zeroes\n+\t`<new-value>` deletes the branch, whereas an all-zeroes `<old-value>`\n+\tensures that the branch does not yet exist.\n+\n OPTIONS\n -------\n \n@@ -99,6 +107,10 @@ include::pack-refs-options.adoc[]\n \n The following options are specific to commands which write references:\n \n+`--create-reflog`::\n+\tCreate a reflog for the reference even if one would not ordinarily be\n+\tcreated.\n+\n `--message=<reason>`::\n \tUse the given <reason> string for the reflog entry associated with the\n \tupdate. An empty message is rejected.\ndiff --git a/builtin/refs.c b/builtin/refs.c\nindex edb7d61663..08453ae1c8 100644\n--- a/builtin/refs.c\n+++ b/builtin/refs.c\n@@ -24,6 +24,9 @@\n #define REFS_DELETE_USAGE \\\n \tN_(\"git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\")\n \n+#define REFS_UPDATE_USAGE \\\n+\tN_(\"git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\")\n+\n static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,\n \t\t\t    struct repository *repo)\n {\n@@ -224,6 +227,56 @@ static int cmd_refs_delete(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int cmd_refs_update(int argc, const char **argv, const char *prefix,\n+\t\t\t   struct repository *repo)\n+{\n+\tstatic char const * const refs_update_usage[] = {\n+\t\tREFS_UPDATE_USAGE,\n+\t\tNULL\n+\t};\n+\tconst char *message = NULL;\n+\tunsigned flags = 0;\n+\tstruct option opts[] = {\n+\t\tOPT_STRING(0, \"message\", &message, N_(\"reason\"),\n+\t\t\t   N_(\"reason of the update\")),\n+\t\tOPT_BIT(0 ,\"no-deref\", &flags,\n+\t\t\tN_(\"update <refname> not the one it points to\"),\n+\t\t\tREF_NO_DEREF),\n+\t\tOPT_BIT(0, \"create-reflog\", &flags, N_(\"create a reflog\"),\n+\t\t\tREF_FORCE_CREATE_REFLOG),\n+\t\tOPT_END(),\n+\t};\n+\tstruct object_id newoid, oldoid;\n+\tconst char *refname;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, refs_update_usage, 0);\n+\tif (argc < 2 || argc > 3)\n+\t\tusage(_(\"update requires reference name, new value and an optional old value\"));\n+\n+\tif (message && !*message)\n+\t\tdie(_(\"refusing to perform update with empty message\"));\n+\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\trefname = argv[0];\n+\tif (repo_get_oid_with_flags(repo, argv[1], &newoid,\n+\t\t\t\t    GET_OID_SKIP_AMBIGUITY_CHECK))\n+\t\tdie(_(\"invalid new object ID: '%s'\"), argv[1]);\n+\tif (argc == 3 &&\n+\t    repo_get_oid_with_flags(repo, argv[2], &oldoid,\n+\t\t\t\t    GET_OID_SKIP_AMBIGUITY_CHECK))\n+\t\tdie(_(\"invalid old object ID: '%s'\"), argv[2]);\n+\n+\tret = refs_update_ref(get_main_ref_store(repo), message, refname,\n+\t\t\t      &newoid, argc == 3 ? &oldoid : NULL, flags,\n+\t\t\t      UPDATE_REFS_MSG_ON_ERR);\n+\n+\tif (ret < 0)\n+\t\tret = 1;\n+\treturn ret;\n+}\n+\n int cmd_refs(int argc,\n \t     const char **argv,\n \t     const char *prefix,\n@@ -236,6 +289,7 @@ int cmd_refs(int argc,\n \t\tREFS_EXISTS_USAGE,\n \t\tREFS_OPTIMIZE_USAGE,\n \t\tREFS_DELETE_USAGE,\n+\t\tREFS_UPDATE_USAGE,\n \t\tNULL,\n \t};\n \tparse_opt_subcommand_fn *fn = NULL;\n@@ -246,6 +300,7 @@ int cmd_refs(int argc,\n \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_refs_exists),\n \t\tOPT_SUBCOMMAND(\"optimize\", &fn, cmd_refs_optimize),\n \t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_refs_delete),\n+\t\tOPT_SUBCOMMAND(\"update\", &fn, cmd_refs_update),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex 1ccf08a3b5..2063962dab 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -224,6 +224,7 @@ integration_tests = [\n   't1462-refs-exists.sh',\n   't1463-refs-optimize.sh',\n   't1464-refs-delete.sh',\n+  't1465-refs-update.sh',\n   't1500-rev-parse.sh',\n   't1501-work-tree.sh',\n   't1502-rev-parse-parseopt.sh',\ndiff --git a/t/t1465-refs-update.sh b/t/t1465-refs-update.sh\nnew file mode 100755\nindex 0000000000..a9becdda99\n--- /dev/null\n+++ b/t/t1465-refs-update.sh\n@@ -0,0 +1,268 @@\n+#!/bin/sh\n+\n+test_description='git refs update'\n+\n+. ./test-lib.sh\n+\n+setup_repo () {\n+\tgit init \"$1\" &&\n+\ttest_commit -C \"$1\" A &&\n+\ttest_commit -C \"$1\" B\n+}\n+\n+test_ref_matches () {\n+\tgit rev-parse \"$1\" >expect &&\n+\techo \"$2\" >actual &&\n+\ttest_cmp expect actual\n+}\n+\n+test_expect_success 'update creates a new reference' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_ref_matches refs/heads/foo \"$A\"\n+\t)\n+'\n+\n+test_expect_success 'update an existing reference without oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs update refs/heads/foo $B &&\n+\t\ttest_ref_matches refs/heads/foo $B\n+\t)\n+'\n+\n+test_expect_success 'update with matching oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs update refs/heads/foo $B $A &&\n+\t\ttest_ref_matches refs/heads/foo $B\n+\t)\n+'\n+\n+test_expect_success 'update with stale oldvalue fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs update refs/heads/foo $B $B 2>err &&\n+\t\ttest_grep \" but expected \" err &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+test_expect_success 'update can create a new branch with oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A $ZERO_OID 2>err &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+test_expect_success 'update can create a new branch without oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A 2>err &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+test_expect_success 'update refuses to create preexisting branch' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err &&\n+\t\ttest_grep \"reference already exists\" err &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+test_expect_success 'update can delete a branch with oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A 2>err &&\n+\t\tgit refs update refs/heads/foo $ZERO_OID $A 2>err &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'update can delete a branch without oldvalue' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A 2>err &&\n+\t\tgit refs update refs/heads/foo $ZERO_OID 2>err &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'update refuses to delete a branch with mismatching value' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A 2>err &&\n+\t\ttest_must_fail git refs update refs/heads/foo $ZERO_OID $B 2>err &&\n+\t\ttest_grep \" but expected \" err &&\n+\t\tgit refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'update refuses to create preexisting branch' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err &&\n+\t\ttest_grep \"reference already exists\" err &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+\n+test_expect_success 'update with invalid new value fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git refs update refs/heads/foo invalid-oid 2>err &&\n+\t\ttest_grep \"invalid new object ID\" err &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'update with invalid old value fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs update refs/heads/foo $B invalid-oid 2>err &&\n+\t\ttest_grep \"invalid old object ID\" err &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+test_expect_success 'update --no-deref rewrites the symref itself' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/foo &&\n+\t\tgit refs update --no-deref refs/heads/symref $B &&\n+\t\ttest_must_fail git symbolic-ref refs/heads/symref &&\n+\t\ttest_ref_matches refs/heads/symref $B &&\n+\t\ttest_ref_matches refs/heads/foo $A\n+\t)\n+'\n+\n+test_expect_success 'update does not create a reflog by default' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/foo $A &&\n+\t\ttest_must_fail git reflog exists refs/foo\n+\t)\n+'\n+\n+test_expect_success 'update creates a reflog with --create-reflog' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update --create-reflog refs/foo $A &&\n+\t\tgit reflog exists refs/foo\n+\t)\n+'\n+\n+test_expect_success 'update with message records reason in reflog' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs update --message=update-reason refs/heads/foo $B &&\n+\t\tgit reflog show refs/heads/foo >actual &&\n+\t\ttest_grep \"update-reason$\" actual\n+\t)\n+'\n+\n+test_expect_success 'update with empty message fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs update --message= refs/heads/foo $B 2>err &&\n+\t\ttest_grep \"empty message\" err\n+\t)\n+'\n+\n+test_expect_success 'update with too few arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git -C repo refs update refs/heads/foo 2>err &&\n+\ttest_grep \"requires reference name, new value\" err\n+'\n+\n+test_expect_success 'update with too many arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\ttest_must_fail git refs update refs/heads/foo $A $B extra 2>err &&\n+\t\ttest_grep \"requires reference name, new value\" err\n+\t)\n+'\n+\n+test_done\n\n-- \n2.55.0.795.g602f6c329a.dirty\n\n"},{"id":"547223","messageId":"20260706-pks-refs-writing-subcommands-v4-4-d51f6ce7f830@pks.im","threadId":"65819","inReplyTo":"20260706-pks-refs-writing-subcommands-v4-0-d51f6ce7f830@pks.im","subject":"[PATCH v4 4/5] builtin/refs: add \"create\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-06T13:27:07Z","receivedAt":"2026-07-06T13:27:21Z","isPatch":true,"body":"The \"update\" subcommand cannot only update an existing reference, but it\ncan also create new branches and delete existing branches by specifying\nthe all-zeroes object ID as either old or new value. Despite that, we\nalready have the \"delete\" subcommand as a handy shortcut so that a user\ncan easily delete a branch. This relieves them of needing to understand\nthe more arcane uses of the \"update\" command, and of counting the number\nof zeroes they need to pass.\n\nBut while we have a \"delete\" subcommand, we don't have an equivalent\nthat would allow the user to create a new branch, which creates a\ncertain asymmetry.\n\nAdd a new \"create\" subcommand to plug this gap.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-refs.adoc |   5 ++\n builtin/refs.c              |  52 +++++++++++++++\n t/meson.build               |   1 +\n t/t1466-refs-create.sh      | 151 ++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 209 insertions(+)\n\ndiff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc\nindex 6475bdcc62..e6a3528349 100644\n--- a/Documentation/git-refs.adoc\n+++ b/Documentation/git-refs.adoc\n@@ -20,6 +20,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]\n \t\t   [ --stdin | (<pattern>...)]\n git refs exists <ref>\n git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]\n+git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>\n git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\n git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\n \n@@ -53,6 +54,10 @@ optimize::\n \tusage. This subcommand is an alias for linkgit:git-pack-refs[1] and\n \toffers identical functionality.\n \n+create::\n+\tCreate the given reference, which must not already exist, pointing at\n+\t`<new-value>`.\n+\n delete::\n \tDelete the given reference. This subcommand mirrors `git update-ref -d`\n \t(see linkgit:git-update-ref[1]). When `<old-value>` is given, the\ndiff --git a/builtin/refs.c b/builtin/refs.c\nindex 08453ae1c8..1ebaf30149 100644\n--- a/builtin/refs.c\n+++ b/builtin/refs.c\n@@ -21,6 +21,9 @@\n #define REFS_OPTIMIZE_USAGE \\\n \tN_(\"git refs optimize \" PACK_REFS_OPTS)\n \n+#define REFS_CREATE_USAGE \\\n+\tN_(\"git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>\")\n+\n #define REFS_DELETE_USAGE \\\n \tN_(\"git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\")\n \n@@ -181,6 +184,53 @@ static int cmd_refs_optimize(int argc, const char **argv, const char *prefix,\n \treturn pack_refs_core(argc, argv, prefix, repo, refs_optimize_usage);\n }\n \n+static int cmd_refs_create(int argc, const char **argv, const char *prefix,\n+\t\t\t   struct repository *repo)\n+{\n+\tstatic char const * const refs_create_usage[] = {\n+\t\tREFS_CREATE_USAGE,\n+\t\tNULL\n+\t};\n+\tconst char *message = NULL;\n+\tunsigned flags = 0;\n+\tstruct option opts[] = {\n+\t\tOPT_STRING(0, \"message\", &message, N_(\"reason\"),\n+\t\t\t   N_(\"reason of the update\")),\n+\t\tOPT_BIT(0 ,\"no-deref\", &flags,\n+\t\t\tN_(\"update <refname> not the one it points to\"),\n+\t\t\tREF_NO_DEREF),\n+\t\tOPT_BIT(0, \"create-reflog\", &flags, N_(\"create a reflog\"),\n+\t\t\tREF_FORCE_CREATE_REFLOG),\n+\t\tOPT_END(),\n+\t};\n+\tstruct object_id newoid;\n+\tconst char *refname;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, refs_create_usage, 0);\n+\tif (argc != 2)\n+\t\tusage(_(\"create requires reference name and an object ID\"));\n+\n+\tif (message && !*message)\n+\t\tdie(_(\"refusing to perform update with empty message\"));\n+\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\trefname = argv[0];\n+\tif (repo_get_oid_with_flags(repo, argv[1], &newoid, GET_OID_SKIP_AMBIGUITY_CHECK))\n+\t\tdie(_(\"invalid object ID: '%s'\"), argv[1]);\n+\tif (is_null_oid(&newoid))\n+\t\tdie(_(\"cannot create reference with null new object ID\"));\n+\n+\tret = refs_update_ref(get_main_ref_store(repo), message, refname,\n+\t\t\t      &newoid, null_oid(repo->hash_algo), flags,\n+\t\t\t      UPDATE_REFS_MSG_ON_ERR);\n+\n+\tif (ret < 0)\n+\t\tret = 1;\n+\treturn ret;\n+}\n+\n static int cmd_refs_delete(int argc, const char **argv, const char *prefix,\n \t\t\t   struct repository *repo)\n {\n@@ -288,6 +338,7 @@ int cmd_refs(int argc,\n \t\t\"git refs list \" COMMON_USAGE_FOR_EACH_REF,\n \t\tREFS_EXISTS_USAGE,\n \t\tREFS_OPTIMIZE_USAGE,\n+\t\tREFS_CREATE_USAGE,\n \t\tREFS_DELETE_USAGE,\n \t\tREFS_UPDATE_USAGE,\n \t\tNULL,\n@@ -299,6 +350,7 @@ int cmd_refs(int argc,\n \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_refs_list),\n \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_refs_exists),\n \t\tOPT_SUBCOMMAND(\"optimize\", &fn, cmd_refs_optimize),\n+\t\tOPT_SUBCOMMAND(\"create\", &fn, cmd_refs_create),\n \t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_refs_delete),\n \t\tOPT_SUBCOMMAND(\"update\", &fn, cmd_refs_update),\n \t\tOPT_END(),\ndiff --git a/t/meson.build b/t/meson.build\nindex 2063962dab..541e6f919c 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -225,6 +225,7 @@ integration_tests = [\n   't1463-refs-optimize.sh',\n   't1464-refs-delete.sh',\n   't1465-refs-update.sh',\n+  't1466-refs-create.sh',\n   't1500-rev-parse.sh',\n   't1501-work-tree.sh',\n   't1502-rev-parse-parseopt.sh',\ndiff --git a/t/t1466-refs-create.sh b/t/t1466-refs-create.sh\nnew file mode 100755\nindex 0000000000..cfb21bf863\n--- /dev/null\n+++ b/t/t1466-refs-create.sh\n@@ -0,0 +1,151 @@\n+#!/bin/sh\n+\n+test_description='git refs create'\n+\n+. ./test-lib.sh\n+\n+setup_repo () {\n+\tgit init \"$1\" &&\n+\ttest_commit -C \"$1\" A &&\n+\ttest_commit -C \"$1\" B\n+}\n+\n+test_ref_matches () {\n+\tgit rev-parse \"$1\" >expect &&\n+\techo \"$2\" >actual &&\n+\ttest_cmp expect actual\n+}\n+\n+test_expect_success 'create a new reference' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs create refs/heads/foo $A &&\n+\t\ttest_ref_matches refs/heads/foo \"$A\"\n+\t)\n+'\n+\n+test_expect_success 'create fails when the reference already exists' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs create refs/heads/foo $A &&\n+\t\ttest_must_fail git refs create refs/heads/foo $B 2>err &&\n+\t\ttest_grep \"reference already exists\" err &&\n+\t\ttest_ref_matches refs/heads/foo \"$A\"\n+\t)\n+'\n+\n+test_expect_success 'create with null new value fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git refs create refs/heads/foo $ZERO_OID 2>err &&\n+\t\ttest_grep \"null new object ID\" err &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'create with invalid new value fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git refs create refs/heads/foo invalid-oid 2>err &&\n+\t\ttest_grep \"invalid object ID\" err &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'create does not create a reflog by default' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs create refs/foo $A &&\n+\t\ttest_must_fail git reflog exists refs/foo\n+\t)\n+'\n+\n+test_expect_success 'create creates a reflog with --create-reflog' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs create --create-reflog refs/foo $A &&\n+\t\tgit reflog exists refs/foo\n+\t)\n+'\n+\n+test_expect_success 'create with message records reason in reflog' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs create --message=\"create reason\" refs/heads/foo $A &&\n+\t\tgit reflog show refs/heads/foo >actual &&\n+\t\ttest_grep \"create reason$\" actual\n+\t)\n+'\n+\n+test_expect_success 'create with symref target creates target reference' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/target &&\n+\t\tgit refs create refs/heads/symref $A &&\n+\t\tgit reflog exists refs/heads/target\n+\t)\n+'\n+\n+test_expect_success 'create with symref target and --no-deref refuses to create reference' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/target &&\n+\t\ttest_must_fail git refs create --no-deref refs/heads/symref $A 2>err &&\n+\t\ttest_grep \"dangling symref already exists\" err &&\n+\t\ttest_must_fail git reflog exists refs/heads/target\n+\t)\n+'\n+\n+test_expect_success 'create with empty message fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\ttest_must_fail git refs create --message= refs/heads/foo $A 2>err &&\n+\t\ttest_grep \"empty message\" err &&\n+\t\ttest_must_fail git refs exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'create without arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git -C repo refs create 2>err &&\n+\ttest_grep \"requires reference name\" err\n+'\n+\n+test_expect_success 'create with too many arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git -C repo refs create refs/heads/foo a b 2>err &&\n+\ttest_grep \"requires reference name\" err\n+'\n+\n+test_done\n\n-- \n2.55.0.795.g602f6c329a.dirty\n\n"},{"id":"547225","messageId":"20260706-pks-refs-writing-subcommands-v4-5-d51f6ce7f830@pks.im","threadId":"65819","inReplyTo":"20260706-pks-refs-writing-subcommands-v4-0-d51f6ce7f830@pks.im","subject":"[PATCH v4 5/5] builtin/refs: add \"rename\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-06T13:27:08Z","receivedAt":"2026-07-06T13:27:26Z","isPatch":true,"body":"Add a \"rename\" subcommand to git-refs(1) with the syntax:\n\n  $ git refs rename <oldref> <newref>\n\nIt renames <oldref> together with its reflog to <newref>; even when used\non a local branch ref, the current value and the reflog of the ref are\nthe only things that are renamed. Document it and redirect casual users\nto \"git branch -m\" if that is what they wanted to do.\n\nCo-authored-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-refs.adoc |   6 ++\n builtin/refs.c              |  49 +++++++++++++++\n t/meson.build               |   1 +\n t/t1467-refs-rename.sh      | 144 ++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 200 insertions(+)\n\ndiff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc\nindex e6a3528349..ce278c59bf 100644\n--- a/Documentation/git-refs.adoc\n+++ b/Documentation/git-refs.adoc\n@@ -23,6 +23,7 @@ git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude\n git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>\n git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]\n git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\n+git refs rename [--message=<reason>] <old-ref> <new-ref>\n \n DESCRIPTION\n -----------\n@@ -71,6 +72,11 @@ update::\n \t`<new-value>` deletes the branch, whereas an all-zeroes `<old-value>`\n \tensures that the branch does not yet exist.\n \n+rename::\n+\tRename the reference `<oldref>` to `<newref>`. The old reference must\n+\texist and the new reference must not yet exist, and both must have a\n+\twell-formed name (see linkgit:git-check-ref-format[1]).\n+\n OPTIONS\n -------\n \ndiff --git a/builtin/refs.c b/builtin/refs.c\nindex 1ebaf30149..a9ca2058ee 100644\n--- a/builtin/refs.c\n+++ b/builtin/refs.c\n@@ -30,6 +30,9 @@\n #define REFS_UPDATE_USAGE \\\n \tN_(\"git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]\")\n \n+#define REFS_RENAME_USAGE \\\n+\tN_(\"git refs rename [--message=<reason>] <old-ref> <new-ref>\")\n+\n static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,\n \t\t\t    struct repository *repo)\n {\n@@ -327,6 +330,50 @@ static int cmd_refs_update(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int cmd_refs_rename(int argc, const char **argv, const char *prefix,\n+\t\t\t   struct repository *repo)\n+{\n+\tstatic char const * const refs_rename_usage[] = {\n+\t\tREFS_RENAME_USAGE,\n+\t\tNULL\n+\t};\n+\tconst char *message = NULL;\n+\tstruct option opts[] = {\n+\t\tOPT_STRING(0, \"message\", &message, N_(\"reason\"),\n+\t\t\t   N_(\"reason of the update\")),\n+\t\tOPT_END(),\n+\t};\n+\tconst char *oldref, *newref;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, refs_rename_usage, 0);\n+\tif (argc != 2)\n+\t\tusage(_(\"rename requires old and new reference name\"));\n+\tif (message && !*message)\n+\t\tdie(_(\"refusing to perform update with empty message\"));\n+\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\toldref = argv[0];\n+\tnewref = argv[1];\n+\n+\tif (check_refname_format(oldref, 0))\n+\t\tdie(_(\"invalid ref format: '%s'\"), oldref);\n+\tif (check_refname_format(newref, 0))\n+\t\tdie(_(\"invalid ref format: '%s'\"), newref);\n+\n+\tif (!refs_ref_exists(get_main_ref_store(repo), oldref))\n+\t\tdie(_(\"reference does not exist: '%s'\"), oldref);\n+\tif (refs_ref_exists(get_main_ref_store(repo), newref))\n+\t\tdie(_(\"reference already exists: '%s'\"), newref);\n+\n+\tret = refs_rename_ref(get_main_ref_store(repo), oldref, newref, message);\n+\n+\tif (ret < 0)\n+\t\tret = 1;\n+\treturn ret;\n+}\n+\n int cmd_refs(int argc,\n \t     const char **argv,\n \t     const char *prefix,\n@@ -341,6 +388,7 @@ int cmd_refs(int argc,\n \t\tREFS_CREATE_USAGE,\n \t\tREFS_DELETE_USAGE,\n \t\tREFS_UPDATE_USAGE,\n+\t\tREFS_RENAME_USAGE,\n \t\tNULL,\n \t};\n \tparse_opt_subcommand_fn *fn = NULL;\n@@ -353,6 +401,7 @@ int cmd_refs(int argc,\n \t\tOPT_SUBCOMMAND(\"create\", &fn, cmd_refs_create),\n \t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_refs_delete),\n \t\tOPT_SUBCOMMAND(\"update\", &fn, cmd_refs_update),\n+\t\tOPT_SUBCOMMAND(\"rename\", &fn, cmd_refs_rename),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex 541e6f919c..a39fd8c4c4 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -226,6 +226,7 @@ integration_tests = [\n   't1464-refs-delete.sh',\n   't1465-refs-update.sh',\n   't1466-refs-create.sh',\n+  't1467-refs-rename.sh',\n   't1500-rev-parse.sh',\n   't1501-work-tree.sh',\n   't1502-rev-parse-parseopt.sh',\ndiff --git a/t/t1467-refs-rename.sh b/t/t1467-refs-rename.sh\nnew file mode 100755\nindex 0000000000..2b28be75c8\n--- /dev/null\n+++ b/t/t1467-refs-rename.sh\n@@ -0,0 +1,144 @@\n+#!/bin/sh\n+\n+test_description='git refs rename'\n+\n+. ./test-lib.sh\n+\n+setup_repo () {\n+\tgit init \"$1\" &&\n+\ttest_commit -C \"$1\" A &&\n+\ttest_commit -C \"$1\" B\n+}\n+\n+test_ref_matches () {\n+\tgit rev-parse \"$1\" >expect &&\n+\techo \"$2\" >actual &&\n+\ttest_cmp expect actual\n+}\n+\n+test_expect_success 'rename an existing reference' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs rename refs/heads/foo refs/heads/bar &&\n+\t\ttest_must_fail git refs exists refs/heads/foo &&\n+\t\ttest_ref_matches refs/heads/bar $A\n+\t)\n+'\n+\n+test_expect_success 'rename moves the reflog along with the reference' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update --message=\"rename me\" refs/heads/foo $A &&\n+\t\tgit refs rename refs/heads/foo refs/heads/bar &&\n+\t\tgit reflog show refs/heads/bar >reflog &&\n+\t\ttest_grep \"rename me\" reflog &&\n+\t\ttest_must_fail git reflog exists refs/heads/foo\n+\t)\n+'\n+\n+test_expect_success 'rename with message records reason in reflog' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs rename --message=\"rename reason\" refs/heads/foo refs/heads/bar &&\n+\t\tgit reflog show refs/heads/bar >actual &&\n+\t\ttest_grep \"rename reason\" actual\n+\t)\n+'\n+\n+test_expect_success 'rename a nonexistent reference fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git refs rename refs/heads/foo refs/heads/bar 2>err &&\n+\t\ttest_grep \"reference does not exist\" err\n+\t)\n+'\n+\n+test_expect_success 'rename to an existing reference fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tB=$(git rev-parse B) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\tgit refs update refs/heads/bar $B &&\n+\t\ttest_must_fail git refs rename refs/heads/foo refs/heads/bar 2>err &&\n+\t\ttest_grep \"reference already exists\" err\n+\t)\n+'\n+\n+test_expect_success 'rename with symbolic ref fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs create refs/heads/target $A &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/target &&\n+\t\t! git refs rename refs/heads/symref refs/heads/renamed 2>err &&\n+\t\ttest_grep \"is a symbolic ref, .* not supported\" err\n+\t)\n+'\n+\n+test_expect_success 'rename with empty message fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs rename --message= refs/heads/foo refs/heads/bar 2>err &&\n+\t\ttest_grep \"empty message\" err\n+\t)\n+'\n+\n+test_expect_success 'rename with invalid old reference name fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git refs rename \"refs/heads/foo..bar\" refs/heads/bar 2>err &&\n+\t\ttest_grep \"invalid ref format\" err\n+\t)\n+'\n+\n+test_expect_success 'rename with invalid new reference name fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tA=$(git rev-parse A) &&\n+\t\tgit refs update refs/heads/foo $A &&\n+\t\ttest_must_fail git refs rename refs/heads/foo \"refs/heads/bar..baz\" 2>err &&\n+\t\ttest_grep \"invalid ref format\" err\n+\t)\n+'\n+\n+test_expect_success 'rename with too few arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git -C repo refs rename refs/heads/foo 2>err &&\n+\ttest_grep \"requires old and new reference name\" err\n+'\n+\n+test_expect_success 'rename with too many arguments fails' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tsetup_repo repo &&\n+\ttest_must_fail git -C repo refs rename refs/heads/foo refs/heads/bar refs/heads/baz 2>err &&\n+\ttest_grep \"requires old and new reference name\" err\n+'\n+\n+test_done\n\n-- \n2.55.0.795.g602f6c329a.dirty\n\n"},{"id":"547246","messageId":"xmqqtsqcb1xv.fsf@gitster.g","threadId":"65819","inReplyTo":"20260706-pks-refs-writing-subcommands-v4-0-d51f6ce7f830@pks.im","subject":"Re: [PATCH v4 0/5] builtin/refs: add ability to write references","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-06T14:57:48Z","receivedAt":"2026-07-06T14:57:50Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Reference-related functionality in Git is currently spread across many\n> different commands: git-update-ref(1), git-for-each-ref(1),\n> git-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it\n> hard for users to discover what functionality we have available to work\n> with references.\n>\n> We have thus started to consolidate this functionality into git-refs(1),\n> which is a toolbox of everything related to references. Until now, the\n> command doesn't handle functionality of git-update-ref(1).\n>\n> This patch series backfills most of the functionality by introducing\n> three new commands:\n>\n>   - `git refs delete` to delete references. This is the equivalent of\n>     `git update-ref -d`.\n>\n>   - `git refs update` to update references. This is the equivalent of\n>     `git update-ref <refname> <oldvalue> <newvalue>`.\n>\n>   - `git refs rename` to rename a reference, including its reflog. This\n>     does not have an equivalent in git-update-ref(1), but is inspired by\n>     and supersedes [1].\n>\n> Changes in v4:\n>   - Add a couple more tests around symrefs.\n>   - Use a subshell in one of the tests for consistency.\n>   - Link to v3: https://patch.msgid.link/20260630-pks-refs-writing-subcommands-v3-0-deb04de1ecef@pks.im\n\nGood that this came just in time before I started merging things\ndown to 'next' ;-)  Will replace.\n"},{"id":"547596","messageId":"874ii8o5tx.fsf@emacs.iotcl.com","threadId":"65819","inReplyTo":"20260706-pks-refs-writing-subcommands-v4-2-d51f6ce7f830@pks.im","subject":"Re: [PATCH v4 2/5] builtin/refs: add \"delete\" subcommand","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-07-09T09:44:26Z","receivedAt":"2026-07-09T09:44:40Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> +test_expect_success 'delete symref with --no-deref verifies target OID' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tsetup_repo repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\tA=$(git rev-parse A) &&\n> +\t\tB=$(git rev-parse B) &&\n> +\t\tgit update-ref refs/heads/foo $A &&\n> +\t\tgit symbolic-ref refs/heads/symref refs/heads/foo &&\n> +\n> +\t\ttest_must_fail git refs delete --no-deref refs/heads/symref $B &&\n> +\t\tgit refs exists refs/heads/symref &&\n> +\n> +\t\tgit refs delete --no-deref refs/heads/symref $A &&\n> +\t\ttest_must_fail git refs exists refs/heads/symref &&\n> +\t\tgit refs exists refs/heads/foo\n> +\t)\n\nSo with --no-deref it still checks the dererenced OID, not the target\nref? I expected it to work like this:\n\n    test_expect_success 'delete symref with --no-deref verifies target ref' '\n            test_when_finished \"rm -rf repo\" &&\n            setup_repo repo &&\n            (\n                    cd repo &&\n                    A=$(git rev-parse A) &&\n                    B=$(git rev-parse B) &&\n                    git update-ref refs/heads/foo $A &&\n                    git symbolic-ref refs/heads/symref refs/heads/foo &&\n\n                    test_must_fail git refs delete --no-deref refs/heads/symref refs/heads/bar &&\n                    git refs exists refs/heads/symref &&\n\n                    git refs delete --no-deref refs/heads/symref refs/heads/foo &&\n                    test_must_fail git refs exists refs/heads/symref &&\n                    git refs exists refs/heads/foo\n            )\n\n\n-- \nCheers,\nToon\n"},{"id":"547597","messageId":"87zf00mqv1.fsf@emacs.iotcl.com","threadId":"65819","inReplyTo":"aktVdaB2xRk-iI_8@pks.im","subject":"Re: [PATCH v3 4/5] builtin/refs: add \"create\" subcommand","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-07-09T09:53:06Z","receivedAt":"2026-07-09T09:53:18Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Fri, Jul 03, 2026 at 04:19:58PM +0200, Toon Claes wrote:\n>> Patrick Steinhardt <ps@pks.im> writes:\n>> > diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc\n>> > index 6475bdcc62..e6a3528349 100644\n>> > --- a/Documentation/git-refs.adoc\n>> > +++ b/Documentation/git-refs.adoc\n>> > @@ -181,6 +184,53 @@ static int cmd_refs_optimize(int argc, const char **argv, const char *prefix,\n>> >  \treturn pack_refs_core(argc, argv, prefix, repo, refs_optimize_usage);\n>> >  }\n>> >  \n>> > +static int cmd_refs_create(int argc, const char **argv, const char *prefix,\n>> > +\t\t\t   struct repository *repo)\n>> > +{\n>> > +\tstatic char const * const refs_create_usage[] = {\n>> > +\t\tREFS_CREATE_USAGE,\n>> > +\t\tNULL\n>> > +\t};\n>> > +\tconst char *message = NULL;\n>> > +\tunsigned flags = 0;\n>> > +\tstruct option opts[] = {\n>> > +\t\tOPT_STRING(0, \"message\", &message, N_(\"reason\"),\n>> > +\t\t\t   N_(\"reason of the update\")),\n>> > +\t\tOPT_BIT(0 ,\"no-deref\", &flags,\n>> > +\t\t\tN_(\"update <refname> not the one it points to\"),\n>> > +\t\t\tREF_NO_DEREF),\n>> \n>> Can `git refs create --no-deref` be used to create symrefs? Should we\n>> add a test for that? Or can it not\n>> \n>> I understand the symmetry, but does it make sense to ask the user to\n>> create symrefs with `--no-deref`? Feels a bit obscure. The docs say:\n>> \n>> `--no-deref`::\n>> \tOperate on <ref> itself rather than the reference it points to via a\n>> \tsymbolic ref.\n>> \n>> That's far from obvious for a user to realize they need to pass that\n>> option if they want to create a symref.\n>\n> It doesn't cause them to create a symref. What this flag controls is\n> whether the command would fail when the refname exists already as a\n> symbolic ref. That is:\n>\n>     $ git symbolic-ref refs/heads/symref refs/heads/target\n>     $ git refs create refs/heads/symref $OID\n>     $ git refs exists refs/heads/target\n\nThat makes sense. Sort of.\n\nSo passing `--no-deref` to `git refs create` in the example above would\nmake sense if you want creation of refs/heads/target to fail. Okay,\ndoesn't seem very obvious, but feels correct.\n\n> The git-refs(1) command would have created \"refs/heads/target\" in this\n> case, and by passing \"--no-deref\" you'd instead make it fail.\n>\n> This flag is somewhat weird. Having it is probably a sensible think to\n> do, but now that I think about it I wonder whether the default makes all\n> that much sense in the first place. That being said, _if_ we want to\n> change it then we should change it for all subcommands.\n\nNot sure how to make it better, so let's leave it like this.\n\n>> > diff --git a/t/t1466-refs-create.sh b/t/t1466-refs-create.sh\n>> > new file mode 100755\n>> > index 0000000000..cfb21bf863\n>> > --- /dev/null\n>> > +++ b/t/t1466-refs-create.sh\n>> > @@ -0,0 +1,151 @@\n> [snip]\n>> > +test_expect_success 'create fails when the reference already exists' '\n>> > +\ttest_when_finished \"rm -rf repo\" &&\n>> > +\tsetup_repo repo &&\n>> > +\t(\n>> > +\t\tcd repo &&\n>> > +\t\tA=$(git rev-parse A) &&\n>> > +\t\tB=$(git rev-parse B) &&\n>> > +\t\tgit refs create refs/heads/foo $A &&\n>> > +\t\ttest_must_fail git refs create refs/heads/foo $B 2>err &&\n>> > +\t\ttest_grep \"reference already exists\" err &&\n>> > +\t\ttest_ref_matches refs/heads/foo \"$A\"\n>> > +\t)\n>> > +'\n>> \n>> I was curious about this test:\n>> \n>> \ttest_expect_success 'create succeed when the reference exists with the same value' '\n>> \t\ttest_when_finished \"rm -rf repo\" &&\n>> \t\tsetup_repo repo &&\n>> \t\t(\n>> \t\t\tcd repo &&\n>> \t\t\tA=$(git rev-parse A) &&\n>> \t\t\tgit refs create refs/heads/foo $A &&\n>> \t\t\tgit refs create refs/heads/foo $A &&\n>> \t\t\ttest_ref_matches refs/heads/foo \"$A\"\n>> \t\t)\n>> \t'\n>> \n>> That fails. It that intentional?\n>\n> Yes, this is intentional. We didn't end up creating the reference, which\n> is what the user has asked us to do, and hence we fail.\n\nUnderstood.\n\n>> > +test_expect_success 'create with symref target and --no-deref refuses to create reference' '\n>> > +\ttest_when_finished \"rm -rf repo\" &&\n>> > +\tsetup_repo repo &&\n>> > +\t(\n>> > +\t\tcd repo &&\n>> > +\t\tA=$(git rev-parse A) &&\n>> > +\t\tgit symbolic-ref refs/heads/symref refs/heads/target &&\n>> > +\t\ttest_must_fail git refs create --no-deref refs/heads/symref $A 2>err &&\n>> > +\t\ttest_grep \"dangling symref already exists\" err &&\n>> > +\t\ttest_must_fail git reflog exists refs/heads/target\n>> > +\t)\n>> > +'\n>> \n>> Would it make sense to add this test:\n>> \n>> \ttest_expect_success 'create with symref target with --no-deref' '\n>> \t\ttest_when_finished \"rm -rf repo\" &&\n>> \t\tsetup_repo repo &&\n>> \t\t(\n>> \t\t\tcd repo &&\n>> \t\t\tA=$(git rev-parse A) &&\n>> \t\t\tgit refs create refs/heads/target $A &&\n>> \t\t\tgit refs create --no-deref refs/heads/symref refs/heads/target &&\n>> \t\t\tgit reflog exists refs/heads/symref && false\n>> \t\t)\n>> \t'\n>> \n>> But that makes me think, this option `--no-deref` is pretty obscure for\n>> use with `git refs create`. There are two situations:\n>> \n>> * The symref doesn't exists: so --no-deref basically is forcing the\n>>   command to create a symref. That's confusing\n>\n> No, it's not. It tells us that we only want to create the reference if\n> it doesn't exist and is not a symref. Otherwise, we'd potentially create\n> the reference that the symref is pointing to.\n\nOkay, I better understand now with the example above. Thanks!\n\n-- \nCheers,\nToon\n"},{"id":"547598","messageId":"87wlv4mq9s.fsf@emacs.iotcl.com","threadId":"65819","inReplyTo":"87zf00mqv1.fsf@emacs.iotcl.com","subject":"Re: [PATCH v3 4/5] builtin/refs: add \"create\" subcommand","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-07-09T10:05:51Z","receivedAt":"2026-07-09T10:05:58Z","isPatch":true,"body":"Toon Claes <toon@iotcl.com> writes:\n\n> Patrick Steinhardt <ps@pks.im> writes:\n\n>> This flag is somewhat weird. Having it is probably a sensible think to\n>> do, but now that I think about it I wonder whether the default makes all\n>> that much sense in the first place. That being said, _if_ we want to\n>> change it then we should change it for all subcommands.\n>\n> Not sure how to make it better, so let's leave it like this.\n\nWell, we could drop the `--no-deref` completely? If you want to modify a\nref, use git-ref(1). If you pass that command a symref, it always\ndereferences down to the ref. If you want to modify a symref, use\ngit-symbolic-ref(1).\n\n-- \nCheers,\nToon\n"}]}