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