update-ref: add support for 'symref-update' command

Add 'symref-update' command to the '--stdin' mode of 'git-update-ref' to allow updates of symbolic refs. The 'symref-update' command takes in a <new-target>, which the <ref> will be updated to. If the <ref> doesn't exist it will be created. It also optionally takes either an `ref <old-target>` or `oid <old-oid>`. If the <old-target> is provided, it checks to see if the <ref> targets the <old-target> before the update. If <old-oid> is provided it checks <ref> to ensure that it is a regular ref and <old-oid> is the OID before the update. This by extension also means that this when a zero <old-oid> is provided, it ensures that the ref didn't exist before. The divergence in syntax from the regular `update` command is because if we don't use a `(ref | oid)` prefix for the old_value, then there is ambiguity around if the value provided should be treated as an oid or a reference. This is more so the reason, because we allow anything committish to be provided as an oid. While 'symref-verify' and 'symref-delete' also take in `<old-target>` we do not have this divergence there as those commands only work with symrefs. Whereas 'symref-update' also works with regular refs and allows users to convert regular refs to symrefs. The command allows users to perform symbolic ref updates within a transaction. This provides atomicity and allows users to perform a set of operations together. This command supports deref mode, to ensure that we can update dereferenced regular refs to symrefs. Helped-by: Patrick Steinhardt <ps@pks.im> Helped-by: Junio C Hamano <gitster@pobox.com> Signed-off-by: Karthik Nayak <karthik.188@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Karthik Nayak committed Jun 7, 2024 at 15:33 UTC 7dd4051b014741732271785c0915599b1f0c1a47
4 files changed +306
Documentation/git-update-ref.txt
+7
@@ -65,6 +65,7 @@ performs all modifications together. Specify commands of the form:
65 create SP <ref> SP <new-oid> LF
66 delete SP <ref> [SP <old-oid>] LF
67 verify SP <ref> [SP <old-oid>] LF
68 + symref-update SP <ref> SP <new-target> [SP (ref SP <old-target> | oid SP <old-oid>)] LF
69 symref-create SP <ref> SP <new-target> LF
70 symref-delete SP <ref> [SP <old-target>] LF
71 symref-verify SP <ref> [SP <old-target>] LF
@@ -89,6 +90,7 @@ quoting:
90 create SP <ref> NUL <new-oid> NUL
91 delete SP <ref> NUL [<old-oid>] NUL
92 verify SP <ref> NUL [<old-oid>] NUL
93 + symref-update SP <ref> NUL <new-target> [NUL (ref NUL <old-target> | oid NUL <old-oid>)] NUL
94 symref-create SP <ref> NUL <new-target> NUL
95 symref-delete SP <ref> [NUL <old-target>] NUL
96 symref-verify SP <ref> [NUL <old-target>] NUL
@@ -119,6 +121,11 @@ delete::
121 Delete <ref> after verifying it exists with <old-oid>, if
122 given. If given, <old-oid> may not be zero.
123
124 +symref-update::
125 + Set <ref> to <new-target> after verifying <old-target> or <old-oid>,
126 + if given. Specify a zero <old-oid> to ensure that the ref does not
127 + exist before the update.
128 +
129 verify::
130 Verify <ref> against <old-oid> but do not change it. If
131 <old-oid> is zero or missing, the ref must not exist.
builtin/update-ref.c
+92
@@ -98,6 +98,42 @@ static char *parse_next_refname(const char **next)
98 return parse_refname(next);
99 }
100
101 +/*
102 + * Wrapper around parse_arg which skips the next delimiter.
103 + */
104 +static char *parse_next_arg(const char **next)
105 +{
106 + struct strbuf arg = STRBUF_INIT;
107 +
108 + if (line_termination) {
109 + /* Without -z, consume SP and use next argument */
110 + if (!**next || **next == line_termination)
111 + return NULL;
112 + if (**next != ' ')
113 + die("expected SP but got: %s", *next);
114 + } else {
115 + /* With -z, read the next NUL-terminated line */
116 + if (**next)
117 + return NULL;
118 + }
119 + /* Skip the delimiter */
120 + (*next)++;
121 +
122 + if (line_termination) {
123 + /* Without -z, use the next argument */
124 + *next = parse_arg(*next, &arg);
125 + } else {
126 + /* With -z, use everything up to the next NUL */
127 + strbuf_addstr(&arg, *next);
128 + *next += arg.len;
129 + }
130 +
131 + if (arg.len)
132 + return strbuf_detach(&arg, NULL);
133 +
134 + strbuf_release(&arg);
135 + return NULL;
136 +}
137
138 /*
139 * The value being parsed is <old-oid> (as opposed to <new-oid>; the
@@ -237,6 +273,61 @@ static void parse_cmd_update(struct ref_transaction *transaction,
273 strbuf_release(&err);
274 }
275
276 +static void parse_cmd_symref_update(struct ref_transaction *transaction,
277 + const char *next, const char *end)
278 +{
279 + char *refname, *new_target, *old_arg;
280 + char *old_target = NULL;
281 + struct strbuf err = STRBUF_INIT;
282 + struct object_id old_oid;
283 + int have_old_oid = 0;
284 +
285 + refname = parse_refname(&next);
286 + if (!refname)
287 + die("symref-update: missing <ref>");
288 +
289 + new_target = parse_next_refname(&next);
290 + if (!new_target)
291 + die("symref-update %s: missing <new-target>", refname);
292 +
293 + old_arg = parse_next_arg(&next);
294 + if (old_arg) {
295 + old_target = parse_next_arg(&next);
296 + if (!old_target)
297 + die("symref-update %s: expected old value", refname);
298 +
299 + if (!strcmp(old_arg, "oid")) {
300 + if (repo_get_oid(the_repository, old_target, &old_oid))
301 + die("symref-update %s: invalid oid: %s", refname, old_target);
302 +
303 + have_old_oid = 1;
304 + } else if (!strcmp(old_arg, "ref")) {
305 + if (check_refname_format(old_target, REFNAME_ALLOW_ONELEVEL))
306 + die("symref-update %s: invalid ref: %s", refname, old_target);
307 + } else {
308 + die("symref-update %s: invalid arg '%s' for old value", refname, old_arg);
309 + }
310 + }
311 +
312 + if (*next != line_termination)
313 + die("symref-update %s: extra input: %s", refname, next);
314 +
315 + if (ref_transaction_update(transaction, refname, NULL,
316 + have_old_oid ? &old_oid : NULL,
317 + new_target,
318 + have_old_oid ? NULL : old_target,
319 + update_flags | create_reflog_flag,
320 + msg, &err))
321 + die("%s", err.buf);
322 +
323 + update_flags = default_flags;
324 + free(refname);
325 + free(old_arg);
326 + free(old_target);
327 + free(new_target);
328 + strbuf_release(&err);
329 +}
330 +
331 static void parse_cmd_create(struct ref_transaction *transaction,
332 const char *next, const char *end)
333 {
@@ -502,6 +593,7 @@ static const struct parse_cmd {
593 { "create", parse_cmd_create, 2, UPDATE_REFS_OPEN },
594 { "delete", parse_cmd_delete, 2, UPDATE_REFS_OPEN },
595 { "verify", parse_cmd_verify, 2, UPDATE_REFS_OPEN },
596 + { "symref-update", parse_cmd_symref_update, 4, UPDATE_REFS_OPEN },
597 { "symref-create", parse_cmd_symref_create, 2, UPDATE_REFS_OPEN },
598 { "symref-delete", parse_cmd_symref_delete, 2, UPDATE_REFS_OPEN },
599 { "symref-verify", parse_cmd_symref_verify, 2, UPDATE_REFS_OPEN },
t/t1400-update-ref.sh
+203
@@ -1360,6 +1360,7 @@ test_expect_success 'fails with duplicate HEAD update' '
1360 '
1361
1362 test_expect_success 'fails with duplicate ref update via symref' '
1363 + test_when_finished "git symbolic-ref -d refs/heads/symref2" &&
1364 git branch target2 $A &&
1365 git symbolic-ref refs/heads/symref2 refs/heads/target2 &&
1366 cat >stdin <<-EOF &&
@@ -1862,6 +1863,208 @@ do
1863 git reflog exists refs/heads/symref
1864 '
1865
1866 + test_expect_success "stdin $type symref-update fails with too many arguments" '
1867 + format_command $type "symref-update refs/heads/symref" "$a" "ref" "$a" "$a" >stdin &&
1868 + test_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&
1869 + if test "$type" = "-z"
1870 + then
1871 + grep "fatal: unknown command: $a" err
1872 + else
1873 + grep "fatal: symref-update refs/heads/symref: extra input: $a" err
1874 + fi
1875 + '
1876 +
1877 + test_expect_success "stdin $type symref-update fails with wrong old value argument" '
1878 + format_command $type "symref-update refs/heads/symref" "$a" "foo" "$a" "$a" >stdin &&
1879 + test_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&
1880 + grep "fatal: symref-update refs/heads/symref: invalid arg ${SQ}foo${SQ} for old value" err
1881 + '
1882 +
1883 + test_expect_success "stdin $type symref-update creates with zero old value" '
1884 + test_when_finished "git symbolic-ref -d refs/heads/symref" &&
1885 + format_command $type "symref-update refs/heads/symref" "$a" "oid" "$Z" >stdin &&
1886 + git update-ref --stdin $type --no-deref <stdin &&
1887 + echo $a >expect &&
1888 + git symbolic-ref refs/heads/symref >actual &&
1889 + test_cmp expect actual
1890 + '
1891 +
1892 + test_expect_success "stdin $type symref-update creates with no old value" '
1893 + test_when_finished "git symbolic-ref -d refs/heads/symref" &&
1894 + format_command $type "symref-update refs/heads/symref" "$a" >stdin &&
1895 + git update-ref --stdin $type --no-deref <stdin &&
1896 + echo $a >expect &&
1897 + git symbolic-ref refs/heads/symref >actual &&
1898 + test_cmp expect actual
1899 + '
1900 +
1901 + test_expect_success "stdin $type symref-update creates dangling" '
1902 + test_when_finished "git symbolic-ref -d refs/heads/symref" &&
1903 + test_must_fail git rev-parse refs/heads/nonexistent &&
1904 + format_command $type "symref-update refs/heads/symref" "refs/heads/nonexistent" >stdin &&
1905 + git update-ref --stdin $type --no-deref <stdin &&
1906 + echo refs/heads/nonexistent >expect &&
1907 + git symbolic-ref refs/heads/symref >actual &&
1908 + test_cmp expect actual
1909 + '
1910 +
1911 + test_expect_success "stdin $type symref-update fails with wrong old value" '
1912 + test_when_finished "git symbolic-ref -d refs/heads/symref" &&
1913 + git symbolic-ref refs/heads/symref $a &&
1914 + format_command $type "symref-update refs/heads/symref" "$m" "ref" "$b" >stdin &&
1915 + test_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&
1916 + grep "fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected $b" err &&
1917 + test_must_fail git rev-parse --verify -q $c
1918 + '
1919 +
1920 + test_expect_success "stdin $type symref-update updates dangling ref" '
1921 + test_when_finished "git symbolic-ref -d refs/heads/symref" &&
1922 + test_must_fail git rev-parse refs/heads/nonexistent &&
1923 + git symbolic-ref refs/heads/symref refs/heads/nonexistent &&
1924 + format_command $type "symref-update refs/heads/symref" "$a" >stdin &&
1925 + git update-ref --stdin $type --no-deref <stdin &&
1926 + echo $a >expect &&
1927 + git symbolic-ref refs/heads/symref >actual &&
1928 + test_cmp expect actual
1929 + '
1930 +
1931 + test_expect_success "stdin $type symref-update updates dangling ref with old value" '
1932 + test_when_finished "git symbolic-ref -d refs/heads/symref" &&
1933 + test_must_fail git rev-parse refs/heads/nonexistent &&
1934 + git symbolic-ref refs/heads/symref refs/heads/nonexistent &&
1935 + format_command $type "symref-update refs/heads/symref" "$a" "ref" "refs/heads/nonexistent" >stdin &&
1936 + git update-ref --stdin $type --no-deref <stdin &&
1937 + echo $a >expect &&
1938 + git symbolic-ref refs/heads/symref >actual &&
1939 + test_cmp expect actual
1940 + '
1941 +
1942 + test_expect_success "stdin $type symref-update fails update dangling ref with wrong old value" '
1943 + test_when_finished "git symbolic-ref -d refs/heads/symref" &&
1944 + test_must_fail git rev-parse refs/heads/nonexistent &&
1945 + git symbolic-ref refs/heads/symref refs/heads/nonexistent &&
1946 + format_command $type "symref-update refs/heads/symref" "$a" "ref" "refs/heads/wrongref" >stdin &&
1947 + test_must_fail git update-ref --stdin $type --no-deref <stdin &&
1948 + echo refs/heads/nonexistent >expect &&
1949 + git symbolic-ref refs/heads/symref >actual &&
1950 + test_cmp expect actual
1951 + '
1952 +
1953 + test_expect_success "stdin $type symref-update works with right old value" '
1954 + test_when_finished "git symbolic-ref -d refs/heads/symref" &&
1955 + git symbolic-ref refs/heads/symref $a &&
1956 + format_command $type "symref-update refs/heads/symref" "$m" "ref" "$a" >stdin &&
1957 + git update-ref --stdin $type --no-deref <stdin &&
1958 + echo $m >expect &&
1959 + git symbolic-ref refs/heads/symref >actual &&
1960 + test_cmp expect actual
1961 + '
1962 +
1963 + test_expect_success "stdin $type symref-update works with no old value" '
1964 + test_when_finished "git symbolic-ref -d refs/heads/symref" &&
1965 + git symbolic-ref refs/heads/symref $a &&
1966 + format_command $type "symref-update refs/heads/symref" "$m" >stdin &&
1967 + git update-ref --stdin $type --no-deref <stdin &&
1968 + echo $m >expect &&
1969 + git symbolic-ref refs/heads/symref >actual &&
1970 + test_cmp expect actual
1971 + '
1972 +
1973 + test_expect_success "stdin $type symref-update fails with empty old ref-target" '
1974 + test_when_finished "git symbolic-ref -d refs/heads/symref" &&
1975 + git symbolic-ref refs/heads/symref $a &&
1976 + format_command $type "symref-update refs/heads/symref" "$m" "ref" "" >stdin &&
1977 + test_must_fail git update-ref --stdin $type --no-deref <stdin &&
1978 + echo $a >expect &&
1979 + git symbolic-ref refs/heads/symref >actual &&
1980 + test_cmp expect actual
1981 + '
1982 +
1983 + test_expect_success "stdin $type symref-update creates (with deref)" '
1984 + test_when_finished "git symbolic-ref -d refs/heads/symref" &&
1985 + format_command $type "symref-update refs/heads/symref" "$a" >stdin &&
1986 + git update-ref --stdin $type <stdin &&
1987 + echo $a >expect &&
1988 + git symbolic-ref --no-recurse refs/heads/symref >actual &&
1989 + test_cmp expect actual &&
1990 + test-tool ref-store main for-each-reflog-ent refs/heads/symref >actual &&
1991 + grep "$Z $(git rev-parse $a)" actual
1992 + '
1993 +
1994 + test_expect_success "stdin $type symref-update regular ref to symref with correct old-oid" '
1995 + test_when_finished "git symbolic-ref -d --no-recurse refs/heads/regularref" &&
1996 + git update-ref --no-deref refs/heads/regularref $a &&
1997 + format_command $type "symref-update refs/heads/regularref" "$a" "oid" "$(git rev-parse $a)" >stdin &&
1998 + git update-ref --stdin $type <stdin &&
1999 + echo $a >expect &&
2000 + git symbolic-ref --no-recurse refs/heads/regularref >actual &&
2001 + test_cmp expect actual &&
2002 + test-tool ref-store main for-each-reflog-ent refs/heads/regularref >actual &&
2003 + grep "$(git rev-parse $a) $(git rev-parse $a)" actual
2004 + '
2005 +
2006 + test_expect_success "stdin $type symref-update regular ref to symref fails with wrong old-oid" '
2007 + test_when_finished "git update-ref -d refs/heads/regularref" &&
2008 + git update-ref --no-deref refs/heads/regularref $a &&
2009 + format_command $type "symref-update refs/heads/regularref" "$a" "oid" "$(git rev-parse refs/heads/target2)" >stdin &&
2010 + test_must_fail git update-ref --stdin $type <stdin 2>err &&
2011 + grep "fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: is at $(git rev-parse $a) but expected $(git rev-parse refs/heads/target2)" err &&
2012 + echo $(git rev-parse $a) >expect &&
2013 + git rev-parse refs/heads/regularref >actual &&
2014 + test_cmp expect actual
2015 + '
2016 +
2017 + test_expect_success "stdin $type symref-update regular ref to symref fails with invalid old-oid" '
2018 + test_when_finished "git update-ref -d refs/heads/regularref" &&
2019 + git update-ref --no-deref refs/heads/regularref $a &&
2020 + format_command $type "symref-update refs/heads/regularref" "$a" "oid" "not-a-ref-oid" >stdin &&
2021 + test_must_fail git update-ref --stdin $type <stdin 2>err &&
2022 + grep "fatal: symref-update refs/heads/regularref: invalid oid: not-a-ref-oid" err &&
2023 + echo $(git rev-parse $a) >expect &&
2024 + git rev-parse refs/heads/regularref >actual &&
2025 + test_cmp expect actual
2026 + '
2027 +
2028 + test_expect_success "stdin $type symref-update existing symref with zero old-oid" '
2029 + test_when_finished "git symbolic-ref -d --no-recurse refs/heads/symref" &&
2030 + git symbolic-ref refs/heads/symref refs/heads/target2 &&
2031 + format_command $type "symref-update refs/heads/symref" "$a" "oid" "$Z" >stdin &&
2032 + test_must_fail git update-ref --stdin $type <stdin 2>err &&
2033 + grep "fatal: cannot lock ref ${SQ}refs/heads/symref${SQ}: reference already exists" err &&
2034 + echo refs/heads/target2 >expect &&
2035 + git symbolic-ref refs/heads/symref >actual &&
2036 + test_cmp expect actual
2037 + '
2038 +
2039 + test_expect_success "stdin $type symref-update regular ref to symref (with deref)" '
2040 + test_when_finished "git symbolic-ref -d refs/heads/symref" &&
2041 + test_when_finished "git update-ref -d --no-deref refs/heads/symref2" &&
2042 + git update-ref refs/heads/symref2 $a &&
2043 + git symbolic-ref --no-recurse refs/heads/symref refs/heads/symref2 &&
2044 + format_command $type "symref-update refs/heads/symref" "$a" >stdin &&
2045 + git update-ref $type --stdin <stdin &&
2046 + echo $a >expect &&
2047 + git symbolic-ref --no-recurse refs/heads/symref2 >actual &&
2048 + test_cmp expect actual &&
2049 + echo refs/heads/symref2 >expect &&
2050 + git symbolic-ref --no-recurse refs/heads/symref >actual &&
2051 + test_cmp expect actual &&
2052 + test-tool ref-store main for-each-reflog-ent refs/heads/symref >actual &&
2053 + grep "$(git rev-parse $a) $(git rev-parse $a)" actual
2054 + '
2055 +
2056 + test_expect_success "stdin $type symref-update regular ref to symref" '
2057 + test_when_finished "git symbolic-ref -d --no-recurse refs/heads/regularref" &&
2058 + git update-ref --no-deref refs/heads/regularref $a &&
2059 + format_command $type "symref-update refs/heads/regularref" "$a" >stdin &&
2060 + git update-ref $type --stdin <stdin &&
2061 + echo $a >expect &&
2062 + git symbolic-ref --no-recurse refs/heads/regularref >actual &&
2063 + test_cmp expect actual &&
2064 + test-tool ref-store main for-each-reflog-ent refs/heads/regularref >actual &&
2065 + grep "$(git rev-parse $a) $(git rev-parse $a)" actual
2066 + '
2067 +
2068 done
2069
2070 test_done
t/t1416-ref-transaction-hooks.sh
+4
@@ -163,6 +163,7 @@ test_expect_success 'hook gets all queued symref updates' '
163 git update-ref refs/heads/branch $POST_OID &&
164 git symbolic-ref refs/heads/symref refs/heads/main &&
165 git symbolic-ref refs/heads/symrefd refs/heads/main &&
166 + git symbolic-ref refs/heads/symrefu refs/heads/main &&
167
168 test_hook reference-transaction <<-\EOF &&
169 echo "$*" >>actual
@@ -190,10 +191,12 @@ test_expect_success 'hook gets all queued symref updates' '
191 ref:refs/heads/main $ZERO_OID refs/heads/symref
192 ref:refs/heads/main $ZERO_OID refs/heads/symrefd
193 $ZERO_OID ref:refs/heads/main refs/heads/symrefc
194 + ref:refs/heads/main ref:refs/heads/branch refs/heads/symrefu
195 committed
196 ref:refs/heads/main $ZERO_OID refs/heads/symref
197 ref:refs/heads/main $ZERO_OID refs/heads/symrefd
198 $ZERO_OID ref:refs/heads/main refs/heads/symrefc
199 + ref:refs/heads/main ref:refs/heads/branch refs/heads/symrefu
200 EOF
201
202 git update-ref --no-deref --stdin <<-EOF &&
@@ -201,6 +204,7 @@ test_expect_success 'hook gets all queued symref updates' '
204 symref-verify refs/heads/symref refs/heads/main
205 symref-delete refs/heads/symrefd refs/heads/main
206 symref-create refs/heads/symrefc refs/heads/main
207 + symref-update refs/heads/symrefu refs/heads/branch ref refs/heads/main
208 prepare
209 commit
210 EOF