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

Add a new command 'symref-delete' to allow deletions of symbolic refs in a transaction via the '--stdin' mode of the 'git-update-ref' command. The 'symref-delete' command can, when given an <old-target>, delete the provided <ref> only when it points to <old-target>. This command is only compatible with the 'no-deref' mode because we optionally want to check the 'old_target' of the ref being deleted. De-referencing a symbolic ref would provide a regular ref and we already have the 'delete' command for regular refs. While users can also use 'git symbolic-ref -d' to delete symbolic refs, the 'symref-delete' command in 'git-update-ref' allows users to do so within a transaction, which promises atomicity of the operation and can be batched with other commands. When no 'old_target' is provided it can also delete regular refs, similar to how the 'delete' command can delete symrefs when no 'old_oid' is provided. Helped-by: Patrick Steinhardt <ps@pks.im> 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 2343720967aac3725148e1a6690dbe6c98fe5f2e
8 files changed +140 -10
Documentation/git-update-ref.txt
+5
@@ -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-delete SP <ref> [SP <old-target>] LF
69 symref-verify SP <ref> [SP <old-target>] LF
70 option SP <opt> LF
71 start LF
@@ -87,6 +88,7 @@ quoting:
88 create SP <ref> NUL <new-oid> NUL
89 delete SP <ref> NUL [<old-oid>] NUL
90 verify SP <ref> NUL [<old-oid>] NUL
91 + symref-delete SP <ref> [NUL <old-target>] NUL
92 symref-verify SP <ref> [NUL <old-target>] NUL
93 option SP <opt> NUL
94 start NUL
@@ -119,6 +121,9 @@ verify::
121 Verify <ref> against <old-oid> but do not change it. If
122 <old-oid> is zero or missing, the ref must not exist.
123
124 +symref-delete::
125 + Delete <ref> after verifying it exists with <old-target>, if given.
126 +
127 symref-verify::
128 Verify symbolic <ref> against <old-target> but do not change it.
129 If <old-target> is missing, the ref must not exist. Can only be
builtin/fetch.c
+2 -2
@@ -1382,8 +1382,8 @@ static int prune_refs(struct display_state *display_state,
1382 if (!dry_run) {
1383 if (transaction) {
1384 for (ref = stale_refs; ref; ref = ref->next) {
1385 - result = ref_transaction_delete(transaction, ref->name, NULL, 0,
1386 - "fetch: prune", &err);
1385 + result = ref_transaction_delete(transaction, ref->name, NULL,
1386 + NULL, 0, "fetch: prune", &err);
1387 if (result)
1388 goto cleanup;
1389 }
builtin/receive-pack.c
+2 -1
@@ -1576,7 +1576,8 @@ static const char *update(struct command *cmd, struct shallow_info *si)
1576 if (ref_transaction_delete(transaction,
1577 namespaced_name,
1578 old_oid,
1579 - 0, "push", &err)) {
1579 + NULL, 0,
1580 + "push", &err)) {
1581 rp_error("%s", err.buf);
1582 ret = "failed to delete";
1583 } else {
builtin/update-ref.c
+32 -1
@@ -293,7 +293,7 @@ static void parse_cmd_delete(struct ref_transaction *transaction,
293
294 if (ref_transaction_delete(transaction, refname,
295 have_old ? &old_oid : NULL,
296 - update_flags, msg, &err))
296 + NULL, update_flags, msg, &err))
297 die("%s", err.buf);
298
299 update_flags = default_flags;
@@ -301,6 +301,36 @@ static void parse_cmd_delete(struct ref_transaction *transaction,
301 strbuf_release(&err);
302 }
303
304 +
305 +static void parse_cmd_symref_delete(struct ref_transaction *transaction,
306 + const char *next, const char *end)
307 +{
308 + struct strbuf err = STRBUF_INIT;
309 + char *refname, *old_target;
310 +
311 + if (!(update_flags & REF_NO_DEREF))
312 + die("symref-delete: cannot operate with deref mode");
313 +
314 + refname = parse_refname(&next);
315 + if (!refname)
316 + die("symref-delete: missing <ref>");
317 +
318 + old_target = parse_next_refname(&next);
319 +
320 + if (*next != line_termination)
321 + die("symref-delete %s: extra input: %s", refname, next);
322 +
323 + if (ref_transaction_delete(transaction, refname, NULL,
324 + old_target, update_flags, msg, &err))
325 + die("%s", err.buf);
326 +
327 + update_flags = default_flags;
328 + free(refname);
329 + free(old_target);
330 + strbuf_release(&err);
331 +}
332 +
333 +
334 static void parse_cmd_verify(struct ref_transaction *transaction,
335 const char *next, const char *end)
336 {
@@ -443,6 +473,7 @@ static const struct parse_cmd {
473 { "create", parse_cmd_create, 2, UPDATE_REFS_OPEN },
474 { "delete", parse_cmd_delete, 2, UPDATE_REFS_OPEN },
475 { "verify", parse_cmd_verify, 2, UPDATE_REFS_OPEN },
476 + { "symref-delete", parse_cmd_symref_delete, 2, UPDATE_REFS_OPEN },
477 { "symref-verify", parse_cmd_symref_verify, 2, UPDATE_REFS_OPEN },
478 { "option", parse_cmd_option, 1, UPDATE_REFS_OPEN },
479 { "start", parse_cmd_start, 0, UPDATE_REFS_STARTED },
refs.c
+10 -4
@@ -979,7 +979,7 @@ int refs_delete_ref(struct ref_store *refs, const char *msg,
979 transaction = ref_store_transaction_begin(refs, &err);
980 if (!transaction ||
981 ref_transaction_delete(transaction, refname, old_oid,
982 - flags, msg, &err) ||
982 + NULL, flags, msg, &err) ||
983 ref_transaction_commit(transaction, &err)) {
984 error("%s", err.buf);
985 ref_transaction_free(transaction);
@@ -1317,14 +1317,20 @@ int ref_transaction_create(struct ref_transaction *transaction,
1317 int ref_transaction_delete(struct ref_transaction *transaction,
1318 const char *refname,
1319 const struct object_id *old_oid,
1320 - unsigned int flags, const char *msg,
1320 + const char *old_target,
1321 + unsigned int flags,
1322 + const char *msg,
1323 struct strbuf *err)
1324 {
1325 if (old_oid && is_null_oid(old_oid))
1326 BUG("delete called with old_oid set to zeros");
1327 + if (old_oid && old_target)
1328 + BUG("delete called with both old_oid and old_target set");
1329 + if (old_target && !(flags & REF_NO_DEREF))
1330 + BUG("delete cannot operate on symrefs with deref mode");
1331 return ref_transaction_update(transaction, refname,
1332 null_oid(), old_oid,
1327 - NULL, NULL, flags,
1333 + NULL, old_target, flags,
1334 msg, err);
1335 }
1336
@@ -2767,7 +2773,7 @@ int refs_delete_refs(struct ref_store *refs, const char *logmsg,
2773
2774 for_each_string_list_item(item, refnames) {
2775 ret = ref_transaction_delete(transaction, item->string,
2770 - NULL, flags, msg, &err);
2776 + NULL, NULL, flags, msg, &err);
2777 if (ret) {
2778 warning(_("could not delete reference %s: %s"),
2779 item->string, err.buf);
refs.h
+3 -1
@@ -767,7 +767,9 @@ int ref_transaction_create(struct ref_transaction *transaction,
767 int ref_transaction_delete(struct ref_transaction *transaction,
768 const char *refname,
769 const struct object_id *old_oid,
770 - unsigned int flags, const char *msg,
770 + const char *old_target,
771 + unsigned int flags,
772 + const char *msg,
773 struct strbuf *err);
774
775 /*
t/t1400-update-ref.sh
+68
@@ -1729,6 +1729,74 @@ do
1729 test_cmp expect actual
1730 '
1731
1732 + test_expect_success "stdin $type symref-delete fails without --no-deref" '
1733 + git symbolic-ref refs/heads/symref $a &&
1734 + format_command $type "symref-delete refs/heads/symref" "$a" >stdin &&
1735 + test_must_fail git update-ref --stdin $type <stdin 2>err &&
1736 + grep "fatal: symref-delete: cannot operate with deref mode" err
1737 + '
1738 +
1739 + test_expect_success "stdin $type symref-delete fails with no ref" '
1740 + format_command $type "symref-delete " >stdin &&
1741 + test_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&
1742 + grep "fatal: symref-delete: missing <ref>" err
1743 + '
1744 +
1745 + test_expect_success "stdin $type symref-delete fails deleting regular ref" '
1746 + test_when_finished "git update-ref -d refs/heads/regularref" &&
1747 + git update-ref refs/heads/regularref $a &&
1748 + format_command $type "symref-delete refs/heads/regularref" "$a" >stdin &&
1749 + test_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&
1750 + grep "fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: expected symref with target ${SQ}$a${SQ}: but is a regular ref" err
1751 + '
1752 +
1753 + test_expect_success "stdin $type symref-delete fails with too many arguments" '
1754 + format_command $type "symref-delete refs/heads/symref" "$a" "$a" >stdin &&
1755 + test_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&
1756 + if test "$type" = "-z"
1757 + then
1758 + grep "fatal: unknown command: $a" err
1759 + else
1760 + grep "fatal: symref-delete refs/heads/symref: extra input: $a" err
1761 + fi
1762 + '
1763 +
1764 + test_expect_success "stdin $type symref-delete fails with wrong old value" '
1765 + format_command $type "symref-delete refs/heads/symref" "$m" >stdin &&
1766 + test_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&
1767 + grep "fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected refs/heads/main" err &&
1768 + git symbolic-ref refs/heads/symref >expect &&
1769 + echo $a >actual &&
1770 + test_cmp expect actual
1771 + '
1772 +
1773 + test_expect_success "stdin $type symref-delete works with right old value" '
1774 + format_command $type "symref-delete refs/heads/symref" "$a" >stdin &&
1775 + git update-ref --stdin $type --no-deref <stdin &&
1776 + test_must_fail git rev-parse --verify -q refs/heads/symref
1777 + '
1778 +
1779 + test_expect_success "stdin $type symref-delete works with empty old value" '
1780 + git symbolic-ref refs/heads/symref $a >stdin &&
1781 + format_command $type "symref-delete refs/heads/symref" "" >stdin &&
1782 + git update-ref --stdin $type --no-deref <stdin &&
1783 + test_must_fail git rev-parse --verify -q $b
1784 + '
1785 +
1786 + test_expect_success "stdin $type symref-delete succeeds for dangling reference" '
1787 + test_must_fail git symbolic-ref refs/heads/nonexistent &&
1788 + git symbolic-ref refs/heads/symref2 refs/heads/nonexistent &&
1789 + format_command $type "symref-delete refs/heads/symref2" "refs/heads/nonexistent" >stdin &&
1790 + git update-ref --stdin $type --no-deref <stdin &&
1791 + test_must_fail git symbolic-ref -d refs/heads/symref2
1792 + '
1793 +
1794 + test_expect_success "stdin $type symref-delete deletes regular ref without target" '
1795 + git update-ref refs/heads/regularref $a &&
1796 + format_command $type "symref-delete refs/heads/regularref" >stdin &&
1797 + git update-ref --stdin $type --no-deref <stdin
1798 + '
1799 +
1800 done
1801
1802 test_done
t/t1416-ref-transaction-hooks.sh
+18 -1
@@ -162,6 +162,7 @@ test_expect_success 'hook gets all queued symref updates' '
162
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
167 test_hook reference-transaction <<-\EOF &&
168 echo "$*" >>actual
@@ -171,16 +172,32 @@ test_expect_success 'hook gets all queued symref updates' '
172 done >>actual
173 EOF
174
174 - cat >expect <<-EOF &&
175 + # In the files backend, "delete" also triggers an additional transaction
176 + # update on the packed-refs backend, which constitutes additional reflog
177 + # entries.
178 + if test_have_prereq REFFILES
179 + then
180 + cat >expect <<-EOF
181 + aborted
182 + $ZERO_OID $ZERO_OID refs/heads/symrefd
183 + EOF
184 + else
185 + >expect
186 + fi &&
187 +
188 + cat >>expect <<-EOF &&
189 prepared
190 ref:refs/heads/main $ZERO_OID refs/heads/symref
191 + ref:refs/heads/main $ZERO_OID refs/heads/symrefd
192 committed
193 ref:refs/heads/main $ZERO_OID refs/heads/symref
194 + ref:refs/heads/main $ZERO_OID refs/heads/symrefd
195 EOF
196
197 git update-ref --no-deref --stdin <<-EOF &&
198 start
199 symref-verify refs/heads/symref refs/heads/main
200 + symref-delete refs/heads/symrefd refs/heads/main
201 prepare
202 commit
203 EOF