builtin/refs: add "rename" subcommand

Add a "rename" subcommand to git-refs(1) with the syntax: $ git refs rename <oldref> <newref> It renames <oldref> together with its reflog to <newref>; 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 <gitster@pobox.com> 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 002fe677caddc8162949315c73e53422d4e0f4e8
4 files changed +200
Documentation/git-refs.adoc
+6
@@ -23,6 +23,7 @@ git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude
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 +git refs rename [--message=<reason>] <old-ref> <new-ref>
27
28 DESCRIPTION
29 -----------
@@ -71,6 +72,11 @@ update::
72 `<new-value>` deletes the branch, whereas an all-zeroes `<old-value>`
73 ensures that the branch does not yet exist.
74
75 +rename::
76 + Rename the reference `<oldref>` to `<newref>`. The old reference must
77 + exist and the new reference must not yet exist, and both must have a
78 + well-formed name (see linkgit:git-check-ref-format[1]).
79 +
80 OPTIONS
81 -------
82
builtin/refs.c
+49
@@ -30,6 +30,9 @@
30 #define REFS_UPDATE_USAGE \
31 N_("git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]")
32
33 +#define REFS_RENAME_USAGE \
34 + N_("git refs rename [--message=<reason>] <old-ref> <new-ref>")
35 +
36 static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
37 struct repository *repo)
38 {
@@ -327,6 +330,50 @@ static int cmd_refs_update(int argc, const char **argv, const char *prefix,
330 return ret;
331 }
332
333 +static int cmd_refs_rename(int argc, const char **argv, const char *prefix,
334 + struct repository *repo)
335 +{
336 + static char const * const refs_rename_usage[] = {
337 + REFS_RENAME_USAGE,
338 + NULL
339 + };
340 + const char *message = NULL;
341 + struct option opts[] = {
342 + OPT_STRING(0, "message", &message, N_("reason"),
343 + N_("reason of the update")),
344 + OPT_END(),
345 + };
346 + const char *oldref, *newref;
347 + int ret;
348 +
349 + argc = parse_options(argc, argv, prefix, opts, refs_rename_usage, 0);
350 + if (argc != 2)
351 + usage(_("rename requires old and new reference name"));
352 + if (message && !*message)
353 + die(_("refusing to perform update with empty message"));
354 +
355 + repo_config(repo, git_default_config, NULL);
356 +
357 + oldref = argv[0];
358 + newref = argv[1];
359 +
360 + if (check_refname_format(oldref, 0))
361 + die(_("invalid ref format: '%s'"), oldref);
362 + if (check_refname_format(newref, 0))
363 + die(_("invalid ref format: '%s'"), newref);
364 +
365 + if (!refs_ref_exists(get_main_ref_store(repo), oldref))
366 + die(_("reference does not exist: '%s'"), oldref);
367 + if (refs_ref_exists(get_main_ref_store(repo), newref))
368 + die(_("reference already exists: '%s'"), newref);
369 +
370 + ret = refs_rename_ref(get_main_ref_store(repo), oldref, newref, message);
371 +
372 + if (ret < 0)
373 + ret = 1;
374 + return ret;
375 +}
376 +
377 int cmd_refs(int argc,
378 const char **argv,
379 const char *prefix,
@@ -341,6 +388,7 @@ int cmd_refs(int argc,
388 REFS_CREATE_USAGE,
389 REFS_DELETE_USAGE,
390 REFS_UPDATE_USAGE,
391 + REFS_RENAME_USAGE,
392 NULL,
393 };
394 parse_opt_subcommand_fn *fn = NULL;
@@ -353,6 +401,7 @@ int cmd_refs(int argc,
401 OPT_SUBCOMMAND("create", &fn, cmd_refs_create),
402 OPT_SUBCOMMAND("delete", &fn, cmd_refs_delete),
403 OPT_SUBCOMMAND("update", &fn, cmd_refs_update),
404 + OPT_SUBCOMMAND("rename", &fn, cmd_refs_rename),
405 OPT_END(),
406 };
407
t/meson.build
+1
@@ -226,6 +226,7 @@ integration_tests = [
226 't1464-refs-delete.sh',
227 't1465-refs-update.sh',
228 't1466-refs-create.sh',
229 + 't1467-refs-rename.sh',
230 't1500-rev-parse.sh',
231 't1501-work-tree.sh',
232 't1502-rev-parse-parseopt.sh',
t/t1467-refs-rename.sh new
+144
@@ -0,0 +1,144 @@
1 +#!/bin/sh
2 +
3 +test_description='git refs rename'
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 'rename an existing 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 + git refs rename refs/heads/foo refs/heads/bar &&
27 + test_must_fail git refs exists refs/heads/foo &&
28 + test_ref_matches refs/heads/bar $A
29 + )
30 +'
31 +
32 +test_expect_success 'rename moves the reflog along with the reference' '
33 + test_when_finished "rm -rf repo" &&
34 + setup_repo repo &&
35 + (
36 + cd repo &&
37 + A=$(git rev-parse A) &&
38 + git refs update --message="rename me" refs/heads/foo $A &&
39 + git refs rename refs/heads/foo refs/heads/bar &&
40 + git reflog show refs/heads/bar >reflog &&
41 + test_grep "rename me" reflog &&
42 + test_must_fail git reflog exists refs/heads/foo
43 + )
44 +'
45 +
46 +test_expect_success 'rename with message records reason in reflog' '
47 + test_when_finished "rm -rf repo" &&
48 + setup_repo repo &&
49 + (
50 + cd repo &&
51 + A=$(git rev-parse A) &&
52 + git refs update refs/heads/foo $A &&
53 + git refs rename --message="rename reason" refs/heads/foo refs/heads/bar &&
54 + git reflog show refs/heads/bar >actual &&
55 + test_grep "rename reason" actual
56 + )
57 +'
58 +
59 +test_expect_success 'rename a nonexistent reference fails' '
60 + test_when_finished "rm -rf repo" &&
61 + setup_repo repo &&
62 + (
63 + cd repo &&
64 + test_must_fail git refs rename refs/heads/foo refs/heads/bar 2>err &&
65 + test_grep "reference does not exist" err
66 + )
67 +'
68 +
69 +test_expect_success 'rename to an existing reference fails' '
70 + test_when_finished "rm -rf repo" &&
71 + setup_repo repo &&
72 + (
73 + cd repo &&
74 + A=$(git rev-parse A) &&
75 + B=$(git rev-parse B) &&
76 + git refs update refs/heads/foo $A &&
77 + git refs update refs/heads/bar $B &&
78 + test_must_fail git refs rename refs/heads/foo refs/heads/bar 2>err &&
79 + test_grep "reference already exists" err
80 + )
81 +'
82 +
83 +test_expect_success 'rename with symbolic ref fails' '
84 + test_when_finished "rm -rf repo" &&
85 + setup_repo repo &&
86 + (
87 + cd repo &&
88 + A=$(git rev-parse A) &&
89 + git refs create refs/heads/target $A &&
90 + git symbolic-ref refs/heads/symref refs/heads/target &&
91 + ! git refs rename refs/heads/symref refs/heads/renamed 2>err &&
92 + test_grep "is a symbolic ref, .* not supported" err
93 + )
94 +'
95 +
96 +test_expect_success 'rename with empty message fails' '
97 + test_when_finished "rm -rf repo" &&
98 + setup_repo repo &&
99 + (
100 + cd repo &&
101 + A=$(git rev-parse A) &&
102 + git refs update refs/heads/foo $A &&
103 + test_must_fail git refs rename --message= refs/heads/foo refs/heads/bar 2>err &&
104 + test_grep "empty message" err
105 + )
106 +'
107 +
108 +test_expect_success 'rename with invalid old reference name fails' '
109 + test_when_finished "rm -rf repo" &&
110 + setup_repo repo &&
111 + (
112 + cd repo &&
113 + test_must_fail git refs rename "refs/heads/foo..bar" refs/heads/bar 2>err &&
114 + test_grep "invalid ref format" err
115 + )
116 +'
117 +
118 +test_expect_success 'rename with invalid new reference name fails' '
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 &&
125 + test_must_fail git refs rename refs/heads/foo "refs/heads/bar..baz" 2>err &&
126 + test_grep "invalid ref format" err
127 + )
128 +'
129 +
130 +test_expect_success 'rename with too few arguments fails' '
131 + test_when_finished "rm -rf repo" &&
132 + setup_repo repo &&
133 + test_must_fail git -C repo refs rename refs/heads/foo 2>err &&
134 + test_grep "requires old and new reference name" err
135 +'
136 +
137 +test_expect_success 'rename with too many arguments fails' '
138 + test_when_finished "rm -rf repo" &&
139 + setup_repo repo &&
140 + test_must_fail git -C repo refs rename refs/heads/foo refs/heads/bar refs/heads/baz 2>err &&
141 + test_grep "requires old and new reference name" err
142 +'
143 +
144 +test_done