builtin/refs: add "update" subcommand

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> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Jul 6, 2026 at 15:27 UTC 2540e35bc185635d7428a1cd0f55caacd68b8ff6
4 files changed +336
Documentation/git-refs.adoc
+12
@@ -21,6 +21,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]
21 git refs exists <ref>
22 git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
23 git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]
24 +git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]
25
26 DESCRIPTION
27 -----------
@@ -58,6 +59,13 @@ delete::
59 reference is only deleted after verifying that it currently contains
60 `<old-value>`.
61
62 +update::
63 + Update the given reference to point at `<new-value>`. If `<old-value>`
64 + is given, the reference is only updated after verifying that it
65 + currently contains `<old-value>`. As a special case, an all-zeroes
66 + `<new-value>` deletes the branch, whereas an all-zeroes `<old-value>`
67 + ensures that the branch does not yet exist.
68 +
69 OPTIONS
70 -------
71
@@ -99,6 +107,10 @@ include::pack-refs-options.adoc[]
107
108 The following options are specific to commands which write references:
109
110 +`--create-reflog`::
111 + Create a reflog for the reference even if one would not ordinarily be
112 + created.
113 +
114 `--message=<reason>`::
115 Use the given <reason> string for the reflog entry associated with the
116 update. An empty message is rejected.
builtin/refs.c
+55
@@ -24,6 +24,9 @@
24 #define REFS_DELETE_USAGE \
25 N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]")
26
27 +#define REFS_UPDATE_USAGE \
28 + N_("git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]")
29 +
30 static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
31 struct repository *repo)
32 {
@@ -224,6 +227,56 @@ static int cmd_refs_delete(int argc, const char **argv, const char *prefix,
227 return ret;
228 }
229
230 +static int cmd_refs_update(int argc, const char **argv, const char *prefix,
231 + struct repository *repo)
232 +{
233 + static char const * const refs_update_usage[] = {
234 + REFS_UPDATE_USAGE,
235 + NULL
236 + };
237 + const char *message = NULL;
238 + unsigned flags = 0;
239 + struct option opts[] = {
240 + OPT_STRING(0, "message", &message, N_("reason"),
241 + N_("reason of the update")),
242 + OPT_BIT(0 ,"no-deref", &flags,
243 + N_("update <refname> not the one it points to"),
244 + REF_NO_DEREF),
245 + OPT_BIT(0, "create-reflog", &flags, N_("create a reflog"),
246 + REF_FORCE_CREATE_REFLOG),
247 + OPT_END(),
248 + };
249 + struct object_id newoid, oldoid;
250 + const char *refname;
251 + int ret;
252 +
253 + argc = parse_options(argc, argv, prefix, opts, refs_update_usage, 0);
254 + if (argc < 2 || argc > 3)
255 + usage(_("update requires reference name, new value and an optional old value"));
256 +
257 + if (message && !*message)
258 + die(_("refusing to perform update with empty message"));
259 +
260 + repo_config(repo, git_default_config, NULL);
261 +
262 + refname = argv[0];
263 + if (repo_get_oid_with_flags(repo, argv[1], &newoid,
264 + GET_OID_SKIP_AMBIGUITY_CHECK))
265 + die(_("invalid new object ID: '%s'"), argv[1]);
266 + if (argc == 3 &&
267 + repo_get_oid_with_flags(repo, argv[2], &oldoid,
268 + GET_OID_SKIP_AMBIGUITY_CHECK))
269 + die(_("invalid old object ID: '%s'"), argv[2]);
270 +
271 + ret = refs_update_ref(get_main_ref_store(repo), message, refname,
272 + &newoid, argc == 3 ? &oldoid : NULL, flags,
273 + UPDATE_REFS_MSG_ON_ERR);
274 +
275 + if (ret < 0)
276 + ret = 1;
277 + return ret;
278 +}
279 +
280 int cmd_refs(int argc,
281 const char **argv,
282 const char *prefix,
@@ -236,6 +289,7 @@ int cmd_refs(int argc,
289 REFS_EXISTS_USAGE,
290 REFS_OPTIMIZE_USAGE,
291 REFS_DELETE_USAGE,
292 + REFS_UPDATE_USAGE,
293 NULL,
294 };
295 parse_opt_subcommand_fn *fn = NULL;
@@ -246,6 +300,7 @@ int cmd_refs(int argc,
300 OPT_SUBCOMMAND("exists", &fn, cmd_refs_exists),
301 OPT_SUBCOMMAND("optimize", &fn, cmd_refs_optimize),
302 OPT_SUBCOMMAND("delete", &fn, cmd_refs_delete),
303 + OPT_SUBCOMMAND("update", &fn, cmd_refs_update),
304 OPT_END(),
305 };
306
t/meson.build
+1
@@ -224,6 +224,7 @@ integration_tests = [
224 't1462-refs-exists.sh',
225 't1463-refs-optimize.sh',
226 't1464-refs-delete.sh',
227 + 't1465-refs-update.sh',
228 't1500-rev-parse.sh',
229 't1501-work-tree.sh',
230 't1502-rev-parse-parseopt.sh',
t/t1465-refs-update.sh new
+268
@@ -0,0 +1,268 @@
1 +#!/bin/sh
2 +
3 +test_description='git refs update'
4 +
5 +. ./test-lib.sh
6 +
7 +setup_repo () {
8 + git init "$1" &&
9 + test_commit -C "$1" A &&
10 + test_commit -C "$1" B
11 +}
12 +
13 +test_ref_matches () {
14 + git rev-parse "$1" >expect &&
15 + echo "$2" >actual &&
16 + test_cmp expect actual
17 +}
18 +
19 +test_expect_success 'update creates a new reference' '
20 + test_when_finished "rm -rf repo" &&
21 + setup_repo repo &&
22 + (
23 + cd repo &&
24 + A=$(git rev-parse A) &&
25 + git refs update refs/heads/foo $A &&
26 + test_ref_matches refs/heads/foo "$A"
27 + )
28 +'
29 +
30 +test_expect_success 'update an existing reference without oldvalue' '
31 + test_when_finished "rm -rf repo" &&
32 + setup_repo repo &&
33 + (
34 + cd repo &&
35 + A=$(git rev-parse A) &&
36 + B=$(git rev-parse B) &&
37 + git refs update refs/heads/foo $A &&
38 + git refs update refs/heads/foo $B &&
39 + test_ref_matches refs/heads/foo $B
40 + )
41 +'
42 +
43 +test_expect_success 'update with matching oldvalue' '
44 + test_when_finished "rm -rf repo" &&
45 + setup_repo repo &&
46 + (
47 + cd repo &&
48 + A=$(git rev-parse A) &&
49 + B=$(git rev-parse B) &&
50 + git refs update refs/heads/foo $A &&
51 + git refs update refs/heads/foo $B $A &&
52 + test_ref_matches refs/heads/foo $B
53 + )
54 +'
55 +
56 +test_expect_success 'update with stale oldvalue fails' '
57 + test_when_finished "rm -rf repo" &&
58 + setup_repo repo &&
59 + (
60 + cd repo &&
61 + A=$(git rev-parse A) &&
62 + B=$(git rev-parse B) &&
63 + git refs update refs/heads/foo $A &&
64 + test_must_fail git refs update refs/heads/foo $B $B 2>err &&
65 + test_grep " but expected " err &&
66 + test_ref_matches refs/heads/foo $A
67 + )
68 +'
69 +
70 +test_expect_success 'update can create a new branch with oldvalue' '
71 + test_when_finished "rm -rf repo" &&
72 + setup_repo repo &&
73 + (
74 + cd repo &&
75 + A=$(git rev-parse A) &&
76 + git refs update refs/heads/foo $A $ZERO_OID 2>err &&
77 + test_ref_matches refs/heads/foo $A
78 + )
79 +'
80 +
81 +test_expect_success 'update can create a new branch without oldvalue' '
82 + test_when_finished "rm -rf repo" &&
83 + setup_repo repo &&
84 + (
85 + cd repo &&
86 + A=$(git rev-parse A) &&
87 + git refs update refs/heads/foo $A 2>err &&
88 + test_ref_matches refs/heads/foo $A
89 + )
90 +'
91 +
92 +test_expect_success 'update refuses to create preexisting branch' '
93 + test_when_finished "rm -rf repo" &&
94 + setup_repo repo &&
95 + (
96 + cd repo &&
97 + A=$(git rev-parse A) &&
98 + B=$(git rev-parse B) &&
99 + git refs update refs/heads/foo $A &&
100 + test_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err &&
101 + test_grep "reference already exists" err &&
102 + test_ref_matches refs/heads/foo $A
103 + )
104 +'
105 +
106 +test_expect_success 'update can delete a branch with oldvalue' '
107 + test_when_finished "rm -rf repo" &&
108 + setup_repo repo &&
109 + (
110 + cd repo &&
111 + A=$(git rev-parse A) &&
112 + git refs update refs/heads/foo $A 2>err &&
113 + git refs update refs/heads/foo $ZERO_OID $A 2>err &&
114 + test_must_fail git refs exists refs/heads/foo
115 + )
116 +'
117 +
118 +test_expect_success 'update can delete a branch without oldvalue' '
119 + test_when_finished "rm -rf repo" &&
120 + setup_repo repo &&
121 + (
122 + cd repo &&
123 + A=$(git rev-parse A) &&
124 + git refs update refs/heads/foo $A 2>err &&
125 + git refs update refs/heads/foo $ZERO_OID 2>err &&
126 + test_must_fail git refs exists refs/heads/foo
127 + )
128 +'
129 +
130 +test_expect_success 'update refuses to delete a branch with mismatching value' '
131 + test_when_finished "rm -rf repo" &&
132 + setup_repo repo &&
133 + (
134 + cd repo &&
135 + A=$(git rev-parse A) &&
136 + B=$(git rev-parse B) &&
137 + git refs update refs/heads/foo $A 2>err &&
138 + test_must_fail git refs update refs/heads/foo $ZERO_OID $B 2>err &&
139 + test_grep " but expected " err &&
140 + git refs exists refs/heads/foo
141 + )
142 +'
143 +
144 +test_expect_success 'update refuses to create preexisting branch' '
145 + test_when_finished "rm -rf repo" &&
146 + setup_repo repo &&
147 + (
148 + cd repo &&
149 + A=$(git rev-parse A) &&
150 + B=$(git rev-parse B) &&
151 + git refs update refs/heads/foo $A &&
152 + test_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err &&
153 + test_grep "reference already exists" err &&
154 + test_ref_matches refs/heads/foo $A
155 + )
156 +'
157 +
158 +
159 +test_expect_success 'update with invalid new value fails' '
160 + test_when_finished "rm -rf repo" &&
161 + setup_repo repo &&
162 + (
163 + cd repo &&
164 + test_must_fail git refs update refs/heads/foo invalid-oid 2>err &&
165 + test_grep "invalid new object ID" err &&
166 + test_must_fail git refs exists refs/heads/foo
167 + )
168 +'
169 +
170 +test_expect_success 'update with invalid old value fails' '
171 + test_when_finished "rm -rf repo" &&
172 + setup_repo repo &&
173 + (
174 + cd repo &&
175 + A=$(git rev-parse A) &&
176 + B=$(git rev-parse B) &&
177 + git refs update refs/heads/foo $A &&
178 + test_must_fail git refs update refs/heads/foo $B invalid-oid 2>err &&
179 + test_grep "invalid old object ID" err &&
180 + test_ref_matches refs/heads/foo $A
181 + )
182 +'
183 +
184 +test_expect_success 'update --no-deref rewrites the symref itself' '
185 + test_when_finished "rm -rf repo" &&
186 + setup_repo repo &&
187 + (
188 + cd repo &&
189 + A=$(git rev-parse A) &&
190 + B=$(git rev-parse B) &&
191 + git refs update refs/heads/foo $A &&
192 + git symbolic-ref refs/heads/symref refs/heads/foo &&
193 + git refs update --no-deref refs/heads/symref $B &&
194 + test_must_fail git symbolic-ref refs/heads/symref &&
195 + test_ref_matches refs/heads/symref $B &&
196 + test_ref_matches refs/heads/foo $A
197 + )
198 +'
199 +
200 +test_expect_success 'update does not create a reflog by default' '
201 + test_when_finished "rm -rf repo" &&
202 + setup_repo repo &&
203 + (
204 + cd repo &&
205 + A=$(git rev-parse A) &&
206 + git refs update refs/foo $A &&
207 + test_must_fail git reflog exists refs/foo
208 + )
209 +'
210 +
211 +test_expect_success 'update creates a reflog with --create-reflog' '
212 + test_when_finished "rm -rf repo" &&
213 + setup_repo repo &&
214 + (
215 + cd repo &&
216 + A=$(git rev-parse A) &&
217 + git refs update --create-reflog refs/foo $A &&
218 + git reflog exists refs/foo
219 + )
220 +'
221 +
222 +test_expect_success 'update with message records reason in reflog' '
223 + test_when_finished "rm -rf repo" &&
224 + setup_repo repo &&
225 + (
226 + cd repo &&
227 + A=$(git rev-parse A) &&
228 + B=$(git rev-parse B) &&
229 + git refs update refs/heads/foo $A &&
230 + git refs update --message=update-reason refs/heads/foo $B &&
231 + git reflog show refs/heads/foo >actual &&
232 + test_grep "update-reason$" actual
233 + )
234 +'
235 +
236 +test_expect_success 'update with empty message fails' '
237 + test_when_finished "rm -rf repo" &&
238 + setup_repo repo &&
239 + (
240 + cd repo &&
241 + A=$(git rev-parse A) &&
242 + B=$(git rev-parse B) &&
243 + git refs update refs/heads/foo $A &&
244 + test_must_fail git refs update --message= refs/heads/foo $B 2>err &&
245 + test_grep "empty message" err
246 + )
247 +'
248 +
249 +test_expect_success 'update with too few arguments fails' '
250 + test_when_finished "rm -rf repo" &&
251 + setup_repo repo &&
252 + test_must_fail git -C repo refs update refs/heads/foo 2>err &&
253 + test_grep "requires reference name, new value" err
254 +'
255 +
256 +test_expect_success 'update with too many arguments fails' '
257 + test_when_finished "rm -rf repo" &&
258 + setup_repo repo &&
259 + (
260 + cd repo &&
261 + A=$(git rev-parse A) &&
262 + B=$(git rev-parse B) &&
263 + test_must_fail git refs update refs/heads/foo $A $B extra 2>err &&
264 + test_grep "requires reference name, new value" err
265 + )
266 +'
267 +
268 +test_done