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

Patrick Steinhardt committed Jul 6, 2026 at 15:27 UTC fa4eefe900b679c58ee0013198a9b11d036531ad
4 files changed +209
Documentation/git-refs.adoc
+5
@@ -20,6 +20,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]
20 [ --stdin | (<pattern>...)]
21 git refs exists <ref>
22 git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
23 +git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>
24 git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]
25 git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]
26
@@ -53,6 +54,10 @@ optimize::
54 usage. This subcommand is an alias for linkgit:git-pack-refs[1] and
55 offers identical functionality.
56
57 +create::
58 + Create the given reference, which must not already exist, pointing at
59 + `<new-value>`.
60 +
61 delete::
62 Delete the given reference. This subcommand mirrors `git update-ref -d`
63 (see linkgit:git-update-ref[1]). When `<old-value>` is given, the
builtin/refs.c
+52
@@ -21,6 +21,9 @@
21 #define REFS_OPTIMIZE_USAGE \
22 N_("git refs optimize " PACK_REFS_OPTS)
23
24 +#define REFS_CREATE_USAGE \
25 + N_("git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>")
26 +
27 #define REFS_DELETE_USAGE \
28 N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]")
29
@@ -181,6 +184,53 @@ static int cmd_refs_optimize(int argc, const char **argv, const char *prefix,
184 return pack_refs_core(argc, argv, prefix, repo, refs_optimize_usage);
185 }
186
187 +static int cmd_refs_create(int argc, const char **argv, const char *prefix,
188 + struct repository *repo)
189 +{
190 + static char const * const refs_create_usage[] = {
191 + REFS_CREATE_USAGE,
192 + NULL
193 + };
194 + const char *message = NULL;
195 + unsigned flags = 0;
196 + struct option opts[] = {
197 + OPT_STRING(0, "message", &message, N_("reason"),
198 + N_("reason of the update")),
199 + OPT_BIT(0 ,"no-deref", &flags,
200 + N_("update <refname> not the one it points to"),
201 + REF_NO_DEREF),
202 + OPT_BIT(0, "create-reflog", &flags, N_("create a reflog"),
203 + REF_FORCE_CREATE_REFLOG),
204 + OPT_END(),
205 + };
206 + struct object_id newoid;
207 + const char *refname;
208 + int ret;
209 +
210 + argc = parse_options(argc, argv, prefix, opts, refs_create_usage, 0);
211 + if (argc != 2)
212 + usage(_("create requires reference name and an object ID"));
213 +
214 + if (message && !*message)
215 + die(_("refusing to perform update with empty message"));
216 +
217 + repo_config(repo, git_default_config, NULL);
218 +
219 + refname = argv[0];
220 + if (repo_get_oid_with_flags(repo, argv[1], &newoid, GET_OID_SKIP_AMBIGUITY_CHECK))
221 + die(_("invalid object ID: '%s'"), argv[1]);
222 + if (is_null_oid(&newoid))
223 + die(_("cannot create reference with null new object ID"));
224 +
225 + ret = refs_update_ref(get_main_ref_store(repo), message, refname,
226 + &newoid, null_oid(repo->hash_algo), flags,
227 + UPDATE_REFS_MSG_ON_ERR);
228 +
229 + if (ret < 0)
230 + ret = 1;
231 + return ret;
232 +}
233 +
234 static int cmd_refs_delete(int argc, const char **argv, const char *prefix,
235 struct repository *repo)
236 {
@@ -288,6 +338,7 @@ int cmd_refs(int argc,
338 "git refs list " COMMON_USAGE_FOR_EACH_REF,
339 REFS_EXISTS_USAGE,
340 REFS_OPTIMIZE_USAGE,
341 + REFS_CREATE_USAGE,
342 REFS_DELETE_USAGE,
343 REFS_UPDATE_USAGE,
344 NULL,
@@ -299,6 +350,7 @@ int cmd_refs(int argc,
350 OPT_SUBCOMMAND("list", &fn, cmd_refs_list),
351 OPT_SUBCOMMAND("exists", &fn, cmd_refs_exists),
352 OPT_SUBCOMMAND("optimize", &fn, cmd_refs_optimize),
353 + OPT_SUBCOMMAND("create", &fn, cmd_refs_create),
354 OPT_SUBCOMMAND("delete", &fn, cmd_refs_delete),
355 OPT_SUBCOMMAND("update", &fn, cmd_refs_update),
356 OPT_END(),
t/meson.build
+1
@@ -225,6 +225,7 @@ integration_tests = [
225 't1463-refs-optimize.sh',
226 't1464-refs-delete.sh',
227 't1465-refs-update.sh',
228 + 't1466-refs-create.sh',
229 't1500-rev-parse.sh',
230 't1501-work-tree.sh',
231 't1502-rev-parse-parseopt.sh',
t/t1466-refs-create.sh new
+151
@@ -0,0 +1,151 @@
1 +#!/bin/sh
2 +
3 +test_description='git refs create'
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 'create 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 create refs/heads/foo $A &&
26 + test_ref_matches refs/heads/foo "$A"
27 + )
28 +'
29 +
30 +test_expect_success 'create fails when the reference already exists' '
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 create refs/heads/foo $A &&
38 + test_must_fail git refs create refs/heads/foo $B 2>err &&
39 + test_grep "reference already exists" err &&
40 + test_ref_matches refs/heads/foo "$A"
41 + )
42 +'
43 +
44 +test_expect_success 'create with null new value fails' '
45 + test_when_finished "rm -rf repo" &&
46 + setup_repo repo &&
47 + (
48 + cd repo &&
49 + test_must_fail git refs create refs/heads/foo $ZERO_OID 2>err &&
50 + test_grep "null new object ID" err &&
51 + test_must_fail git refs exists refs/heads/foo
52 + )
53 +'
54 +
55 +test_expect_success 'create with invalid new value fails' '
56 + test_when_finished "rm -rf repo" &&
57 + setup_repo repo &&
58 + (
59 + cd repo &&
60 + test_must_fail git refs create refs/heads/foo invalid-oid 2>err &&
61 + test_grep "invalid object ID" err &&
62 + test_must_fail git refs exists refs/heads/foo
63 + )
64 +'
65 +
66 +test_expect_success 'create does not create a reflog by default' '
67 + test_when_finished "rm -rf repo" &&
68 + setup_repo repo &&
69 + (
70 + cd repo &&
71 + A=$(git rev-parse A) &&
72 + git refs create refs/foo $A &&
73 + test_must_fail git reflog exists refs/foo
74 + )
75 +'
76 +
77 +test_expect_success 'create creates a reflog with --create-reflog' '
78 + test_when_finished "rm -rf repo" &&
79 + setup_repo repo &&
80 + (
81 + cd repo &&
82 + A=$(git rev-parse A) &&
83 + git refs create --create-reflog refs/foo $A &&
84 + git reflog exists refs/foo
85 + )
86 +'
87 +
88 +test_expect_success 'create with message records reason in reflog' '
89 + test_when_finished "rm -rf repo" &&
90 + setup_repo repo &&
91 + (
92 + cd repo &&
93 + A=$(git rev-parse A) &&
94 + git refs create --message="create reason" refs/heads/foo $A &&
95 + git reflog show refs/heads/foo >actual &&
96 + test_grep "create reason$" actual
97 + )
98 +'
99 +
100 +test_expect_success 'create with symref target creates target reference' '
101 + test_when_finished "rm -rf repo" &&
102 + setup_repo repo &&
103 + (
104 + cd repo &&
105 + A=$(git rev-parse A) &&
106 + git symbolic-ref refs/heads/symref refs/heads/target &&
107 + git refs create refs/heads/symref $A &&
108 + git reflog exists refs/heads/target
109 + )
110 +'
111 +
112 +test_expect_success 'create with symref target and --no-deref refuses to create reference' '
113 + test_when_finished "rm -rf repo" &&
114 + setup_repo repo &&
115 + (
116 + cd repo &&
117 + A=$(git rev-parse A) &&
118 + git symbolic-ref refs/heads/symref refs/heads/target &&
119 + test_must_fail git refs create --no-deref refs/heads/symref $A 2>err &&
120 + test_grep "dangling symref already exists" err &&
121 + test_must_fail git reflog exists refs/heads/target
122 + )
123 +'
124 +
125 +test_expect_success 'create with empty message fails' '
126 + test_when_finished "rm -rf repo" &&
127 + setup_repo repo &&
128 + (
129 + cd repo &&
130 + A=$(git rev-parse A) &&
131 + test_must_fail git refs create --message= refs/heads/foo $A 2>err &&
132 + test_grep "empty message" err &&
133 + test_must_fail git refs exists refs/heads/foo
134 + )
135 +'
136 +
137 +test_expect_success 'create without arguments fails' '
138 + test_when_finished "rm -rf repo" &&
139 + setup_repo repo &&
140 + test_must_fail git -C repo refs create 2>err &&
141 + test_grep "requires reference name" err
142 +'
143 +
144 +test_expect_success 'create with too many arguments fails' '
145 + test_when_finished "rm -rf repo" &&
146 + setup_repo repo &&
147 + test_must_fail git -C repo refs create refs/heads/foo a b 2>err &&
148 + test_grep "requires reference name" err
149 +'
150 +
151 +test_done