From 8b3bc491d883ff53b47ac589d6ecba93944b0d40 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 17 Jun 2026 12:16:00 +0200 Subject: [PATCH] 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