From 4a59ffe84961f6a3de29b302bcef188583a29dfa Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 17 Jun 2026 12:15:58 +0200 Subject: [PATCH 1/5] builtin/refs: drop `the_repository` 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 Signed-off-by: Junio C Hamano --- 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) { From 52fb7f94fcc20199cc7dc23db8d7f71ec37b80f8 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 17 Jun 2026 12:15:59 +0200 Subject: [PATCH 2/5] builtin/refs: add "delete" subcommand 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 Signed-off-by: Junio C Hamano --- Documentation/git-refs.adoc | 17 +++++ builtin/refs.c | 51 ++++++++++++++ t/meson.build | 1 + t/t1464-refs-delete.sh | 130 ++++++++++++++++++++++++++++++++++++ 4 files changed, 199 insertions(+) create mode 100755 t/t1464-refs-delete.sh 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=] [--shell|--perl|--python|--tcl] [ --stdin | (...)] git refs exists git refs optimize [--all] [--no-prune] [--auto] [--include ] [--exclude ] +git refs delete [--message=] [--no-deref] [] 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 `` is given, the + reference is only deleted after verifying that it currently contains + ``. + 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=`:: + Use the given string for the reflog entry associated with the + update. An empty message is rejected. + +`--no-deref`:: + Operate on 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=] [--no-deref] []") + 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 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 From 8b3bc491d883ff53b47ac589d6ecba93944b0d40 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 17 Jun 2026 12:16:00 +0200 Subject: [PATCH 3/5] builtin/refs: add "update" subcommand Add a new "update" subcommand which mirrors `git update-ref `. This follows the same reasoning as the preceding commit. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Documentation/git-refs.adoc | 12 ++ builtin/refs.c | 55 ++++++++ t/meson.build | 1 + t/t1465-refs-update.sh | 268 ++++++++++++++++++++++++++++++++++++ 4 files changed, 336 insertions(+) create mode 100755 t/t1465-refs-update.sh 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=] [--shell|--perl|--python|--tcl] git refs exists git refs optimize [--all] [--no-prune] [--auto] [--include ] [--exclude ] git refs delete [--message=] [--no-deref] [] +git refs update [--message=] [--no-deref] [--create-reflog] [] DESCRIPTION ----------- @@ -58,6 +59,13 @@ delete:: reference is only deleted after verifying that it currently contains ``. +update:: + Update the given reference to point at ``. If `` + is given, the reference is only updated after verifying that it + currently contains ``. As a special case, an all-zeroes + `` deletes the branch, whereas an all-zeroes `` + 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=`:: Use the given 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=] [--no-deref] []") +#define REFS_UPDATE_USAGE \ + N_("git refs update [--message=] [--no-deref] [--create-reflog] []") + 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 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 From e993bd4b59834a157e878d8ad6c97ee6076198d8 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 17 Jun 2026 12:16:01 +0200 Subject: [PATCH 4/5] builtin/refs: add "create" subcommand 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 Signed-off-by: Junio C Hamano --- Documentation/git-refs.adoc | 5 ++ builtin/refs.c | 52 +++++++++++++ t/meson.build | 1 + t/t1466-refs-create.sh | 151 ++++++++++++++++++++++++++++++++++++ 4 files changed, 209 insertions(+) create mode 100755 t/t1466-refs-create.sh 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=] [--shell|--perl|--python|--tcl] [ --stdin | (...)] git refs exists git refs optimize [--all] [--no-prune] [--auto] [--include ] [--exclude ] +git refs create [--message=] [--no-deref] [--create-reflog] git refs delete [--message=] [--no-deref] [] git refs update [--message=] [--no-deref] [--create-reflog] [] @@ -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 + ``. + delete:: Delete the given reference. This subcommand mirrors `git update-ref -d` (see linkgit:git-update-ref[1]). When `` 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=] [--no-deref] [--create-reflog] ") + #define REFS_DELETE_USAGE \ N_("git refs delete [--message=] [--no-deref] []") @@ -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 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 From 7d6f98431e09d4434313540c81b52578a61a41e5 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 17 Jun 2026 12:16:02 +0200 Subject: [PATCH 5/5] builtin/refs: add "rename" subcommand Add a "rename" subcommand to git-refs(1) with the syntax: $ git refs rename It renames together with its reflog to ; 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 Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Documentation/git-refs.adoc | 6 ++ builtin/refs.c | 49 ++++++++++++++ t/meson.build | 1 + t/t1467-refs-rename.sh | 131 ++++++++++++++++++++++++++++++++++++ 4 files changed, 187 insertions(+) create mode 100755 t/t1467-refs-rename.sh 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 ] [--exclude git refs create [--message=] [--no-deref] [--create-reflog] git refs delete [--message=] [--no-deref] [] git refs update [--message=] [--no-deref] [--create-reflog] [] +git refs rename [--message=] DESCRIPTION ----------- @@ -71,6 +72,11 @@ update:: `` deletes the branch, whereas an all-zeroes `` ensures that the branch does not yet exist. +rename:: + Rename the reference `` to ``. 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=] [--no-deref] [--create-reflog] []") +#define REFS_RENAME_USAGE \ + N_("git refs rename [--message=] ") + 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