* [PATCH 0/4] builtin/refs: add ability to write references
@ 2026-06-16 8:44 Patrick Steinhardt
2026-06-16 8:44 ` [PATCH 1/4] builtin/refs: drop `the_repository` Patrick Steinhardt
` (4 more replies)
0 siblings, 5 replies; 22+ messages in thread
From: Patrick Steinhardt @ 2026-06-16 8:44 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano
Hi,
Reference-related functionality in Git is currently spread across many
different commands: git-update-ref(1), git-for-each-ref(1),
git-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it
hard for users to discover what functionality we have available to work
with references.
We have thus started to consolidate this functionality into git-refs(1),
which is a toolbox of everything related to references. Until now, the
command doesn't handle functionality of git-update-ref(1).
This patch series backfills most of the functionality by introducing
three new commands:
- `git refs delete` to delete references. This is the equivalent of
`git update-ref -d`.
- `git refs update` to update references. This is the equivalent of
`git update-ref <refname> <oldvalue> <newvalue>`.
- `git refs rename` to rename a reference, including its reflog. This
does not have an equivalent in git-update-ref(1), but is inspired by
and supersedes [1].
Thanks!
Patrick
[1]: <xmqqv7brz9ba.fsf@gitster.g>
---
Patrick Steinhardt (4):
builtin/refs: drop `the_repository`
builtin/refs: add "delete" subcommand
builtin/refs: add "update" subcommand
builtin/refs: add "rename" subcommand
Documentation/git-refs.adoc | 34 +++++++++
builtin/refs.c | 153 +++++++++++++++++++++++++++++++++++--
t/meson.build | 3 +
t/t1464-refs-delete.sh | 133 ++++++++++++++++++++++++++++++++
t/t1465-refs-update.sh | 179 ++++++++++++++++++++++++++++++++++++++++++++
t/t1466-refs-rename.sh | 131 ++++++++++++++++++++++++++++++++
6 files changed, 625 insertions(+), 8 deletions(-)
---
base-commit: 700432b2ba22603a0bcb71475c9c333d17c9b0d1
change-id: 20260616-pks-refs-writing-subcommands-7a77be5bda9b
^ permalink raw reply [flat|nested] 22+ messages in thread
* [PATCH 1/4] builtin/refs: drop `the_repository`
2026-06-16 8:44 [PATCH 0/4] builtin/refs: add ability to write references Patrick Steinhardt
@ 2026-06-16 8:44 ` Patrick Steinhardt
2026-06-16 8:44 ` [PATCH 2/4] builtin/refs: add "delete" subcommand Patrick Steinhardt
` (3 subsequent siblings)
4 siblings, 0 replies; 22+ messages in thread
From: Patrick Steinhardt @ 2026-06-16 8:44 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano
We still have a couple of uses of `the_repository` in "builtin/refs.c".
All of those are trivial to convert though as the command always
requires a repository to exist.
Convert them to use the passed-in repository and drop
`USE_THE_REPOSITORY_VARIABLE`.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
builtin/refs.c | 15 +++++++--------
1 file changed, 7 insertions(+), 8 deletions(-)
diff --git a/builtin/refs.c b/builtin/refs.c
index e3125bc61b..f0faabf45a 100644
--- a/builtin/refs.c
+++ b/builtin/refs.c
@@ -1,4 +1,3 @@
-#define USE_THE_REPOSITORY_VARIABLE
#include "builtin.h"
#include "config.h"
#include "fsck.h"
@@ -23,7 +22,7 @@
N_("git refs optimize " PACK_REFS_OPTS)
static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
- struct repository *repo UNUSED)
+ struct repository *repo)
{
const char * const migrate_usage[] = {
REFS_MIGRATE_USAGE,
@@ -59,13 +58,13 @@ static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
goto out;
}
- if (the_repository->ref_storage_format == format) {
+ if (repo->ref_storage_format == format) {
err = error(_("repository already uses '%s' format"),
ref_storage_format_to_name(format));
goto out;
}
- if (repo_migrate_ref_storage_format(the_repository, format, flags, &errbuf) < 0) {
+ if (repo_migrate_ref_storage_format(repo, format, flags, &errbuf) < 0) {
err = error("%s", errbuf.buf);
goto out;
}
@@ -99,8 +98,8 @@ static int cmd_refs_verify(int argc, const char **argv, const char *prefix,
if (argc)
usage(_("'git refs verify' takes no arguments"));
- repo_config(the_repository, git_fsck_config, &fsck_refs_options);
- prepare_repo_settings(the_repository);
+ repo_config(repo, git_fsck_config, &fsck_refs_options);
+ prepare_repo_settings(repo);
worktrees = get_worktrees_without_reading_head();
for (size_t i = 0; worktrees[i]; i++)
@@ -124,7 +123,7 @@ static int cmd_refs_list(int argc, const char **argv, const char *prefix,
}
static int cmd_refs_exists(int argc, const char **argv, const char *prefix,
- struct repository *repo UNUSED)
+ struct repository *repo)
{
struct strbuf unused_referent = STRBUF_INIT;
struct object_id unused_oid;
@@ -145,7 +144,7 @@ static int cmd_refs_exists(int argc, const char **argv, const char *prefix,
die(_("'git refs exists' requires a reference"));
ref = *argv++;
- if (refs_read_raw_ref(get_main_ref_store(the_repository), ref,
+ if (refs_read_raw_ref(get_main_ref_store(repo), ref,
&unused_oid, &unused_referent, &unused_type,
&failure_errno)) {
if (failure_errno == ENOENT || failure_errno == EISDIR) {
--
2.55.0.rc0.786.g65d90a0328.dirty
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 2/4] builtin/refs: add "delete" subcommand
2026-06-16 8:44 [PATCH 0/4] builtin/refs: add ability to write references Patrick Steinhardt
2026-06-16 8:44 ` [PATCH 1/4] builtin/refs: drop `the_repository` Patrick Steinhardt
@ 2026-06-16 8:44 ` Patrick Steinhardt
2026-06-16 8:44 ` [PATCH 3/4] builtin/refs: add "update" subcommand Patrick Steinhardt
` (2 subsequent siblings)
4 siblings, 0 replies; 22+ messages in thread
From: Patrick Steinhardt @ 2026-06-16 8:44 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano
Reference-related functionality in Git is currently spread across many
different commands: git-update-ref(1), git-for-each-ref(1),
git-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it
hard for users to discover what functionality we have available to work
with references.
We have thus started to consolidate this functionality into git-refs(1),
which is a toolbox of everything related to references. Until now, the
command doesn't handle functionality of git-update-ref(1).
Fix this gap by introducing a new "delete" subcommand, which is the
equivalent of `git update-ref -d`.
Note that we're intentionally not using a generic "write" subcommand
with a "-d" flag. This is rather harder to discover, and subcommands
that are implmented as flags tend to be hard to reason about in the code
as we'd have to handle mutually-exclusive flags that stem from the other
subcommand-like modes.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-refs.adoc | 17 ++++++
builtin/refs.c | 46 +++++++++++++++
t/meson.build | 1 +
t/t1464-refs-delete.sh | 133 ++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 197 insertions(+)
diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc
index fa33680cc7..c03e8e6ac3 100644
--- a/Documentation/git-refs.adoc
+++ b/Documentation/git-refs.adoc
@@ -20,6 +20,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]
[ --stdin | (<pattern>...)]
git refs exists <ref>
git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
+git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]
DESCRIPTION
-----------
@@ -51,6 +52,12 @@ optimize::
usage. This subcommand is an alias for linkgit:git-pack-refs[1] and
offers identical functionality.
+delete::
+ Delete the given reference. This subcommand mirrors `git update-ref -d`
+ (see linkgit:git-update-ref[1]). When `<oldvalue>` is given, the
+ reference is only deleted after verifying that it currently contains
+ `<oldvalue>`.
+
OPTIONS
-------
@@ -90,6 +97,16 @@ The following options are specific to 'git refs optimize':
include::pack-refs-options.adoc[]
+The following options are specific to commands which write references:
+
+`--message=<reason>`::
+ Use the given <reason> string for the reflog entry associated with the
+ update. An empty message is rejected.
+
+`--no-deref`::
+ Operate on <ref> itself rather than the reference it points to via a
+ symbolic ref.
+
KNOWN LIMITATIONS
-----------------
diff --git a/builtin/refs.c b/builtin/refs.c
index f0faabf45a..69eb528522 100644
--- a/builtin/refs.c
+++ b/builtin/refs.c
@@ -21,6 +21,9 @@
#define REFS_OPTIMIZE_USAGE \
N_("git refs optimize " PACK_REFS_OPTS)
+#define REFS_DELETE_USAGE \
+ N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]")
+
static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
struct repository *repo)
{
@@ -175,6 +178,47 @@ static int cmd_refs_optimize(int argc, const char **argv, const char *prefix,
return pack_refs_core(argc, argv, prefix, repo, refs_optimize_usage);
}
+static int cmd_refs_delete(int argc, const char **argv, const char *prefix,
+ struct repository *repo)
+{
+ static char const * const refs_delete_usage[] = {
+ REFS_DELETE_USAGE,
+ NULL
+ };
+ const char *message = NULL;
+ unsigned flags = 0;
+ struct option opts[] = {
+ OPT_STRING(0, "message", &message, N_("reason"),
+ N_("reason of the update")),
+ OPT_BIT(0 ,"no-deref", &flags,
+ N_("update <refname> not the one it points to"),
+ REF_NO_DEREF),
+ OPT_END(),
+ };
+ struct object_id oldoid;
+ const char *refname;
+
+ argc = parse_options(argc, argv, prefix, opts, refs_delete_usage, 0);
+ if (argc < 1 || argc > 2)
+ usage(_("delete requires reference name and an optional old object ID"));
+
+ if (message && !*message)
+ die(_("refusing to perform update with empty message"));
+
+ repo_config(repo, git_default_config, NULL);
+
+ refname = argv[0];
+ if (argc == 2) {
+ if (repo_get_oid_with_flags(repo, argv[1], &oldoid, GET_OID_SKIP_AMBIGUITY_CHECK))
+ die(_("invalid old object ID: '%s'"), argv[1]);
+ if (is_null_oid(&oldoid))
+ die(_("cannot delete object with null old object ID"));
+ }
+
+ return refs_delete_ref(get_main_ref_store(repo), message, refname,
+ argc == 2 ? &oldoid : NULL, flags);
+}
+
int cmd_refs(int argc,
const char **argv,
const char *prefix,
@@ -186,6 +230,7 @@ int cmd_refs(int argc,
"git refs list " COMMON_USAGE_FOR_EACH_REF,
REFS_EXISTS_USAGE,
REFS_OPTIMIZE_USAGE,
+ REFS_DELETE_USAGE,
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
@@ -195,6 +240,7 @@ int cmd_refs(int argc,
OPT_SUBCOMMAND("list", &fn, cmd_refs_list),
OPT_SUBCOMMAND("exists", &fn, cmd_refs_exists),
OPT_SUBCOMMAND("optimize", &fn, cmd_refs_optimize),
+ OPT_SUBCOMMAND("delete", &fn, cmd_refs_delete),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index c5832fee05..1ccf08a3b5 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -223,6 +223,7 @@ integration_tests = [
't1461-refs-list.sh',
't1462-refs-exists.sh',
't1463-refs-optimize.sh',
+ 't1464-refs-delete.sh',
't1500-rev-parse.sh',
't1501-work-tree.sh',
't1502-rev-parse-parseopt.sh',
diff --git a/t/t1464-refs-delete.sh b/t/t1464-refs-delete.sh
new file mode 100755
index 0000000000..4a36d3866b
--- /dev/null
+++ b/t/t1464-refs-delete.sh
@@ -0,0 +1,133 @@
+#!/bin/sh
+
+test_description='git refs delete'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+
+setup_repo () {
+ git init "$1" &&
+ test_commit -C "$1" A &&
+ test_commit -C "$1" B
+}
+
+test_expect_success 'delete without oldvalue verification' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ A=$(git -C repo rev-parse A) &&
+ git -C repo update-ref refs/heads/foo $A &&
+ git -C repo refs delete refs/heads/foo &&
+ test_must_fail git -C repo show-ref --verify -q refs/heads/foo
+'
+
+test_expect_success 'delete with matching oldvalue' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git update-ref refs/heads/foo $A &&
+ git refs delete refs/heads/foo $A &&
+ test_must_fail git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'delete with stale oldvalue fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git update-ref refs/heads/foo $A &&
+ test_must_fail git refs delete refs/heads/foo $B 2>err &&
+ test_grep " but expected " err &&
+ git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'delete with null oldvalue fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git update-ref refs/heads/foo $A &&
+ test_must_fail git refs delete refs/heads/foo $ZERO_OID 2>err &&
+ test_grep "null old object ID" err &&
+ git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'delete with invalid oldvalue fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git update-ref refs/heads/foo $A &&
+ test_must_fail git refs delete refs/heads/foo invalid-oid 2>err &&
+ test_grep "invalid old object ID" err &&
+ git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'delete symref with --no-deref leaves target intact' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git update-ref refs/heads/foo $A &&
+ git symbolic-ref refs/heads/symref refs/heads/foo &&
+ git refs delete --no-deref refs/heads/symref &&
+ test_must_fail git refs exists refs/heads/symref &&
+ git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'delete with message records reason in reflog' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git update-ref refs/heads/foo $A &&
+ git symbolic-ref HEAD refs/heads/foo &&
+ git refs delete --message=delete-reason refs/heads/foo &&
+ test_must_fail git refs exists refs/heads/foo &&
+ test-tool ref-store main for-each-reflog-ent HEAD >actual &&
+ test_grep "delete-reason$" actual
+ )
+'
+
+test_expect_success 'delete with empty message fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git update-ref refs/heads/foo $A &&
+ test_must_fail git refs delete --message= refs/heads/foo 2>err &&
+ test_grep "empty message" err &&
+ git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'delete without arguments fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ test_must_fail git -C repo refs delete 2>err &&
+ test_grep "requires reference name" err
+'
+
+test_expect_success 'delete with too many arguments fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ test_must_fail git refs delete one two three 2>err &&
+ test_grep "requires reference name" err
+'
+
+test_done
--
2.55.0.rc0.786.g65d90a0328.dirty
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 3/4] builtin/refs: add "update" subcommand
2026-06-16 8:44 [PATCH 0/4] builtin/refs: add ability to write references Patrick Steinhardt
2026-06-16 8:44 ` [PATCH 1/4] builtin/refs: drop `the_repository` Patrick Steinhardt
2026-06-16 8:44 ` [PATCH 2/4] builtin/refs: add "delete" subcommand Patrick Steinhardt
@ 2026-06-16 8:44 ` Patrick Steinhardt
2026-06-16 11:17 ` Junio C Hamano
2026-06-16 14:52 ` Junio C Hamano
2026-06-16 8:44 ` [PATCH 4/4] builtin/refs: add "rename" subcommand Patrick Steinhardt
2026-06-17 10:15 ` [PATCH v2 0/5] builtin/refs: add ability to write references Patrick Steinhardt
4 siblings, 2 replies; 22+ messages in thread
From: Patrick Steinhardt @ 2026-06-16 8:44 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano
Add a new "update" subcommand which mirrors `git update-ref <refname>
<oldoid> <newoid>`. This follows the same reasoning as the preceding
commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-refs.adoc | 7 ++
builtin/refs.c | 50 +++++++++++++
t/meson.build | 1 +
t/t1465-refs-update.sh | 179 ++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 237 insertions(+)
diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc
index c03e8e6ac3..0a887cf5e5 100644
--- a/Documentation/git-refs.adoc
+++ b/Documentation/git-refs.adoc
@@ -21,6 +21,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]
git refs exists <ref>
git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]
+git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]
DESCRIPTION
-----------
@@ -58,6 +59,12 @@ delete::
reference is only deleted after verifying that it currently contains
`<oldvalue>`.
+update::
+ Update the given reference to point at `<new-value>`. This subcommand
+ mirrors `git update-ref` (see linkgit:git-update-ref[1]). When
+ `<old-value>` is given, the reference is only updated after verifying
+ that it currently contains `<old-value>`.
+
OPTIONS
-------
diff --git a/builtin/refs.c b/builtin/refs.c
index 69eb528522..3238ddf3f0 100644
--- a/builtin/refs.c
+++ b/builtin/refs.c
@@ -24,6 +24,9 @@
#define REFS_DELETE_USAGE \
N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]")
+#define REFS_UPDATE_USAGE \
+ N_("git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]")
+
static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
struct repository *repo)
{
@@ -219,6 +222,51 @@ static int cmd_refs_delete(int argc, const char **argv, const char *prefix,
argc == 2 ? &oldoid : NULL, flags);
}
+static int cmd_refs_update(int argc, const char **argv, const char *prefix,
+ struct repository *repo)
+{
+ static char const * const refs_update_usage[] = {
+ REFS_UPDATE_USAGE,
+ NULL
+ };
+ const char *message = NULL;
+ unsigned flags = 0;
+ struct option opts[] = {
+ OPT_STRING(0, "message", &message, N_("reason"),
+ N_("reason of the update")),
+ OPT_BIT(0 ,"no-deref", &flags,
+ N_("update <refname> not the one it points to"),
+ REF_NO_DEREF),
+ OPT_BIT(0, "create-reflog", &flags, N_("create a reflog"),
+ REF_FORCE_CREATE_REFLOG),
+ OPT_END(),
+ };
+ struct object_id newoid, oldoid;
+ const char *refname;
+
+ argc = parse_options(argc, argv, prefix, opts, refs_update_usage, 0);
+ if (argc < 2 || argc > 3)
+ usage(_("update requires reference name, new value and an optional old value"));
+
+ if (message && !*message)
+ die(_("refusing to perform update with empty message"));
+
+ repo_config(repo, git_default_config, NULL);
+
+ refname = argv[0];
+ if (repo_get_oid_with_flags(repo, argv[1], &newoid,
+ GET_OID_SKIP_AMBIGUITY_CHECK))
+ die(_("invalid new object ID: %s"), argv[1]);
+ if (argc == 3 &&
+ repo_get_oid_with_flags(repo, argv[2], &oldoid,
+ GET_OID_SKIP_AMBIGUITY_CHECK))
+ die(_("invalid old object ID: %s"), argv[2]);
+
+ return refs_update_ref(get_main_ref_store(repo), message, refname,
+ &newoid, argc == 3 ? &oldoid : NULL, flags,
+ UPDATE_REFS_DIE_ON_ERR);
+}
+
int cmd_refs(int argc,
const char **argv,
const char *prefix,
@@ -231,6 +279,7 @@ int cmd_refs(int argc,
REFS_EXISTS_USAGE,
REFS_OPTIMIZE_USAGE,
REFS_DELETE_USAGE,
+ REFS_UPDATE_USAGE,
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
@@ -241,6 +290,7 @@ int cmd_refs(int argc,
OPT_SUBCOMMAND("exists", &fn, cmd_refs_exists),
OPT_SUBCOMMAND("optimize", &fn, cmd_refs_optimize),
OPT_SUBCOMMAND("delete", &fn, cmd_refs_delete),
+ OPT_SUBCOMMAND("update", &fn, cmd_refs_update),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index 1ccf08a3b5..2063962dab 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -224,6 +224,7 @@ integration_tests = [
't1462-refs-exists.sh',
't1463-refs-optimize.sh',
't1464-refs-delete.sh',
+ 't1465-refs-update.sh',
't1500-rev-parse.sh',
't1501-work-tree.sh',
't1502-rev-parse-parseopt.sh',
diff --git a/t/t1465-refs-update.sh b/t/t1465-refs-update.sh
new file mode 100755
index 0000000000..e7582a6195
--- /dev/null
+++ b/t/t1465-refs-update.sh
@@ -0,0 +1,179 @@
+#!/bin/sh
+
+test_description='git refs update'
+
+. ./test-lib.sh
+
+setup_repo () {
+ git init "$1" &&
+ test_commit -C "$1" A &&
+ test_commit -C "$1" B
+}
+
+test_ref_matches () {
+ git rev-parse "$1" >expect &&
+ echo "$2" >actual &&
+ test_cmp expect actual
+}
+
+test_expect_success 'update creates a new reference' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/heads/foo $A &&
+ test_ref_matches refs/heads/foo "$A"
+ )
+'
+
+test_expect_success 'update an existing reference without oldvalue' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ git refs update refs/heads/foo $B &&
+ test_ref_matches refs/heads/foo $B
+ )
+'
+
+test_expect_success 'update with matching oldvalue' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ git refs update refs/heads/foo $B $A &&
+ test_ref_matches refs/heads/foo $B
+ )
+'
+
+test_expect_success 'update with stale oldvalue fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ test_must_fail git refs update refs/heads/foo $B $B 2>err &&
+ test_grep " but expected " err &&
+ test_ref_matches refs/heads/foo $A
+ )
+'
+
+test_expect_success 'update with invalid new value fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ test_must_fail git refs update refs/heads/foo invalid-oid 2>err &&
+ test_grep "invalid new object ID" err &&
+ test_must_fail git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'update with invalid old value fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ test_must_fail git refs update refs/heads/foo $B invalid-oid 2>err &&
+ test_grep "invalid old object ID" err &&
+ test_ref_matches refs/heads/foo $A
+ )
+'
+
+test_expect_success 'update --no-deref rewrites the symref itself' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ git symbolic-ref refs/heads/symref refs/heads/foo &&
+ git refs update --no-deref refs/heads/symref $B &&
+ test_must_fail git symbolic-ref refs/heads/symref &&
+ test_ref_matches refs/heads/symref $B &&
+ test_ref_matches refs/heads/foo $A
+ )
+'
+
+test_expect_success 'update does not create a reflog by default' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/foo $A &&
+ test_must_fail git reflog exists refs/foo
+ )
+'
+
+test_expect_success 'update creates a reflog with --create-reflog' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update --create-reflog refs/foo $A &&
+ git reflog exists refs/foo
+ )
+'
+
+test_expect_success 'update with message records reason in reflog' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ git refs update --message=update-reason refs/heads/foo $B &&
+ git reflog show refs/heads/foo >actual &&
+ test_grep "update-reason$" actual
+ )
+'
+
+test_expect_success 'update with empty message fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ test_must_fail git refs update --message= refs/heads/foo $B 2>err &&
+ test_grep "empty message" err
+ )
+'
+
+test_expect_success 'update with too few arguments fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ test_must_fail git -C repo refs update refs/heads/foo 2>err &&
+ test_grep "requires reference name, new value" err
+'
+
+test_expect_success 'update with too many arguments fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ test_must_fail git refs update refs/heads/foo $A $B extra 2>err &&
+ test_grep "requires reference name, new value" err
+ )
+'
+
+test_done
--
2.55.0.rc0.786.g65d90a0328.dirty
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 4/4] builtin/refs: add "rename" subcommand
2026-06-16 8:44 [PATCH 0/4] builtin/refs: add ability to write references Patrick Steinhardt
` (2 preceding siblings ...)
2026-06-16 8:44 ` [PATCH 3/4] builtin/refs: add "update" subcommand Patrick Steinhardt
@ 2026-06-16 8:44 ` Patrick Steinhardt
2026-06-16 14:53 ` Junio C Hamano
2026-06-17 10:15 ` [PATCH v2 0/5] builtin/refs: add ability to write references Patrick Steinhardt
4 siblings, 1 reply; 22+ messages in thread
From: Patrick Steinhardt @ 2026-06-16 8:44 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano
Add a "rename" subcommand to git-refs(1) with the syntax:
$ git refs rename <oldref> <newref>
It renames <oldref> together with its reflog to <newref>; even when used
on a local branch ref, the current value and the reflog of the ref are
the only things that are renamed. Document it and redirect casual users
to "git branch -m" if that is what they wanted to do.
Co-authored-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-refs.adoc | 10 ++++
builtin/refs.c | 42 ++++++++++++++
t/meson.build | 1 +
t/t1466-refs-rename.sh | 131 ++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 184 insertions(+)
diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc
index 0a887cf5e5..85eb100205 100644
--- a/Documentation/git-refs.adoc
+++ b/Documentation/git-refs.adoc
@@ -22,6 +22,7 @@ git refs exists <ref>
git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]
git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]
+git refs rename [--message=<reason>] <oldref> <newref>
DESCRIPTION
-----------
@@ -65,6 +66,11 @@ update::
`<old-value>` is given, the reference is only updated after verifying
that it currently contains `<old-value>`.
+rename::
+ Rename the reference `<oldref>` to `<newref>`. The old reference must
+ exist and the new reference must not yet exist, and both must have a
+ well-formed name (see linkgit:git-check-ref-format[1]).
+
OPTIONS
-------
@@ -106,6 +112,10 @@ include::pack-refs-options.adoc[]
The following options are specific to commands which write references:
+`--create-reflog`::
+ Create a reflog for the reference even if one would not ordinarily be
+ created.
+
`--message=<reason>`::
Use the given <reason> string for the reflog entry associated with the
update. An empty message is rejected.
diff --git a/builtin/refs.c b/builtin/refs.c
index 3238ddf3f0..b90baf5633 100644
--- a/builtin/refs.c
+++ b/builtin/refs.c
@@ -27,6 +27,9 @@
#define REFS_UPDATE_USAGE \
N_("git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]")
+#define REFS_RENAME_USAGE \
+ N_("git refs rename [--message=<reason>] <oldref> <newref>")
+
static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
struct repository *repo)
{
@@ -267,6 +270,43 @@ static int cmd_refs_update(int argc, const char **argv, const char *prefix,
UPDATE_REFS_DIE_ON_ERR);
}
+static int cmd_refs_rename(int argc, const char **argv, const char *prefix,
+ struct repository *repo)
+{
+ static char const * const refs_rename_usage[] = {
+ REFS_RENAME_USAGE,
+ NULL
+ };
+ const char *message = NULL;
+ struct option opts[] = {
+ OPT_STRING(0, "message", &message, N_("reason"),
+ N_("reason of the update")),
+ OPT_END(),
+ };
+ const char *oldref, *newref;
+
+ argc = parse_options(argc, argv, prefix, opts, refs_rename_usage, 0);
+ if (argc != 2)
+ usage(_("rename requires old and new reference name"));
+ if (message && !*message)
+ die(_("refusing to perform update with empty message"));
+
+ oldref = argv[0];
+ newref = argv[1];
+
+ if (check_refname_format(oldref, 0))
+ die(_("invalid ref format: %s"), oldref);
+ if (check_refname_format(newref, 0))
+ die(_("invalid ref format: %s"), newref);
+
+ if (!refs_ref_exists(get_main_ref_store(repo), oldref))
+ die(_("reference does not exist: '%s'"), oldref);
+ if (refs_ref_exists(get_main_ref_store(repo), newref))
+ die(_("reference already exists: '%s'"), newref);
+
+ return refs_rename_ref(get_main_ref_store(repo), oldref, newref, message);
+}
+
int cmd_refs(int argc,
const char **argv,
const char *prefix,
@@ -280,6 +320,7 @@ int cmd_refs(int argc,
REFS_OPTIMIZE_USAGE,
REFS_DELETE_USAGE,
REFS_UPDATE_USAGE,
+ REFS_RENAME_USAGE,
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
@@ -291,6 +332,7 @@ int cmd_refs(int argc,
OPT_SUBCOMMAND("optimize", &fn, cmd_refs_optimize),
OPT_SUBCOMMAND("delete", &fn, cmd_refs_delete),
OPT_SUBCOMMAND("update", &fn, cmd_refs_update),
+ OPT_SUBCOMMAND("rename", &fn, cmd_refs_rename),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index 2063962dab..a1a6880fe6 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -225,6 +225,7 @@ integration_tests = [
't1463-refs-optimize.sh',
't1464-refs-delete.sh',
't1465-refs-update.sh',
+ 't1466-refs-rename.sh',
't1500-rev-parse.sh',
't1501-work-tree.sh',
't1502-rev-parse-parseopt.sh',
diff --git a/t/t1466-refs-rename.sh b/t/t1466-refs-rename.sh
new file mode 100755
index 0000000000..f80d58e0f4
--- /dev/null
+++ b/t/t1466-refs-rename.sh
@@ -0,0 +1,131 @@
+#!/bin/sh
+
+test_description='git refs rename'
+
+. ./test-lib.sh
+
+setup_repo () {
+ git init "$1" &&
+ test_commit -C "$1" A &&
+ test_commit -C "$1" B
+}
+
+test_ref_matches () {
+ git rev-parse "$1" >expect &&
+ echo "$2" >actual &&
+ test_cmp expect actual
+}
+
+test_expect_success 'rename an existing reference' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/heads/foo $A &&
+ git refs rename refs/heads/foo refs/heads/bar &&
+ test_must_fail git refs exists refs/heads/foo &&
+ test_ref_matches refs/heads/bar $A
+ )
+'
+
+test_expect_success 'rename moves the reflog along with the reference' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update --message="rename me" refs/heads/foo $A &&
+ git refs rename refs/heads/foo refs/heads/bar &&
+ git reflog show refs/heads/bar >reflog &&
+ test_grep "rename me" reflog &&
+ test_must_fail git reflog exists refs/heads/foo
+ )
+'
+
+test_expect_success 'rename with message records reason in reflog' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/heads/foo $A &&
+ git refs rename --message="rename reason" refs/heads/foo refs/heads/bar &&
+ git reflog show refs/heads/bar >actual &&
+ test_grep "rename reason" actual
+ )
+'
+
+test_expect_success 'rename a nonexistent reference fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ test_must_fail git refs rename refs/heads/foo refs/heads/bar 2>err &&
+ test_grep "reference does not exist" err
+ )
+'
+
+test_expect_success 'rename to an existing reference fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ git refs update refs/heads/bar $B &&
+ test_must_fail git refs rename refs/heads/foo refs/heads/bar 2>err &&
+ test_grep "reference already exists" err
+ )
+'
+
+test_expect_success 'rename with empty message fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/heads/foo $A &&
+ test_must_fail git refs rename --message= refs/heads/foo refs/heads/bar 2>err &&
+ test_grep "empty message" err
+ )
+'
+
+test_expect_success 'rename with invalid old reference name fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ test_must_fail git refs rename "refs/heads/foo..bar" refs/heads/bar 2>err &&
+ test_grep "invalid ref format" err
+ )
+'
+
+test_expect_success 'rename with invalid new reference name fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/heads/foo $A &&
+ test_must_fail git refs rename refs/heads/foo "refs/heads/bar..baz" 2>err &&
+ test_grep "invalid ref format" err
+ )
+'
+
+test_expect_success 'rename with too few arguments fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ test_must_fail git -C repo refs rename refs/heads/foo 2>err &&
+ test_grep "requires old and new reference name" err
+'
+
+test_expect_success 'rename with too many arguments fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ test_must_fail git -C repo refs rename refs/heads/foo refs/heads/bar refs/heads/baz 2>err &&
+ test_grep "requires old and new reference name" err
+'
+
+test_done
--
2.55.0.rc0.786.g65d90a0328.dirty
^ permalink raw reply related [flat|nested] 22+ messages in thread
* Re: [PATCH 3/4] builtin/refs: add "update" subcommand
2026-06-16 8:44 ` [PATCH 3/4] builtin/refs: add "update" subcommand Patrick Steinhardt
@ 2026-06-16 11:17 ` Junio C Hamano
2026-06-17 7:28 ` Patrick Steinhardt
2026-06-16 14:52 ` Junio C Hamano
1 sibling, 1 reply; 22+ messages in thread
From: Junio C Hamano @ 2026-06-16 11:17 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
Patrick Steinhardt <ps@pks.im> writes:
> Add a new "update" subcommand which mirrors `git update-ref <refname>
> <oldoid> <newoid>`. This follows the same reasoning as the preceding
> commit.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> Documentation/git-refs.adoc | 7 ++
> builtin/refs.c | 50 +++++++++++++
> t/meson.build | 1 +
> t/t1465-refs-update.sh | 179 ++++++++++++++++++++++++++++++++++++++++++++
> 4 files changed, 237 insertions(+)
I do not offhand know (and I am still away by 2 hours from the time
I wake up and start functioning) if update-ref shares the same
issue, but with "delete, update, rename" combo, lack of "create"
feels a bit annoying. Wouldn't we want to offer an option to users
who want to ensure that the refs they create are truly new and they
are not overwriting a ref somebody has created? Either (1) drop
"delete" and take a special value (e.g. "") as <newvalue> to signal
deletion and make the same special value used as <oldvalue> signals
creation, or (2) add "create" and insist that "update" takes only an
existing ref, would make the annoyance go away, I guess.
> +test_expect_success 'update creates a new reference' '
> + test_when_finished "rm -rf repo" &&
> + setup_repo repo &&
> + (
> + cd repo &&
> + A=$(git rev-parse A) &&
> + git refs update refs/heads/foo $A &&
> + test_ref_matches refs/heads/foo "$A"
> + )
> +'
Here we cannot test (and I strongly suspect that "git refs update"
and "git update-ref" lack ability to do so) a case where a creation
is attempted on an existing ref and fails.
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH 3/4] builtin/refs: add "update" subcommand
2026-06-16 8:44 ` [PATCH 3/4] builtin/refs: add "update" subcommand Patrick Steinhardt
2026-06-16 11:17 ` Junio C Hamano
@ 2026-06-16 14:52 ` Junio C Hamano
2026-06-17 7:28 ` Patrick Steinhardt
1 sibling, 1 reply; 22+ messages in thread
From: Junio C Hamano @ 2026-06-16 14:52 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
Patrick Steinhardt <ps@pks.im> writes:
> git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]
> +git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]
"<old-value> vs <new-value>" is good, we should update "delete" to
use "<old-value>" to match.
> DESCRIPTION
> -----------
> @@ -58,6 +59,12 @@ delete::
> reference is only deleted after verifying that it currently contains
> `<oldvalue>`.
>
> +update::
> + Update the given reference to point at `<new-value>`. This subcommand
> + mirrors `git update-ref` (see linkgit:git-update-ref[1]). When
> + `<old-value>` is given, the reference is only updated after verifying
> + that it currently contains `<old-value>`.
As to the lack of "create", among the two potential changes, I have
a slight preference for adding "create" and failing "update" that
does not refer to an existing ref. If we go that route, the
"--create-reflog" option should move to "create", as "update" will
never be used to create a new ref.
> + if (repo_get_oid_with_flags(repo, argv[1], &newoid,
> + GET_OID_SKIP_AMBIGUITY_CHECK))
> + die(_("invalid new object ID: %s"), argv[1]);
> + if (argc == 3 &&
> + repo_get_oid_with_flags(repo, argv[2], &oldoid,
> + GET_OID_SKIP_AMBIGUITY_CHECK))
> + die(_("invalid old object ID: %s"), argv[2]);
On the "delete" side, these messages quote the object name, i.e.,
die(_("invalid old object ID: '%s'"), argv[1]);
We should be consistent.
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH 4/4] builtin/refs: add "rename" subcommand
2026-06-16 8:44 ` [PATCH 4/4] builtin/refs: add "rename" subcommand Patrick Steinhardt
@ 2026-06-16 14:53 ` Junio C Hamano
2026-06-17 7:28 ` Patrick Steinhardt
0 siblings, 1 reply; 22+ messages in thread
From: Junio C Hamano @ 2026-06-16 14:53 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
Patrick Steinhardt <ps@pks.im> writes:
> +static int cmd_refs_rename(int argc, const char **argv, const char *prefix,
> + struct repository *repo)
> +{
> + static char const * const refs_rename_usage[] = {
> + REFS_RENAME_USAGE,
> + NULL
> + };
> + const char *message = NULL;
> + struct option opts[] = {
> + OPT_STRING(0, "message", &message, N_("reason"),
> + N_("reason of the update")),
> + OPT_END(),
> + };
> + const char *oldref, *newref;
> +
> + argc = parse_options(argc, argv, prefix, opts, refs_rename_usage, 0);
> + if (argc != 2)
> + usage(_("rename requires old and new reference name"));
> + if (message && !*message)
> + die(_("refusing to perform update with empty message"));
> +
> + oldref = argv[0];
> + newref = argv[1];
> +
> + if (check_refname_format(oldref, 0))
> + die(_("invalid ref format: %s"), oldref);
> + if (check_refname_format(newref, 0))
> + die(_("invalid ref format: %s"), newref);
Do we want to quote the value? What other subcommands do in "git refs"?
> + if (!refs_ref_exists(get_main_ref_store(repo), oldref))
> + die(_("reference does not exist: '%s'"), oldref);
> + if (refs_ref_exists(get_main_ref_store(repo), newref))
> + die(_("reference already exists: '%s'"), newref);
> +
> + return refs_rename_ref(get_main_ref_store(repo), oldref, newref, message);
> +}
I suspect that my version shared the same issue, but doesn't
refs_rename_ref() return -1 for failure, which we may want to turn
to positive 1 before returning?
This is a tangent but git.c:handle_builtin() that calls
git.c:run_builtin() may want to do the "negative return? flip the
polarity" conversion to make this worry go away. I dunno what such
a change would break, though.
If we rename a ref that does not have a reflog, would it leave the
ref under the new name without reflog, or would we get a reflog with
a single entry that marks the fact the old ref was renamed into the
new ref? Should that be controlled via --create-reflog option?
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH 3/4] builtin/refs: add "update" subcommand
2026-06-16 11:17 ` Junio C Hamano
@ 2026-06-17 7:28 ` Patrick Steinhardt
2026-06-17 12:11 ` Junio C Hamano
0 siblings, 1 reply; 22+ messages in thread
From: Patrick Steinhardt @ 2026-06-17 7:28 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git
On Tue, Jun 16, 2026 at 04:17:27AM -0700, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
>
> > Add a new "update" subcommand which mirrors `git update-ref <refname>
> > <oldoid> <newoid>`. This follows the same reasoning as the preceding
> > commit.
> >
> > Signed-off-by: Patrick Steinhardt <ps@pks.im>
> > ---
> > Documentation/git-refs.adoc | 7 ++
> > builtin/refs.c | 50 +++++++++++++
> > t/meson.build | 1 +
> > t/t1465-refs-update.sh | 179 ++++++++++++++++++++++++++++++++++++++++++++
> > 4 files changed, 237 insertions(+)
>
> I do not offhand know (and I am still away by 2 hours from the time
> I wake up and start functioning) if update-ref shares the same
> issue, but with "delete, update, rename" combo, lack of "create"
> feels a bit annoying. Wouldn't we want to offer an option to users
> who want to ensure that the refs they create are truly new and they
> are not overwriting a ref somebody has created? Either (1) drop
> "delete" and take a special value (e.g. "") as <newvalue> to signal
> deletion and make the same special value used as <oldvalue> signals
> creation, or (2) add "create" and insist that "update" takes only an
> existing ref, would make the annoyance go away, I guess.
In theory "update" can handle both updating existing references,
deleting them and creating them race-free by providing NUL object IDs
for either old or new value. But I agree that this is cumbersome, and
adding another "create" subcommand as an easy-to-understand shortcut
feels sensible.
> > +test_expect_success 'update creates a new reference' '
> > + test_when_finished "rm -rf repo" &&
> > + setup_repo repo &&
> > + (
> > + cd repo &&
> > + A=$(git rev-parse A) &&
> > + git refs update refs/heads/foo $A &&
> > + test_ref_matches refs/heads/foo "$A"
> > + )
> > +'
>
> Here we cannot test (and I strongly suspect that "git refs update"
> and "git update-ref" lack ability to do so) a case where a creation
> is attempted on an existing ref and fails.
We can:
$ git update-ref $NEW_OID $NULL_OID
$ git refs update $NEW_OID $NULL_OID
This will verify that the reference doesn't exist before actually
writing it. Will add a test.
Patrick
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH 3/4] builtin/refs: add "update" subcommand
2026-06-16 14:52 ` Junio C Hamano
@ 2026-06-17 7:28 ` Patrick Steinhardt
0 siblings, 0 replies; 22+ messages in thread
From: Patrick Steinhardt @ 2026-06-17 7:28 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git
On Tue, Jun 16, 2026 at 07:52:59AM -0700, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
>
> > git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]
> > +git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]
>
> "<old-value> vs <new-value>" is good, we should update "delete" to
> use "<old-value>" to match.
Good catch.
> > @@ -58,6 +59,12 @@ delete::
> > reference is only deleted after verifying that it currently contains
> > `<oldvalue>`.
> >
> > +update::
> > + Update the given reference to point at `<new-value>`. This subcommand
> > + mirrors `git update-ref` (see linkgit:git-update-ref[1]). When
> > + `<old-value>` is given, the reference is only updated after verifying
> > + that it currently contains `<old-value>`.
>
> As to the lack of "create", among the two potential changes, I have
> a slight preference for adding "create" and failing "update" that
> does not refer to an existing ref. If we go that route, the
> "--create-reflog" option should move to "create", as "update" will
> never be used to create a new ref.
I agree that we should add "create", but I also think that we should
keep the special syntax of:
- "git refs update $NULL_OID [$OLD_OID]" to delete a branch.
- "git refs update $NEW_OID $NULL_OID" to create a branch.
While the other commands are more ergonomic, I don't see a strong reason
to restrict the "update" subcommand. Also, this ensures that it's in
line with git-update-ref(1), which also allows for these use cases.
> > + if (repo_get_oid_with_flags(repo, argv[1], &newoid,
> > + GET_OID_SKIP_AMBIGUITY_CHECK))
> > + die(_("invalid new object ID: %s"), argv[1]);
> > + if (argc == 3 &&
> > + repo_get_oid_with_flags(repo, argv[2], &oldoid,
> > + GET_OID_SKIP_AMBIGUITY_CHECK))
> > + die(_("invalid old object ID: %s"), argv[2]);
>
> On the "delete" side, these messages quote the object name, i.e.,
>
> die(_("invalid old object ID: '%s'"), argv[1]);
>
> We should be consistent.
Yup, fixed now.
Patrick
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH 4/4] builtin/refs: add "rename" subcommand
2026-06-16 14:53 ` Junio C Hamano
@ 2026-06-17 7:28 ` Patrick Steinhardt
2026-06-17 12:13 ` Junio C Hamano
0 siblings, 1 reply; 22+ messages in thread
From: Patrick Steinhardt @ 2026-06-17 7:28 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git
On Tue, Jun 16, 2026 at 07:53:02AM -0700, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
[snip]
> > + if (!refs_ref_exists(get_main_ref_store(repo), oldref))
> > + die(_("reference does not exist: '%s'"), oldref);
> > + if (refs_ref_exists(get_main_ref_store(repo), newref))
> > + die(_("reference already exists: '%s'"), newref);
> > +
> > + return refs_rename_ref(get_main_ref_store(repo), oldref, newref, message);
> > +}
>
> I suspect that my version shared the same issue, but doesn't
> refs_rename_ref() return -1 for failure, which we may want to turn
> to positive 1 before returning?
>
> This is a tangent but git.c:handle_builtin() that calls
> git.c:run_builtin() may want to do the "negative return? flip the
> polarity" conversion to make this worry go away. I dunno what such
> a change would break, though.
Fair. The other subcommands also suffer from the same problem, so I'll
update all of them to return 1 explicitly.
> If we rename a ref that does not have a reflog, would it leave the
> ref under the new name without reflog, or would we get a reflog with
> a single entry that marks the fact the old ref was renamed into the
> new ref? Should that be controlled via --create-reflog option?
It would leave it without a reflog. In theory I agree that it might make
sense to introduce a "--create-reflog" option, but that would require
some new plumbing in `refs_rename_ref()`. So I'd say that we can add it
at a later point as needed.
Patrick
^ permalink raw reply [flat|nested] 22+ messages in thread
* [PATCH v2 0/5] builtin/refs: add ability to write references
2026-06-16 8:44 [PATCH 0/4] builtin/refs: add ability to write references Patrick Steinhardt
` (3 preceding siblings ...)
2026-06-16 8:44 ` [PATCH 4/4] builtin/refs: add "rename" subcommand Patrick Steinhardt
@ 2026-06-17 10:15 ` Patrick Steinhardt
2026-06-17 10:15 ` [PATCH v2 1/5] builtin/refs: drop `the_repository` Patrick Steinhardt
` (6 more replies)
4 siblings, 7 replies; 22+ messages in thread
From: Patrick Steinhardt @ 2026-06-17 10:15 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano
Hi,
Reference-related functionality in Git is currently spread across many
different commands: git-update-ref(1), git-for-each-ref(1),
git-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it
hard for users to discover what functionality we have available to work
with references.
We have thus started to consolidate this functionality into git-refs(1),
which is a toolbox of everything related to references. Until now, the
command doesn't handle functionality of git-update-ref(1).
This patch series backfills most of the functionality by introducing
three new commands:
- `git refs delete` to delete references. This is the equivalent of
`git update-ref -d`.
- `git refs update` to update references. This is the equivalent of
`git update-ref <refname> <oldvalue> <newvalue>`.
- `git refs rename` to rename a reference, including its reflog. This
does not have an equivalent in git-update-ref(1), but is inspired by
and supersedes [1].
Changes in v2:
- Add a new "create" subcommand.
- Consistently quote in error messages.
- Consistently use `<old-value>` in the synopsis.
- Don't return negative exit codes.
- Improve documentation of "update" subcommand to mention that you can
create and delete branches.
- Add tests to verify that we can use "update" to do this, both in
racy and raceless ways.
- Add missing calls to `repo_config()`.
- Drop useless `GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME` variable.
- Link to v1: https://patch.msgid.link/20260616-pks-refs-writing-subcommands-v1-0-9f5219b6109d@pks.im
Thanks!
Patrick
[1]: <xmqqv7brz9ba.fsf@gitster.g>
---
Patrick Steinhardt (5):
builtin/refs: drop `the_repository`
builtin/refs: add "delete" subcommand
builtin/refs: add "update" subcommand
builtin/refs: add "create" subcommand
builtin/refs: add "rename" subcommand
Documentation/git-refs.adoc | 40 +++++++
builtin/refs.c | 222 ++++++++++++++++++++++++++++++++++--
t/meson.build | 4 +
t/t1464-refs-delete.sh | 130 +++++++++++++++++++++
t/t1465-refs-update.sh | 268 ++++++++++++++++++++++++++++++++++++++++++++
t/t1466-refs-create.sh | 151 +++++++++++++++++++++++++
t/t1467-refs-rename.sh | 131 ++++++++++++++++++++++
7 files changed, 938 insertions(+), 8 deletions(-)
Range-diff versus v1:
1: cfbc247e81 = 1: 6d0c5bd06f builtin/refs: drop `the_repository`
2: f5f33e5c5b ! 2: db55d87116 builtin/refs: add "delete" subcommand
@@ Documentation/git-refs.adoc: git refs list [--count=<count>] [--shell|--perl|--p
[ --stdin | (<pattern>...)]
git refs exists <ref>
git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
-+git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]
++git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]
DESCRIPTION
-----------
@@ Documentation/git-refs.adoc: optimize::
+delete::
+ Delete the given reference. This subcommand mirrors `git update-ref -d`
-+ (see linkgit:git-update-ref[1]). When `<oldvalue>` is given, the
++ (see linkgit:git-update-ref[1]). When `<old-value>` is given, the
+ reference is only deleted after verifying that it currently contains
-+ `<oldvalue>`.
++ `<old-value>`.
+
OPTIONS
-------
@@ builtin/refs.c
N_("git refs optimize " PACK_REFS_OPTS)
+#define REFS_DELETE_USAGE \
-+ N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]")
++ N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]")
+
static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
struct repository *repo)
@@ builtin/refs.c: static int cmd_refs_optimize(int argc, const char **argv, const
+ };
+ struct object_id oldoid;
+ const char *refname;
++ int ret;
+
+ argc = parse_options(argc, argv, prefix, opts, refs_delete_usage, 0);
+ if (argc < 1 || argc > 2)
@@ builtin/refs.c: static int cmd_refs_optimize(int argc, const char **argv, const
+ if (repo_get_oid_with_flags(repo, argv[1], &oldoid, GET_OID_SKIP_AMBIGUITY_CHECK))
+ die(_("invalid old object ID: '%s'"), argv[1]);
+ if (is_null_oid(&oldoid))
-+ die(_("cannot delete object with null old object ID"));
++ die(_("cannot delete reference with null old object ID"));
+ }
+
-+ return refs_delete_ref(get_main_ref_store(repo), message, refname,
-+ argc == 2 ? &oldoid : NULL, flags);
++ ret = refs_delete_ref(get_main_ref_store(repo), message, refname,
++ argc == 2 ? &oldoid : NULL, flags);
++
++ if (ret < 0)
++ ret = 1;
++ return ret;
+}
+
int cmd_refs(int argc,
@@ t/t1464-refs-delete.sh (new)
+
+test_description='git refs delete'
+
-+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
-+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
-+
+. ./test-lib.sh
+
+setup_repo () {
3: 1fc1bed619 ! 3: 85f07a2cb0 builtin/refs: add "update" subcommand
@@ Documentation/git-refs.adoc
@@ Documentation/git-refs.adoc: git refs list [--count=<count>] [--shell|--perl|--python|--tcl]
git refs exists <ref>
git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
- git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]
+ git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]
+git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]
DESCRIPTION
-----------
@@ Documentation/git-refs.adoc: delete::
reference is only deleted after verifying that it currently contains
- `<oldvalue>`.
+ `<old-value>`.
+update::
-+ Update the given reference to point at `<new-value>`. This subcommand
-+ mirrors `git update-ref` (see linkgit:git-update-ref[1]). When
-+ `<old-value>` is given, the reference is only updated after verifying
-+ that it currently contains `<old-value>`.
++ Update the given reference to point at `<new-value>`. If `<old-value>`
++ is given, the reference is only updated after verifying that it
++ currently contains `<old-value>`. As a special case, an all-zeroes
++ `<new-value>` deletes the branch, whereas an all-zeroes `<old-value>`
++ ensures that the branch does not yet exist.
+
OPTIONS
-------
+@@ Documentation/git-refs.adoc: include::pack-refs-options.adoc[]
+
+ The following options are specific to commands which write references:
+
++`--create-reflog`::
++ Create a reflog for the reference even if one would not ordinarily be
++ created.
++
+ `--message=<reason>`::
+ Use the given <reason> string for the reflog entry associated with the
+ update. An empty message is rejected.
## builtin/refs.c ##
@@
#define REFS_DELETE_USAGE \
- N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]")
+ N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]")
+#define REFS_UPDATE_USAGE \
+ N_("git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]")
@@ builtin/refs.c
struct repository *repo)
{
@@ builtin/refs.c: static int cmd_refs_delete(int argc, const char **argv, const char *prefix,
- argc == 2 ? &oldoid : NULL, flags);
+ return ret;
}
+static int cmd_refs_update(int argc, const char **argv, const char *prefix,
@@ builtin/refs.c: static int cmd_refs_delete(int argc, const char **argv, const ch
+ };
+ struct object_id newoid, oldoid;
+ const char *refname;
++ int ret;
+
+ argc = parse_options(argc, argv, prefix, opts, refs_update_usage, 0);
+ if (argc < 2 || argc > 3)
@@ builtin/refs.c: static int cmd_refs_delete(int argc, const char **argv, const ch
+ refname = argv[0];
+ if (repo_get_oid_with_flags(repo, argv[1], &newoid,
+ GET_OID_SKIP_AMBIGUITY_CHECK))
-+ die(_("invalid new object ID: %s"), argv[1]);
++ die(_("invalid new object ID: '%s'"), argv[1]);
+ if (argc == 3 &&
+ repo_get_oid_with_flags(repo, argv[2], &oldoid,
+ GET_OID_SKIP_AMBIGUITY_CHECK))
-+ die(_("invalid old object ID: %s"), argv[2]);
++ die(_("invalid old object ID: '%s'"), argv[2]);
+
-+ return refs_update_ref(get_main_ref_store(repo), message, refname,
-+ &newoid, argc == 3 ? &oldoid : NULL, flags,
-+ UPDATE_REFS_DIE_ON_ERR);
++ ret = refs_update_ref(get_main_ref_store(repo), message, refname,
++ &newoid, argc == 3 ? &oldoid : NULL, flags,
++ UPDATE_REFS_MSG_ON_ERR);
++
++ if (ret < 0)
++ ret = 1;
++ return ret;
+}
+
int cmd_refs(int argc,
@@ t/t1465-refs-update.sh (new)
+ )
+'
+
++test_expect_success 'update can create a new branch with oldvalue' '
++ test_when_finished "rm -rf repo" &&
++ setup_repo repo &&
++ (
++ cd repo &&
++ A=$(git rev-parse A) &&
++ git refs update refs/heads/foo $A $ZERO_OID 2>err &&
++ test_ref_matches refs/heads/foo $A
++ )
++'
++
++test_expect_success 'update can create a new branch without oldvalue' '
++ test_when_finished "rm -rf repo" &&
++ setup_repo repo &&
++ (
++ cd repo &&
++ A=$(git rev-parse A) &&
++ git refs update refs/heads/foo $A 2>err &&
++ test_ref_matches refs/heads/foo $A
++ )
++'
++
++test_expect_success 'update refuses to create preexisting branch' '
++ test_when_finished "rm -rf repo" &&
++ setup_repo repo &&
++ (
++ cd repo &&
++ A=$(git rev-parse A) &&
++ B=$(git rev-parse B) &&
++ git refs update refs/heads/foo $A &&
++ test_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err &&
++ test_grep "reference already exists" err &&
++ test_ref_matches refs/heads/foo $A
++ )
++'
++
++test_expect_success 'update can delete a branch with oldvalue' '
++ test_when_finished "rm -rf repo" &&
++ setup_repo repo &&
++ (
++ cd repo &&
++ A=$(git rev-parse A) &&
++ git refs update refs/heads/foo $A 2>err &&
++ git refs update refs/heads/foo $ZERO_OID $A 2>err &&
++ test_must_fail git refs exists refs/heads/foo
++ )
++'
++
++test_expect_success 'update can delete a branch without oldvalue' '
++ test_when_finished "rm -rf repo" &&
++ setup_repo repo &&
++ (
++ cd repo &&
++ A=$(git rev-parse A) &&
++ git refs update refs/heads/foo $A 2>err &&
++ git refs update refs/heads/foo $ZERO_OID 2>err &&
++ test_must_fail git refs exists refs/heads/foo
++ )
++'
++
++test_expect_success 'update refuses to delete a branch with mismatching value' '
++ test_when_finished "rm -rf repo" &&
++ setup_repo repo &&
++ (
++ cd repo &&
++ A=$(git rev-parse A) &&
++ B=$(git rev-parse B) &&
++ git refs update refs/heads/foo $A 2>err &&
++ test_must_fail git refs update refs/heads/foo $ZERO_OID $B 2>err &&
++ test_grep " but expected " err &&
++ git refs exists refs/heads/foo
++ )
++'
++
++test_expect_success 'update refuses to create preexisting branch' '
++ test_when_finished "rm -rf repo" &&
++ setup_repo repo &&
++ (
++ cd repo &&
++ A=$(git rev-parse A) &&
++ B=$(git rev-parse B) &&
++ git refs update refs/heads/foo $A &&
++ test_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err &&
++ test_grep "reference already exists" err &&
++ test_ref_matches refs/heads/foo $A
++ )
++'
++
++
+test_expect_success 'update with invalid new value fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
-: ---------- > 4: 03036ef730 builtin/refs: add "create" subcommand
4: aadedb14e1 ! 5: 65f0ee4f03 builtin/refs: add "rename" subcommand
@@ Commit message
Signed-off-by: Patrick Steinhardt <ps@pks.im>
## Documentation/git-refs.adoc ##
-@@ Documentation/git-refs.adoc: git refs exists <ref>
- git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
- git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]
+@@ Documentation/git-refs.adoc: git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude
+ git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>
+ git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]
git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]
-+git refs rename [--message=<reason>] <oldref> <newref>
++git refs rename [--message=<reason>] <old-ref> <new-ref>
DESCRIPTION
-----------
@@ Documentation/git-refs.adoc: update::
- `<old-value>` is given, the reference is only updated after verifying
- that it currently contains `<old-value>`.
+ `<new-value>` deletes the branch, whereas an all-zeroes `<old-value>`
+ ensures that the branch does not yet exist.
+rename::
+ Rename the reference `<oldref>` to `<newref>`. The old reference must
@@ Documentation/git-refs.adoc: update::
OPTIONS
-------
-@@ Documentation/git-refs.adoc: include::pack-refs-options.adoc[]
-
- The following options are specific to commands which write references:
-
-+`--create-reflog`::
-+ Create a reflog for the reference even if one would not ordinarily be
-+ created.
-+
- `--message=<reason>`::
- Use the given <reason> string for the reflog entry associated with the
- update. An empty message is rejected.
## builtin/refs.c ##
@@
@@ builtin/refs.c
N_("git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]")
+#define REFS_RENAME_USAGE \
-+ N_("git refs rename [--message=<reason>] <oldref> <newref>")
++ N_("git refs rename [--message=<reason>] <old-ref> <new-ref>")
+
static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
struct repository *repo)
{
@@ builtin/refs.c: static int cmd_refs_update(int argc, const char **argv, const char *prefix,
- UPDATE_REFS_DIE_ON_ERR);
+ return ret;
}
+static int cmd_refs_rename(int argc, const char **argv, const char *prefix,
@@ builtin/refs.c: static int cmd_refs_update(int argc, const char **argv, const ch
+ OPT_END(),
+ };
+ const char *oldref, *newref;
++ int ret;
+
+ argc = parse_options(argc, argv, prefix, opts, refs_rename_usage, 0);
+ if (argc != 2)
@@ builtin/refs.c: static int cmd_refs_update(int argc, const char **argv, const ch
+ if (message && !*message)
+ die(_("refusing to perform update with empty message"));
+
++ repo_config(repo, git_default_config, NULL);
++
+ oldref = argv[0];
+ newref = argv[1];
+
+ if (check_refname_format(oldref, 0))
-+ die(_("invalid ref format: %s"), oldref);
++ die(_("invalid ref format: '%s'"), oldref);
+ if (check_refname_format(newref, 0))
-+ die(_("invalid ref format: %s"), newref);
++ die(_("invalid ref format: '%s'"), newref);
+
+ if (!refs_ref_exists(get_main_ref_store(repo), oldref))
+ die(_("reference does not exist: '%s'"), oldref);
+ if (refs_ref_exists(get_main_ref_store(repo), newref))
+ die(_("reference already exists: '%s'"), newref);
+
-+ return refs_rename_ref(get_main_ref_store(repo), oldref, newref, message);
++ ret = refs_rename_ref(get_main_ref_store(repo), oldref, newref, message);
++
++ if (ret < 0)
++ ret = 1;
++ return ret;
+}
+
int cmd_refs(int argc,
const char **argv,
const char *prefix,
@@ builtin/refs.c: int cmd_refs(int argc,
- REFS_OPTIMIZE_USAGE,
+ REFS_CREATE_USAGE,
REFS_DELETE_USAGE,
REFS_UPDATE_USAGE,
+ REFS_RENAME_USAGE,
@@ builtin/refs.c: int cmd_refs(int argc,
};
parse_opt_subcommand_fn *fn = NULL;
@@ builtin/refs.c: int cmd_refs(int argc,
- OPT_SUBCOMMAND("optimize", &fn, cmd_refs_optimize),
+ OPT_SUBCOMMAND("create", &fn, cmd_refs_create),
OPT_SUBCOMMAND("delete", &fn, cmd_refs_delete),
OPT_SUBCOMMAND("update", &fn, cmd_refs_update),
+ OPT_SUBCOMMAND("rename", &fn, cmd_refs_rename),
@@ builtin/refs.c: int cmd_refs(int argc,
## t/meson.build ##
@@ t/meson.build: integration_tests = [
- 't1463-refs-optimize.sh',
't1464-refs-delete.sh',
't1465-refs-update.sh',
-+ 't1466-refs-rename.sh',
+ 't1466-refs-create.sh',
++ 't1467-refs-rename.sh',
't1500-rev-parse.sh',
't1501-work-tree.sh',
't1502-rev-parse-parseopt.sh',
- ## t/t1466-refs-rename.sh (new) ##
+ ## t/t1467-refs-rename.sh (new) ##
@@
+#!/bin/sh
+
---
base-commit: 700432b2ba22603a0bcb71475c9c333d17c9b0d1
change-id: 20260616-pks-refs-writing-subcommands-7a77be5bda9b
^ permalink raw reply [flat|nested] 22+ messages in thread
* [PATCH v2 1/5] builtin/refs: drop `the_repository`
2026-06-17 10:15 ` [PATCH v2 0/5] builtin/refs: add ability to write references Patrick Steinhardt
@ 2026-06-17 10:15 ` Patrick Steinhardt
2026-06-17 10:15 ` [PATCH v2 2/5] builtin/refs: add "delete" subcommand Patrick Steinhardt
` (5 subsequent siblings)
6 siblings, 0 replies; 22+ messages in thread
From: Patrick Steinhardt @ 2026-06-17 10:15 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano
We still have a couple of uses of `the_repository` in "builtin/refs.c".
All of those are trivial to convert though as the command always
requires a repository to exist.
Convert them to use the passed-in repository and drop
`USE_THE_REPOSITORY_VARIABLE`.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
builtin/refs.c | 15 +++++++--------
1 file changed, 7 insertions(+), 8 deletions(-)
diff --git a/builtin/refs.c b/builtin/refs.c
index e3125bc61b..f0faabf45a 100644
--- a/builtin/refs.c
+++ b/builtin/refs.c
@@ -1,4 +1,3 @@
-#define USE_THE_REPOSITORY_VARIABLE
#include "builtin.h"
#include "config.h"
#include "fsck.h"
@@ -23,7 +22,7 @@
N_("git refs optimize " PACK_REFS_OPTS)
static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
- struct repository *repo UNUSED)
+ struct repository *repo)
{
const char * const migrate_usage[] = {
REFS_MIGRATE_USAGE,
@@ -59,13 +58,13 @@ static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
goto out;
}
- if (the_repository->ref_storage_format == format) {
+ if (repo->ref_storage_format == format) {
err = error(_("repository already uses '%s' format"),
ref_storage_format_to_name(format));
goto out;
}
- if (repo_migrate_ref_storage_format(the_repository, format, flags, &errbuf) < 0) {
+ if (repo_migrate_ref_storage_format(repo, format, flags, &errbuf) < 0) {
err = error("%s", errbuf.buf);
goto out;
}
@@ -99,8 +98,8 @@ static int cmd_refs_verify(int argc, const char **argv, const char *prefix,
if (argc)
usage(_("'git refs verify' takes no arguments"));
- repo_config(the_repository, git_fsck_config, &fsck_refs_options);
- prepare_repo_settings(the_repository);
+ repo_config(repo, git_fsck_config, &fsck_refs_options);
+ prepare_repo_settings(repo);
worktrees = get_worktrees_without_reading_head();
for (size_t i = 0; worktrees[i]; i++)
@@ -124,7 +123,7 @@ static int cmd_refs_list(int argc, const char **argv, const char *prefix,
}
static int cmd_refs_exists(int argc, const char **argv, const char *prefix,
- struct repository *repo UNUSED)
+ struct repository *repo)
{
struct strbuf unused_referent = STRBUF_INIT;
struct object_id unused_oid;
@@ -145,7 +144,7 @@ static int cmd_refs_exists(int argc, const char **argv, const char *prefix,
die(_("'git refs exists' requires a reference"));
ref = *argv++;
- if (refs_read_raw_ref(get_main_ref_store(the_repository), ref,
+ if (refs_read_raw_ref(get_main_ref_store(repo), ref,
&unused_oid, &unused_referent, &unused_type,
&failure_errno)) {
if (failure_errno == ENOENT || failure_errno == EISDIR) {
--
2.55.0.rc0.786.g65d90a0328.dirty
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH v2 2/5] builtin/refs: add "delete" subcommand
2026-06-17 10:15 ` [PATCH v2 0/5] builtin/refs: add ability to write references Patrick Steinhardt
2026-06-17 10:15 ` [PATCH v2 1/5] builtin/refs: drop `the_repository` Patrick Steinhardt
@ 2026-06-17 10:15 ` Patrick Steinhardt
2026-06-17 10:16 ` [PATCH v2 3/5] builtin/refs: add "update" subcommand Patrick Steinhardt
` (4 subsequent siblings)
6 siblings, 0 replies; 22+ messages in thread
From: Patrick Steinhardt @ 2026-06-17 10:15 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano
Reference-related functionality in Git is currently spread across many
different commands: git-update-ref(1), git-for-each-ref(1),
git-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it
hard for users to discover what functionality we have available to work
with references.
We have thus started to consolidate this functionality into git-refs(1),
which is a toolbox of everything related to references. Until now, the
command doesn't handle functionality of git-update-ref(1).
Fix this gap by introducing a new "delete" subcommand, which is the
equivalent of `git update-ref -d`.
Note that we're intentionally not using a generic "write" subcommand
with a "-d" flag. This is rather harder to discover, and subcommands
that are implmented as flags tend to be hard to reason about in the code
as we'd have to handle mutually-exclusive flags that stem from the other
subcommand-like modes.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-refs.adoc | 17 ++++++
builtin/refs.c | 51 +++++++++++++++++
t/meson.build | 1 +
t/t1464-refs-delete.sh | 130 ++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 199 insertions(+)
diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc
index fa33680cc7..2633934463 100644
--- a/Documentation/git-refs.adoc
+++ b/Documentation/git-refs.adoc
@@ -20,6 +20,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]
[ --stdin | (<pattern>...)]
git refs exists <ref>
git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
+git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]
DESCRIPTION
-----------
@@ -51,6 +52,12 @@ optimize::
usage. This subcommand is an alias for linkgit:git-pack-refs[1] and
offers identical functionality.
+delete::
+ Delete the given reference. This subcommand mirrors `git update-ref -d`
+ (see linkgit:git-update-ref[1]). When `<old-value>` is given, the
+ reference is only deleted after verifying that it currently contains
+ `<old-value>`.
+
OPTIONS
-------
@@ -90,6 +97,16 @@ The following options are specific to 'git refs optimize':
include::pack-refs-options.adoc[]
+The following options are specific to commands which write references:
+
+`--message=<reason>`::
+ Use the given <reason> string for the reflog entry associated with the
+ update. An empty message is rejected.
+
+`--no-deref`::
+ Operate on <ref> itself rather than the reference it points to via a
+ symbolic ref.
+
KNOWN LIMITATIONS
-----------------
diff --git a/builtin/refs.c b/builtin/refs.c
index f0faabf45a..edb7d61663 100644
--- a/builtin/refs.c
+++ b/builtin/refs.c
@@ -21,6 +21,9 @@
#define REFS_OPTIMIZE_USAGE \
N_("git refs optimize " PACK_REFS_OPTS)
+#define REFS_DELETE_USAGE \
+ N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]")
+
static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
struct repository *repo)
{
@@ -175,6 +178,52 @@ static int cmd_refs_optimize(int argc, const char **argv, const char *prefix,
return pack_refs_core(argc, argv, prefix, repo, refs_optimize_usage);
}
+static int cmd_refs_delete(int argc, const char **argv, const char *prefix,
+ struct repository *repo)
+{
+ static char const * const refs_delete_usage[] = {
+ REFS_DELETE_USAGE,
+ NULL
+ };
+ const char *message = NULL;
+ unsigned flags = 0;
+ struct option opts[] = {
+ OPT_STRING(0, "message", &message, N_("reason"),
+ N_("reason of the update")),
+ OPT_BIT(0 ,"no-deref", &flags,
+ N_("update <refname> not the one it points to"),
+ REF_NO_DEREF),
+ OPT_END(),
+ };
+ struct object_id oldoid;
+ const char *refname;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, opts, refs_delete_usage, 0);
+ if (argc < 1 || argc > 2)
+ usage(_("delete requires reference name and an optional old object ID"));
+
+ if (message && !*message)
+ die(_("refusing to perform update with empty message"));
+
+ repo_config(repo, git_default_config, NULL);
+
+ refname = argv[0];
+ if (argc == 2) {
+ if (repo_get_oid_with_flags(repo, argv[1], &oldoid, GET_OID_SKIP_AMBIGUITY_CHECK))
+ die(_("invalid old object ID: '%s'"), argv[1]);
+ if (is_null_oid(&oldoid))
+ die(_("cannot delete reference with null old object ID"));
+ }
+
+ ret = refs_delete_ref(get_main_ref_store(repo), message, refname,
+ argc == 2 ? &oldoid : NULL, flags);
+
+ if (ret < 0)
+ ret = 1;
+ return ret;
+}
+
int cmd_refs(int argc,
const char **argv,
const char *prefix,
@@ -186,6 +235,7 @@ int cmd_refs(int argc,
"git refs list " COMMON_USAGE_FOR_EACH_REF,
REFS_EXISTS_USAGE,
REFS_OPTIMIZE_USAGE,
+ REFS_DELETE_USAGE,
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
@@ -195,6 +245,7 @@ int cmd_refs(int argc,
OPT_SUBCOMMAND("list", &fn, cmd_refs_list),
OPT_SUBCOMMAND("exists", &fn, cmd_refs_exists),
OPT_SUBCOMMAND("optimize", &fn, cmd_refs_optimize),
+ OPT_SUBCOMMAND("delete", &fn, cmd_refs_delete),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index c5832fee05..1ccf08a3b5 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -223,6 +223,7 @@ integration_tests = [
't1461-refs-list.sh',
't1462-refs-exists.sh',
't1463-refs-optimize.sh',
+ 't1464-refs-delete.sh',
't1500-rev-parse.sh',
't1501-work-tree.sh',
't1502-rev-parse-parseopt.sh',
diff --git a/t/t1464-refs-delete.sh b/t/t1464-refs-delete.sh
new file mode 100755
index 0000000000..efff7d0574
--- /dev/null
+++ b/t/t1464-refs-delete.sh
@@ -0,0 +1,130 @@
+#!/bin/sh
+
+test_description='git refs delete'
+
+. ./test-lib.sh
+
+setup_repo () {
+ git init "$1" &&
+ test_commit -C "$1" A &&
+ test_commit -C "$1" B
+}
+
+test_expect_success 'delete without oldvalue verification' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ A=$(git -C repo rev-parse A) &&
+ git -C repo update-ref refs/heads/foo $A &&
+ git -C repo refs delete refs/heads/foo &&
+ test_must_fail git -C repo show-ref --verify -q refs/heads/foo
+'
+
+test_expect_success 'delete with matching oldvalue' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git update-ref refs/heads/foo $A &&
+ git refs delete refs/heads/foo $A &&
+ test_must_fail git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'delete with stale oldvalue fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git update-ref refs/heads/foo $A &&
+ test_must_fail git refs delete refs/heads/foo $B 2>err &&
+ test_grep " but expected " err &&
+ git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'delete with null oldvalue fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git update-ref refs/heads/foo $A &&
+ test_must_fail git refs delete refs/heads/foo $ZERO_OID 2>err &&
+ test_grep "null old object ID" err &&
+ git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'delete with invalid oldvalue fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git update-ref refs/heads/foo $A &&
+ test_must_fail git refs delete refs/heads/foo invalid-oid 2>err &&
+ test_grep "invalid old object ID" err &&
+ git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'delete symref with --no-deref leaves target intact' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git update-ref refs/heads/foo $A &&
+ git symbolic-ref refs/heads/symref refs/heads/foo &&
+ git refs delete --no-deref refs/heads/symref &&
+ test_must_fail git refs exists refs/heads/symref &&
+ git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'delete with message records reason in reflog' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git update-ref refs/heads/foo $A &&
+ git symbolic-ref HEAD refs/heads/foo &&
+ git refs delete --message=delete-reason refs/heads/foo &&
+ test_must_fail git refs exists refs/heads/foo &&
+ test-tool ref-store main for-each-reflog-ent HEAD >actual &&
+ test_grep "delete-reason$" actual
+ )
+'
+
+test_expect_success 'delete with empty message fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git update-ref refs/heads/foo $A &&
+ test_must_fail git refs delete --message= refs/heads/foo 2>err &&
+ test_grep "empty message" err &&
+ git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'delete without arguments fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ test_must_fail git -C repo refs delete 2>err &&
+ test_grep "requires reference name" err
+'
+
+test_expect_success 'delete with too many arguments fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ test_must_fail git refs delete one two three 2>err &&
+ test_grep "requires reference name" err
+'
+
+test_done
--
2.55.0.rc0.786.g65d90a0328.dirty
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH v2 3/5] builtin/refs: add "update" subcommand
2026-06-17 10:15 ` [PATCH v2 0/5] builtin/refs: add ability to write references Patrick Steinhardt
2026-06-17 10:15 ` [PATCH v2 1/5] builtin/refs: drop `the_repository` Patrick Steinhardt
2026-06-17 10:15 ` [PATCH v2 2/5] builtin/refs: add "delete" subcommand Patrick Steinhardt
@ 2026-06-17 10:16 ` Patrick Steinhardt
2026-06-17 10:16 ` [PATCH v2 4/5] builtin/refs: add "create" subcommand Patrick Steinhardt
` (3 subsequent siblings)
6 siblings, 0 replies; 22+ messages in thread
From: Patrick Steinhardt @ 2026-06-17 10:16 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano
Add a new "update" subcommand which mirrors `git update-ref <refname>
<oldoid> <newoid>`. This follows the same reasoning as the preceding
commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-refs.adoc | 12 ++
builtin/refs.c | 55 +++++++++
t/meson.build | 1 +
t/t1465-refs-update.sh | 268 ++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 336 insertions(+)
diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc
index 2633934463..6475bdcc62 100644
--- a/Documentation/git-refs.adoc
+++ b/Documentation/git-refs.adoc
@@ -21,6 +21,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]
git refs exists <ref>
git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]
+git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]
DESCRIPTION
-----------
@@ -58,6 +59,13 @@ delete::
reference is only deleted after verifying that it currently contains
`<old-value>`.
+update::
+ Update the given reference to point at `<new-value>`. If `<old-value>`
+ is given, the reference is only updated after verifying that it
+ currently contains `<old-value>`. As a special case, an all-zeroes
+ `<new-value>` deletes the branch, whereas an all-zeroes `<old-value>`
+ ensures that the branch does not yet exist.
+
OPTIONS
-------
@@ -99,6 +107,10 @@ include::pack-refs-options.adoc[]
The following options are specific to commands which write references:
+`--create-reflog`::
+ Create a reflog for the reference even if one would not ordinarily be
+ created.
+
`--message=<reason>`::
Use the given <reason> string for the reflog entry associated with the
update. An empty message is rejected.
diff --git a/builtin/refs.c b/builtin/refs.c
index edb7d61663..08453ae1c8 100644
--- a/builtin/refs.c
+++ b/builtin/refs.c
@@ -24,6 +24,9 @@
#define REFS_DELETE_USAGE \
N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]")
+#define REFS_UPDATE_USAGE \
+ N_("git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]")
+
static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
struct repository *repo)
{
@@ -224,6 +227,56 @@ static int cmd_refs_delete(int argc, const char **argv, const char *prefix,
return ret;
}
+static int cmd_refs_update(int argc, const char **argv, const char *prefix,
+ struct repository *repo)
+{
+ static char const * const refs_update_usage[] = {
+ REFS_UPDATE_USAGE,
+ NULL
+ };
+ const char *message = NULL;
+ unsigned flags = 0;
+ struct option opts[] = {
+ OPT_STRING(0, "message", &message, N_("reason"),
+ N_("reason of the update")),
+ OPT_BIT(0 ,"no-deref", &flags,
+ N_("update <refname> not the one it points to"),
+ REF_NO_DEREF),
+ OPT_BIT(0, "create-reflog", &flags, N_("create a reflog"),
+ REF_FORCE_CREATE_REFLOG),
+ OPT_END(),
+ };
+ struct object_id newoid, oldoid;
+ const char *refname;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, opts, refs_update_usage, 0);
+ if (argc < 2 || argc > 3)
+ usage(_("update requires reference name, new value and an optional old value"));
+
+ if (message && !*message)
+ die(_("refusing to perform update with empty message"));
+
+ repo_config(repo, git_default_config, NULL);
+
+ refname = argv[0];
+ if (repo_get_oid_with_flags(repo, argv[1], &newoid,
+ GET_OID_SKIP_AMBIGUITY_CHECK))
+ die(_("invalid new object ID: '%s'"), argv[1]);
+ if (argc == 3 &&
+ repo_get_oid_with_flags(repo, argv[2], &oldoid,
+ GET_OID_SKIP_AMBIGUITY_CHECK))
+ die(_("invalid old object ID: '%s'"), argv[2]);
+
+ ret = refs_update_ref(get_main_ref_store(repo), message, refname,
+ &newoid, argc == 3 ? &oldoid : NULL, flags,
+ UPDATE_REFS_MSG_ON_ERR);
+
+ if (ret < 0)
+ ret = 1;
+ return ret;
+}
+
int cmd_refs(int argc,
const char **argv,
const char *prefix,
@@ -236,6 +289,7 @@ int cmd_refs(int argc,
REFS_EXISTS_USAGE,
REFS_OPTIMIZE_USAGE,
REFS_DELETE_USAGE,
+ REFS_UPDATE_USAGE,
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
@@ -246,6 +300,7 @@ int cmd_refs(int argc,
OPT_SUBCOMMAND("exists", &fn, cmd_refs_exists),
OPT_SUBCOMMAND("optimize", &fn, cmd_refs_optimize),
OPT_SUBCOMMAND("delete", &fn, cmd_refs_delete),
+ OPT_SUBCOMMAND("update", &fn, cmd_refs_update),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index 1ccf08a3b5..2063962dab 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -224,6 +224,7 @@ integration_tests = [
't1462-refs-exists.sh',
't1463-refs-optimize.sh',
't1464-refs-delete.sh',
+ 't1465-refs-update.sh',
't1500-rev-parse.sh',
't1501-work-tree.sh',
't1502-rev-parse-parseopt.sh',
diff --git a/t/t1465-refs-update.sh b/t/t1465-refs-update.sh
new file mode 100755
index 0000000000..a9becdda99
--- /dev/null
+++ b/t/t1465-refs-update.sh
@@ -0,0 +1,268 @@
+#!/bin/sh
+
+test_description='git refs update'
+
+. ./test-lib.sh
+
+setup_repo () {
+ git init "$1" &&
+ test_commit -C "$1" A &&
+ test_commit -C "$1" B
+}
+
+test_ref_matches () {
+ git rev-parse "$1" >expect &&
+ echo "$2" >actual &&
+ test_cmp expect actual
+}
+
+test_expect_success 'update creates a new reference' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/heads/foo $A &&
+ test_ref_matches refs/heads/foo "$A"
+ )
+'
+
+test_expect_success 'update an existing reference without oldvalue' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ git refs update refs/heads/foo $B &&
+ test_ref_matches refs/heads/foo $B
+ )
+'
+
+test_expect_success 'update with matching oldvalue' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ git refs update refs/heads/foo $B $A &&
+ test_ref_matches refs/heads/foo $B
+ )
+'
+
+test_expect_success 'update with stale oldvalue fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ test_must_fail git refs update refs/heads/foo $B $B 2>err &&
+ test_grep " but expected " err &&
+ test_ref_matches refs/heads/foo $A
+ )
+'
+
+test_expect_success 'update can create a new branch with oldvalue' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/heads/foo $A $ZERO_OID 2>err &&
+ test_ref_matches refs/heads/foo $A
+ )
+'
+
+test_expect_success 'update can create a new branch without oldvalue' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/heads/foo $A 2>err &&
+ test_ref_matches refs/heads/foo $A
+ )
+'
+
+test_expect_success 'update refuses to create preexisting branch' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ test_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err &&
+ test_grep "reference already exists" err &&
+ test_ref_matches refs/heads/foo $A
+ )
+'
+
+test_expect_success 'update can delete a branch with oldvalue' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/heads/foo $A 2>err &&
+ git refs update refs/heads/foo $ZERO_OID $A 2>err &&
+ test_must_fail git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'update can delete a branch without oldvalue' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/heads/foo $A 2>err &&
+ git refs update refs/heads/foo $ZERO_OID 2>err &&
+ test_must_fail git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'update refuses to delete a branch with mismatching value' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A 2>err &&
+ test_must_fail git refs update refs/heads/foo $ZERO_OID $B 2>err &&
+ test_grep " but expected " err &&
+ git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'update refuses to create preexisting branch' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ test_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err &&
+ test_grep "reference already exists" err &&
+ test_ref_matches refs/heads/foo $A
+ )
+'
+
+
+test_expect_success 'update with invalid new value fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ test_must_fail git refs update refs/heads/foo invalid-oid 2>err &&
+ test_grep "invalid new object ID" err &&
+ test_must_fail git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'update with invalid old value fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ test_must_fail git refs update refs/heads/foo $B invalid-oid 2>err &&
+ test_grep "invalid old object ID" err &&
+ test_ref_matches refs/heads/foo $A
+ )
+'
+
+test_expect_success 'update --no-deref rewrites the symref itself' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ git symbolic-ref refs/heads/symref refs/heads/foo &&
+ git refs update --no-deref refs/heads/symref $B &&
+ test_must_fail git symbolic-ref refs/heads/symref &&
+ test_ref_matches refs/heads/symref $B &&
+ test_ref_matches refs/heads/foo $A
+ )
+'
+
+test_expect_success 'update does not create a reflog by default' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/foo $A &&
+ test_must_fail git reflog exists refs/foo
+ )
+'
+
+test_expect_success 'update creates a reflog with --create-reflog' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update --create-reflog refs/foo $A &&
+ git reflog exists refs/foo
+ )
+'
+
+test_expect_success 'update with message records reason in reflog' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ git refs update --message=update-reason refs/heads/foo $B &&
+ git reflog show refs/heads/foo >actual &&
+ test_grep "update-reason$" actual
+ )
+'
+
+test_expect_success 'update with empty message fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ test_must_fail git refs update --message= refs/heads/foo $B 2>err &&
+ test_grep "empty message" err
+ )
+'
+
+test_expect_success 'update with too few arguments fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ test_must_fail git -C repo refs update refs/heads/foo 2>err &&
+ test_grep "requires reference name, new value" err
+'
+
+test_expect_success 'update with too many arguments fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ test_must_fail git refs update refs/heads/foo $A $B extra 2>err &&
+ test_grep "requires reference name, new value" err
+ )
+'
+
+test_done
--
2.55.0.rc0.786.g65d90a0328.dirty
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH v2 4/5] builtin/refs: add "create" subcommand
2026-06-17 10:15 ` [PATCH v2 0/5] builtin/refs: add ability to write references Patrick Steinhardt
` (2 preceding siblings ...)
2026-06-17 10:16 ` [PATCH v2 3/5] builtin/refs: add "update" subcommand Patrick Steinhardt
@ 2026-06-17 10:16 ` Patrick Steinhardt
2026-06-29 20:58 ` Junio C Hamano
2026-06-17 10:16 ` [PATCH v2 5/5] builtin/refs: add "rename" subcommand Patrick Steinhardt
` (2 subsequent siblings)
6 siblings, 1 reply; 22+ messages in thread
From: Patrick Steinhardt @ 2026-06-17 10:16 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano
The "update" subcommand cannot only update an existing reference, but it
can also create new branches and delete existing branches by specifying
the all-zeroes object ID as either old or new value. Despite that, we
already have the "delete" subcommand as a handy shortcut so that a user
can easily delete a branch. This relieves them of needing to understand
the more arcane uses of the "update" command, and of counting the number
of zeroes they need to pass.
But while we have a "delete" subcommand, we don't have an equivalent
that would allow the user to create a new branch, which creates a
certain asymmetry.
Add a new "create" subcommand to plug this gap.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-refs.adoc | 5 ++
builtin/refs.c | 52 +++++++++++++++
t/meson.build | 1 +
t/t1466-refs-create.sh | 151 ++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 209 insertions(+)
diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc
index 6475bdcc62..e6a3528349 100644
--- a/Documentation/git-refs.adoc
+++ b/Documentation/git-refs.adoc
@@ -20,6 +20,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]
[ --stdin | (<pattern>...)]
git refs exists <ref>
git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
+git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>
git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]
git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]
@@ -53,6 +54,10 @@ optimize::
usage. This subcommand is an alias for linkgit:git-pack-refs[1] and
offers identical functionality.
+create::
+ Create the given reference, which must not already exist, pointing at
+ `<new-value>`.
+
delete::
Delete the given reference. This subcommand mirrors `git update-ref -d`
(see linkgit:git-update-ref[1]). When `<old-value>` is given, the
diff --git a/builtin/refs.c b/builtin/refs.c
index 08453ae1c8..92e62fd5df 100644
--- a/builtin/refs.c
+++ b/builtin/refs.c
@@ -21,6 +21,9 @@
#define REFS_OPTIMIZE_USAGE \
N_("git refs optimize " PACK_REFS_OPTS)
+#define REFS_CREATE_USAGE \
+ N_("git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>")
+
#define REFS_DELETE_USAGE \
N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]")
@@ -181,6 +184,53 @@ static int cmd_refs_optimize(int argc, const char **argv, const char *prefix,
return pack_refs_core(argc, argv, prefix, repo, refs_optimize_usage);
}
+static int cmd_refs_create(int argc, const char **argv, const char *prefix,
+ struct repository *repo)
+{
+ static char const * const refs_create_usage[] = {
+ REFS_CREATE_USAGE,
+ NULL
+ };
+ const char *message = NULL;
+ unsigned flags = 0;
+ struct option opts[] = {
+ OPT_STRING(0, "message", &message, N_("reason"),
+ N_("reason of the update")),
+ OPT_BIT(0 ,"no-deref", &flags,
+ N_("update <refname> not the one it points to"),
+ REF_NO_DEREF),
+ OPT_BIT(0, "create-reflog", &flags, N_("create a reflog"),
+ REF_FORCE_CREATE_REFLOG),
+ OPT_END(),
+ };
+ struct object_id newoid;
+ const char *refname;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, opts, refs_create_usage, 0);
+ if (argc != 2)
+ usage(_("create requires reference name and an object ID"));
+
+ if (message && !*message)
+ die(_("refusing to perform update with empty message"));
+
+ repo_config(repo, git_default_config, NULL);
+
+ refname = argv[0];
+ if (repo_get_oid_with_flags(repo, argv[1], &newoid, GET_OID_SKIP_AMBIGUITY_CHECK))
+ die(_("invalid object ID: '%s'"), argv[1]);
+ if (is_null_oid(&newoid))
+ die(_("cannot create reference with null old object ID"));
+
+ ret = refs_update_ref(get_main_ref_store(repo), message, refname,
+ &newoid, null_oid(repo->hash_algo), flags,
+ UPDATE_REFS_MSG_ON_ERR);
+
+ if (ret < 0)
+ ret = 1;
+ return ret;
+}
+
static int cmd_refs_delete(int argc, const char **argv, const char *prefix,
struct repository *repo)
{
@@ -288,6 +338,7 @@ int cmd_refs(int argc,
"git refs list " COMMON_USAGE_FOR_EACH_REF,
REFS_EXISTS_USAGE,
REFS_OPTIMIZE_USAGE,
+ REFS_CREATE_USAGE,
REFS_DELETE_USAGE,
REFS_UPDATE_USAGE,
NULL,
@@ -299,6 +350,7 @@ int cmd_refs(int argc,
OPT_SUBCOMMAND("list", &fn, cmd_refs_list),
OPT_SUBCOMMAND("exists", &fn, cmd_refs_exists),
OPT_SUBCOMMAND("optimize", &fn, cmd_refs_optimize),
+ OPT_SUBCOMMAND("create", &fn, cmd_refs_create),
OPT_SUBCOMMAND("delete", &fn, cmd_refs_delete),
OPT_SUBCOMMAND("update", &fn, cmd_refs_update),
OPT_END(),
diff --git a/t/meson.build b/t/meson.build
index 2063962dab..541e6f919c 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -225,6 +225,7 @@ integration_tests = [
't1463-refs-optimize.sh',
't1464-refs-delete.sh',
't1465-refs-update.sh',
+ 't1466-refs-create.sh',
't1500-rev-parse.sh',
't1501-work-tree.sh',
't1502-rev-parse-parseopt.sh',
diff --git a/t/t1466-refs-create.sh b/t/t1466-refs-create.sh
new file mode 100755
index 0000000000..85c8bd6ea2
--- /dev/null
+++ b/t/t1466-refs-create.sh
@@ -0,0 +1,151 @@
+#!/bin/sh
+
+test_description='git refs create'
+
+. ./test-lib.sh
+
+setup_repo () {
+ git init "$1" &&
+ test_commit -C "$1" A &&
+ test_commit -C "$1" B
+}
+
+test_ref_matches () {
+ git rev-parse "$1" >expect &&
+ echo "$2" >actual &&
+ test_cmp expect actual
+}
+
+test_expect_success 'create a new reference' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs create refs/heads/foo $A &&
+ test_ref_matches refs/heads/foo "$A"
+ )
+'
+
+test_expect_success 'create fails when the reference already exists' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs create refs/heads/foo $A &&
+ test_must_fail git refs create refs/heads/foo $B 2>err &&
+ test_grep "reference already exists" err &&
+ test_ref_matches refs/heads/foo "$A"
+ )
+'
+
+test_expect_success 'create with null new value fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ test_must_fail git refs create refs/heads/foo $ZERO_OID 2>err &&
+ test_grep "null old object ID" err &&
+ test_must_fail git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'create with invalid new value fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ test_must_fail git refs create refs/heads/foo invalid-oid 2>err &&
+ test_grep "invalid object ID" err &&
+ test_must_fail git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'create does not create a reflog by default' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs create refs/foo $A &&
+ test_must_fail git reflog exists refs/foo
+ )
+'
+
+test_expect_success 'create creates a reflog with --create-reflog' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs create --create-reflog refs/foo $A &&
+ git reflog exists refs/foo
+ )
+'
+
+test_expect_success 'create with message records reason in reflog' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs create --message="create reason" refs/heads/foo $A &&
+ git reflog show refs/heads/foo >actual &&
+ test_grep "create reason$" actual
+ )
+'
+
+test_expect_success 'create with symref target creates target reference' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git symbolic-ref refs/heads/symref refs/heads/target &&
+ git refs create refs/heads/symref $A &&
+ git reflog exists refs/heads/target
+ )
+'
+
+test_expect_success 'create with symref target and --no-deref refuses to create reference' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git symbolic-ref refs/heads/symref refs/heads/target &&
+ test_must_fail git refs create --no-deref refs/heads/symref $A 2>err &&
+ test_grep "dangling symref already exists" err &&
+ test_must_fail git reflog exists refs/heads/target
+ )
+'
+
+test_expect_success 'create with empty message fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ test_must_fail git refs create --message= refs/heads/foo $A 2>err &&
+ test_grep "empty message" err &&
+ test_must_fail git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'create without arguments fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ test_must_fail git -C repo refs create 2>err &&
+ test_grep "requires reference name" err
+'
+
+test_expect_success 'create with too many arguments fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ test_must_fail git -C repo refs create refs/heads/foo a b 2>err &&
+ test_grep "requires reference name" err
+'
+
+test_done
--
2.55.0.rc0.786.g65d90a0328.dirty
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH v2 5/5] builtin/refs: add "rename" subcommand
2026-06-17 10:15 ` [PATCH v2 0/5] builtin/refs: add ability to write references Patrick Steinhardt
` (3 preceding siblings ...)
2026-06-17 10:16 ` [PATCH v2 4/5] builtin/refs: add "create" subcommand Patrick Steinhardt
@ 2026-06-17 10:16 ` Patrick Steinhardt
2026-06-17 12:26 ` [PATCH v2 0/5] builtin/refs: add ability to write references Junio C Hamano
2026-06-29 20:52 ` Junio C Hamano
6 siblings, 0 replies; 22+ messages in thread
From: Patrick Steinhardt @ 2026-06-17 10:16 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano
Add a "rename" subcommand to git-refs(1) with the syntax:
$ git refs rename <oldref> <newref>
It renames <oldref> together with its reflog to <newref>; even when used
on a local branch ref, the current value and the reflog of the ref are
the only things that are renamed. Document it and redirect casual users
to "git branch -m" if that is what they wanted to do.
Co-authored-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-refs.adoc | 6 ++
builtin/refs.c | 49 +++++++++++++++++
t/meson.build | 1 +
t/t1467-refs-rename.sh | 131 ++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 187 insertions(+)
diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc
index e6a3528349..ce278c59bf 100644
--- a/Documentation/git-refs.adoc
+++ b/Documentation/git-refs.adoc
@@ -23,6 +23,7 @@ git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude
git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>
git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]
git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]
+git refs rename [--message=<reason>] <old-ref> <new-ref>
DESCRIPTION
-----------
@@ -71,6 +72,11 @@ update::
`<new-value>` deletes the branch, whereas an all-zeroes `<old-value>`
ensures that the branch does not yet exist.
+rename::
+ Rename the reference `<oldref>` to `<newref>`. The old reference must
+ exist and the new reference must not yet exist, and both must have a
+ well-formed name (see linkgit:git-check-ref-format[1]).
+
OPTIONS
-------
diff --git a/builtin/refs.c b/builtin/refs.c
index 92e62fd5df..c7aa1a327f 100644
--- a/builtin/refs.c
+++ b/builtin/refs.c
@@ -30,6 +30,9 @@
#define REFS_UPDATE_USAGE \
N_("git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]")
+#define REFS_RENAME_USAGE \
+ N_("git refs rename [--message=<reason>] <old-ref> <new-ref>")
+
static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
struct repository *repo)
{
@@ -327,6 +330,50 @@ static int cmd_refs_update(int argc, const char **argv, const char *prefix,
return ret;
}
+static int cmd_refs_rename(int argc, const char **argv, const char *prefix,
+ struct repository *repo)
+{
+ static char const * const refs_rename_usage[] = {
+ REFS_RENAME_USAGE,
+ NULL
+ };
+ const char *message = NULL;
+ struct option opts[] = {
+ OPT_STRING(0, "message", &message, N_("reason"),
+ N_("reason of the update")),
+ OPT_END(),
+ };
+ const char *oldref, *newref;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, opts, refs_rename_usage, 0);
+ if (argc != 2)
+ usage(_("rename requires old and new reference name"));
+ if (message && !*message)
+ die(_("refusing to perform update with empty message"));
+
+ repo_config(repo, git_default_config, NULL);
+
+ oldref = argv[0];
+ newref = argv[1];
+
+ if (check_refname_format(oldref, 0))
+ die(_("invalid ref format: '%s'"), oldref);
+ if (check_refname_format(newref, 0))
+ die(_("invalid ref format: '%s'"), newref);
+
+ if (!refs_ref_exists(get_main_ref_store(repo), oldref))
+ die(_("reference does not exist: '%s'"), oldref);
+ if (refs_ref_exists(get_main_ref_store(repo), newref))
+ die(_("reference already exists: '%s'"), newref);
+
+ ret = refs_rename_ref(get_main_ref_store(repo), oldref, newref, message);
+
+ if (ret < 0)
+ ret = 1;
+ return ret;
+}
+
int cmd_refs(int argc,
const char **argv,
const char *prefix,
@@ -341,6 +388,7 @@ int cmd_refs(int argc,
REFS_CREATE_USAGE,
REFS_DELETE_USAGE,
REFS_UPDATE_USAGE,
+ REFS_RENAME_USAGE,
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
@@ -353,6 +401,7 @@ int cmd_refs(int argc,
OPT_SUBCOMMAND("create", &fn, cmd_refs_create),
OPT_SUBCOMMAND("delete", &fn, cmd_refs_delete),
OPT_SUBCOMMAND("update", &fn, cmd_refs_update),
+ OPT_SUBCOMMAND("rename", &fn, cmd_refs_rename),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index 541e6f919c..a39fd8c4c4 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -226,6 +226,7 @@ integration_tests = [
't1464-refs-delete.sh',
't1465-refs-update.sh',
't1466-refs-create.sh',
+ 't1467-refs-rename.sh',
't1500-rev-parse.sh',
't1501-work-tree.sh',
't1502-rev-parse-parseopt.sh',
diff --git a/t/t1467-refs-rename.sh b/t/t1467-refs-rename.sh
new file mode 100755
index 0000000000..f80d58e0f4
--- /dev/null
+++ b/t/t1467-refs-rename.sh
@@ -0,0 +1,131 @@
+#!/bin/sh
+
+test_description='git refs rename'
+
+. ./test-lib.sh
+
+setup_repo () {
+ git init "$1" &&
+ test_commit -C "$1" A &&
+ test_commit -C "$1" B
+}
+
+test_ref_matches () {
+ git rev-parse "$1" >expect &&
+ echo "$2" >actual &&
+ test_cmp expect actual
+}
+
+test_expect_success 'rename an existing reference' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/heads/foo $A &&
+ git refs rename refs/heads/foo refs/heads/bar &&
+ test_must_fail git refs exists refs/heads/foo &&
+ test_ref_matches refs/heads/bar $A
+ )
+'
+
+test_expect_success 'rename moves the reflog along with the reference' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update --message="rename me" refs/heads/foo $A &&
+ git refs rename refs/heads/foo refs/heads/bar &&
+ git reflog show refs/heads/bar >reflog &&
+ test_grep "rename me" reflog &&
+ test_must_fail git reflog exists refs/heads/foo
+ )
+'
+
+test_expect_success 'rename with message records reason in reflog' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/heads/foo $A &&
+ git refs rename --message="rename reason" refs/heads/foo refs/heads/bar &&
+ git reflog show refs/heads/bar >actual &&
+ test_grep "rename reason" actual
+ )
+'
+
+test_expect_success 'rename a nonexistent reference fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ test_must_fail git refs rename refs/heads/foo refs/heads/bar 2>err &&
+ test_grep "reference does not exist" err
+ )
+'
+
+test_expect_success 'rename to an existing reference fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ git refs update refs/heads/bar $B &&
+ test_must_fail git refs rename refs/heads/foo refs/heads/bar 2>err &&
+ test_grep "reference already exists" err
+ )
+'
+
+test_expect_success 'rename with empty message fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/heads/foo $A &&
+ test_must_fail git refs rename --message= refs/heads/foo refs/heads/bar 2>err &&
+ test_grep "empty message" err
+ )
+'
+
+test_expect_success 'rename with invalid old reference name fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ test_must_fail git refs rename "refs/heads/foo..bar" refs/heads/bar 2>err &&
+ test_grep "invalid ref format" err
+ )
+'
+
+test_expect_success 'rename with invalid new reference name fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/heads/foo $A &&
+ test_must_fail git refs rename refs/heads/foo "refs/heads/bar..baz" 2>err &&
+ test_grep "invalid ref format" err
+ )
+'
+
+test_expect_success 'rename with too few arguments fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ test_must_fail git -C repo refs rename refs/heads/foo 2>err &&
+ test_grep "requires old and new reference name" err
+'
+
+test_expect_success 'rename with too many arguments fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ test_must_fail git -C repo refs rename refs/heads/foo refs/heads/bar refs/heads/baz 2>err &&
+ test_grep "requires old and new reference name" err
+'
+
+test_done
--
2.55.0.rc0.786.g65d90a0328.dirty
^ permalink raw reply related [flat|nested] 22+ messages in thread
* Re: [PATCH 3/4] builtin/refs: add "update" subcommand
2026-06-17 7:28 ` Patrick Steinhardt
@ 2026-06-17 12:11 ` Junio C Hamano
0 siblings, 0 replies; 22+ messages in thread
From: Junio C Hamano @ 2026-06-17 12:11 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
Patrick Steinhardt <ps@pks.im> writes:
> We can:
>
> $ git update-ref $NEW_OID $NULL_OID
> $ git refs update $NEW_OID $NULL_OID
>
> This will verify that the reference doesn't exist before actually
> writing it. Will add a test.
I think refname is missing from the command line, but the above is
good. I forgot we had update-ref already doing that ;-)
Thanks.
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH 4/4] builtin/refs: add "rename" subcommand
2026-06-17 7:28 ` Patrick Steinhardt
@ 2026-06-17 12:13 ` Junio C Hamano
0 siblings, 0 replies; 22+ messages in thread
From: Junio C Hamano @ 2026-06-17 12:13 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
Patrick Steinhardt <ps@pks.im> writes:
>> If we rename a ref that does not have a reflog, would it leave the
>> ref under the new name without reflog, or would we get a reflog with
>> a single entry that marks the fact the old ref was renamed into the
>> new ref? Should that be controlled via --create-reflog option?
>
> It would leave it without a reflog. In theory I agree that it might make
> sense to introduce a "--create-reflog" option, but that would require
> some new plumbing in `refs_rename_ref()`. So I'd say that we can add it
> at a later point as needed.
OK.
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH v2 0/5] builtin/refs: add ability to write references
2026-06-17 10:15 ` [PATCH v2 0/5] builtin/refs: add ability to write references Patrick Steinhardt
` (4 preceding siblings ...)
2026-06-17 10:16 ` [PATCH v2 5/5] builtin/refs: add "rename" subcommand Patrick Steinhardt
@ 2026-06-17 12:26 ` Junio C Hamano
2026-06-29 20:52 ` Junio C Hamano
6 siblings, 0 replies; 22+ messages in thread
From: Junio C Hamano @ 2026-06-17 12:26 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
Patrick Steinhardt <ps@pks.im> writes:
> Hi,
>
> Reference-related functionality in Git is currently spread across many
> different commands: git-update-ref(1), git-for-each-ref(1),
> git-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it
> hard for users to discover what functionality we have available to work
> with references.
>
> We have thus started to consolidate this functionality into git-refs(1),
> which is a toolbox of everything related to references. Until now, the
> command doesn't handle functionality of git-update-ref(1).
>
> This patch series backfills most of the functionality by introducing
> three new commands:
>
> - `git refs delete` to delete references. This is the equivalent of
> `git update-ref -d`.
>
> - `git refs update` to update references. This is the equivalent of
> `git update-ref <refname> <oldvalue> <newvalue>`.
>
> - `git refs rename` to rename a reference, including its reflog. This
> does not have an equivalent in git-update-ref(1), but is inspired by
> and supersedes [1].
... and `git refs create`, but we can guess what it would do ;-).
Will queue. Thanks.
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH v2 0/5] builtin/refs: add ability to write references
2026-06-17 10:15 ` [PATCH v2 0/5] builtin/refs: add ability to write references Patrick Steinhardt
` (5 preceding siblings ...)
2026-06-17 12:26 ` [PATCH v2 0/5] builtin/refs: add ability to write references Junio C Hamano
@ 2026-06-29 20:52 ` Junio C Hamano
6 siblings, 0 replies; 22+ messages in thread
From: Junio C Hamano @ 2026-06-29 20:52 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
Patrick Steinhardt <ps@pks.im> writes:
> Reference-related functionality in Git is currently spread across many
> different commands: git-update-ref(1), git-for-each-ref(1),
> git-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it
> hard for users to discover what functionality we have available to work
> with references.
>
> We have thus started to consolidate this functionality into git-refs(1),
> which is a toolbox of everything related to references. Until now, the
> command doesn't handle functionality of git-update-ref(1).
This unfortunately hasn't heard any responses since June 17th, so I
took a look at it again myself. All the things we discussed during
the review of the initial round has been addressed, it seems.
Shall we mark the topic ready for 'next' now?
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH v2 4/5] builtin/refs: add "create" subcommand
2026-06-17 10:16 ` [PATCH v2 4/5] builtin/refs: add "create" subcommand Patrick Steinhardt
@ 2026-06-29 20:58 ` Junio C Hamano
0 siblings, 0 replies; 22+ messages in thread
From: Junio C Hamano @ 2026-06-29 20:58 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
Patrick Steinhardt <ps@pks.im> writes:
> + if (repo_get_oid_with_flags(repo, argv[1], &newoid, GET_OID_SKIP_AMBIGUITY_CHECK))
> + die(_("invalid object ID: '%s'"), argv[1]);
> + if (is_null_oid(&newoid))
> + die(_("cannot create reference with null old object ID"));
An apparent typo here, "with null old" -> "with null new object
name".
Other than that, I think this one is good.
^ permalink raw reply [flat|nested] 22+ messages in thread
end of thread, other threads:[~2026-06-29 20:58 UTC | newest]
Thread overview: 22+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-16 8:44 [PATCH 0/4] builtin/refs: add ability to write references Patrick Steinhardt
2026-06-16 8:44 ` [PATCH 1/4] builtin/refs: drop `the_repository` Patrick Steinhardt
2026-06-16 8:44 ` [PATCH 2/4] builtin/refs: add "delete" subcommand Patrick Steinhardt
2026-06-16 8:44 ` [PATCH 3/4] builtin/refs: add "update" subcommand Patrick Steinhardt
2026-06-16 11:17 ` Junio C Hamano
2026-06-17 7:28 ` Patrick Steinhardt
2026-06-17 12:11 ` Junio C Hamano
2026-06-16 14:52 ` Junio C Hamano
2026-06-17 7:28 ` Patrick Steinhardt
2026-06-16 8:44 ` [PATCH 4/4] builtin/refs: add "rename" subcommand Patrick Steinhardt
2026-06-16 14:53 ` Junio C Hamano
2026-06-17 7:28 ` Patrick Steinhardt
2026-06-17 12:13 ` Junio C Hamano
2026-06-17 10:15 ` [PATCH v2 0/5] builtin/refs: add ability to write references Patrick Steinhardt
2026-06-17 10:15 ` [PATCH v2 1/5] builtin/refs: drop `the_repository` Patrick Steinhardt
2026-06-17 10:15 ` [PATCH v2 2/5] builtin/refs: add "delete" subcommand Patrick Steinhardt
2026-06-17 10:16 ` [PATCH v2 3/5] builtin/refs: add "update" subcommand Patrick Steinhardt
2026-06-17 10:16 ` [PATCH v2 4/5] builtin/refs: add "create" subcommand Patrick Steinhardt
2026-06-29 20:58 ` Junio C Hamano
2026-06-17 10:16 ` [PATCH v2 5/5] builtin/refs: add "rename" subcommand Patrick Steinhardt
2026-06-17 12:26 ` [PATCH v2 0/5] builtin/refs: add ability to write references Junio C Hamano
2026-06-29 20:52 ` Junio C Hamano
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox