{"thread":{"id":"61469","subject":"[PATCH 0/6] update-ref: add symref support for --stdin","startedAt":"2024-05-14T12:44:16Z","lastAt":"2024-06-05T16:22:08Z","messageCount":50,"participants":["Karthik Nayak","Patrick Steinhardt","Junio C Hamano","Eric Sunshine"],"isPatch":true,"patchVersion":1,"patchTotal":6},"messages":[{"id":"494710","messageId":"20240514124411.1037019-1-knayak@gitlab.com","threadId":"61469","inReplyTo":null,"subject":"[PATCH 0/6] update-ref: add symref support for --stdin","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-14T12:44:05Z","receivedAt":"2024-05-14T12:44:16Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"From: Karthik Nayak <karthik.188@gmail.com>\n\nThe 'update-ref' command is used to update refs using transactions. The\ncommand allows users to also utilize a '--stdin' mode to provide a\nbatch of sub-commands which can be processed in a transaction.\n      \nCurrently, the sub-commands involve {verify, delete, create, update}\nand they allow users to work with regular refs in the repository. To\nwork with symrefs, users only have the option of using\n'git-symbolic-ref', which doesn't provide transaction support to the\nusers eventhough it uses the same behind the hood. \n\nRecently, we modified the reference backend to add symref support,\nfollowing which, 'git-symbolic-ref' also uses the transaction backend.\nBut, it doesn't expose this to the user. To allow users to work with\nsymrefs via transaction, this series adds support for new sub-commands\n{symrer-verify, symref-delete, symref-create, symref-update} to the\n'--stdin' mode of update-ref. These complement the existing\nsub-commands.\n\nThe patches 1, 5 fix small issues in the reference backends. The other\npatches 2, 3, 4 & 6, each add one of the new sub-commands.\n\nThe series is based off master, with 'kn/ref-transaction-symref' merged\nin. \n\nKarthik Nayak (6):\n  refs: create and use `ref_update_ref_must_exist()`\n  update-ref: add support for 'symref-verify' command\n  update-ref: add support for 'symref-delete' command\n  update-ref: add support for 'symref-create' command\n  reftable: pick either 'oid' or 'target' for new updates\n  update-ref: add support for 'symref-update' command\n\n Documentation/git-update-ref.txt |  25 ++\n builtin/clone.c                  |   2 +-\n builtin/fetch.c                  |   2 +-\n builtin/receive-pack.c           |   3 +-\n builtin/update-ref.c             | 230 +++++++++++++++++-\n refs.c                           |  34 ++-\n refs.h                           |   6 +-\n refs/files-backend.c             |   3 +-\n refs/refs-internal.h             |   6 +\n refs/reftable-backend.c          |   7 +-\n t/t0600-reffiles-backend.sh      |  32 +++\n t/t1400-update-ref.sh            | 404 ++++++++++++++++++++++++++++++-\n t/t1416-ref-transaction-hooks.sh |  54 +++++\n 13 files changed, 775 insertions(+), 33 deletions(-)\n\n-- \n2.43.GIT\n\n"},{"id":"494711","messageId":"20240514124411.1037019-2-knayak@gitlab.com","threadId":"61469","inReplyTo":"20240514124411.1037019-1-knayak@gitlab.com","subject":"[PATCH 1/6] refs: create and use `ref_update_ref_must_exist()`","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-14T12:44:06Z","receivedAt":"2024-05-14T12:44:16Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"From: Karthik Nayak <karthik.188@gmail.com>\n\nThe files and reftable backend, need to check if a ref must exist, so\nthat the required validation can be done. A ref must exist only when the\n`old_oid` value of the update has been explicitly set and it is not the\n`null_oid` value.\n\nSince we also support symrefs now, we need to ensure that even when\n`old_target` is set a ref must exist. While this was missed when we\nadded symref support in transactions, there are no active users of this\npath. As we introduce the 'symref-verify' command in the upcoming\ncommits, it is important to fix this.\n\nSo let's export this to a function called `ref_update_ref_must_exist()`\nand expose it internally via 'refs-internal.h'.\n\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n refs.c                  | 6 ++++++\n refs/files-backend.c    | 3 +--\n refs/refs-internal.h    | 6 ++++++\n refs/reftable-backend.c | 2 +-\n 4 files changed, 14 insertions(+), 3 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex fa5471d219..59858fafdb 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2863,3 +2863,9 @@ int ref_update_check_old_target(const char *referent, struct ref_update *update,\n \t\t\t    referent, update->old_target);\n \treturn -1;\n }\n+\n+int ref_update_ref_must_exist(struct ref_update *update)\n+{\n+\treturn (update->flags & REF_HAVE_OLD) &&\n+\t\t(!is_null_oid(&update->old_oid) || update->old_target);\n+}\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 3957bfa579..2df204f891 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2411,8 +2411,7 @@ static int lock_ref_for_update(struct files_ref_store *refs,\n \t\t\t       struct strbuf *err)\n {\n \tstruct strbuf referent = STRBUF_INIT;\n-\tint mustexist = (update->flags & REF_HAVE_OLD) &&\n-\t\t!is_null_oid(&update->old_oid);\n+\tint mustexist = ref_update_ref_must_exist(update);\n \tint ret = 0;\n \tstruct ref_lock *lock;\n \ndiff --git a/refs/refs-internal.h b/refs/refs-internal.h\nindex 53a6c5d842..5da3029e6c 100644\n--- a/refs/refs-internal.h\n+++ b/refs/refs-internal.h\n@@ -765,4 +765,10 @@ int ref_update_has_null_new_value(struct ref_update *update);\n int ref_update_check_old_target(const char *referent, struct ref_update *update,\n \t\t\t\tstruct strbuf *err);\n \n+/*\n+ * Check if the ref must exist, this means that the old_oid or\n+ * old_target is non NULL.\n+ */\n+int ref_update_ref_must_exist(struct ref_update *update);\n+\n #endif /* REFS_REFS_INTERNAL_H */\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 98cebbcf39..975061d103 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -825,7 +825,7 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n \t\t\t\t\t      &current_oid, &referent, &u->type);\n \t\tif (ret < 0)\n \t\t\tgoto done;\n-\t\tif (ret > 0 && (!(u->flags & REF_HAVE_OLD) || is_null_oid(&u->old_oid))) {\n+\t\tif (ret > 0 && !ref_update_ref_must_exist(u)) {\n \t\t\t/*\n \t\t\t * The reference does not exist, and we either have no\n \t\t\t * old object ID or expect the reference to not exist.\n-- \n2.43.GIT\n\n"},{"id":"494712","messageId":"20240514124411.1037019-3-knayak@gitlab.com","threadId":"61469","inReplyTo":"20240514124411.1037019-1-knayak@gitlab.com","subject":"[PATCH 2/6] update-ref: add support for 'symref-verify' command","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-14T12:44:07Z","receivedAt":"2024-05-14T12:44:18Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"From: Karthik Nayak <karthik.188@gmail.com>\n\nThe 'symref-verify' command allows users to verify if a provided <ref>\ncontains the provided <old-target> without changing the <ref>. If\n<old-target> is not provided, the command will verify that the <ref>\ndoesn't exist.\n\nThe command allows users to verify symbolic refs within a transaction,\nand this means users can perform a set of changes in a transaction only\nwhen the verification holds good.\n\nSince we're checking for symbolic refs, this command will only work with\nthe 'no-deref' mode. This is because any dereferenced symbolic ref will\npoint to an object and not a ref and the regular 'verify' command can be\nused in such situations.\n\nAdd required tests for symref support in 'verify' while also adding\nreflog checks for the pre-existing 'verify' tests.\n\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n Documentation/git-update-ref.txt |  7 +++\n builtin/update-ref.c             | 80 +++++++++++++++++++++++----\n refs.c                           |  9 ++-\n refs.h                           |  1 +\n t/t1400-update-ref.sh            | 94 +++++++++++++++++++++++++++++++-\n t/t1416-ref-transaction-hooks.sh | 30 ++++++++++\n 6 files changed, 206 insertions(+), 15 deletions(-)\n\ndiff --git a/Documentation/git-update-ref.txt b/Documentation/git-update-ref.txt\nindex 374a2ebd2b..9fe78b3501 100644\n--- a/Documentation/git-update-ref.txt\n+++ b/Documentation/git-update-ref.txt\n@@ -65,6 +65,7 @@ performs all modifications together.  Specify commands of the form:\n \tcreate SP <ref> SP <new-oid> LF\n \tdelete SP <ref> [SP <old-oid>] LF\n \tverify SP <ref> [SP <old-oid>] LF\n+\tsymref-verify SP <ref> [SP <old-target>] LF\n \toption SP <opt> LF\n \tstart LF\n \tprepare LF\n@@ -86,6 +87,7 @@ quoting:\n \tcreate SP <ref> NUL <new-oid> NUL\n \tdelete SP <ref> NUL [<old-oid>] NUL\n \tverify SP <ref> NUL [<old-oid>] NUL\n+\tsymref-verify SP <ref> [NUL <old-target>] NUL\n \toption SP <opt> NUL\n \tstart NUL\n \tprepare NUL\n@@ -117,6 +119,11 @@ verify::\n \tVerify <ref> against <old-oid> but do not change it.  If\n \t<old-oid> is zero or missing, the ref must not exist.\n \n+symref-verify::\n+\tVerify symbolic <ref> against <old-target> but do not change it.\n+\tIf <old-target> is missing, the ref must not exist.  Can only be\n+\tused in `no-deref` mode.\n+\n option::\n \tModify the behavior of the next command naming a <ref>.\n \tThe only valid option is `no-deref` to avoid dereferencing\ndiff --git a/builtin/update-ref.c b/builtin/update-ref.c\nindex 21fdbf6ac8..6dce1cd663 100644\n--- a/builtin/update-ref.c\n+++ b/builtin/update-ref.c\n@@ -76,6 +76,29 @@ static char *parse_refname(const char **next)\n \treturn strbuf_detach(&ref, NULL);\n }\n \n+/*\n+ * Wrapper around parse_refname which skips the next delimiter.\n+ */\n+static char *parse_next_refname(const char **next)\n+{\n+\tif (line_termination) {\n+\t\t/* Without -z, consume SP and use next argument */\n+\t\tif (!**next || **next == line_termination)\n+\t\t\treturn NULL;\n+\t\tif (**next != ' ')\n+\t\t\tdie(\"expected SP but got: %s\", *next);\n+\t} else {\n+\t\t/* With -z, read the next NUL-terminated line */\n+\t\tif (**next)\n+\t\t\treturn NULL;\n+\t}\n+\t/* Skip the delimiter */\n+\t(*next)++;\n+\n+\treturn parse_refname(next);\n+}\n+\n+\n /*\n  * The value being parsed is <old-oid> (as opposed to <new-oid>; the\n  * difference affects which error messages are generated):\n@@ -297,11 +320,47 @@ static void parse_cmd_verify(struct ref_transaction *transaction,\n \t\tdie(\"verify %s: extra input: %s\", refname, next);\n \n \tif (ref_transaction_verify(transaction, refname, &old_oid,\n-\t\t\t\t   update_flags, &err))\n+\t\t\t\t   NULL, update_flags, &err))\n+\t\tdie(\"%s\", err.buf);\n+\n+\tupdate_flags = default_flags;\n+\tfree(refname);\n+\tstrbuf_release(&err);\n+}\n+\n+static void parse_cmd_symref_verify(struct ref_transaction *transaction,\n+\t\t\t\t    const char *next, const char *end)\n+{\n+\tstruct strbuf err = STRBUF_INIT;\n+\tstruct object_id old_oid;\n+\tchar *refname, *old_target;\n+\n+\tif (!(update_flags & REF_NO_DEREF))\n+\t\tdie(\"symref-verify: cannot operate with deref mode\");\n+\n+\trefname = parse_refname(&next);\n+\tif (!refname)\n+\t\tdie(\"symref-verify: missing <ref>\");\n+\n+\t/*\n+\t * old_ref is optional, if not provided, we need to ensure that the\n+\t * ref doesn't exist.\n+\t */\n+\told_target = parse_next_refname(&next);\n+\tif (!old_target)\n+\t\toidcpy(&old_oid, null_oid());\n+\n+\tif (*next != line_termination)\n+\t\tdie(\"symref-verify %s: extra input: %s\", refname, next);\n+\n+\tif (ref_transaction_verify(transaction, refname,\n+\t\t\t\t   old_target ? NULL : &old_oid,\n+\t\t\t\t   old_target, update_flags, &err))\n \t\tdie(\"%s\", err.buf);\n \n \tupdate_flags = default_flags;\n \tfree(refname);\n+\tfree(old_target);\n \tstrbuf_release(&err);\n }\n \n@@ -380,15 +439,16 @@ static const struct parse_cmd {\n \tunsigned args;\n \tenum update_refs_state state;\n } command[] = {\n-\t{ \"update\",  parse_cmd_update,  3, UPDATE_REFS_OPEN },\n-\t{ \"create\",  parse_cmd_create,  2, UPDATE_REFS_OPEN },\n-\t{ \"delete\",  parse_cmd_delete,  2, UPDATE_REFS_OPEN },\n-\t{ \"verify\",  parse_cmd_verify,  2, UPDATE_REFS_OPEN },\n-\t{ \"option\",  parse_cmd_option,  1, UPDATE_REFS_OPEN },\n-\t{ \"start\",   parse_cmd_start,   0, UPDATE_REFS_STARTED },\n-\t{ \"prepare\", parse_cmd_prepare, 0, UPDATE_REFS_PREPARED },\n-\t{ \"abort\",   parse_cmd_abort,   0, UPDATE_REFS_CLOSED },\n-\t{ \"commit\",  parse_cmd_commit,  0, UPDATE_REFS_CLOSED },\n+\t{ \"update\",        parse_cmd_update,        3, UPDATE_REFS_OPEN },\n+\t{ \"create\",        parse_cmd_create,        2, UPDATE_REFS_OPEN },\n+\t{ \"delete\",        parse_cmd_delete,        2, UPDATE_REFS_OPEN },\n+\t{ \"verify\",        parse_cmd_verify,        2, UPDATE_REFS_OPEN },\n+\t{ \"symref-verify\", parse_cmd_symref_verify, 2, UPDATE_REFS_OPEN },\n+\t{ \"option\",        parse_cmd_option,        1, UPDATE_REFS_OPEN },\n+\t{ \"start\",         parse_cmd_start,         0, UPDATE_REFS_STARTED },\n+\t{ \"prepare\",       parse_cmd_prepare,       0, UPDATE_REFS_PREPARED },\n+\t{ \"abort\",         parse_cmd_abort,         0, UPDATE_REFS_CLOSED },\n+\t{ \"commit\",        parse_cmd_commit,        0, UPDATE_REFS_CLOSED },\n };\n \n static void update_refs_stdin(void)\ndiff --git a/refs.c b/refs.c\nindex 59858fafdb..ee4c6ed99c 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1331,14 +1331,17 @@ int ref_transaction_delete(struct ref_transaction *transaction,\n int ref_transaction_verify(struct ref_transaction *transaction,\n \t\t\t   const char *refname,\n \t\t\t   const struct object_id *old_oid,\n+\t\t\t   const char *old_target,\n \t\t\t   unsigned int flags,\n \t\t\t   struct strbuf *err)\n {\n-\tif (!old_oid)\n-\t\tBUG(\"verify called with old_oid set to NULL\");\n+\tif (!old_target && !old_oid)\n+\t\tBUG(\"verify called with old_oid and old_target set to NULL\");\n+\tif (old_target && !(flags & REF_NO_DEREF))\n+\t\tBUG(\"verify cannot operate on symrefs with deref mode\");\n \treturn ref_transaction_update(transaction, refname,\n \t\t\t\t      NULL, old_oid,\n-\t\t\t\t      NULL, NULL,\n+\t\t\t\t      NULL, old_target,\n \t\t\t\t      flags, NULL, err);\n }\n \ndiff --git a/refs.h b/refs.h\nindex 71cc1c58e0..48cec1ba72 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -781,6 +781,7 @@ int ref_transaction_delete(struct ref_transaction *transaction,\n int ref_transaction_verify(struct ref_transaction *transaction,\n \t\t\t   const char *refname,\n \t\t\t   const struct object_id *old_oid,\n+\t\t\t   const char *old_target,\n \t\t\t   unsigned int flags,\n \t\t\t   struct strbuf *err);\n \ndiff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\nindex ec3443cc87..8784c59a53 100755\n--- a/t/t1400-update-ref.sh\n+++ b/t/t1400-update-ref.sh\n@@ -890,17 +890,23 @@ test_expect_success 'stdin update/create/verify combination works' '\n '\n \n test_expect_success 'stdin verify succeeds for correct value' '\n+\ttest-tool ref-store main for-each-reflog-ent $m >before &&\n \tgit rev-parse $m >expect &&\n \techo \"verify $m $m\" >stdin &&\n \tgit update-ref --stdin <stdin &&\n \tgit rev-parse $m >actual &&\n-\ttest_cmp expect actual\n+\ttest_cmp expect actual &&\n+\ttest-tool ref-store main for-each-reflog-ent $m >after &&\n+\ttest_cmp before after\n '\n \n test_expect_success 'stdin verify succeeds for missing reference' '\n+\ttest-tool ref-store main for-each-reflog-ent $m >before &&\n \techo \"verify refs/heads/missing $Z\" >stdin &&\n \tgit update-ref --stdin <stdin &&\n-\ttest_must_fail git rev-parse --verify -q refs/heads/missing\n+\ttest_must_fail git rev-parse --verify -q refs/heads/missing &&\n+\ttest-tool ref-store main for-each-reflog-ent $m >after &&\n+\ttest_cmp before after\n '\n \n test_expect_success 'stdin verify treats no value as missing' '\n@@ -1641,4 +1647,88 @@ test_expect_success PIPE 'transaction flushes status updates' '\n \ttest_cmp expected actual\n '\n \n+create_stdin_buf () {\n+\tif test \"$1\" = \"-z\"\n+\tthen\n+\t\tshift\n+\t\tprintf \"$F\" \"$@\" >stdin\n+\telse\n+\t\techo \"$@\" >stdin\n+\tfi\n+}\n+\n+for type in \"\" \"-z\"\n+do\n+\n+\ttest_expect_success \"stdin ${type} symref-verify fails without --no-deref\" '\n+\t\tgit symbolic-ref refs/heads/symref $a &&\n+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref\" \"$a\" &&\n+\t\ttest_must_fail git update-ref --stdin ${type} <stdin 2>err &&\n+\t\tgrep \"fatal: symref-verify: cannot operate with deref mode\" err\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-verify fails with too many arguments\" '\n+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref\" \"$a\" \"$a\" &&\n+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err  &&\n+\t\tif test \"$type\" = \"-z\"\n+\t\tthen\n+\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\telse\n+\t\t\tgrep \"fatal: symref-verify refs/heads/symref: extra input:  $a\" err\n+\t\tfi\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-verify succeeds for correct value\" '\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >before &&\n+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref\" \"$a\" &&\n+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >after &&\n+\t\ttest_cmp before after\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-verify no value is treated as zero value\" '\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref\" \"\" &&\n+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-verify succeeds for dangling reference\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref2\" &&\n+\t\ttest_must_fail git symbolic-ref refs/heads/nonexistent &&\n+\t\tgit symbolic-ref refs/heads/symref2 refs/heads/nonexistent &&\n+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref2\" \"refs/heads/nonexistent\" &&\n+\t\tgit update-ref --stdin ${type} --no-deref <stdin\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-verify fails for missing reference\" '\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >before &&\n+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/missing\" \"refs/heads/unknown\" &&\n+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n+\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/missing${SQ}: unable to resolve reference ${SQ}refs/heads/missing${SQ}\" err &&\n+\t\ttest_must_fail git rev-parse --verify -q refs/heads/missing &&\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >after &&\n+\t\ttest_cmp before after\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-verify fails for wrong value\" '\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref\" \"$b\" &&\n+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-verify fails for mistaken null value\" '\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref\" \"$Z\" &&\n+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+done\n+\n test_done\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex 067fd57290..fd58b902f4 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -157,4 +157,34 @@ test_expect_success 'hook captures git-symbolic-ref updates' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'hook gets all queued symref updates' '\n+\ttest_when_finished \"rm actual\" &&\n+\n+\tgit update-ref refs/heads/branch $POST_OID &&\n+\tgit symbolic-ref refs/heads/symref refs/heads/main &&\n+\n+\ttest_hook reference-transaction <<-\\EOF &&\n+\techo \"$*\" >>actual\n+\twhile read -r line\n+\tdo\n+\t\tprintf \"%s\\n\" \"$line\"\n+\tdone >>actual\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\tprepared\n+\tref:refs/heads/main $ZERO_OID refs/heads/symref\n+\tcommitted\n+\tref:refs/heads/main $ZERO_OID refs/heads/symref\n+\tEOF\n+\n+\tgit update-ref --no-deref --stdin <<-EOF &&\n+\tstart\n+\tsymref-verify refs/heads/symref refs/heads/main\n+\tprepare\n+\tcommit\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \n2.43.GIT\n\n"},{"id":"494713","messageId":"20240514124411.1037019-4-knayak@gitlab.com","threadId":"61469","inReplyTo":"20240514124411.1037019-1-knayak@gitlab.com","subject":"[PATCH 3/6] update-ref: add support for 'symref-delete' command","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-14T12:44:08Z","receivedAt":"2024-05-14T12:44:18Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"From: Karthik Nayak <karthik.188@gmail.com>\n\nAdd a new command 'symref-delete' to allow deletions of symbolic refs in\na transaction via the '--stdin' mode of the 'git-update-ref' command.\nThe 'symref-delete' command can, when given an <old-target>, delete the\nprovided <ref> only when it points to <old-target>.\n\nThis command is only compatible with the 'no-deref' mode because we\noptionally want to check the 'old_target' of the ref being deleted.\nDe-referencing a symbolic ref would provide a regular ref and we already\nhave the 'delete' command for regular refs.\n\nWhile users can also use 'git symbolic-ref -d' to delete symbolic refs,\nthe 'symref-delete' command in 'git-update-ref' allows users to do so\nwithin a transaction, which promises atomicity of the operation and can\nbe batched with other commands.\n\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n Documentation/git-update-ref.txt |  5 +++\n builtin/fetch.c                  |  2 +-\n builtin/receive-pack.c           |  3 +-\n builtin/update-ref.c             | 33 ++++++++++++++++++-\n refs.c                           | 12 ++++---\n refs.h                           |  4 ++-\n t/t1400-update-ref.sh            | 56 +++++++++++++++++++++++++++++++-\n t/t1416-ref-transaction-hooks.sh | 19 ++++++++++-\n 8 files changed, 124 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/git-update-ref.txt b/Documentation/git-update-ref.txt\nindex 9fe78b3501..16e02f6979 100644\n--- a/Documentation/git-update-ref.txt\n+++ b/Documentation/git-update-ref.txt\n@@ -65,6 +65,7 @@ performs all modifications together.  Specify commands of the form:\n \tcreate SP <ref> SP <new-oid> LF\n \tdelete SP <ref> [SP <old-oid>] LF\n \tverify SP <ref> [SP <old-oid>] LF\n+\tsymref-delete SP <ref> [SP <old-target>] LF\n \tsymref-verify SP <ref> [SP <old-target>] LF\n \toption SP <opt> LF\n \tstart LF\n@@ -87,6 +88,7 @@ quoting:\n \tcreate SP <ref> NUL <new-oid> NUL\n \tdelete SP <ref> NUL [<old-oid>] NUL\n \tverify SP <ref> NUL [<old-oid>] NUL\n+\tsymref-delete SP <ref> [NUL <old-target>] NUL\n \tsymref-verify SP <ref> [NUL <old-target>] NUL\n \toption SP <opt> NUL\n \tstart NUL\n@@ -119,6 +121,9 @@ verify::\n \tVerify <ref> against <old-oid> but do not change it.  If\n \t<old-oid> is zero or missing, the ref must not exist.\n \n+symref-delete::\n+\tDelete <ref> after verifying it exists with <old-target>, if given.\n+\n symref-verify::\n \tVerify symbolic <ref> against <old-target> but do not change it.\n \tIf <old-target> is missing, the ref must not exist.  Can only be\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 66840b7c5b..d02592efca 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1383,7 +1383,7 @@ static int prune_refs(struct display_state *display_state,\n \t\tif (transaction) {\n \t\t\tfor (ref = stale_refs; ref; ref = ref->next) {\n \t\t\t\tresult = ref_transaction_delete(transaction, ref->name, NULL, 0,\n-\t\t\t\t\t\t\t\t\"fetch: prune\", &err);\n+\t\t\t\t\t\t\t\tNULL, \"fetch: prune\", &err);\n \t\t\t\tif (result)\n \t\t\t\t\tgoto cleanup;\n \t\t\t}\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex b150ef39a8..9a4667d57d 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1576,7 +1576,8 @@ static const char *update(struct command *cmd, struct shallow_info *si)\n \t\tif (ref_transaction_delete(transaction,\n \t\t\t\t\t   namespaced_name,\n \t\t\t\t\t   old_oid,\n-\t\t\t\t\t   0, \"push\", &err)) {\n+\t\t\t\t\t   0, NULL,\n+\t\t\t\t\t   \"push\", &err)) {\n \t\t\trp_error(\"%s\", err.buf);\n \t\t\tret = \"failed to delete\";\n \t\t} else {\ndiff --git a/builtin/update-ref.c b/builtin/update-ref.c\nindex 6dce1cd663..0ef9c38d8d 100644\n--- a/builtin/update-ref.c\n+++ b/builtin/update-ref.c\n@@ -293,7 +293,7 @@ static void parse_cmd_delete(struct ref_transaction *transaction,\n \n \tif (ref_transaction_delete(transaction, refname,\n \t\t\t\t   have_old ? &old_oid : NULL,\n-\t\t\t\t   update_flags, msg, &err))\n+\t\t\t\t   update_flags, NULL, msg, &err))\n \t\tdie(\"%s\", err.buf);\n \n \tupdate_flags = default_flags;\n@@ -301,6 +301,36 @@ static void parse_cmd_delete(struct ref_transaction *transaction,\n \tstrbuf_release(&err);\n }\n \n+\n+static void parse_cmd_symref_delete(struct ref_transaction *transaction,\n+\t\t\t\t    const char *next, const char *end)\n+{\n+\tstruct strbuf err = STRBUF_INIT;\n+\tchar *refname, *old_target;\n+\n+\tif (!(update_flags & REF_NO_DEREF))\n+\t\tdie(\"symref-delete: cannot operate with deref mode\");\n+\n+\trefname = parse_refname(&next);\n+\tif (!refname)\n+\t\tdie(\"symref-delete: missing <ref>\");\n+\n+\told_target = parse_next_refname(&next);\n+\n+\tif (*next != line_termination)\n+\t\tdie(\"symref-delete %s: extra input: %s\", refname, next);\n+\n+\tif (ref_transaction_delete(transaction, refname, NULL,\n+\t\t\t\t   update_flags, old_target, msg, &err))\n+\t\tdie(\"%s\", err.buf);\n+\n+\tupdate_flags = default_flags;\n+\tfree(refname);\n+\tfree(old_target);\n+\tstrbuf_release(&err);\n+}\n+\n+\n static void parse_cmd_verify(struct ref_transaction *transaction,\n \t\t\t     const char *next, const char *end)\n {\n@@ -443,6 +473,7 @@ static const struct parse_cmd {\n \t{ \"create\",        parse_cmd_create,        2, UPDATE_REFS_OPEN },\n \t{ \"delete\",        parse_cmd_delete,        2, UPDATE_REFS_OPEN },\n \t{ \"verify\",        parse_cmd_verify,        2, UPDATE_REFS_OPEN },\n+\t{ \"symref-delete\", parse_cmd_symref_delete, 2, UPDATE_REFS_OPEN },\n \t{ \"symref-verify\", parse_cmd_symref_verify, 2, UPDATE_REFS_OPEN },\n \t{ \"option\",        parse_cmd_option,        1, UPDATE_REFS_OPEN },\n \t{ \"start\",         parse_cmd_start,         0, UPDATE_REFS_STARTED },\ndiff --git a/refs.c b/refs.c\nindex ee4c6ed99c..c2c9889466 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -979,7 +979,7 @@ int refs_delete_ref(struct ref_store *refs, const char *msg,\n \ttransaction = ref_store_transaction_begin(refs, &err);\n \tif (!transaction ||\n \t    ref_transaction_delete(transaction, refname, old_oid,\n-\t\t\t\t   flags, msg, &err) ||\n+\t\t\t\t   flags, NULL, msg, &err) ||\n \t    ref_transaction_commit(transaction, &err)) {\n \t\terror(\"%s\", err.buf);\n \t\tref_transaction_free(transaction);\n@@ -1317,14 +1317,18 @@ int ref_transaction_create(struct ref_transaction *transaction,\n int ref_transaction_delete(struct ref_transaction *transaction,\n \t\t\t   const char *refname,\n \t\t\t   const struct object_id *old_oid,\n-\t\t\t   unsigned int flags, const char *msg,\n+\t\t\t   unsigned int flags,\n+\t\t\t   const char *old_target,\n+\t\t\t   const char *msg,\n \t\t\t   struct strbuf *err)\n {\n \tif (old_oid && is_null_oid(old_oid))\n \t\tBUG(\"delete called with old_oid set to zeros\");\n+\tif (old_target && !(flags & REF_NO_DEREF))\n+\t\tBUG(\"delete cannot operate on symrefs with deref mode\");\n \treturn ref_transaction_update(transaction, refname,\n \t\t\t\t      null_oid(), old_oid,\n-\t\t\t\t      NULL, NULL, flags,\n+\t\t\t\t      NULL, old_target, flags,\n \t\t\t\t      msg, err);\n }\n \n@@ -2765,7 +2769,7 @@ int refs_delete_refs(struct ref_store *refs, const char *logmsg,\n \n \tfor_each_string_list_item(item, refnames) {\n \t\tret = ref_transaction_delete(transaction, item->string,\n-\t\t\t\t\t     NULL, flags, msg, &err);\n+\t\t\t\t\t     NULL, flags, NULL, msg, &err);\n \t\tif (ret) {\n \t\t\twarning(_(\"could not delete reference %s: %s\"),\n \t\t\t\titem->string, err.buf);\ndiff --git a/refs.h b/refs.h\nindex 48cec1ba72..053c382f0b 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -767,7 +767,9 @@ int ref_transaction_create(struct ref_transaction *transaction,\n int ref_transaction_delete(struct ref_transaction *transaction,\n \t\t\t   const char *refname,\n \t\t\t   const struct object_id *old_oid,\n-\t\t\t   unsigned int flags, const char *msg,\n+\t\t\t   unsigned int flags,\n+\t\t\t   const char *old_target,\n+\t\t\t   const char *msg,\n \t\t\t   struct strbuf *err);\n \n /*\ndiff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\nindex 8784c59a53..fbc6b234cb 100755\n--- a/t/t1400-update-ref.sh\n+++ b/t/t1400-update-ref.sh\n@@ -1689,7 +1689,7 @@ do\n \t\ttest_cmp before after\n \t'\n \n-\ttest_expect_success \"stdin ${type} symref-verify no value is treated as zero value\" '\n+\ttest_expect_success \"stdin ${type} symref-verify fails with no value\" '\n \t\tgit symbolic-ref refs/heads/symref >expect &&\n \t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref\" \"\" &&\n \t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin\n@@ -1729,6 +1729,60 @@ do\n \t\ttest_cmp expect actual\n \t'\n \n+\ttest_expect_success \"stdin ${type} symref-delete fails without --no-deref\" '\n+\t\tgit symbolic-ref refs/heads/symref $a &&\n+\t\tcreate_stdin_buf ${type} \"symref-delete refs/heads/symref\" \"$a\" &&\n+\t\ttest_must_fail git update-ref --stdin ${type} <stdin 2>err &&\n+\t\tgrep \"fatal: symref-delete: cannot operate with deref mode\" err\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-delete fails with no ref\" '\n+\t\tcreate_stdin_buf ${type} \"symref-delete \" &&\n+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n+\t\tgrep \"fatal: symref-delete: missing <ref>\" err\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-delete fails with too many arguments\" '\n+\t\tcreate_stdin_buf ${type} \"symref-delete refs/heads/symref\" \"$a\" \"$a\" &&\n+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n+\t\tif test \"$type\" = \"-z\"\n+\t\tthen\n+\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\telse\n+\t\t\tgrep \"fatal: symref-delete refs/heads/symref: extra input:  $a\" err\n+\t\tfi\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-delete fails with wrong old value\" '\n+\t\tcreate_stdin_buf ${type} \"symref-delete refs/heads/symref\" \"$m\" &&\n+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n+\t\tgrep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected refs/heads/main\" err &&\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\techo $a >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-delete works with right old value\" '\n+\t\tcreate_stdin_buf ${type} \"symref-delete refs/heads/symref\" \"$a\" &&\n+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n+\t\ttest_must_fail git rev-parse --verify -q refs/heads/symref\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-delete works with empty old value\" '\n+\t\tgit symbolic-ref refs/heads/symref $a &&\n+\t\tcreate_stdin_buf ${type} \"symref-delete refs/heads/symref\" \"\" &&\n+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n+\t\ttest_must_fail git rev-parse --verify -q $b\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-delete succeeds for dangling reference\" '\n+\t\ttest_must_fail git symbolic-ref refs/heads/nonexistent &&\n+\t\tgit symbolic-ref refs/heads/symref2 refs/heads/nonexistent &&\n+\t\tcreate_stdin_buf ${type} \"symref-delete refs/heads/symref2\" \"refs/heads/nonexistent\" &&\n+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n+\t\ttest_must_fail git symbolic-ref -d refs/heads/symref2\n+\t'\n+\n done\n \n test_done\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex fd58b902f4..ccde1b944b 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -162,6 +162,7 @@ test_expect_success 'hook gets all queued symref updates' '\n \n \tgit update-ref refs/heads/branch $POST_OID &&\n \tgit symbolic-ref refs/heads/symref refs/heads/main &&\n+\tgit symbolic-ref refs/heads/symrefd refs/heads/main &&\n \n \ttest_hook reference-transaction <<-\\EOF &&\n \techo \"$*\" >>actual\n@@ -171,16 +172,32 @@ test_expect_success 'hook gets all queued symref updates' '\n \tdone >>actual\n \tEOF\n \n-\tcat >expect <<-EOF &&\n+\t# In the files backend, \"delete\" also triggers an additional transaction\n+\t# update on the packed-refs backend, which constitutes additional reflog\n+\t# entries.\n+\tif test_have_prereq REFFILES\n+\tthen\n+\t\tcat >expect <<-EOF\n+\t\taborted\n+\t\t$ZERO_OID $ZERO_OID refs/heads/symrefd\n+\t\tEOF\n+\telse\n+\t\t>expect\n+\tfi &&\n+\n+\tcat >>expect <<-EOF &&\n \tprepared\n \tref:refs/heads/main $ZERO_OID refs/heads/symref\n+\tref:refs/heads/main $ZERO_OID refs/heads/symrefd\n \tcommitted\n \tref:refs/heads/main $ZERO_OID refs/heads/symref\n+\tref:refs/heads/main $ZERO_OID refs/heads/symrefd\n \tEOF\n \n \tgit update-ref --no-deref --stdin <<-EOF &&\n \tstart\n \tsymref-verify refs/heads/symref refs/heads/main\n+\tsymref-delete refs/heads/symrefd refs/heads/main\n \tprepare\n \tcommit\n \tEOF\n-- \n2.43.GIT\n\n"},{"id":"494714","messageId":"20240514124411.1037019-5-knayak@gitlab.com","threadId":"61469","inReplyTo":"20240514124411.1037019-1-knayak@gitlab.com","subject":"[PATCH 4/6] update-ref: add support for 'symref-create' command","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-14T12:44:09Z","receivedAt":"2024-05-14T12:44:20Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"From: Karthik Nayak <karthik.188@gmail.com>\n\nAdd 'symref-create' command to the '--stdin' mode 'git-update-ref' to\nallow creation of symbolic refs in a transaction. The 'symref-create'\ncommand takes in a <new-target>, which the created <ref> will point to.\n\nAlso, support the 'core.prefersymlinkrefs' config, wherein if the config\nis set and the filesystem supports symlinks, we create the symbolic ref\nas a symlink. We fallback to creating a regular symref if creating the\nsymlink is unsuccessful.\n\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n Documentation/git-update-ref.txt |  6 +++\n builtin/clone.c                  |  2 +-\n builtin/update-ref.c             | 32 +++++++++++++++-\n refs.c                           |  7 ++--\n refs.h                           |  1 +\n t/t0600-reffiles-backend.sh      | 32 ++++++++++++++++\n t/t1400-update-ref.sh            | 65 ++++++++++++++++++++++++++++++++\n t/t1416-ref-transaction-hooks.sh |  3 ++\n 8 files changed, 143 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/git-update-ref.txt b/Documentation/git-update-ref.txt\nindex 16e02f6979..364ef78af1 100644\n--- a/Documentation/git-update-ref.txt\n+++ b/Documentation/git-update-ref.txt\n@@ -65,6 +65,7 @@ performs all modifications together.  Specify commands of the form:\n \tcreate SP <ref> SP <new-oid> LF\n \tdelete SP <ref> [SP <old-oid>] LF\n \tverify SP <ref> [SP <old-oid>] LF\n+\tsymref-create SP <ref> SP <new-target> LF\n \tsymref-delete SP <ref> [SP <old-target>] LF\n \tsymref-verify SP <ref> [SP <old-target>] LF\n \toption SP <opt> LF\n@@ -88,6 +89,7 @@ quoting:\n \tcreate SP <ref> NUL <new-oid> NUL\n \tdelete SP <ref> NUL [<old-oid>] NUL\n \tverify SP <ref> NUL [<old-oid>] NUL\n+\tsymref-create SP <ref> NUL <new-target> NUL\n \tsymref-delete SP <ref> [NUL <old-target>] NUL\n \tsymref-verify SP <ref> [NUL <old-target>] NUL\n \toption SP <opt> NUL\n@@ -121,6 +123,10 @@ verify::\n \tVerify <ref> against <old-oid> but do not change it.  If\n \t<old-oid> is zero or missing, the ref must not exist.\n \n+symref-create:\n+\tCreate symbolic ref <ref> with <new-target> after verifying\n+\tit does not exist.\n+\n symref-delete::\n \tDelete <ref> after verifying it exists with <old-target>, if given.\n \ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 93fdfc945a..ac1e131d8b 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -546,7 +546,7 @@ static void write_remote_refs(const struct ref *local_refs)\n \t\tif (!r->peer_ref)\n \t\t\tcontinue;\n \t\tif (ref_transaction_create(t, r->peer_ref->name, &r->old_oid,\n-\t\t\t\t\t   0, NULL, &err))\n+\t\t\t\t\t   NULL, 0, NULL, &err))\n \t\t\tdie(\"%s\", err.buf);\n \t}\n \ndiff --git a/builtin/update-ref.c b/builtin/update-ref.c\nindex 0ef9c38d8d..16d184603b 100644\n--- a/builtin/update-ref.c\n+++ b/builtin/update-ref.c\n@@ -257,7 +257,7 @@ static void parse_cmd_create(struct ref_transaction *transaction,\n \tif (*next != line_termination)\n \t\tdie(\"create %s: extra input: %s\", refname, next);\n \n-\tif (ref_transaction_create(transaction, refname, &new_oid,\n+\tif (ref_transaction_create(transaction, refname, &new_oid, NULL,\n \t\t\t\t   update_flags | create_reflog_flag,\n \t\t\t\t   msg, &err))\n \t\tdie(\"%s\", err.buf);\n@@ -267,6 +267,35 @@ static void parse_cmd_create(struct ref_transaction *transaction,\n \tstrbuf_release(&err);\n }\n \n+\n+static void parse_cmd_symref_create(struct ref_transaction *transaction,\n+\t\t\t\t    const char *next, const char *end)\n+{\n+\tstruct strbuf err = STRBUF_INIT;\n+\tchar *refname, *new_target;\n+\n+\trefname = parse_refname(&next);\n+\tif (!refname)\n+\t\tdie(\"symref-create: missing <ref>\");\n+\n+\tnew_target = parse_next_refname(&next);\n+\tif (!new_target)\n+\t\tdie(\"symref-create %s: missing <new-target>\", refname);\n+\n+\tif (*next != line_termination)\n+\t\tdie(\"symref-create %s: extra input: %s\", refname, next);\n+\n+\tif (ref_transaction_create(transaction, refname, NULL, new_target,\n+\t\t\t\t   update_flags | create_reflog_flag,\n+\t\t\t\t   msg, &err))\n+\t\tdie(\"%s\", err.buf);\n+\n+\tupdate_flags = default_flags;\n+\tfree(refname);\n+\tfree(new_target);\n+\tstrbuf_release(&err);\n+}\n+\n static void parse_cmd_delete(struct ref_transaction *transaction,\n \t\t\t     const char *next, const char *end)\n {\n@@ -473,6 +502,7 @@ static const struct parse_cmd {\n \t{ \"create\",        parse_cmd_create,        2, UPDATE_REFS_OPEN },\n \t{ \"delete\",        parse_cmd_delete,        2, UPDATE_REFS_OPEN },\n \t{ \"verify\",        parse_cmd_verify,        2, UPDATE_REFS_OPEN },\n+\t{ \"symref-create\", parse_cmd_symref_create, 2, UPDATE_REFS_OPEN },\n \t{ \"symref-delete\", parse_cmd_symref_delete, 2, UPDATE_REFS_OPEN },\n \t{ \"symref-verify\", parse_cmd_symref_verify, 2, UPDATE_REFS_OPEN },\n \t{ \"option\",        parse_cmd_option,        1, UPDATE_REFS_OPEN },\ndiff --git a/refs.c b/refs.c\nindex c2c9889466..6b724343fe 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1302,15 +1302,16 @@ int ref_transaction_update(struct ref_transaction *transaction,\n int ref_transaction_create(struct ref_transaction *transaction,\n \t\t\t   const char *refname,\n \t\t\t   const struct object_id *new_oid,\n+\t\t\t   const char *new_target,\n \t\t\t   unsigned int flags, const char *msg,\n \t\t\t   struct strbuf *err)\n {\n-\tif (!new_oid || is_null_oid(new_oid)) {\n-\t\tstrbuf_addf(err, \"'%s' has a null OID\", refname);\n+\tif ((!new_oid || is_null_oid(new_oid)) && !new_target) {\n+\t\tstrbuf_addf(err, \"'%s' has a null OID or no new target\", refname);\n \t\treturn 1;\n \t}\n \treturn ref_transaction_update(transaction, refname, new_oid,\n-\t\t\t\t      null_oid(), NULL, NULL, flags,\n+\t\t\t\t      null_oid(), new_target, NULL, flags,\n \t\t\t\t      msg, err);\n }\n \ndiff --git a/refs.h b/refs.h\nindex 053c382f0b..055cc9173b 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -753,6 +753,7 @@ int ref_transaction_update(struct ref_transaction *transaction,\n int ref_transaction_create(struct ref_transaction *transaction,\n \t\t\t   const char *refname,\n \t\t\t   const struct object_id *new_oid,\n+\t\t\t   const char *new_target,\n \t\t\t   unsigned int flags, const char *msg,\n \t\t\t   struct strbuf *err);\n \ndiff --git a/t/t0600-reffiles-backend.sh b/t/t0600-reffiles-backend.sh\nindex a390cffc80..cc7e20431e 100755\n--- a/t/t0600-reffiles-backend.sh\n+++ b/t/t0600-reffiles-backend.sh\n@@ -468,4 +468,36 @@ test_expect_success POSIXPERM 'git reflog expire honors core.sharedRepository' '\n \tesac\n '\n \n+test_expect_success SYMLINKS 'symref transaction supports symlinks' '\n+\ttest_when_finished \"git symbolic-ref -d TESTSYMREFONE\" &&\n+\tgit update-ref refs/heads/new @ &&\n+\ttest_config core.prefersymlinkrefs true &&\n+\tcat >stdin <<-EOF &&\n+\tstart\n+\tsymref-create TESTSYMREFONE refs/heads/new\n+\tprepare\n+\tcommit\n+\tEOF\n+\tgit update-ref --no-deref --stdin <stdin &&\n+\ttest_path_is_symlink .git/TESTSYMREFONE &&\n+\ttest \"$(test_readlink .git/TESTSYMREFONE)\" = refs/heads/new\n+'\n+\n+test_expect_success 'symref transaction supports false symlink config' '\n+\ttest_when_finished \"git symbolic-ref -d TESTSYMREFONE\" &&\n+\tgit update-ref refs/heads/new @ &&\n+\ttest_config core.prefersymlinkrefs false &&\n+\tcat >stdin <<-EOF &&\n+\tstart\n+\tsymref-create TESTSYMREFONE refs/heads/new\n+\tprepare\n+\tcommit\n+\tEOF\n+\tgit update-ref --no-deref --stdin <stdin &&\n+\ttest_path_is_file .git/TESTSYMREFONE &&\n+\tgit symbolic-ref TESTSYMREFONE >actual &&\n+\techo refs/heads/new >expect &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\ndiff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\nindex fbc6b234cb..7955988ecc 100755\n--- a/t/t1400-update-ref.sh\n+++ b/t/t1400-update-ref.sh\n@@ -1783,6 +1783,71 @@ do\n \t\ttest_must_fail git symbolic-ref -d refs/heads/symref2\n \t'\n \n+\ttest_expect_success \"stdin ${type} symref-create fails with too many arguments\" '\n+\t\tcreate_stdin_buf ${type} \"symref-create refs/heads/symref\" \"$a\" \"$a\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n+\t\tif test \"$type\" = \"-z\"\n+\t\tthen\n+\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\telse\n+\t\t\tgrep \"fatal: symref-create refs/heads/symref: extra input:  $a\" err\n+\t\tfi\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-create fails with no target\" '\n+\t\tcreate_stdin_buf ${type} \"symref-create refs/heads/symref\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-create fails with empty target\" '\n+\t\tcreate_stdin_buf ${type} \"symref-create refs/heads/symref\" \"\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-create works\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tcreate_stdin_buf ${type} \"symref-create refs/heads/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\techo $a >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-create works with --no-deref\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tcreate_stdin_buf ${type} \"symref-create refs/heads/symref\" \"$a\" &&\n+\t\tgit update-ref --stdin ${type} <stdin 2>err\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} create dangling symref ref works\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tcreate_stdin_buf ${type} \"symref-create refs/heads/symref\" \"refs/heads/unkown\" >stdin &&\n+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\techo refs/heads/unkown >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-create does not create reflogs by default\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/symref\" &&\n+\t\tcreate_stdin_buf ${type} \"symref-create refs/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n+\t\tgit symbolic-ref refs/symref >expect &&\n+\t\techo $a >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest_must_fail git reflog exists refs/symref\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-create reflogs with --create-reflog\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tcreate_stdin_buf ${type} \"symref-create refs/heads/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref --create-reflog --stdin ${type} --no-deref <stdin &&\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\techo $a >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\tgit reflog exists refs/heads/symref\n+\t'\n+\n done\n \n test_done\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex ccde1b944b..ff77dcca6b 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -189,15 +189,18 @@ test_expect_success 'hook gets all queued symref updates' '\n \tprepared\n \tref:refs/heads/main $ZERO_OID refs/heads/symref\n \tref:refs/heads/main $ZERO_OID refs/heads/symrefd\n+\t$ZERO_OID ref:refs/heads/main refs/heads/symrefc\n \tcommitted\n \tref:refs/heads/main $ZERO_OID refs/heads/symref\n \tref:refs/heads/main $ZERO_OID refs/heads/symrefd\n+\t$ZERO_OID ref:refs/heads/main refs/heads/symrefc\n \tEOF\n \n \tgit update-ref --no-deref --stdin <<-EOF &&\n \tstart\n \tsymref-verify refs/heads/symref refs/heads/main\n \tsymref-delete refs/heads/symrefd refs/heads/main\n+\tsymref-create refs/heads/symrefc refs/heads/main\n \tprepare\n \tcommit\n \tEOF\n-- \n2.43.GIT\n\n"},{"id":"494715","messageId":"20240514124411.1037019-6-knayak@gitlab.com","threadId":"61469","inReplyTo":"20240514124411.1037019-1-knayak@gitlab.com","subject":"[PATCH 5/6] reftable: pick either 'oid' or 'target' for new updates","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-14T12:44:10Z","receivedAt":"2024-05-14T12:44:21Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"From: Karthik Nayak <karthik.188@gmail.com>\n\nWhen creating a reference transaction update, we can provide the old/new\noid/target for the update. We have checks in place to ensure that for\neach old/new, either oid or target is set and not both.\n\nIn the reftable backend, when dealing with updates without the\n`REF_NO_DEREF` flag, we don't selectively propagate data as needed.\nSince there are no active users of the path, this is not caught. As we\nwant to introduce the 'symref-update' command in the upcoming commit,\nwhich would use this flow, correct it.\n\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n refs/reftable-backend.c | 5 +++--\n 1 file changed, 3 insertions(+), 2 deletions(-)\n\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 975061d103..c17e68100d 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -896,8 +896,9 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n \t\t\t\t */\n \t\t\t\tnew_update = ref_transaction_add_update(\n \t\t\t\t\ttransaction, referent.buf, new_flags,\n-\t\t\t\t\t&u->new_oid, &u->old_oid, u->new_target,\n-\t\t\t\t\tu->old_target, u->msg);\n+\t\t\t\t\tu->new_target ? NULL : &u->new_oid,\n+\t\t\t\t\tu->old_target ? NULL : &u->old_oid,\n+\t\t\t\t\tu->new_target, u->old_target, u->msg);\n \n \t\t\t\tnew_update->parent_update = u;\n \n-- \n2.43.GIT\n\n"},{"id":"494716","messageId":"20240514124411.1037019-7-knayak@gitlab.com","threadId":"61469","inReplyTo":"20240514124411.1037019-1-knayak@gitlab.com","subject":"[PATCH 6/6] update-ref: add support for 'symref-update' command","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-14T12:44:11Z","receivedAt":"2024-05-14T12:44:22Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"From: Karthik Nayak <karthik.188@gmail.com>\n\nAdd 'symref-update' command to the '--stdin' mode of 'git-update-ref' to\nallow updates of symbolic refs. The 'symref-update' command takes in a\n<new-target>, which the <ref> will be updated to. If the <ref> doesn't\nexist it will be created.\n\nIt also optionally takes either an `ref <old-target>` or `oid\n<old-oid>`. If the <old-target> is provided, it checks to see if the\n<ref> targets the <old-target> before the update. If <old-oid> is provided\nit checks <ref> to ensure that it is a regular ref and <old-oid> is the\nOID before the update. This by extension also means that this when a\nzero <old-oid> is provided, it ensures that the ref didn't exist before.\n\nThe command allows users to perform symbolic ref updates within a\ntransaction. This provides atomicity and allows users to perform a set\nof operations together.\n\nThis command will also support deref mode, to ensure that we can update\ndereferenced regular refs to symrefs.\n\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n Documentation/git-update-ref.txt |   7 ++\n builtin/update-ref.c             |  85 ++++++++++++++\n t/t1400-update-ref.sh            | 191 +++++++++++++++++++++++++++++++\n t/t1416-ref-transaction-hooks.sh |   4 +\n 4 files changed, 287 insertions(+)\n\ndiff --git a/Documentation/git-update-ref.txt b/Documentation/git-update-ref.txt\nindex 364ef78af1..afcf33cf60 100644\n--- a/Documentation/git-update-ref.txt\n+++ b/Documentation/git-update-ref.txt\n@@ -65,6 +65,7 @@ performs all modifications together.  Specify commands of the form:\n \tcreate SP <ref> SP <new-oid> LF\n \tdelete SP <ref> [SP <old-oid>] LF\n \tverify SP <ref> [SP <old-oid>] LF\n+\tsymref-update SP <ref> SP <new-target> [SP (ref SP <old-target> | oid SP <old-oid>)] LF\n \tsymref-create SP <ref> SP <new-target> LF\n \tsymref-delete SP <ref> [SP <old-target>] LF\n \tsymref-verify SP <ref> [SP <old-target>] LF\n@@ -89,6 +90,7 @@ quoting:\n \tcreate SP <ref> NUL <new-oid> NUL\n \tdelete SP <ref> NUL [<old-oid>] NUL\n \tverify SP <ref> NUL [<old-oid>] NUL\n+\tsymref-update SP <ref> NUL <new-target> [NUL (ref NUL <old-target> | oid NUL <old-oid>)] NUL\n \tsymref-create SP <ref> NUL <new-target> NUL\n \tsymref-delete SP <ref> [NUL <old-target>] NUL\n \tsymref-verify SP <ref> [NUL <old-target>] NUL\n@@ -119,6 +121,11 @@ delete::\n \tDelete <ref> after verifying it exists with <old-oid>, if\n \tgiven.  If given, <old-oid> may not be zero.\n \n+symref-update::\n+\tSet <ref> to <new-target> after verifying <old-target> or <old-oid>,\n+\tif given. Specify a zero <old-oid> to ensure that the ref does not\n+\texist before the update.\n+\n verify::\n \tVerify <ref> against <old-oid> but do not change it.  If\n \t<old-oid> is zero or missing, the ref must not exist.\ndiff --git a/builtin/update-ref.c b/builtin/update-ref.c\nindex 16d184603b..389136dc2f 100644\n--- a/builtin/update-ref.c\n+++ b/builtin/update-ref.c\n@@ -98,6 +98,41 @@ static char *parse_next_refname(const char **next)\n \treturn parse_refname(next);\n }\n \n+/*\n+ * Wrapper around parse_arg which skips the next delimiter.\n+ */\n+static char *parse_next_arg(const char **next)\n+{\n+\tstruct strbuf arg = STRBUF_INIT;\n+\n+\tif (line_termination) {\n+\t\t/* Without -z, consume SP and use next argument */\n+\t\tif (!**next || **next == line_termination)\n+\t\t\treturn NULL;\n+\t\tif (**next != ' ')\n+\t\t\tdie(\"expected SP but got: %s\", *next);\n+\t} else {\n+\t\t/* With -z, read the next NUL-terminated line */\n+\t\tif (**next)\n+\t\t\treturn NULL;\n+\t}\n+\t/* Skip the delimiter */\n+\t(*next)++;\n+\n+\tif (line_termination) {\n+\t\t/* Without -z, use the next argument */\n+\t\t*next = parse_arg(*next, &arg);\n+\t} else {\n+\t\t/* With -z, use everything up to the next NUL */\n+\t\tstrbuf_addstr(&arg, *next);\n+\t\t*next += arg.len;\n+\t}\n+\n+\tif (arg.len)\n+\t\treturn strbuf_detach(&arg, NULL);\n+\treturn NULL;\n+}\n+\n \n /*\n  * The value being parsed is <old-oid> (as opposed to <new-oid>; the\n@@ -237,6 +272,55 @@ static void parse_cmd_update(struct ref_transaction *transaction,\n \tstrbuf_release(&err);\n }\n \n+static void parse_cmd_symref_update(struct ref_transaction *transaction,\n+\t\t\t\t    const char *next, const char *end)\n+{\n+\tchar *refname, *new_target, *old_arg;\n+\tchar *old_target = NULL;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tstruct object_id old_oid;\n+\tint have_old = 0;\n+\n+\trefname = parse_refname(&next);\n+\tif (!refname)\n+\t\tdie(\"symref-update: missing <ref>\");\n+\n+\tnew_target = parse_next_refname(&next);\n+\tif (!new_target)\n+\t\tdie(\"symref-update %s: missing <new-target>\", refname);\n+\n+\told_arg = parse_next_arg(&next);\n+\tif (old_arg) {\n+\t\told_target = parse_next_refname(&next);\n+\t\tif (!old_target)\n+\t\t\tdie(\"symref-update %s: expected old value\", refname);\n+\n+\t\tif (!strcmp(old_arg, \"oid\") &&\n+\t\t    !repo_get_oid(the_repository, old_target, &old_oid)) {\n+\t\t\told_target = NULL;\n+\t\t\thave_old = 1;\n+\t\t} else if (strcmp(old_arg, \"ref\"))\n+\t\t\tdie(\"symref-update %s: invalid arg '%s' for old value\", refname, old_arg);\n+\t}\n+\n+\tif (*next != line_termination)\n+\t\tdie(\"symref-update %s: extra input: %s\", refname, next);\n+\n+\tif (ref_transaction_update(transaction, refname, NULL,\n+\t\t\t\t   have_old ? &old_oid : NULL,\n+\t\t\t\t   new_target, old_target,\n+\t\t\t\t   update_flags |= create_reflog_flag,\n+\t\t\t\t   msg, &err))\n+\t\tdie(\"%s\", err.buf);\n+\n+\tupdate_flags = default_flags;\n+\tfree(refname);\n+\tfree(old_arg);\n+\tfree(old_target);\n+\tfree(new_target);\n+\tstrbuf_release(&err);\n+}\n+\n static void parse_cmd_create(struct ref_transaction *transaction,\n \t\t\t     const char *next, const char *end)\n {\n@@ -502,6 +586,7 @@ static const struct parse_cmd {\n \t{ \"create\",        parse_cmd_create,        2, UPDATE_REFS_OPEN },\n \t{ \"delete\",        parse_cmd_delete,        2, UPDATE_REFS_OPEN },\n \t{ \"verify\",        parse_cmd_verify,        2, UPDATE_REFS_OPEN },\n+\t{ \"symref-update\", parse_cmd_symref_update, 4, UPDATE_REFS_OPEN },\n \t{ \"symref-create\", parse_cmd_symref_create, 2, UPDATE_REFS_OPEN },\n \t{ \"symref-delete\", parse_cmd_symref_delete, 2, UPDATE_REFS_OPEN },\n \t{ \"symref-verify\", parse_cmd_symref_verify, 2, UPDATE_REFS_OPEN },\ndiff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\nindex 7955988ecc..f7d00cc024 100755\n--- a/t/t1400-update-ref.sh\n+++ b/t/t1400-update-ref.sh\n@@ -1360,6 +1360,7 @@ test_expect_success 'fails with duplicate HEAD update' '\n '\n \n test_expect_success 'fails with duplicate ref update via symref' '\n+\ttest_when_finished \"git symbolic-ref -d refs/heads/symref2\" &&\n \tgit branch target2 $A &&\n \tgit symbolic-ref refs/heads/symref2 refs/heads/target2 &&\n \tcat >stdin <<-EOF &&\n@@ -1848,6 +1849,196 @@ do\n \t\tgit reflog exists refs/heads/symref\n \t'\n \n+\ttest_expect_success \"stdin ${type} symref-update fails with too many arguments\" '\n+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$a\" \"ref\" \"$a\" \"$a\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n+\t\tif test \"$type\" = \"-z\"\n+\t\tthen\n+\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\telse\n+\t\t\tgrep \"fatal: symref-update refs/heads/symref: extra input:  $a\" err\n+\t\tfi\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-update fails with wrong old value argument\" '\n+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$a\" \"foo\" \"$a\" \"$a\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n+\t\tgrep \"fatal: symref-update refs/heads/symref: invalid arg ${SQ}foo${SQ} for old value\" err\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-update creates with zero old value\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$a\" \"oid\" \"$Z\" >stdin &&\n+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-update creates with no old value\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-update creates dangling\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\ttest_must_fail git rev-parse refs/heads/nonexistent &&\n+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"refs/heads/nonexistent\" >stdin &&\n+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n+\t\techo refs/heads/nonexistent >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-update fails with wrong old value\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tgit symbolic-ref refs/heads/symref $a &&\n+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$m\" \"ref\" \"$b\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n+\t\tgrep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected $b\" err &&\n+\t\ttest_must_fail git rev-parse --verify -q $c\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-update updates dangling ref\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\ttest_must_fail git rev-parse refs/heads/nonexistent &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/nonexistent &&\n+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-update updates dangling ref with old value\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\ttest_must_fail git rev-parse refs/heads/nonexistent &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/nonexistent &&\n+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$a\" \"ref\" \"refs/heads/nonexistent\" >stdin &&\n+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-update fails update dangling ref with wrong old value\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\ttest_must_fail git rev-parse refs/heads/nonexistent &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/nonexistent &&\n+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$a\" \"ref\" \"refs/heads/wrongref\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin &&\n+\t\techo refs/heads/nonexistent >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-update works with right old value\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tgit symbolic-ref refs/heads/symref $a &&\n+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$m\" \"ref\" \"$a\" >stdin &&\n+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n+\t\techo $m >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-update works with no old value\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tgit symbolic-ref refs/heads/symref $a &&\n+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$m\" >stdin &&\n+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n+\t\techo $m >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-update fails with empty old ref-target\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tgit symbolic-ref refs/heads/symref $a &&\n+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$m\" \"ref\" \"\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-update creates (with deref)\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref --stdin ${type} <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref --no-recurse refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >actual &&\n+\t\tgrep \"$Z $(git rev-parse $a)\" actual\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-update regular ref to symref with correct old-oid\" '\n+\t\ttest_when_finished \"git symbolic-ref -d --no-recurse refs/heads/regularref\" &&\n+\t\tgit update-ref --no-deref refs/heads/regularref $a &&\n+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/regularref\" \"$a\" \"oid\" \"$(git rev-parse $a)\" >stdin &&\n+\t\tgit update-ref --stdin ${type} <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref --no-recurse refs/heads/regularref >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/regularref >actual &&\n+\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-update regular ref to symref fails with wrong old-oid\" '\n+\t\ttest_when_finished \"git update-ref -d refs/heads/regularref\" &&\n+\t\tgit update-ref --no-deref refs/heads/regularref $a &&\n+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/regularref\" \"$a\" \"oid\" \"$(git rev-parse refs/heads/target2)\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin ${type} <stdin 2>err &&\n+\t\techo $(git rev-parse $a) >expect &&\n+\t\tgit rev-parse refs/heads/regularref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-update existing symref with zero old-oid\" '\n+\t\ttest_when_finished \"git symbolic-ref -d --no-recurse refs/heads/symref\" &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/target2 &&\n+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$a\" \"oid\" \"$Z\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin ${type} <stdin 2>err &&\n+\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/symref${SQ}: reference already exists\" err &&\n+\t\techo refs/heads/target2 >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-update regular ref to symref (with deref)\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\ttest_when_finished \"git update-ref -d --no-deref refs/heads/symref2\" &&\n+\t\tgit update-ref refs/heads/symref2 $a &&\n+\t\tgit symbolic-ref --no-recurse refs/heads/symref refs/heads/symref2 &&\n+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref ${type} --stdin <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref --no-recurse refs/heads/symref2 >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\techo refs/heads/symref2 >expect &&\n+\t\tgit symbolic-ref --no-recurse refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >actual &&\n+\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n+\t'\n+\n+\ttest_expect_success \"stdin ${type} symref-update regular ref to symref\" '\n+\t\ttest_when_finished \"git symbolic-ref -d --no-recurse refs/heads/regularref\" &&\n+\t\tgit update-ref --no-deref refs/heads/regularref $a &&\n+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/regularref\" \"$a\" >stdin &&\n+\t\tgit update-ref ${type} --stdin <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref --no-recurse refs/heads/regularref >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/regularref >actual &&\n+\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n+\t'\n+\n done\n \n test_done\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex ff77dcca6b..5a812ca3c0 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -163,6 +163,7 @@ test_expect_success 'hook gets all queued symref updates' '\n \tgit update-ref refs/heads/branch $POST_OID &&\n \tgit symbolic-ref refs/heads/symref refs/heads/main &&\n \tgit symbolic-ref refs/heads/symrefd refs/heads/main &&\n+\tgit symbolic-ref refs/heads/symrefu refs/heads/main &&\n \n \ttest_hook reference-transaction <<-\\EOF &&\n \techo \"$*\" >>actual\n@@ -190,10 +191,12 @@ test_expect_success 'hook gets all queued symref updates' '\n \tref:refs/heads/main $ZERO_OID refs/heads/symref\n \tref:refs/heads/main $ZERO_OID refs/heads/symrefd\n \t$ZERO_OID ref:refs/heads/main refs/heads/symrefc\n+\tref:refs/heads/main ref:refs/heads/branch refs/heads/symrefu\n \tcommitted\n \tref:refs/heads/main $ZERO_OID refs/heads/symref\n \tref:refs/heads/main $ZERO_OID refs/heads/symrefd\n \t$ZERO_OID ref:refs/heads/main refs/heads/symrefc\n+\tref:refs/heads/main ref:refs/heads/branch refs/heads/symrefu\n \tEOF\n \n \tgit update-ref --no-deref --stdin <<-EOF &&\n@@ -201,6 +204,7 @@ test_expect_success 'hook gets all queued symref updates' '\n \tsymref-verify refs/heads/symref refs/heads/main\n \tsymref-delete refs/heads/symrefd refs/heads/main\n \tsymref-create refs/heads/symrefc refs/heads/main\n+\tsymref-update refs/heads/symrefu refs/heads/branch ref refs/heads/main\n \tprepare\n \tcommit\n \tEOF\n-- \n2.43.GIT\n\n"},{"id":"494877","messageId":"ZkXpW0BPvR3vr2jx@tanuki","threadId":"61469","inReplyTo":"20240514124411.1037019-2-knayak@gitlab.com","subject":"Re: [PATCH 1/6] refs: create and use `ref_update_ref_must_exist()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-16T11:09:15Z","receivedAt":"2024-05-16T11:09:21Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, May 14, 2024 at 02:44:06PM +0200, Karthik Nayak wrote:\n> From: Karthik Nayak <karthik.188@gmail.com>\n> \n> The files and reftable backend, need to check if a ref must exist, so\n> that the required validation can be done. A ref must exist only when the\n> `old_oid` value of the update has been explicitly set and it is not the\n> `null_oid` value.\n> \n> Since we also support symrefs now, we need to ensure that even when\n> `old_target` is set a ref must exist. While this was missed when we\n> added symref support in transactions, there are no active users of this\n> path. As we introduce the 'symref-verify' command in the upcoming\n> commits, it is important to fix this.\n> \n> So let's export this to a function called `ref_update_ref_must_exist()`\n> and expose it internally via 'refs-internal.h'.\n> \n> Signed-off-by: Karthik Nayak <karthik.188@gmail.com>\n> ---\n>  refs.c                  | 6 ++++++\n>  refs/files-backend.c    | 3 +--\n>  refs/refs-internal.h    | 6 ++++++\n>  refs/reftable-backend.c | 2 +-\n>  4 files changed, 14 insertions(+), 3 deletions(-)\n> \n> diff --git a/refs.c b/refs.c\n> index fa5471d219..59858fafdb 100644\n> --- a/refs.c\n> +++ b/refs.c\n> @@ -2863,3 +2863,9 @@ int ref_update_check_old_target(const char *referent, struct ref_update *update,\n>  \t\t\t    referent, update->old_target);\n>  \treturn -1;\n>  }\n> +\n> +int ref_update_ref_must_exist(struct ref_update *update)\n> +{\n> +\treturn (update->flags & REF_HAVE_OLD) &&\n> +\t\t(!is_null_oid(&update->old_oid) || update->old_target);\n> +}\n> diff --git a/refs/files-backend.c b/refs/files-backend.c\n> index 3957bfa579..2df204f891 100644\n> --- a/refs/files-backend.c\n> +++ b/refs/files-backend.c\n> @@ -2411,8 +2411,7 @@ static int lock_ref_for_update(struct files_ref_store *refs,\n>  \t\t\t       struct strbuf *err)\n>  {\n>  \tstruct strbuf referent = STRBUF_INIT;\n> -\tint mustexist = (update->flags & REF_HAVE_OLD) &&\n> -\t\t!is_null_oid(&update->old_oid);\n> +\tint mustexist = ref_update_ref_must_exist(update);\n\nOkay. So we didn't notice this was broken because even though we started\nwriting symrefs via transactions now, none of the calles ever assert\nthat the old ref exists?\n\n>  \tint ret = 0;\n>  \tstruct ref_lock *lock;\n>  \n> diff --git a/refs/refs-internal.h b/refs/refs-internal.h\n> index 53a6c5d842..5da3029e6c 100644\n> --- a/refs/refs-internal.h\n> +++ b/refs/refs-internal.h\n> @@ -765,4 +765,10 @@ int ref_update_has_null_new_value(struct ref_update *update);\n>  int ref_update_check_old_target(const char *referent, struct ref_update *update,\n>  \t\t\t\tstruct strbuf *err);\n>  \n> +/*\n> + * Check if the ref must exist, this means that the old_oid or\n> + * old_target is non NULL.\n> + */\n> +int ref_update_ref_must_exist(struct ref_update *update);\n\nSeeing `ref_update_ref_must_exist()` as a standalone function wouldn't\nquite tell me what it really does. It sounds a bit like this would\nalready assert the ref exists at the time of calling it.\n\nWe could call this `ref_upate_expects_existing_old_ref()`, which might\nclarify the intent a bit.\n\nPatrick\n"},{"id":"494878","messageId":"ZkXpYep2MKdsyNyV@tanuki","threadId":"61469","inReplyTo":"20240514124411.1037019-3-knayak@gitlab.com","subject":"Re: [PATCH 2/6] update-ref: add support for 'symref-verify' command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-16T11:09:21Z","receivedAt":"2024-05-16T11:09:25Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, May 14, 2024 at 02:44:07PM +0200, Karthik Nayak wrote:\n> From: Karthik Nayak <karthik.188@gmail.com>\n> \n> The 'symref-verify' command allows users to verify if a provided <ref>\n> contains the provided <old-target> without changing the <ref>. If\n> <old-target> is not provided, the command will verify that the <ref>\n> doesn't exist.\n> \n> The command allows users to verify symbolic refs within a transaction,\n> and this means users can perform a set of changes in a transaction only\n> when the verification holds good.\n> \n> Since we're checking for symbolic refs, this command will only work with\n> the 'no-deref' mode. This is because any dereferenced symbolic ref will\n> point to an object and not a ref and the regular 'verify' command can be\n> used in such situations.\n> \n> Add required tests for symref support in 'verify' while also adding\n> reflog checks for the pre-existing 'verify' tests.\n\nI'm a bit surprised that you add reflog-related tests, and you don't\nreally explain why you do it. Do we change any behaviour relating to\nreflogs here? If there is a particular reason that is independent of the\nnew \"symref-verify\" command, then I'd expect this to be part of a\nseparate commit.\n\n[snip]\n> diff --git a/refs.c b/refs.c\n> index 59858fafdb..ee4c6ed99c 100644\n> --- a/refs.c\n> +++ b/refs.c\n> @@ -1331,14 +1331,17 @@ int ref_transaction_delete(struct ref_transaction *transaction,\n>  int ref_transaction_verify(struct ref_transaction *transaction,\n>  \t\t\t   const char *refname,\n>  \t\t\t   const struct object_id *old_oid,\n> +\t\t\t   const char *old_target,\n>  \t\t\t   unsigned int flags,\n>  \t\t\t   struct strbuf *err)\n>  {\n> -\tif (!old_oid)\n> -\t\tBUG(\"verify called with old_oid set to NULL\");\n> +\tif (!old_target && !old_oid)\n> +\t\tBUG(\"verify called with old_oid and old_target set to NULL\");\n> +\tif (old_target && !(flags & REF_NO_DEREF))\n> +\t\tBUG(\"verify cannot operate on symrefs with deref mode\");\n\nShould we also BUG on `old_target && old_oid`?\n\n> @@ -1641,4 +1647,88 @@ test_expect_success PIPE 'transaction flushes status updates' '\n>  \ttest_cmp expected actual\n>  '\n>  \n> +create_stdin_buf () {\n> +\tif test \"$1\" = \"-z\"\n> +\tthen\n> +\t\tshift\n> +\t\tprintf \"$F\" \"$@\" >stdin\n> +\telse\n> +\t\techo \"$@\" >stdin\n> +\tfi\n> +}\n\nI think this would be easier to use if you didn't handle the redirect to\n\"stdin\" over here, but at the calling site. Otherwise, the caller needs\nto be aware of the inner workings.\n\n> +for type in \"\" \"-z\"\n> +do\n> +\n> +\ttest_expect_success \"stdin ${type} symref-verify fails without --no-deref\" '\n\nWe typically avoid curly braces unless required.\n\n[snip]\n> diff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\n> index 067fd57290..fd58b902f4 100755\n> --- a/t/t1416-ref-transaction-hooks.sh\n> +++ b/t/t1416-ref-transaction-hooks.sh\n> @@ -157,4 +157,34 @@ test_expect_success 'hook captures git-symbolic-ref updates' '\n>  \ttest_cmp expect actual\n>  '\n>  \n> +test_expect_success 'hook gets all queued symref updates' '\n> +\ttest_when_finished \"rm actual\" &&\n> +\n> +\tgit update-ref refs/heads/branch $POST_OID &&\n> +\tgit symbolic-ref refs/heads/symref refs/heads/main &&\n> +\n> +\ttest_hook reference-transaction <<-\\EOF &&\n> +\techo \"$*\" >>actual\n> +\twhile read -r line\n> +\tdo\n> +\t\tprintf \"%s\\n\" \"$line\"\n> +\tdone >>actual\n> +\tEOF\n> +\n> +\tcat >expect <<-EOF &&\n> +\tprepared\n> +\tref:refs/heads/main $ZERO_OID refs/heads/symref\n> +\tcommitted\n> +\tref:refs/heads/main $ZERO_OID refs/heads/symref\n> +\tEOF\n> +\n> +\tgit update-ref --no-deref --stdin <<-EOF &&\n> +\tstart\n> +\tsymref-verify refs/heads/symref refs/heads/main\n> +\tprepare\n> +\tcommit\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n>  test_done\n\nSo the reference-transaction hook executes even for \"symref-verify\"?\nThis feels quite unexpected to me. Do we do the same for \"verify\"?\n\nPatrick\n"},{"id":"494879","messageId":"ZkXpZss_RZbIvZp2@tanuki","threadId":"61469","inReplyTo":"20240514124411.1037019-4-knayak@gitlab.com","subject":"Re: [PATCH 3/6] update-ref: add support for 'symref-delete' command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-16T11:09:26Z","receivedAt":"2024-05-16T11:09:30Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, May 14, 2024 at 02:44:08PM +0200, Karthik Nayak wrote:\n> From: Karthik Nayak <karthik.188@gmail.com>\n[snip]\n> @@ -1317,14 +1317,18 @@ int ref_transaction_create(struct ref_transaction *transaction,\n>  int ref_transaction_delete(struct ref_transaction *transaction,\n>  \t\t\t   const char *refname,\n>  \t\t\t   const struct object_id *old_oid,\n> -\t\t\t   unsigned int flags, const char *msg,\n> +\t\t\t   unsigned int flags,\n> +\t\t\t   const char *old_target,\n> +\t\t\t   const char *msg,\n>  \t\t\t   struct strbuf *err)\n>  {\n>  \tif (old_oid && is_null_oid(old_oid))\n>  \t\tBUG(\"delete called with old_oid set to zeros\");\n> +\tif (old_target && !(flags & REF_NO_DEREF))\n> +\t\tBUG(\"delete cannot operate on symrefs with deref mode\");\n>  \treturn ref_transaction_update(transaction, refname,\n>  \t\t\t\t      null_oid(), old_oid,\n> -\t\t\t\t      NULL, NULL, flags,\n> +\t\t\t\t      NULL, old_target, flags,\n>  \t\t\t\t      msg, err);\n>  }\n\nSame comment here, should we BUG on `old_oid && old_target`?\n\n> diff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\n> index 8784c59a53..fbc6b234cb 100755\n> --- a/t/t1400-update-ref.sh\n> +++ b/t/t1400-update-ref.sh\n> @@ -1689,7 +1689,7 @@ do\n>  \t\ttest_cmp before after\n>  \t'\n>  \n> -\ttest_expect_success \"stdin ${type} symref-verify no value is treated as zero value\" '\n> +\ttest_expect_success \"stdin ${type} symref-verify fails with no value\" '\n>  \t\tgit symbolic-ref refs/heads/symref >expect &&\n>  \t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref\" \"\" &&\n>  \t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin\n\nThis change belongs into the preceding commit.\n\nPatrick\n"},{"id":"494880","messageId":"ZkXpbLPGgdqy6YJk@tanuki","threadId":"61469","inReplyTo":"20240514124411.1037019-5-knayak@gitlab.com","subject":"Re: [PATCH 4/6] update-ref: add support for 'symref-create' command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-16T11:09:32Z","receivedAt":"2024-05-16T11:09:36Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, May 14, 2024 at 02:44:09PM +0200, Karthik Nayak wrote:\n> From: Karthik Nayak <karthik.188@gmail.com>\n> diff --git a/refs.c b/refs.c\n> index c2c9889466..6b724343fe 100644\n> --- a/refs.c\n> +++ b/refs.c\n> @@ -1302,15 +1302,16 @@ int ref_transaction_update(struct ref_transaction *transaction,\n>  int ref_transaction_create(struct ref_transaction *transaction,\n>  \t\t\t   const char *refname,\n>  \t\t\t   const struct object_id *new_oid,\n> +\t\t\t   const char *new_target,\n>  \t\t\t   unsigned int flags, const char *msg,\n>  \t\t\t   struct strbuf *err)\n>  {\n> -\tif (!new_oid || is_null_oid(new_oid)) {\n> -\t\tstrbuf_addf(err, \"'%s' has a null OID\", refname);\n> +\tif ((!new_oid || is_null_oid(new_oid)) && !new_target) {\n> +\t\tstrbuf_addf(err, \"'%s' has a null OID or no new target\", refname);\n>  \t\treturn 1;\n>  \t}\n\nShouldn't this be \"'%s' has neither an OID nor a target\"?\n\nAlso, we again miss `new_oid && new_target`.\n\n> diff --git a/t/t0600-reffiles-backend.sh b/t/t0600-reffiles-backend.sh\n> index a390cffc80..cc7e20431e 100755\n> --- a/t/t0600-reffiles-backend.sh\n> +++ b/t/t0600-reffiles-backend.sh\n> @@ -468,4 +468,36 @@ test_expect_success POSIXPERM 'git reflog expire honors core.sharedRepository' '\n>  \tesac\n>  '\n>  \n> +test_expect_success SYMLINKS 'symref transaction supports symlinks' '\n> +\ttest_when_finished \"git symbolic-ref -d TESTSYMREFONE\" &&\n> +\tgit update-ref refs/heads/new @ &&\n> +\ttest_config core.prefersymlinkrefs true &&\n> +\tcat >stdin <<-EOF &&\n> +\tstart\n> +\tsymref-create TESTSYMREFONE refs/heads/new\n\nLet's future proof this and create the ref with a name that matches our\nroot ref restrictions, like \"TEST_SYMREF_HEAD\". We do plan to enforce\nthose soonish, so these tests would break.\n\nPatrick\n"},{"id":"494881","messageId":"ZkXpcvF6dxGr6qmj@tanuki","threadId":"61469","inReplyTo":"20240514124411.1037019-7-knayak@gitlab.com","subject":"Re: [PATCH 6/6] update-ref: add support for 'symref-update' command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-16T11:09:38Z","receivedAt":"2024-05-16T11:09:42Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, May 14, 2024 at 02:44:11PM +0200, Karthik Nayak wrote:\n> From: Karthik Nayak <karthik.188@gmail.com>\n> \n> Add 'symref-update' command to the '--stdin' mode of 'git-update-ref' to\n> allow updates of symbolic refs. The 'symref-update' command takes in a\n> <new-target>, which the <ref> will be updated to. If the <ref> doesn't\n> exist it will be created.\n> \n> It also optionally takes either an `ref <old-target>` or `oid\n> <old-oid>`. If the <old-target> is provided, it checks to see if the\n> <ref> targets the <old-target> before the update. If <old-oid> is provided\n> it checks <ref> to ensure that it is a regular ref and <old-oid> is the\n> OID before the update. This by extension also means that this when a\n> zero <old-oid> is provided, it ensures that the ref didn't exist before.\n\nIt's somewhat unfortunate that the syntax diverges from the \"update\"\ncommand. \"update\" also has essentially the same issue now, that we\ncannot verify that its old value is a symref, right? Can we fix that in\na backwards compatible way?\n\nIt would also be great to explain why exactly the syntax needs to\ndiverge.\n\n> The command allows users to perform symbolic ref updates within a\n> transaction. This provides atomicity and allows users to perform a set\n> of operations together.\n> \n> This command will also support deref mode, to ensure that we can update\n> dereferenced regular refs to symrefs.\n\nWill it support deref mode or does it support it with this patch?\n\n> diff --git a/builtin/update-ref.c b/builtin/update-ref.c\n> index 16d184603b..389136dc2f 100644\n> --- a/builtin/update-ref.c\n> +++ b/builtin/update-ref.c\n> @@ -98,6 +98,41 @@ static char *parse_next_refname(const char **next)\n>  \treturn parse_refname(next);\n>  }\n>  \n> +/*\n> + * Wrapper around parse_arg which skips the next delimiter.\n> + */\n> +static char *parse_next_arg(const char **next)\n> +{\n> +\tstruct strbuf arg = STRBUF_INIT;\n> +\n> +\tif (line_termination) {\n> +\t\t/* Without -z, consume SP and use next argument */\n> +\t\tif (!**next || **next == line_termination)\n> +\t\t\treturn NULL;\n> +\t\tif (**next != ' ')\n> +\t\t\tdie(\"expected SP but got: %s\", *next);\n> +\t} else {\n> +\t\t/* With -z, read the next NUL-terminated line */\n> +\t\tif (**next)\n> +\t\t\treturn NULL;\n> +\t}\n> +\t/* Skip the delimiter */\n> +\t(*next)++;\n> +\n> +\tif (line_termination) {\n> +\t\t/* Without -z, use the next argument */\n> +\t\t*next = parse_arg(*next, &arg);\n> +\t} else {\n> +\t\t/* With -z, use everything up to the next NUL */\n> +\t\tstrbuf_addstr(&arg, *next);\n> +\t\t*next += arg.len;\n> +\t}\n> +\n> +\tif (arg.len)\n> +\t\treturn strbuf_detach(&arg, NULL);\n> +\treturn NULL;\n> +}\n> +\n>  \n\nThere's an extra newline here.\n\n>  /*\n>   * The value being parsed is <old-oid> (as opposed to <new-oid>; the\n> @@ -237,6 +272,55 @@ static void parse_cmd_update(struct ref_transaction *transaction,\n>  \tstrbuf_release(&err);\n>  }\n>  \n> +static void parse_cmd_symref_update(struct ref_transaction *transaction,\n> +\t\t\t\t    const char *next, const char *end)\n> +{\n> +\tchar *refname, *new_target, *old_arg;\n> +\tchar *old_target = NULL;\n> +\tstruct strbuf err = STRBUF_INIT;\n> +\tstruct object_id old_oid;\n> +\tint have_old = 0;\n> +\n> +\trefname = parse_refname(&next);\n> +\tif (!refname)\n> +\t\tdie(\"symref-update: missing <ref>\");\n> +\n> +\tnew_target = parse_next_refname(&next);\n> +\tif (!new_target)\n> +\t\tdie(\"symref-update %s: missing <new-target>\", refname);\n> +\n> +\told_arg = parse_next_arg(&next);\n> +\tif (old_arg) {\n> +\t\told_target = parse_next_refname(&next);\n> +\t\tif (!old_target)\n> +\t\t\tdie(\"symref-update %s: expected old value\", refname);\n> +\n> +\t\tif (!strcmp(old_arg, \"oid\") &&\n> +\t\t    !repo_get_oid(the_repository, old_target, &old_oid)) {\n> +\t\t\told_target = NULL;\n> +\t\t\thave_old = 1;\n\nThis one feels weird to me. Shouldn't we return an error in case we are\nunable to parse the old OID?\n\n> +\t\t} else if (strcmp(old_arg, \"ref\"))\n> +\t\t\tdie(\"symref-update %s: invalid arg '%s' for old value\", refname, old_arg);\n\nWhen one of the branches has curly braces, then all branches should.\n\n> +\t}\n> +\n> +\tif (*next != line_termination)\n> +\t\tdie(\"symref-update %s: extra input: %s\", refname, next);\n> +\n> +\tif (ref_transaction_update(transaction, refname, NULL,\n> +\t\t\t\t   have_old ? &old_oid : NULL,\n> +\t\t\t\t   new_target, old_target,\n> +\t\t\t\t   update_flags |= create_reflog_flag,\n\nThis should be `update_flags | create_reflog_flag`, shouldn't it?\n\nPatrick\n"},{"id":"494966","messageId":"CAOLa=ZQz=adrazym_bPehDMGOLN1Lt-7QOcd_LgZb5K+trg=EA@mail.gmail.com","threadId":"61469","inReplyTo":"ZkXpW0BPvR3vr2jx@tanuki","subject":"Re: [PATCH 1/6] refs: create and use `ref_update_ref_must_exist()`","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-17T13:08:41Z","receivedAt":"2024-05-17T13:08:43Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Tue, May 14, 2024 at 02:44:06PM +0200, Karthik Nayak wrote:\n>> From: Karthik Nayak <karthik.188@gmail.com>\n>>\n>> The files and reftable backend, need to check if a ref must exist, so\n>> that the required validation can be done. A ref must exist only when the\n>> `old_oid` value of the update has been explicitly set and it is not the\n>> `null_oid` value.\n>>\n>> Since we also support symrefs now, we need to ensure that even when\n>> `old_target` is set a ref must exist. While this was missed when we\n>> added symref support in transactions, there are no active users of this\n>> path. As we introduce the 'symref-verify' command in the upcoming\n>> commits, it is important to fix this.\n>>\n>> So let's export this to a function called `ref_update_ref_must_exist()`\n>> and expose it internally via 'refs-internal.h'.\n>>\n>> Signed-off-by: Karthik Nayak <karthik.188@gmail.com>\n>> ---\n>>  refs.c                  | 6 ++++++\n>>  refs/files-backend.c    | 3 +--\n>>  refs/refs-internal.h    | 6 ++++++\n>>  refs/reftable-backend.c | 2 +-\n>>  4 files changed, 14 insertions(+), 3 deletions(-)\n>>\n>> diff --git a/refs.c b/refs.c\n>> index fa5471d219..59858fafdb 100644\n>> --- a/refs.c\n>> +++ b/refs.c\n>> @@ -2863,3 +2863,9 @@ int ref_update_check_old_target(const char *referent, struct ref_update *update,\n>>  \t\t\t    referent, update->old_target);\n>>  \treturn -1;\n>>  }\n>> +\n>> +int ref_update_ref_must_exist(struct ref_update *update)\n>> +{\n>> +\treturn (update->flags & REF_HAVE_OLD) &&\n>> +\t\t(!is_null_oid(&update->old_oid) || update->old_target);\n>> +}\n>> diff --git a/refs/files-backend.c b/refs/files-backend.c\n>> index 3957bfa579..2df204f891 100644\n>> --- a/refs/files-backend.c\n>> +++ b/refs/files-backend.c\n>> @@ -2411,8 +2411,7 @@ static int lock_ref_for_update(struct files_ref_store *refs,\n>>  \t\t\t       struct strbuf *err)\n>>  {\n>>  \tstruct strbuf referent = STRBUF_INIT;\n>> -\tint mustexist = (update->flags & REF_HAVE_OLD) &&\n>> -\t\t!is_null_oid(&update->old_oid);\n>> +\tint mustexist = ref_update_ref_must_exist(update);\n>\n> Okay. So we didn't notice this was broken because even though we started\n> writing symrefs via transactions now, none of the calles ever assert\n> that the old ref exists?\n>\n\nYup, that's correct. The `git-symbolic-ref(1)` command doesn't ever\ncheck for the old value and it is the only user of transactional symrefs\nat this point.\n\n>>  \tint ret = 0;\n>>  \tstruct ref_lock *lock;\n>>\n>> diff --git a/refs/refs-internal.h b/refs/refs-internal.h\n>> index 53a6c5d842..5da3029e6c 100644\n>> --- a/refs/refs-internal.h\n>> +++ b/refs/refs-internal.h\n>> @@ -765,4 +765,10 @@ int ref_update_has_null_new_value(struct ref_update *update);\n>>  int ref_update_check_old_target(const char *referent, struct ref_update *update,\n>>  \t\t\t\tstruct strbuf *err);\n>>\n>> +/*\n>> + * Check if the ref must exist, this means that the old_oid or\n>> + * old_target is non NULL.\n>> + */\n>> +int ref_update_ref_must_exist(struct ref_update *update);\n>\n> Seeing `ref_update_ref_must_exist()` as a standalone function wouldn't\n> quite tell me what it really does. It sounds a bit like this would\n> already assert the ref exists at the time of calling it.\n>\n> We could call this `ref_upate_expects_existing_old_ref()`, which might\n> clarify the intent a bit.\n>\n> Patrick\n\nYeah, that's better, will change. Thanks.\n"},{"id":"494980","messageId":"CAOLa=ZSUV1X==x2CBivgu=L7SQryXNZZkLwxgyNth=a+bH9SQg@mail.gmail.com","threadId":"61469","inReplyTo":"ZkXpYep2MKdsyNyV@tanuki","subject":"Re: [PATCH 2/6] update-ref: add support for 'symref-verify' command","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-17T16:21:53Z","receivedAt":"2024-05-17T16:21:55Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Tue, May 14, 2024 at 02:44:07PM +0200, Karthik Nayak wrote:\n>> From: Karthik Nayak <karthik.188@gmail.com>\n>>\n>> The 'symref-verify' command allows users to verify if a provided <ref>\n>> contains the provided <old-target> without changing the <ref>. If\n>> <old-target> is not provided, the command will verify that the <ref>\n>> doesn't exist.\n>>\n>> The command allows users to verify symbolic refs within a transaction,\n>> and this means users can perform a set of changes in a transaction only\n>> when the verification holds good.\n>>\n>> Since we're checking for symbolic refs, this command will only work with\n>> the 'no-deref' mode. This is because any dereferenced symbolic ref will\n>> point to an object and not a ref and the regular 'verify' command can be\n>> used in such situations.\n>>\n>> Add required tests for symref support in 'verify' while also adding\n>> reflog checks for the pre-existing 'verify' tests.\n>\n> I'm a bit surprised that you add reflog-related tests, and you don't\n> really explain why you do it. Do we change any behaviour relating to\n> reflogs here? If there is a particular reason that is independent of the\n> new \"symref-verify\" command, then I'd expect this to be part of a\n> separate commit.\n>\n\nAh! There is no divergence in behavior, rather this is behavior which is\nnever captured in tests. So I thought it makes to have tests around it.\n\n> [snip]\n>> diff --git a/refs.c b/refs.c\n>> index 59858fafdb..ee4c6ed99c 100644\n>> --- a/refs.c\n>> +++ b/refs.c\n>> @@ -1331,14 +1331,17 @@ int ref_transaction_delete(struct ref_transaction *transaction,\n>>  int ref_transaction_verify(struct ref_transaction *transaction,\n>>  \t\t\t   const char *refname,\n>>  \t\t\t   const struct object_id *old_oid,\n>> +\t\t\t   const char *old_target,\n>>  \t\t\t   unsigned int flags,\n>>  \t\t\t   struct strbuf *err)\n>>  {\n>> -\tif (!old_oid)\n>> -\t\tBUG(\"verify called with old_oid set to NULL\");\n>> +\tif (!old_target && !old_oid)\n>> +\t\tBUG(\"verify called with old_oid and old_target set to NULL\");\n>> +\tif (old_target && !(flags & REF_NO_DEREF))\n>> +\t\tBUG(\"verify cannot operate on symrefs with deref mode\");\n>\n> Should we also BUG on `old_target && old_oid`?\n>\n\nI didn't do this, because `ref_transaction_add_update` downstream from\nthis already does that. But I guess no harm in adding it here too.\n\n>> @@ -1641,4 +1647,88 @@ test_expect_success PIPE 'transaction flushes status updates' '\n>>  \ttest_cmp expected actual\n>>  '\n>>\n>> +create_stdin_buf () {\n>> +\tif test \"$1\" = \"-z\"\n>> +\tthen\n>> +\t\tshift\n>> +\t\tprintf \"$F\" \"$@\" >stdin\n>> +\telse\n>> +\t\techo \"$@\" >stdin\n>> +\tfi\n>> +}\n>\n> I think this would be easier to use if you didn't handle the redirect to\n> \"stdin\" over here, but at the calling site. Otherwise, the caller needs\n> to be aware of the inner workings.\n>\n\nNot sure what you mean by easier here, but I think it would be nicer to\nread, since the client would now determine the destination of the\nformatting and this would align with what the test needs to do. Will\nchange!\n\n>> +for type in \"\" \"-z\"\n>> +do\n>> +\n>> +\ttest_expect_success \"stdin ${type} symref-verify fails without --no-deref\" '\n>\n> We typically avoid curly braces unless required.\n>\n\nWill change, thanks!\n\n> [snip]\n>> diff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\n>> index 067fd57290..fd58b902f4 100755\n>> --- a/t/t1416-ref-transaction-hooks.sh\n>> +++ b/t/t1416-ref-transaction-hooks.sh\n>> @@ -157,4 +157,34 @@ test_expect_success 'hook captures git-symbolic-ref updates' '\n>>  \ttest_cmp expect actual\n>>  '\n>>\n>> +test_expect_success 'hook gets all queued symref updates' '\n>> +\ttest_when_finished \"rm actual\" &&\n>> +\n>> +\tgit update-ref refs/heads/branch $POST_OID &&\n>> +\tgit symbolic-ref refs/heads/symref refs/heads/main &&\n>> +\n>> +\ttest_hook reference-transaction <<-\\EOF &&\n>> +\techo \"$*\" >>actual\n>> +\twhile read -r line\n>> +\tdo\n>> +\t\tprintf \"%s\\n\" \"$line\"\n>> +\tdone >>actual\n>> +\tEOF\n>> +\n>> +\tcat >expect <<-EOF &&\n>> +\tprepared\n>> +\tref:refs/heads/main $ZERO_OID refs/heads/symref\n>> +\tcommitted\n>> +\tref:refs/heads/main $ZERO_OID refs/heads/symref\n>> +\tEOF\n>> +\n>> +\tgit update-ref --no-deref --stdin <<-EOF &&\n>> +\tstart\n>> +\tsymref-verify refs/heads/symref refs/heads/main\n>> +\tprepare\n>> +\tcommit\n>> +\tEOF\n>> +\ttest_cmp expect actual\n>> +'\n>> +\n>>  test_done\n>\n> So the reference-transaction hook executes even for \"symref-verify\"?\n> This feels quite unexpected to me. Do we do the same for \"verify\"?\n>\n> Patrick\n\nYes this is the same for verify as well. I was surprised to find this\ntoo. It's just the way ref update code is written, all updates land\ntrigger the hook. This means verify, which is also a form of update,\nwith just the new value not set, also triggers the hook. I've kept the\nsame behavior with symref-verify.\n"},{"id":"495061","messageId":"CAOLa=ZQzxax0GoCKMTNxdwy+0f+nU3HFC_ixp24RXgW0+jeXWQ@mail.gmail.com","threadId":"61469","inReplyTo":"ZkXpbLPGgdqy6YJk@tanuki","subject":"Re: [PATCH 4/6] update-ref: add support for 'symref-create' command","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-19T14:01:09Z","receivedAt":"2024-05-19T14:01:11Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Tue, May 14, 2024 at 02:44:09PM +0200, Karthik Nayak wrote:\n>> From: Karthik Nayak <karthik.188@gmail.com>\n>> diff --git a/refs.c b/refs.c\n>> index c2c9889466..6b724343fe 100644\n>> --- a/refs.c\n>> +++ b/refs.c\n>> @@ -1302,15 +1302,16 @@ int ref_transaction_update(struct ref_transaction *transaction,\n>>  int ref_transaction_create(struct ref_transaction *transaction,\n>>  \t\t\t   const char *refname,\n>>  \t\t\t   const struct object_id *new_oid,\n>> +\t\t\t   const char *new_target,\n>>  \t\t\t   unsigned int flags, const char *msg,\n>>  \t\t\t   struct strbuf *err)\n>>  {\n>> -\tif (!new_oid || is_null_oid(new_oid)) {\n>> -\t\tstrbuf_addf(err, \"'%s' has a null OID\", refname);\n>> +\tif ((!new_oid || is_null_oid(new_oid)) && !new_target) {\n>> +\t\tstrbuf_addf(err, \"'%s' has a null OID or no new target\", refname);\n>>  \t\treturn 1;\n>>  \t}\n>\n> Shouldn't this be \"'%s' has neither an OID nor a target\"?\n>\n\nWell it's actually all three. If new_oid is not set, or if new_oid is\nNULL or there is no new_target.\n\nI think \"'%s' has neither a valid OID nor a target\" would be best.\n\n> Also, we again miss `new_oid && new_target`.\n>\n\nWill fix for all the commands!\n\n>> diff --git a/t/t0600-reffiles-backend.sh b/t/t0600-reffiles-backend.sh\n>> index a390cffc80..cc7e20431e 100755\n>> --- a/t/t0600-reffiles-backend.sh\n>> +++ b/t/t0600-reffiles-backend.sh\n>> @@ -468,4 +468,36 @@ test_expect_success POSIXPERM 'git reflog expire honors core.sharedRepository' '\n>>  \tesac\n>>  '\n>>\n>> +test_expect_success SYMLINKS 'symref transaction supports symlinks' '\n>> +\ttest_when_finished \"git symbolic-ref -d TESTSYMREFONE\" &&\n>> +\tgit update-ref refs/heads/new @ &&\n>> +\ttest_config core.prefersymlinkrefs true &&\n>> +\tcat >stdin <<-EOF &&\n>> +\tstart\n>> +\tsymref-create TESTSYMREFONE refs/heads/new\n>\n> Let's future proof this and create the ref with a name that matches our\n> root ref restrictions, like \"TEST_SYMREF_HEAD\". We do plan to enforce\n> those soonish, so these tests would break.\n>\n> Patrick\n\nMakes sense, will fix. Thanks.\n"},{"id":"495146","messageId":"ZkxCLeLlrzHYC5aW@tanuki","threadId":"61469","inReplyTo":"CAOLa=ZSUV1X==x2CBivgu=L7SQryXNZZkLwxgyNth=a+bH9SQg@mail.gmail.com","subject":"Re: [PATCH 2/6] update-ref: add support for 'symref-verify' command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-21T06:41:49Z","receivedAt":"2024-05-21T06:41:55Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, May 17, 2024 at 06:21:53PM +0200, Karthik Nayak wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> > On Tue, May 14, 2024 at 02:44:07PM +0200, Karthik Nayak wrote:\n> > So the reference-transaction hook executes even for \"symref-verify\"?\n> > This feels quite unexpected to me. Do we do the same for \"verify\"?\n> \n> Yes this is the same for verify as well. I was surprised to find this\n> too. It's just the way ref update code is written, all updates land\n> trigger the hook. This means verify, which is also a form of update,\n> with just the new value not set, also triggers the hook. I've kept the\n> same behavior with symref-verify.\n\nWeird. I wonder whether we should declare this a bug. In any case, that\ncan be handled in a separate patch series.\n\nPatrick\n"},{"id":"495155","messageId":"CAOLa=ZQW-cCV5BP_fCvuZimfkjwAzjEiqXYRPft1Wf9kAX=_bw@mail.gmail.com","threadId":"61469","inReplyTo":"ZkXpcvF6dxGr6qmj@tanuki","subject":"Re: [PATCH 6/6] update-ref: add support for 'symref-update' command","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-21T09:49:48Z","receivedAt":"2024-05-21T09:49:50Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Tue, May 14, 2024 at 02:44:11PM +0200, Karthik Nayak wrote:\n>> From: Karthik Nayak <karthik.188@gmail.com>\n>>\n>> Add 'symref-update' command to the '--stdin' mode of 'git-update-ref' to\n>> allow updates of symbolic refs. The 'symref-update' command takes in a\n>> <new-target>, which the <ref> will be updated to. If the <ref> doesn't\n>> exist it will be created.\n>>\n>> It also optionally takes either an `ref <old-target>` or `oid\n>> <old-oid>`. If the <old-target> is provided, it checks to see if the\n>> <ref> targets the <old-target> before the update. If <old-oid> is provided\n>> it checks <ref> to ensure that it is a regular ref and <old-oid> is the\n>> OID before the update. This by extension also means that this when a\n>> zero <old-oid> is provided, it ensures that the ref didn't exist before.\n>\n> It's somewhat unfortunate that the syntax diverges from the \"update\"\n> command. \"update\" also has essentially the same issue now, that we\n> cannot verify that its old value is a symref, right? Can we fix that in\n> a backwards compatible way?\n>\n\nI think Peff mentioned [1] of a way. So we convert the existing\n\n    update SP <ref> SP <newvalue> [SP <oldvalue>] LF\n    update SP <ref> NUL <newvalue> NUL [<oldvalue>] NUL // -z\n\nto\n\n    update SP <ref> SP <newvalue> [SP (<oldvalue> | ref <old_target>)] LF\n    update SP <ref> NUL <newvalue> NUL [(<oldvalue> | ref NUL\n<old_target>)] NUL // -z\n\nthis should work, I think. I will play around this and add it in. Please\nlet me know if you can think of a scenario where this breaks.\n\n> It would also be great to explain why exactly the syntax needs to\n> diverge.\n>\n\nYeah makes sense, will add it to the commit message.\n\n>> The command allows users to perform symbolic ref updates within a\n>> transaction. This provides atomicity and allows users to perform a set\n>> of operations together.\n>>\n>> This command will also support deref mode, to ensure that we can update\n>> dereferenced regular refs to symrefs.\n>\n> Will it support deref mode or does it support it with this patch?\n>\n\nWill rephrase, in this patch itself though.\n\n>> diff --git a/builtin/update-ref.c b/builtin/update-ref.c\n>> index 16d184603b..389136dc2f 100644\n>> --- a/builtin/update-ref.c\n>> +++ b/builtin/update-ref.c\n>> @@ -98,6 +98,41 @@ static char *parse_next_refname(const char **next)\n>>  \treturn parse_refname(next);\n>>  }\n>>\n>> +/*\n>> + * Wrapper around parse_arg which skips the next delimiter.\n>> + */\n>> +static char *parse_next_arg(const char **next)\n>> +{\n>> +\tstruct strbuf arg = STRBUF_INIT;\n>> +\n>> +\tif (line_termination) {\n>> +\t\t/* Without -z, consume SP and use next argument */\n>> +\t\tif (!**next || **next == line_termination)\n>> +\t\t\treturn NULL;\n>> +\t\tif (**next != ' ')\n>> +\t\t\tdie(\"expected SP but got: %s\", *next);\n>> +\t} else {\n>> +\t\t/* With -z, read the next NUL-terminated line */\n>> +\t\tif (**next)\n>> +\t\t\treturn NULL;\n>> +\t}\n>> +\t/* Skip the delimiter */\n>> +\t(*next)++;\n>> +\n>> +\tif (line_termination) {\n>> +\t\t/* Without -z, use the next argument */\n>> +\t\t*next = parse_arg(*next, &arg);\n>> +\t} else {\n>> +\t\t/* With -z, use everything up to the next NUL */\n>> +\t\tstrbuf_addstr(&arg, *next);\n>> +\t\t*next += arg.len;\n>> +\t}\n>> +\n>> +\tif (arg.len)\n>> +\t\treturn strbuf_detach(&arg, NULL);\n>> +\treturn NULL;\n>> +}\n>> +\n>>\n>\n> There's an extra newline here.\n>\n\nWill remove.\n\n>>  /*\n>>   * The value being parsed is <old-oid> (as opposed to <new-oid>; the\n>> @@ -237,6 +272,55 @@ static void parse_cmd_update(struct ref_transaction *transaction,\n>>  \tstrbuf_release(&err);\n>>  }\n>>\n>> +static void parse_cmd_symref_update(struct ref_transaction *transaction,\n>> +\t\t\t\t    const char *next, const char *end)\n>> +{\n>> +\tchar *refname, *new_target, *old_arg;\n>> +\tchar *old_target = NULL;\n>> +\tstruct strbuf err = STRBUF_INIT;\n>> +\tstruct object_id old_oid;\n>> +\tint have_old = 0;\n>> +\n>> +\trefname = parse_refname(&next);\n>> +\tif (!refname)\n>> +\t\tdie(\"symref-update: missing <ref>\");\n>> +\n>> +\tnew_target = parse_next_refname(&next);\n>> +\tif (!new_target)\n>> +\t\tdie(\"symref-update %s: missing <new-target>\", refname);\n>> +\n>> +\told_arg = parse_next_arg(&next);\n>> +\tif (old_arg) {\n>> +\t\told_target = parse_next_refname(&next);\n>> +\t\tif (!old_target)\n>> +\t\t\tdie(\"symref-update %s: expected old value\", refname);\n>> +\n>> +\t\tif (!strcmp(old_arg, \"oid\") &&\n>> +\t\t    !repo_get_oid(the_repository, old_target, &old_oid)) {\n>> +\t\t\told_target = NULL;\n>> +\t\t\thave_old = 1;\n>\n> This one feels weird to me. Shouldn't we return an error in case we are\n> unable to parse the old OID?\n>\n\nYup, that was missed, will add a check. I also realized that at the top\nwe do `old_target = parse_next_refname()`, while it makes more sense to\ndo `old_target = parse_next_arg()`.\n\n>> +\t\t} else if (strcmp(old_arg, \"ref\"))\n>> +\t\t\tdie(\"symref-update %s: invalid arg '%s' for old value\", refname, old_arg);\n>\n> When one of the branches has curly braces, then all branches should.\n>\n\nWill change.\n\n>> +\t}\n>> +\n>> +\tif (*next != line_termination)\n>> +\t\tdie(\"symref-update %s: extra input: %s\", refname, next);\n>> +\n>> +\tif (ref_transaction_update(transaction, refname, NULL,\n>> +\t\t\t\t   have_old ? &old_oid : NULL,\n>> +\t\t\t\t   new_target, old_target,\n>> +\t\t\t\t   update_flags |= create_reflog_flag,\n>\n> This should be `update_flags | create_reflog_flag`, shouldn't it?\n>\n\nDefinitely.\n\n> Patrick\n\nThanks for the review.\n\n[1]: https://lore.kernel.org/git/20240426204145.GC13703@coredump.intra.peff.net/\n"},{"id":"495276","messageId":"CAOLa=ZRNNBOzsw+EzEJVnAU0ZsJ-bcTpciExdsQz2iDatYkqNQ@mail.gmail.com","threadId":"61469","inReplyTo":"CAOLa=ZQW-cCV5BP_fCvuZimfkjwAzjEiqXYRPft1Wf9kAX=_bw@mail.gmail.com","subject":"Re: [PATCH 6/6] update-ref: add support for 'symref-update' command","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-22T07:59:30Z","receivedAt":"2024-05-22T07:59:32Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Karthik Nayak <karthik.188@gmail.com> writes:\n\n> Patrick Steinhardt <ps@pks.im> writes:\n>\n>> On Tue, May 14, 2024 at 02:44:11PM +0200, Karthik Nayak wrote:\n>>> From: Karthik Nayak <karthik.188@gmail.com>\n>>>\n>>> Add 'symref-update' command to the '--stdin' mode of 'git-update-ref' to\n>>> allow updates of symbolic refs. The 'symref-update' command takes in a\n>>> <new-target>, which the <ref> will be updated to. If the <ref> doesn't\n>>> exist it will be created.\n>>>\n>>> It also optionally takes either an `ref <old-target>` or `oid\n>>> <old-oid>`. If the <old-target> is provided, it checks to see if the\n>>> <ref> targets the <old-target> before the update. If <old-oid> is provided\n>>> it checks <ref> to ensure that it is a regular ref and <old-oid> is the\n>>> OID before the update. This by extension also means that this when a\n>>> zero <old-oid> is provided, it ensures that the ref didn't exist before.\n>>\n>> It's somewhat unfortunate that the syntax diverges from the \"update\"\n>> command. \"update\" also has essentially the same issue now, that we\n>> cannot verify that its old value is a symref, right? Can we fix that in\n>> a backwards compatible way?\n>>\n>\n> I think Peff mentioned [1] of a way. So we convert the existing\n>\n>     update SP <ref> SP <newvalue> [SP <oldvalue>] LF\n>     update SP <ref> NUL <newvalue> NUL [<oldvalue>] NUL // -z\n>\n> to\n>\n>     update SP <ref> SP <newvalue> [SP (<oldvalue> | ref <old_target>)] LF\n>     update SP <ref> NUL <newvalue> NUL [(<oldvalue> | ref NUL <old_target>)] NUL // -z\n>\n> this should work, I think. I will play around this and add it in. Please\n> let me know if you can think of a scenario where this breaks.\n\nThinking about this more, I'll actually do this in a follow up series. I\nwant to cleanup the existing tests already to move them to the loop that\nI introduced in this series. That would make it easier to test and\nensure that we can port the existing 'update' command to also support\n`old_target`.\n\n[snip]\n"},{"id":"495286","messageId":"20240522090326.1268326-1-knayak@gitlab.com","threadId":"61469","inReplyTo":"20240514124411.1037019-1-knayak@gitlab.com","subject":"[PATCH v2 0/6] update-ref: add symref support for --stdin","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-22T09:03:20Z","receivedAt":"2024-05-22T09:03:31Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"From: Karthik Nayak <karthik.188@gmail.com>\n\nThe 'update-ref' command is used to update refs using transactions. The\ncommand allows users to also utilize a '--stdin' mode to provide a\nbatch of sub-commands which can be processed in a transaction.\n\nCurrently, the sub-commands involve {verify, delete, create, update}\nand they allow users to work with regular refs in the repository. To\nwork with symrefs, users only have the option of using\n'git-symbolic-ref', which doesn't provide transaction support to the\nusers eventhough it uses the same behind the hood. \n\nRecently, we modified the reference backend to add symref support,\nfollowing which, 'git-symbolic-ref' also uses the transaction backend.\nBut, it doesn't expose this to the user. To allow users to work with\nsymrefs via transaction, this series adds support for new sub-commands\n{symrer-verify, symref-delete, symref-create, symref-update} to the\n'--stdin' mode of update-ref. These complement the existing\nsub-commands.\n\nThe patches 1, 5 fix small issues in the reference backends. The other\npatches 2, 3, 4 & 6, each add one of the new sub-commands.\n\nThe series is based off master, with 'kn/ref-transaction-symref' merged\nin. There seem to be no conflicts with 'next' or 'seen'.\n\nThere was some discussion [1] also about adding `old_target` support to\nthe existing `update` command. I think its worthwhile to do this with\nsome tests cleanup, will follow that up as a separate series.  \n\nChanges since v1:\n* Rename the function `ref_update_ref_must_exist` to `ref_update_expects_existing_old_ref`\nto better clarify its usage.\n* Add checks in all the `ref_transaction_*()` functions to check if both\n_target_ and _oid_ values are set.\n* Clean up the tests\n  - Change `create_stdin_buf` to `format_command` and allow clients to\n  decide the destination.\n  - Remove unecessary curly braces.\n  - Rename TESTSYMREFONE to TEST_SYMREF_HEAD to stay compatible with our\n  new definitions of refs and pseudorefs.\n* Add more explanation in the commit message around the deviation of syntax\nfor the `symref-update` command.\n\n[1]: https://lore.kernel.org/r/CAOLa=ZQW-cCV5BP_fCvuZimfkjwAzjEiqXYRPft1Wf9kAX=_bw@mail.gmail.com\n\nRange diff vs v1:\n 1:  1bc4cc3fc4 =  1:  1bc4cc3fc4 refs: accept symref values in `ref_transaction_update()`\n 2:  57d0b1e2ea =  2:  57d0b1e2ea files-backend: extract out `create_symref_lock()`\n 3:  a8ae923f85 =  3:  a8ae923f85 refs: support symrefs in 'reference-transaction' hook\n 4:  e9965ba477 =  4:  e9965ba477 refs: move `original_update_refname` to 'refs.c'\n 5:  644daf7785 =  5:  644daf7785 refs: add support for transactional symref updates\n 6:  300b38e46f =  6:  300b38e46f refs: use transaction in `refs_create_symref()`\n 7:  f151dfe3c9 =  7:  f151dfe3c9 refs: rename `refs_create_symref()` to `refs_update_symref()`\n 8:  4865707bda =  8:  4865707bda refs: remove `create_symref` and associated dead code\n 9:  4cb67dce7c !  9:  2bbdeff798 refs: create and use `ref_update_ref_must_exist()`\n    @@ Metadata\n     Author: Karthik Nayak <karthik.188@gmail.com>\n     \n      ## Commit message ##\n    -    refs: create and use `ref_update_ref_must_exist()`\n    +    refs: create and use `ref_update_expects_existing_old_ref()`\n     \n         The files and reftable backend, need to check if a ref must exist, so\n         that the required validation can be done. A ref must exist only when the\n    @@ Commit message\n         path. As we introduce the 'symref-verify' command in the upcoming\n         commits, it is important to fix this.\n     \n    -    So let's export this to a function called `ref_update_ref_must_exist()`\n    -    and expose it internally via 'refs-internal.h'.\n    +    So let's export this to a function called\n    +    `ref_update_expects_existing_old_ref()` and expose it internally via\n    +    'refs-internal.h'.\n     \n         Signed-off-by: Karthik Nayak <karthik.188@gmail.com>\n     \n    @@ refs.c: int ref_update_check_old_target(const char *referent, struct ref_update\n      \treturn -1;\n      }\n     +\n    -+int ref_update_ref_must_exist(struct ref_update *update)\n    ++int ref_update_expects_existing_old_ref(struct ref_update *update)\n     +{\n     +\treturn (update->flags & REF_HAVE_OLD) &&\n     +\t\t(!is_null_oid(&update->old_oid) || update->old_target);\n    @@ refs/files-backend.c: static int lock_ref_for_update(struct files_ref_store *ref\n      \tstruct strbuf referent = STRBUF_INIT;\n     -\tint mustexist = (update->flags & REF_HAVE_OLD) &&\n     -\t\t!is_null_oid(&update->old_oid);\n    -+\tint mustexist = ref_update_ref_must_exist(update);\n    ++\tint mustexist = ref_update_expects_existing_old_ref(update);\n      \tint ret = 0;\n      \tstruct ref_lock *lock;\n      \n    @@ refs/refs-internal.h: int ref_update_has_null_new_value(struct ref_update *updat\n     + * Check if the ref must exist, this means that the old_oid or\n     + * old_target is non NULL.\n     + */\n    -+int ref_update_ref_must_exist(struct ref_update *update);\n    ++int ref_update_expects_existing_old_ref(struct ref_update *update);\n     +\n      #endif /* REFS_REFS_INTERNAL_H */\n     \n    @@ refs/reftable-backend.c: static int reftable_be_transaction_prepare(struct ref_s\n      \t\tif (ret < 0)\n      \t\t\tgoto done;\n     -\t\tif (ret > 0 && (!(u->flags & REF_HAVE_OLD) || is_null_oid(&u->old_oid))) {\n    -+\t\tif (ret > 0 && !ref_update_ref_must_exist(u)) {\n    ++\t\tif (ret > 0 && !ref_update_expects_existing_old_ref(u)) {\n      \t\t\t/*\n      \t\t\t * The reference does not exist, and we either have no\n      \t\t\t * old object ID or expect the reference to not exist.\n10:  9fda13b468 ! 10:  f509066cab update-ref: add support for 'symref-verify' command\n    @@ Commit message\n         point to an object and not a ref and the regular 'verify' command can be\n         used in such situations.\n     \n    -    Add required tests for symref support in 'verify' while also adding\n    -    reflog checks for the pre-existing 'verify' tests.\n    +    Add required tests for symref support in 'verify'. Since we're here,\n    +    also add reflog checks for the pre-existing 'verify' tests, there is no\n    +    divergence from behavior, but we never tested to ensure that reflog\n    +    wasn't affected by the 'verify' command.\n     \n         Signed-off-by: Karthik Nayak <karthik.188@gmail.com>\n     \n    @@ refs.c: int ref_transaction_delete(struct ref_transaction *transaction,\n     -\t\tBUG(\"verify called with old_oid set to NULL\");\n     +\tif (!old_target && !old_oid)\n     +\t\tBUG(\"verify called with old_oid and old_target set to NULL\");\n    ++\tif (old_oid && old_target)\n    ++\t\tBUG(\"verify called with both old_oid and old_target set\");\n     +\tif (old_target && !(flags & REF_NO_DEREF))\n     +\t\tBUG(\"verify cannot operate on symrefs with deref mode\");\n      \treturn ref_transaction_update(transaction, refname,\n    @@ t/t1400-update-ref.sh: test_expect_success PIPE 'transaction flushes status upda\n      \ttest_cmp expected actual\n      '\n      \n    -+create_stdin_buf () {\n    ++format_command () {\n     +\tif test \"$1\" = \"-z\"\n     +\tthen\n     +\t\tshift\n    -+\t\tprintf \"$F\" \"$@\" >stdin\n    ++\t\tprintf \"$F\" \"$@\"\n     +\telse\n    -+\t\techo \"$@\" >stdin\n    ++\t\techo \"$@\"\n     +\tfi\n     +}\n     +\n     +for type in \"\" \"-z\"\n     +do\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-verify fails without --no-deref\" '\n    ++\ttest_expect_success \"stdin $type symref-verify fails without --no-deref\" '\n     +\t\tgit symbolic-ref refs/heads/symref $a &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref\" \"$a\" &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} <stdin 2>err &&\n    ++\t\tformat_command $type \"symref-verify refs/heads/symref\" \"$a\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n     +\t\tgrep \"fatal: symref-verify: cannot operate with deref mode\" err\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-verify fails with too many arguments\" '\n    -+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref\" \"$a\" \"$a\" &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err  &&\n    ++\ttest_expect_success \"stdin $type symref-verify fails with too many arguments\" '\n    ++\t\tformat_command $type \"symref-verify refs/heads/symref\" \"$a\" \"$a\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err  &&\n     +\t\tif test \"$type\" = \"-z\"\n     +\t\tthen\n     +\t\t\tgrep \"fatal: unknown command: $a\" err\n    @@ t/t1400-update-ref.sh: test_expect_success PIPE 'transaction flushes status upda\n     +\t\tfi\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-verify succeeds for correct value\" '\n    ++\ttest_expect_success \"stdin $type symref-verify succeeds for correct value\" '\n     +\t\tgit symbolic-ref refs/heads/symref >expect &&\n     +\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >before &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref\" \"$a\" &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-verify refs/heads/symref\" \"$a\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual &&\n     +\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >after &&\n     +\t\ttest_cmp before after\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-verify no value is treated as zero value\" '\n    ++\ttest_expect_success \"stdin $type symref-verify fails with no value\" '\n     +\t\tgit symbolic-ref refs/heads/symref >expect &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref\" \"\" &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin\n    ++\t\tformat_command $type \"symref-verify refs/heads/symref\" \"\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-verify succeeds for dangling reference\" '\n    ++\ttest_expect_success \"stdin $type symref-verify succeeds for dangling reference\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref2\" &&\n     +\t\ttest_must_fail git symbolic-ref refs/heads/nonexistent &&\n     +\t\tgit symbolic-ref refs/heads/symref2 refs/heads/nonexistent &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref2\" \"refs/heads/nonexistent\" &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin\n    ++\t\tformat_command $type \"symref-verify refs/heads/symref2\" \"refs/heads/nonexistent\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-verify fails for missing reference\" '\n    ++\ttest_expect_success \"stdin $type symref-verify fails for missing reference\" '\n     +\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >before &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/missing\" \"refs/heads/unknown\" &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n    ++\t\tformat_command $type \"symref-verify refs/heads/missing\" \"refs/heads/unknown\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n     +\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/missing${SQ}: unable to resolve reference ${SQ}refs/heads/missing${SQ}\" err &&\n     +\t\ttest_must_fail git rev-parse --verify -q refs/heads/missing &&\n     +\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >after &&\n     +\t\ttest_cmp before after\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-verify fails for wrong value\" '\n    ++\ttest_expect_success \"stdin $type symref-verify fails for wrong value\" '\n     +\t\tgit symbolic-ref refs/heads/symref >expect &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref\" \"$b\" &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-verify refs/heads/symref\" \"$b\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-verify fails for mistaken null value\" '\n    ++\ttest_expect_success \"stdin $type symref-verify fails for mistaken null value\" '\n     +\t\tgit symbolic-ref refs/heads/symref >expect &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref\" \"$Z\" &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-verify refs/heads/symref\" \"$Z\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n11:  d07031827b ! 11:  a11f4c1e48 update-ref: add support for 'symref-delete' command\n    @@ refs.c: int ref_transaction_create(struct ref_transaction *transaction,\n      {\n      \tif (old_oid && is_null_oid(old_oid))\n      \t\tBUG(\"delete called with old_oid set to zeros\");\n    ++\tif (old_oid && old_target)\n    ++\t\tBUG(\"delete called with both old_oid and old_target set\");\n     +\tif (old_target && !(flags & REF_NO_DEREF))\n     +\t\tBUG(\"delete cannot operate on symrefs with deref mode\");\n      \treturn ref_transaction_update(transaction, refname,\n    @@ refs.h: int ref_transaction_create(struct ref_transaction *transaction,\n      /*\n     \n      ## t/t1400-update-ref.sh ##\n    -@@ t/t1400-update-ref.sh: do\n    - \t\ttest_cmp before after\n    - \t'\n    - \n    --\ttest_expect_success \"stdin ${type} symref-verify no value is treated as zero value\" '\n    -+\ttest_expect_success \"stdin ${type} symref-verify fails with no value\" '\n    - \t\tgit symbolic-ref refs/heads/symref >expect &&\n    - \t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref\" \"\" &&\n    - \t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin\n     @@ t/t1400-update-ref.sh: do\n      \t\ttest_cmp expect actual\n      \t'\n      \n    -+\ttest_expect_success \"stdin ${type} symref-delete fails without --no-deref\" '\n    ++\ttest_expect_success \"stdin $type symref-delete fails without --no-deref\" '\n     +\t\tgit symbolic-ref refs/heads/symref $a &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-delete refs/heads/symref\" \"$a\" &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} <stdin 2>err &&\n    ++\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$a\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n     +\t\tgrep \"fatal: symref-delete: cannot operate with deref mode\" err\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-delete fails with no ref\" '\n    -+\t\tcreate_stdin_buf ${type} \"symref-delete \" &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n    ++\ttest_expect_success \"stdin $type symref-delete fails with no ref\" '\n    ++\t\tformat_command $type \"symref-delete \" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n     +\t\tgrep \"fatal: symref-delete: missing <ref>\" err\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-delete fails with too many arguments\" '\n    -+\t\tcreate_stdin_buf ${type} \"symref-delete refs/heads/symref\" \"$a\" \"$a\" &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n    ++\ttest_expect_success \"stdin $type symref-delete fails with too many arguments\" '\n    ++\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$a\" \"$a\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n     +\t\tif test \"$type\" = \"-z\"\n     +\t\tthen\n     +\t\t\tgrep \"fatal: unknown command: $a\" err\n    @@ t/t1400-update-ref.sh: do\n     +\t\tfi\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-delete fails with wrong old value\" '\n    -+\t\tcreate_stdin_buf ${type} \"symref-delete refs/heads/symref\" \"$m\" &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n    ++\ttest_expect_success \"stdin $type symref-delete fails with wrong old value\" '\n    ++\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$m\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n     +\t\tgrep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected refs/heads/main\" err &&\n     +\t\tgit symbolic-ref refs/heads/symref >expect &&\n     +\t\techo $a >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-delete works with right old value\" '\n    -+\t\tcreate_stdin_buf ${type} \"symref-delete refs/heads/symref\" \"$a\" &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\ttest_expect_success \"stdin $type symref-delete works with right old value\" '\n    ++\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$a\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\ttest_must_fail git rev-parse --verify -q refs/heads/symref\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-delete works with empty old value\" '\n    -+\t\tgit symbolic-ref refs/heads/symref $a &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-delete refs/heads/symref\" \"\" &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\ttest_expect_success \"stdin $type symref-delete works with empty old value\" '\n    ++\t\tgit symbolic-ref refs/heads/symref $a >stdin &&\n    ++\t\tformat_command $type \"symref-delete refs/heads/symref\" \"\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\ttest_must_fail git rev-parse --verify -q $b\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-delete succeeds for dangling reference\" '\n    ++\ttest_expect_success \"stdin $type symref-delete succeeds for dangling reference\" '\n     +\t\ttest_must_fail git symbolic-ref refs/heads/nonexistent &&\n     +\t\tgit symbolic-ref refs/heads/symref2 refs/heads/nonexistent &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-delete refs/heads/symref2\" \"refs/heads/nonexistent\" &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-delete refs/heads/symref2\" \"refs/heads/nonexistent\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\ttest_must_fail git symbolic-ref -d refs/heads/symref2\n     +\t'\n     +\n12:  1038e96a44 ! 12:  9b71c9e07b update-ref: add support for 'symref-create' command\n    @@ refs.c: int ref_transaction_update(struct ref_transaction *transaction,\n      {\n     -\tif (!new_oid || is_null_oid(new_oid)) {\n     -\t\tstrbuf_addf(err, \"'%s' has a null OID\", refname);\n    ++\tif (new_oid && new_target)\n    ++\t\tBUG(\"create called with both new_oid and new_target set\");\n     +\tif ((!new_oid || is_null_oid(new_oid)) && !new_target) {\n    -+\t\tstrbuf_addf(err, \"'%s' has a null OID or no new target\", refname);\n    ++\t\tstrbuf_addf(err, \"'%s' has neither a valid OID nor a target\", refname);\n      \t\treturn 1;\n      \t}\n      \treturn ref_transaction_update(transaction, refname, new_oid,\n    @@ t/t0600-reffiles-backend.sh: test_expect_success POSIXPERM 'git reflog expire ho\n      '\n      \n     +test_expect_success SYMLINKS 'symref transaction supports symlinks' '\n    -+\ttest_when_finished \"git symbolic-ref -d TESTSYMREFONE\" &&\n    ++\ttest_when_finished \"git symbolic-ref -d TEST_SYMREF_HEAD\" &&\n     +\tgit update-ref refs/heads/new @ &&\n     +\ttest_config core.prefersymlinkrefs true &&\n     +\tcat >stdin <<-EOF &&\n     +\tstart\n    -+\tsymref-create TESTSYMREFONE refs/heads/new\n    ++\tsymref-create TEST_SYMREF_HEAD refs/heads/new\n     +\tprepare\n     +\tcommit\n     +\tEOF\n     +\tgit update-ref --no-deref --stdin <stdin &&\n    -+\ttest_path_is_symlink .git/TESTSYMREFONE &&\n    -+\ttest \"$(test_readlink .git/TESTSYMREFONE)\" = refs/heads/new\n    ++\ttest_path_is_symlink .git/TEST_SYMREF_HEAD &&\n    ++\ttest \"$(test_readlink .git/TEST_SYMREF_HEAD)\" = refs/heads/new\n     +'\n     +\n     +test_expect_success 'symref transaction supports false symlink config' '\n    -+\ttest_when_finished \"git symbolic-ref -d TESTSYMREFONE\" &&\n    ++\ttest_when_finished \"git symbolic-ref -d TEST_SYMREF_HEAD\" &&\n     +\tgit update-ref refs/heads/new @ &&\n     +\ttest_config core.prefersymlinkrefs false &&\n     +\tcat >stdin <<-EOF &&\n     +\tstart\n    -+\tsymref-create TESTSYMREFONE refs/heads/new\n    ++\tsymref-create TEST_SYMREF_HEAD refs/heads/new\n     +\tprepare\n     +\tcommit\n     +\tEOF\n     +\tgit update-ref --no-deref --stdin <stdin &&\n    -+\ttest_path_is_file .git/TESTSYMREFONE &&\n    -+\tgit symbolic-ref TESTSYMREFONE >actual &&\n    ++\ttest_path_is_file .git/TEST_SYMREF_HEAD &&\n    ++\tgit symbolic-ref TEST_SYMREF_HEAD >actual &&\n     +\techo refs/heads/new >expect &&\n     +\ttest_cmp expect actual\n     +'\n    @@ t/t1400-update-ref.sh: do\n      \t\ttest_must_fail git symbolic-ref -d refs/heads/symref2\n      \t'\n      \n    -+\ttest_expect_success \"stdin ${type} symref-create fails with too many arguments\" '\n    -+\t\tcreate_stdin_buf ${type} \"symref-create refs/heads/symref\" \"$a\" \"$a\" >stdin &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n    ++\ttest_expect_success \"stdin $type symref-create fails with too many arguments\" '\n    ++\t\tformat_command $type \"symref-create refs/heads/symref\" \"$a\" \"$a\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n     +\t\tif test \"$type\" = \"-z\"\n     +\t\tthen\n     +\t\t\tgrep \"fatal: unknown command: $a\" err\n    @@ t/t1400-update-ref.sh: do\n     +\t\tfi\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-create fails with no target\" '\n    -+\t\tcreate_stdin_buf ${type} \"symref-create refs/heads/symref\" >stdin &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin\n    ++\ttest_expect_success \"stdin $type symref-create fails with no target\" '\n    ++\t\tformat_command $type \"symref-create refs/heads/symref\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-create fails with empty target\" '\n    -+\t\tcreate_stdin_buf ${type} \"symref-create refs/heads/symref\" \"\" >stdin &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin\n    ++\ttest_expect_success \"stdin $type symref-create fails with empty target\" '\n    ++\t\tformat_command $type \"symref-create refs/heads/symref\" \"\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-create works\" '\n    ++\ttest_expect_success \"stdin $type symref-create works\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-create refs/heads/symref\" \"$a\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-create refs/heads/symref\" \"$a\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\tgit symbolic-ref refs/heads/symref >expect &&\n     +\t\techo $a >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-create works with --no-deref\" '\n    ++\ttest_expect_success \"stdin $type symref-create works with --no-deref\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-create refs/heads/symref\" \"$a\" &&\n    -+\t\tgit update-ref --stdin ${type} <stdin 2>err\n    ++\t\tformat_command $type \"symref-create refs/heads/symref\" \"$a\" &&\n    ++\t\tgit update-ref --stdin $type <stdin 2>err\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} create dangling symref ref works\" '\n    ++\ttest_expect_success \"stdin $type create dangling symref ref works\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-create refs/heads/symref\" \"refs/heads/unkown\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-create refs/heads/symref\" \"refs/heads/unkown\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\tgit symbolic-ref refs/heads/symref >expect &&\n     +\t\techo refs/heads/unkown >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-create does not create reflogs by default\" '\n    ++\ttest_expect_success \"stdin $type symref-create does not create reflogs by default\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/symref\" &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-create refs/symref\" \"$a\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-create refs/symref\" \"$a\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\tgit symbolic-ref refs/symref >expect &&\n     +\t\techo $a >actual &&\n     +\t\ttest_cmp expect actual &&\n     +\t\ttest_must_fail git reflog exists refs/symref\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-create reflogs with --create-reflog\" '\n    ++\ttest_expect_success \"stdin $type symref-create reflogs with --create-reflog\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-create refs/heads/symref\" \"$a\" >stdin &&\n    -+\t\tgit update-ref --create-reflog --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-create refs/heads/symref\" \"$a\" >stdin &&\n    ++\t\tgit update-ref --create-reflog --stdin $type --no-deref <stdin &&\n     +\t\tgit symbolic-ref refs/heads/symref >expect &&\n     +\t\techo $a >actual &&\n     +\t\ttest_cmp expect actual &&\n    @@ t/t1416-ref-transaction-hooks.sh: test_expect_success 'hook gets all queued symr\n      \tprepare\n      \tcommit\n      \tEOF\n    +\n    + ## t/t5605-clone-local.sh ##\n    +@@ t/t5605-clone-local.sh: test_expect_success REFFILES 'local clone from repo with corrupt refs fails grac\n    + \techo a >corrupt/.git/refs/heads/topic &&\n    + \n    + \ttest_must_fail git clone corrupt working 2>err &&\n    +-\tgrep \"has a null OID\" err\n    ++\tgrep \"has neither a valid OID nor a target\" err\n    + '\n    + \n    + test_done\n13:  78dd51b65f = 13:  a9b1a31756 reftable: pick either 'oid' or 'target' for new updates\n14:  562e061063 ! 14:  1bbbe86743 update-ref: add support for 'symref-update' command\n    @@ Commit message\n         OID before the update. This by extension also means that this when a\n         zero <old-oid> is provided, it ensures that the ref didn't exist before.\n     \n    +    The divergence in syntax from the regular `update` command is because if\n    +    we don't use a `(ref | oid)` prefix for the old_value, then there is\n    +    ambiguity around if the value provided should be treated as an oid or a\n    +    reference. This is more so the reason, because we allow anything\n    +    committish to be provided as an oid.\n    +\n         The command allows users to perform symbolic ref updates within a\n         transaction. This provides atomicity and allows users to perform a set\n         of operations together.\n     \n    -    This command will also support deref mode, to ensure that we can update\n    +    This command supports deref mode, to ensure that we can update\n         dereferenced regular refs to symrefs.\n     \n         Signed-off-by: Karthik Nayak <karthik.188@gmail.com>\n    @@ builtin/update-ref.c: static char *parse_next_refname(const char **next)\n     +\t\treturn strbuf_detach(&arg, NULL);\n     +\treturn NULL;\n     +}\n    -+\n      \n      /*\n       * The value being parsed is <old-oid> (as opposed to <new-oid>; the\n    @@ builtin/update-ref.c: static void parse_cmd_update(struct ref_transaction *trans\n     +\tchar *old_target = NULL;\n     +\tstruct strbuf err = STRBUF_INIT;\n     +\tstruct object_id old_oid;\n    -+\tint have_old = 0;\n    ++\tint have_old_oid = 0;\n     +\n     +\trefname = parse_refname(&next);\n     +\tif (!refname)\n    @@ builtin/update-ref.c: static void parse_cmd_update(struct ref_transaction *trans\n     +\n     +\told_arg = parse_next_arg(&next);\n     +\tif (old_arg) {\n    -+\t\told_target = parse_next_refname(&next);\n    ++\t\told_target = parse_next_arg(&next);\n     +\t\tif (!old_target)\n     +\t\t\tdie(\"symref-update %s: expected old value\", refname);\n     +\n    -+\t\tif (!strcmp(old_arg, \"oid\") &&\n    -+\t\t    !repo_get_oid(the_repository, old_target, &old_oid)) {\n    ++\t\tif (!strcmp(old_arg, \"oid\")) {\n    ++\t\t\tif (repo_get_oid(the_repository, old_target, &old_oid))\n    ++\t\t\t\tdie(\"symref-update %s: invalid oid: %s\", refname, old_target);\n    ++\n     +\t\t\told_target = NULL;\n    -+\t\t\thave_old = 1;\n    -+\t\t} else if (strcmp(old_arg, \"ref\"))\n    ++\t\t\thave_old_oid = 1;\n    ++\t\t} else if (!strcmp(old_arg, \"ref\")) {\n    ++\t\t\tif (check_refname_format(old_target, REFNAME_ALLOW_ONELEVEL))\n    ++\t\t\t\tdie(\"symref-update %s: invalid ref: %s\", refname, old_target);\n    ++\t\t} else {\n     +\t\t\tdie(\"symref-update %s: invalid arg '%s' for old value\", refname, old_arg);\n    ++\t\t}\n     +\t}\n     +\n     +\tif (*next != line_termination)\n     +\t\tdie(\"symref-update %s: extra input: %s\", refname, next);\n     +\n     +\tif (ref_transaction_update(transaction, refname, NULL,\n    -+\t\t\t\t   have_old ? &old_oid : NULL,\n    ++\t\t\t\t   have_old_oid ? &old_oid : NULL,\n     +\t\t\t\t   new_target, old_target,\n    -+\t\t\t\t   update_flags |= create_reflog_flag,\n    ++\t\t\t\t   update_flags | create_reflog_flag,\n     +\t\t\t\t   msg, &err))\n     +\t\tdie(\"%s\", err.buf);\n     +\n    @@ t/t1400-update-ref.sh: do\n      \t\tgit reflog exists refs/heads/symref\n      \t'\n      \n    -+\ttest_expect_success \"stdin ${type} symref-update fails with too many arguments\" '\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$a\" \"ref\" \"$a\" \"$a\" >stdin &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n    ++\ttest_expect_success \"stdin $type symref-update fails with too many arguments\" '\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"ref\" \"$a\" \"$a\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n     +\t\tif test \"$type\" = \"-z\"\n     +\t\tthen\n     +\t\t\tgrep \"fatal: unknown command: $a\" err\n    @@ t/t1400-update-ref.sh: do\n     +\t\tfi\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update fails with wrong old value argument\" '\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$a\" \"foo\" \"$a\" \"$a\" >stdin &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n    ++\ttest_expect_success \"stdin $type symref-update fails with wrong old value argument\" '\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"foo\" \"$a\" \"$a\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n     +\t\tgrep \"fatal: symref-update refs/heads/symref: invalid arg ${SQ}foo${SQ} for old value\" err\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update creates with zero old value\" '\n    ++\ttest_expect_success \"stdin $type symref-update creates with zero old value\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$a\" \"oid\" \"$Z\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"oid\" \"$Z\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\techo $a >expect &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update creates with no old value\" '\n    ++\ttest_expect_success \"stdin $type symref-update creates with no old value\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\techo $a >expect &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update creates dangling\" '\n    ++\ttest_expect_success \"stdin $type symref-update creates dangling\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n     +\t\ttest_must_fail git rev-parse refs/heads/nonexistent &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"refs/heads/nonexistent\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"refs/heads/nonexistent\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\techo refs/heads/nonexistent >expect &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update fails with wrong old value\" '\n    ++\ttest_expect_success \"stdin $type symref-update fails with wrong old value\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n     +\t\tgit symbolic-ref refs/heads/symref $a &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$m\" \"ref\" \"$b\" >stdin &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$m\" \"ref\" \"$b\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n     +\t\tgrep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected $b\" err &&\n     +\t\ttest_must_fail git rev-parse --verify -q $c\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update updates dangling ref\" '\n    ++\ttest_expect_success \"stdin $type symref-update updates dangling ref\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n     +\t\ttest_must_fail git rev-parse refs/heads/nonexistent &&\n     +\t\tgit symbolic-ref refs/heads/symref refs/heads/nonexistent &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\techo $a >expect &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update updates dangling ref with old value\" '\n    ++\ttest_expect_success \"stdin $type symref-update updates dangling ref with old value\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n     +\t\ttest_must_fail git rev-parse refs/heads/nonexistent &&\n     +\t\tgit symbolic-ref refs/heads/symref refs/heads/nonexistent &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$a\" \"ref\" \"refs/heads/nonexistent\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"ref\" \"refs/heads/nonexistent\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\techo $a >expect &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update fails update dangling ref with wrong old value\" '\n    ++\ttest_expect_success \"stdin $type symref-update fails update dangling ref with wrong old value\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n     +\t\ttest_must_fail git rev-parse refs/heads/nonexistent &&\n     +\t\tgit symbolic-ref refs/heads/symref refs/heads/nonexistent &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$a\" \"ref\" \"refs/heads/wrongref\" >stdin &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"ref\" \"refs/heads/wrongref\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin &&\n     +\t\techo refs/heads/nonexistent >expect &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update works with right old value\" '\n    ++\ttest_expect_success \"stdin $type symref-update works with right old value\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n     +\t\tgit symbolic-ref refs/heads/symref $a &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$m\" \"ref\" \"$a\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$m\" \"ref\" \"$a\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\techo $m >expect &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update works with no old value\" '\n    ++\ttest_expect_success \"stdin $type symref-update works with no old value\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n     +\t\tgit symbolic-ref refs/heads/symref $a &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$m\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$m\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\techo $m >expect &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update fails with empty old ref-target\" '\n    ++\ttest_expect_success \"stdin $type symref-update fails with empty old ref-target\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n     +\t\tgit symbolic-ref refs/heads/symref $a &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$m\" \"ref\" \"\" >stdin &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$m\" \"ref\" \"\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin &&\n     +\t\techo $a >expect &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update creates (with deref)\" '\n    ++\ttest_expect_success \"stdin $type symref-update creates (with deref)\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n    ++\t\tgit update-ref --stdin $type <stdin &&\n     +\t\techo $a >expect &&\n     +\t\tgit symbolic-ref --no-recurse refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual &&\n    @@ t/t1400-update-ref.sh: do\n     +\t\tgrep \"$Z $(git rev-parse $a)\" actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update regular ref to symref with correct old-oid\" '\n    ++\ttest_expect_success \"stdin $type symref-update regular ref to symref with correct old-oid\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d --no-recurse refs/heads/regularref\" &&\n     +\t\tgit update-ref --no-deref refs/heads/regularref $a &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/regularref\" \"$a\" \"oid\" \"$(git rev-parse $a)\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/regularref\" \"$a\" \"oid\" \"$(git rev-parse $a)\" >stdin &&\n    ++\t\tgit update-ref --stdin $type <stdin &&\n     +\t\techo $a >expect &&\n     +\t\tgit symbolic-ref --no-recurse refs/heads/regularref >actual &&\n     +\t\ttest_cmp expect actual &&\n    @@ t/t1400-update-ref.sh: do\n     +\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update regular ref to symref fails with wrong old-oid\" '\n    ++\ttest_expect_success \"stdin $type symref-update regular ref to symref fails with wrong old-oid\" '\n    ++\t\ttest_when_finished \"git update-ref -d refs/heads/regularref\" &&\n    ++\t\tgit update-ref --no-deref refs/heads/regularref $a &&\n    ++\t\tformat_command $type \"symref-update refs/heads/regularref\" \"$a\" \"oid\" \"$(git rev-parse refs/heads/target2)\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n    ++\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: is at $(git rev-parse $a) but expected $(git rev-parse refs/heads/target2)\" err &&\n    ++\t\techo $(git rev-parse $a) >expect &&\n    ++\t\tgit rev-parse refs/heads/regularref >actual &&\n    ++\t\ttest_cmp expect actual\n    ++\t'\n    ++\n    ++\ttest_expect_success \"stdin $type symref-update regular ref to symref fails with invalid old-oid\" '\n     +\t\ttest_when_finished \"git update-ref -d refs/heads/regularref\" &&\n     +\t\tgit update-ref --no-deref refs/heads/regularref $a &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/regularref\" \"$a\" \"oid\" \"$(git rev-parse refs/heads/target2)\" >stdin &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} <stdin 2>err &&\n    ++\t\tformat_command $type \"symref-update refs/heads/regularref\" \"$a\" \"oid\" \"not-a-ref-oid\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n    ++\t\tgrep \"fatal: symref-update refs/heads/regularref: invalid oid: not-a-ref-oid\" err &&\n     +\t\techo $(git rev-parse $a) >expect &&\n     +\t\tgit rev-parse refs/heads/regularref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update existing symref with zero old-oid\" '\n    ++\ttest_expect_success \"stdin $type symref-update existing symref with zero old-oid\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d --no-recurse refs/heads/symref\" &&\n     +\t\tgit symbolic-ref refs/heads/symref refs/heads/target2 &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$a\" \"oid\" \"$Z\" >stdin &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} <stdin 2>err &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"oid\" \"$Z\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n     +\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/symref${SQ}: reference already exists\" err &&\n     +\t\techo refs/heads/target2 >expect &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update regular ref to symref (with deref)\" '\n    ++\ttest_expect_success \"stdin $type symref-update regular ref to symref (with deref)\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n     +\t\ttest_when_finished \"git update-ref -d --no-deref refs/heads/symref2\" &&\n     +\t\tgit update-ref refs/heads/symref2 $a &&\n     +\t\tgit symbolic-ref --no-recurse refs/heads/symref refs/heads/symref2 &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n    -+\t\tgit update-ref ${type} --stdin <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n    ++\t\tgit update-ref $type --stdin <stdin &&\n     +\t\techo $a >expect &&\n     +\t\tgit symbolic-ref --no-recurse refs/heads/symref2 >actual &&\n     +\t\ttest_cmp expect actual &&\n    @@ t/t1400-update-ref.sh: do\n     +\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update regular ref to symref\" '\n    ++\ttest_expect_success \"stdin $type symref-update regular ref to symref\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d --no-recurse refs/heads/regularref\" &&\n     +\t\tgit update-ref --no-deref refs/heads/regularref $a &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/regularref\" \"$a\" >stdin &&\n    -+\t\tgit update-ref ${type} --stdin <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/regularref\" \"$a\" >stdin &&\n    ++\t\tgit update-ref $type --stdin <stdin &&\n     +\t\techo $a >expect &&\n     +\t\tgit symbolic-ref --no-recurse refs/heads/regularref >actual &&\n     +\t\ttest_cmp expect actual &&\n\nKarthik Nayak (6):\n  refs: create and use `ref_update_expects_existing_old_ref()`\n  update-ref: add support for 'symref-verify' command\n  update-ref: add support for 'symref-delete' command\n  update-ref: add support for 'symref-create' command\n  reftable: pick either 'oid' or 'target' for new updates\n  update-ref: add support for 'symref-update' command\n\n Documentation/git-update-ref.txt |  25 ++\n builtin/clone.c                  |   2 +-\n builtin/fetch.c                  |   2 +-\n builtin/receive-pack.c           |   3 +-\n builtin/update-ref.c             | 235 ++++++++++++++++-\n refs.c                           |  40 ++-\n refs.h                           |   6 +-\n refs/files-backend.c             |   3 +-\n refs/refs-internal.h             |   6 +\n refs/reftable-backend.c          |   7 +-\n t/t0600-reffiles-backend.sh      |  32 +++\n t/t1400-update-ref.sh            | 416 ++++++++++++++++++++++++++++++-\n t/t1416-ref-transaction-hooks.sh |  54 ++++\n t/t5605-clone-local.sh           |   2 +-\n 14 files changed, 799 insertions(+), 34 deletions(-)\n\n-- \n2.43.GIT\n\n"},{"id":"495285","messageId":"20240522090326.1268326-2-knayak@gitlab.com","threadId":"61469","inReplyTo":"20240522090326.1268326-1-knayak@gitlab.com","subject":"[PATCH v2 1/6] refs: create and use `ref_update_expects_existing_old_ref()`","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-22T09:03:21Z","receivedAt":"2024-05-22T09:03:32Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"From: Karthik Nayak <karthik.188@gmail.com>\n\nThe files and reftable backend, need to check if a ref must exist, so\nthat the required validation can be done. A ref must exist only when the\n`old_oid` value of the update has been explicitly set and it is not the\n`null_oid` value.\n\nSince we also support symrefs now, we need to ensure that even when\n`old_target` is set a ref must exist. While this was missed when we\nadded symref support in transactions, there are no active users of this\npath. As we introduce the 'symref-verify' command in the upcoming\ncommits, it is important to fix this.\n\nSo let's export this to a function called\n`ref_update_expects_existing_old_ref()` and expose it internally via\n'refs-internal.h'.\n\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n refs.c                  | 6 ++++++\n refs/files-backend.c    | 3 +--\n refs/refs-internal.h    | 6 ++++++\n refs/reftable-backend.c | 2 +-\n 4 files changed, 14 insertions(+), 3 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex fa5471d219..00e87f8ee3 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2863,3 +2863,9 @@ int ref_update_check_old_target(const char *referent, struct ref_update *update,\n \t\t\t    referent, update->old_target);\n \treturn -1;\n }\n+\n+int ref_update_expects_existing_old_ref(struct ref_update *update)\n+{\n+\treturn (update->flags & REF_HAVE_OLD) &&\n+\t\t(!is_null_oid(&update->old_oid) || update->old_target);\n+}\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 3957bfa579..ef760869ca 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2411,8 +2411,7 @@ static int lock_ref_for_update(struct files_ref_store *refs,\n \t\t\t       struct strbuf *err)\n {\n \tstruct strbuf referent = STRBUF_INIT;\n-\tint mustexist = (update->flags & REF_HAVE_OLD) &&\n-\t\t!is_null_oid(&update->old_oid);\n+\tint mustexist = ref_update_expects_existing_old_ref(update);\n \tint ret = 0;\n \tstruct ref_lock *lock;\n \ndiff --git a/refs/refs-internal.h b/refs/refs-internal.h\nindex 53a6c5d842..ee298ec0d5 100644\n--- a/refs/refs-internal.h\n+++ b/refs/refs-internal.h\n@@ -765,4 +765,10 @@ int ref_update_has_null_new_value(struct ref_update *update);\n int ref_update_check_old_target(const char *referent, struct ref_update *update,\n \t\t\t\tstruct strbuf *err);\n \n+/*\n+ * Check if the ref must exist, this means that the old_oid or\n+ * old_target is non NULL.\n+ */\n+int ref_update_expects_existing_old_ref(struct ref_update *update);\n+\n #endif /* REFS_REFS_INTERNAL_H */\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 98cebbcf39..dc7aa7f274 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -825,7 +825,7 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n \t\t\t\t\t      &current_oid, &referent, &u->type);\n \t\tif (ret < 0)\n \t\t\tgoto done;\n-\t\tif (ret > 0 && (!(u->flags & REF_HAVE_OLD) || is_null_oid(&u->old_oid))) {\n+\t\tif (ret > 0 && !ref_update_expects_existing_old_ref(u)) {\n \t\t\t/*\n \t\t\t * The reference does not exist, and we either have no\n \t\t\t * old object ID or expect the reference to not exist.\n-- \n2.43.GIT\n\n"},{"id":"495287","messageId":"20240522090326.1268326-3-knayak@gitlab.com","threadId":"61469","inReplyTo":"20240522090326.1268326-1-knayak@gitlab.com","subject":"[PATCH v2 2/6] update-ref: add support for 'symref-verify' command","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-22T09:03:22Z","receivedAt":"2024-05-22T09:03:33Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"From: Karthik Nayak <karthik.188@gmail.com>\n\nThe 'symref-verify' command allows users to verify if a provided <ref>\ncontains the provided <old-target> without changing the <ref>. If\n<old-target> is not provided, the command will verify that the <ref>\ndoesn't exist.\n\nThe command allows users to verify symbolic refs within a transaction,\nand this means users can perform a set of changes in a transaction only\nwhen the verification holds good.\n\nSince we're checking for symbolic refs, this command will only work with\nthe 'no-deref' mode. This is because any dereferenced symbolic ref will\npoint to an object and not a ref and the regular 'verify' command can be\nused in such situations.\n\nAdd required tests for symref support in 'verify'. Since we're here,\nalso add reflog checks for the pre-existing 'verify' tests, there is no\ndivergence from behavior, but we never tested to ensure that reflog\nwasn't affected by the 'verify' command.\n\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n Documentation/git-update-ref.txt |  7 +++\n builtin/update-ref.c             | 80 +++++++++++++++++++++++----\n refs.c                           | 11 +++-\n refs.h                           |  1 +\n t/t1400-update-ref.sh            | 94 +++++++++++++++++++++++++++++++-\n t/t1416-ref-transaction-hooks.sh | 30 ++++++++++\n 6 files changed, 208 insertions(+), 15 deletions(-)\n\ndiff --git a/Documentation/git-update-ref.txt b/Documentation/git-update-ref.txt\nindex 374a2ebd2b..9fe78b3501 100644\n--- a/Documentation/git-update-ref.txt\n+++ b/Documentation/git-update-ref.txt\n@@ -65,6 +65,7 @@ performs all modifications together.  Specify commands of the form:\n \tcreate SP <ref> SP <new-oid> LF\n \tdelete SP <ref> [SP <old-oid>] LF\n \tverify SP <ref> [SP <old-oid>] LF\n+\tsymref-verify SP <ref> [SP <old-target>] LF\n \toption SP <opt> LF\n \tstart LF\n \tprepare LF\n@@ -86,6 +87,7 @@ quoting:\n \tcreate SP <ref> NUL <new-oid> NUL\n \tdelete SP <ref> NUL [<old-oid>] NUL\n \tverify SP <ref> NUL [<old-oid>] NUL\n+\tsymref-verify SP <ref> [NUL <old-target>] NUL\n \toption SP <opt> NUL\n \tstart NUL\n \tprepare NUL\n@@ -117,6 +119,11 @@ verify::\n \tVerify <ref> against <old-oid> but do not change it.  If\n \t<old-oid> is zero or missing, the ref must not exist.\n \n+symref-verify::\n+\tVerify symbolic <ref> against <old-target> but do not change it.\n+\tIf <old-target> is missing, the ref must not exist.  Can only be\n+\tused in `no-deref` mode.\n+\n option::\n \tModify the behavior of the next command naming a <ref>.\n \tThe only valid option is `no-deref` to avoid dereferencing\ndiff --git a/builtin/update-ref.c b/builtin/update-ref.c\nindex 21fdbf6ac8..6dce1cd663 100644\n--- a/builtin/update-ref.c\n+++ b/builtin/update-ref.c\n@@ -76,6 +76,29 @@ static char *parse_refname(const char **next)\n \treturn strbuf_detach(&ref, NULL);\n }\n \n+/*\n+ * Wrapper around parse_refname which skips the next delimiter.\n+ */\n+static char *parse_next_refname(const char **next)\n+{\n+\tif (line_termination) {\n+\t\t/* Without -z, consume SP and use next argument */\n+\t\tif (!**next || **next == line_termination)\n+\t\t\treturn NULL;\n+\t\tif (**next != ' ')\n+\t\t\tdie(\"expected SP but got: %s\", *next);\n+\t} else {\n+\t\t/* With -z, read the next NUL-terminated line */\n+\t\tif (**next)\n+\t\t\treturn NULL;\n+\t}\n+\t/* Skip the delimiter */\n+\t(*next)++;\n+\n+\treturn parse_refname(next);\n+}\n+\n+\n /*\n  * The value being parsed is <old-oid> (as opposed to <new-oid>; the\n  * difference affects which error messages are generated):\n@@ -297,11 +320,47 @@ static void parse_cmd_verify(struct ref_transaction *transaction,\n \t\tdie(\"verify %s: extra input: %s\", refname, next);\n \n \tif (ref_transaction_verify(transaction, refname, &old_oid,\n-\t\t\t\t   update_flags, &err))\n+\t\t\t\t   NULL, update_flags, &err))\n+\t\tdie(\"%s\", err.buf);\n+\n+\tupdate_flags = default_flags;\n+\tfree(refname);\n+\tstrbuf_release(&err);\n+}\n+\n+static void parse_cmd_symref_verify(struct ref_transaction *transaction,\n+\t\t\t\t    const char *next, const char *end)\n+{\n+\tstruct strbuf err = STRBUF_INIT;\n+\tstruct object_id old_oid;\n+\tchar *refname, *old_target;\n+\n+\tif (!(update_flags & REF_NO_DEREF))\n+\t\tdie(\"symref-verify: cannot operate with deref mode\");\n+\n+\trefname = parse_refname(&next);\n+\tif (!refname)\n+\t\tdie(\"symref-verify: missing <ref>\");\n+\n+\t/*\n+\t * old_ref is optional, if not provided, we need to ensure that the\n+\t * ref doesn't exist.\n+\t */\n+\told_target = parse_next_refname(&next);\n+\tif (!old_target)\n+\t\toidcpy(&old_oid, null_oid());\n+\n+\tif (*next != line_termination)\n+\t\tdie(\"symref-verify %s: extra input: %s\", refname, next);\n+\n+\tif (ref_transaction_verify(transaction, refname,\n+\t\t\t\t   old_target ? NULL : &old_oid,\n+\t\t\t\t   old_target, update_flags, &err))\n \t\tdie(\"%s\", err.buf);\n \n \tupdate_flags = default_flags;\n \tfree(refname);\n+\tfree(old_target);\n \tstrbuf_release(&err);\n }\n \n@@ -380,15 +439,16 @@ static const struct parse_cmd {\n \tunsigned args;\n \tenum update_refs_state state;\n } command[] = {\n-\t{ \"update\",  parse_cmd_update,  3, UPDATE_REFS_OPEN },\n-\t{ \"create\",  parse_cmd_create,  2, UPDATE_REFS_OPEN },\n-\t{ \"delete\",  parse_cmd_delete,  2, UPDATE_REFS_OPEN },\n-\t{ \"verify\",  parse_cmd_verify,  2, UPDATE_REFS_OPEN },\n-\t{ \"option\",  parse_cmd_option,  1, UPDATE_REFS_OPEN },\n-\t{ \"start\",   parse_cmd_start,   0, UPDATE_REFS_STARTED },\n-\t{ \"prepare\", parse_cmd_prepare, 0, UPDATE_REFS_PREPARED },\n-\t{ \"abort\",   parse_cmd_abort,   0, UPDATE_REFS_CLOSED },\n-\t{ \"commit\",  parse_cmd_commit,  0, UPDATE_REFS_CLOSED },\n+\t{ \"update\",        parse_cmd_update,        3, UPDATE_REFS_OPEN },\n+\t{ \"create\",        parse_cmd_create,        2, UPDATE_REFS_OPEN },\n+\t{ \"delete\",        parse_cmd_delete,        2, UPDATE_REFS_OPEN },\n+\t{ \"verify\",        parse_cmd_verify,        2, UPDATE_REFS_OPEN },\n+\t{ \"symref-verify\", parse_cmd_symref_verify, 2, UPDATE_REFS_OPEN },\n+\t{ \"option\",        parse_cmd_option,        1, UPDATE_REFS_OPEN },\n+\t{ \"start\",         parse_cmd_start,         0, UPDATE_REFS_STARTED },\n+\t{ \"prepare\",       parse_cmd_prepare,       0, UPDATE_REFS_PREPARED },\n+\t{ \"abort\",         parse_cmd_abort,         0, UPDATE_REFS_CLOSED },\n+\t{ \"commit\",        parse_cmd_commit,        0, UPDATE_REFS_CLOSED },\n };\n \n static void update_refs_stdin(void)\ndiff --git a/refs.c b/refs.c\nindex 00e87f8ee3..865264d487 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1331,14 +1331,19 @@ int ref_transaction_delete(struct ref_transaction *transaction,\n int ref_transaction_verify(struct ref_transaction *transaction,\n \t\t\t   const char *refname,\n \t\t\t   const struct object_id *old_oid,\n+\t\t\t   const char *old_target,\n \t\t\t   unsigned int flags,\n \t\t\t   struct strbuf *err)\n {\n-\tif (!old_oid)\n-\t\tBUG(\"verify called with old_oid set to NULL\");\n+\tif (!old_target && !old_oid)\n+\t\tBUG(\"verify called with old_oid and old_target set to NULL\");\n+\tif (old_oid && old_target)\n+\t\tBUG(\"verify called with both old_oid and old_target set\");\n+\tif (old_target && !(flags & REF_NO_DEREF))\n+\t\tBUG(\"verify cannot operate on symrefs with deref mode\");\n \treturn ref_transaction_update(transaction, refname,\n \t\t\t\t      NULL, old_oid,\n-\t\t\t\t      NULL, NULL,\n+\t\t\t\t      NULL, old_target,\n \t\t\t\t      flags, NULL, err);\n }\n \ndiff --git a/refs.h b/refs.h\nindex 71cc1c58e0..48cec1ba72 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -781,6 +781,7 @@ int ref_transaction_delete(struct ref_transaction *transaction,\n int ref_transaction_verify(struct ref_transaction *transaction,\n \t\t\t   const char *refname,\n \t\t\t   const struct object_id *old_oid,\n+\t\t\t   const char *old_target,\n \t\t\t   unsigned int flags,\n \t\t\t   struct strbuf *err);\n \ndiff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\nindex ec3443cc87..07e111b063 100755\n--- a/t/t1400-update-ref.sh\n+++ b/t/t1400-update-ref.sh\n@@ -890,17 +890,23 @@ test_expect_success 'stdin update/create/verify combination works' '\n '\n \n test_expect_success 'stdin verify succeeds for correct value' '\n+\ttest-tool ref-store main for-each-reflog-ent $m >before &&\n \tgit rev-parse $m >expect &&\n \techo \"verify $m $m\" >stdin &&\n \tgit update-ref --stdin <stdin &&\n \tgit rev-parse $m >actual &&\n-\ttest_cmp expect actual\n+\ttest_cmp expect actual &&\n+\ttest-tool ref-store main for-each-reflog-ent $m >after &&\n+\ttest_cmp before after\n '\n \n test_expect_success 'stdin verify succeeds for missing reference' '\n+\ttest-tool ref-store main for-each-reflog-ent $m >before &&\n \techo \"verify refs/heads/missing $Z\" >stdin &&\n \tgit update-ref --stdin <stdin &&\n-\ttest_must_fail git rev-parse --verify -q refs/heads/missing\n+\ttest_must_fail git rev-parse --verify -q refs/heads/missing &&\n+\ttest-tool ref-store main for-each-reflog-ent $m >after &&\n+\ttest_cmp before after\n '\n \n test_expect_success 'stdin verify treats no value as missing' '\n@@ -1641,4 +1647,88 @@ test_expect_success PIPE 'transaction flushes status updates' '\n \ttest_cmp expected actual\n '\n \n+format_command () {\n+\tif test \"$1\" = \"-z\"\n+\tthen\n+\t\tshift\n+\t\tprintf \"$F\" \"$@\"\n+\telse\n+\t\techo \"$@\"\n+\tfi\n+}\n+\n+for type in \"\" \"-z\"\n+do\n+\n+\ttest_expect_success \"stdin $type symref-verify fails without --no-deref\" '\n+\t\tgit symbolic-ref refs/heads/symref $a &&\n+\t\tformat_command $type \"symref-verify refs/heads/symref\" \"$a\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n+\t\tgrep \"fatal: symref-verify: cannot operate with deref mode\" err\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-verify fails with too many arguments\" '\n+\t\tformat_command $type \"symref-verify refs/heads/symref\" \"$a\" \"$a\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err  &&\n+\t\tif test \"$type\" = \"-z\"\n+\t\tthen\n+\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\telse\n+\t\t\tgrep \"fatal: symref-verify refs/heads/symref: extra input:  $a\" err\n+\t\tfi\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-verify succeeds for correct value\" '\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >before &&\n+\t\tformat_command $type \"symref-verify refs/heads/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >after &&\n+\t\ttest_cmp before after\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-verify fails with no value\" '\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\tformat_command $type \"symref-verify refs/heads/symref\" \"\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-verify succeeds for dangling reference\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref2\" &&\n+\t\ttest_must_fail git symbolic-ref refs/heads/nonexistent &&\n+\t\tgit symbolic-ref refs/heads/symref2 refs/heads/nonexistent &&\n+\t\tformat_command $type \"symref-verify refs/heads/symref2\" \"refs/heads/nonexistent\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-verify fails for missing reference\" '\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >before &&\n+\t\tformat_command $type \"symref-verify refs/heads/missing\" \"refs/heads/unknown\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n+\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/missing${SQ}: unable to resolve reference ${SQ}refs/heads/missing${SQ}\" err &&\n+\t\ttest_must_fail git rev-parse --verify -q refs/heads/missing &&\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >after &&\n+\t\ttest_cmp before after\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-verify fails for wrong value\" '\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\tformat_command $type \"symref-verify refs/heads/symref\" \"$b\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-verify fails for mistaken null value\" '\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\tformat_command $type \"symref-verify refs/heads/symref\" \"$Z\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+done\n+\n test_done\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex 067fd57290..fd58b902f4 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -157,4 +157,34 @@ test_expect_success 'hook captures git-symbolic-ref updates' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'hook gets all queued symref updates' '\n+\ttest_when_finished \"rm actual\" &&\n+\n+\tgit update-ref refs/heads/branch $POST_OID &&\n+\tgit symbolic-ref refs/heads/symref refs/heads/main &&\n+\n+\ttest_hook reference-transaction <<-\\EOF &&\n+\techo \"$*\" >>actual\n+\twhile read -r line\n+\tdo\n+\t\tprintf \"%s\\n\" \"$line\"\n+\tdone >>actual\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\tprepared\n+\tref:refs/heads/main $ZERO_OID refs/heads/symref\n+\tcommitted\n+\tref:refs/heads/main $ZERO_OID refs/heads/symref\n+\tEOF\n+\n+\tgit update-ref --no-deref --stdin <<-EOF &&\n+\tstart\n+\tsymref-verify refs/heads/symref refs/heads/main\n+\tprepare\n+\tcommit\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \n2.43.GIT\n\n"},{"id":"495288","messageId":"20240522090326.1268326-4-knayak@gitlab.com","threadId":"61469","inReplyTo":"20240522090326.1268326-1-knayak@gitlab.com","subject":"[PATCH v2 3/6] update-ref: add support for 'symref-delete' command","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-22T09:03:23Z","receivedAt":"2024-05-22T09:03:34Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"From: Karthik Nayak <karthik.188@gmail.com>\n\nAdd a new command 'symref-delete' to allow deletions of symbolic refs in\na transaction via the '--stdin' mode of the 'git-update-ref' command.\nThe 'symref-delete' command can, when given an <old-target>, delete the\nprovided <ref> only when it points to <old-target>.\n\nThis command is only compatible with the 'no-deref' mode because we\noptionally want to check the 'old_target' of the ref being deleted.\nDe-referencing a symbolic ref would provide a regular ref and we already\nhave the 'delete' command for regular refs.\n\nWhile users can also use 'git symbolic-ref -d' to delete symbolic refs,\nthe 'symref-delete' command in 'git-update-ref' allows users to do so\nwithin a transaction, which promises atomicity of the operation and can\nbe batched with other commands.\n\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n Documentation/git-update-ref.txt |  5 +++\n builtin/fetch.c                  |  2 +-\n builtin/receive-pack.c           |  3 +-\n builtin/update-ref.c             | 33 ++++++++++++++++++-\n refs.c                           | 14 ++++++---\n refs.h                           |  4 ++-\n t/t1400-update-ref.sh            | 54 ++++++++++++++++++++++++++++++++\n t/t1416-ref-transaction-hooks.sh | 19 ++++++++++-\n 8 files changed, 125 insertions(+), 9 deletions(-)\n\ndiff --git a/Documentation/git-update-ref.txt b/Documentation/git-update-ref.txt\nindex 9fe78b3501..16e02f6979 100644\n--- a/Documentation/git-update-ref.txt\n+++ b/Documentation/git-update-ref.txt\n@@ -65,6 +65,7 @@ performs all modifications together.  Specify commands of the form:\n \tcreate SP <ref> SP <new-oid> LF\n \tdelete SP <ref> [SP <old-oid>] LF\n \tverify SP <ref> [SP <old-oid>] LF\n+\tsymref-delete SP <ref> [SP <old-target>] LF\n \tsymref-verify SP <ref> [SP <old-target>] LF\n \toption SP <opt> LF\n \tstart LF\n@@ -87,6 +88,7 @@ quoting:\n \tcreate SP <ref> NUL <new-oid> NUL\n \tdelete SP <ref> NUL [<old-oid>] NUL\n \tverify SP <ref> NUL [<old-oid>] NUL\n+\tsymref-delete SP <ref> [NUL <old-target>] NUL\n \tsymref-verify SP <ref> [NUL <old-target>] NUL\n \toption SP <opt> NUL\n \tstart NUL\n@@ -119,6 +121,9 @@ verify::\n \tVerify <ref> against <old-oid> but do not change it.  If\n \t<old-oid> is zero or missing, the ref must not exist.\n \n+symref-delete::\n+\tDelete <ref> after verifying it exists with <old-target>, if given.\n+\n symref-verify::\n \tVerify symbolic <ref> against <old-target> but do not change it.\n \tIf <old-target> is missing, the ref must not exist.  Can only be\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 66840b7c5b..d02592efca 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1383,7 +1383,7 @@ static int prune_refs(struct display_state *display_state,\n \t\tif (transaction) {\n \t\t\tfor (ref = stale_refs; ref; ref = ref->next) {\n \t\t\t\tresult = ref_transaction_delete(transaction, ref->name, NULL, 0,\n-\t\t\t\t\t\t\t\t\"fetch: prune\", &err);\n+\t\t\t\t\t\t\t\tNULL, \"fetch: prune\", &err);\n \t\t\t\tif (result)\n \t\t\t\t\tgoto cleanup;\n \t\t\t}\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex b150ef39a8..9a4667d57d 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1576,7 +1576,8 @@ static const char *update(struct command *cmd, struct shallow_info *si)\n \t\tif (ref_transaction_delete(transaction,\n \t\t\t\t\t   namespaced_name,\n \t\t\t\t\t   old_oid,\n-\t\t\t\t\t   0, \"push\", &err)) {\n+\t\t\t\t\t   0, NULL,\n+\t\t\t\t\t   \"push\", &err)) {\n \t\t\trp_error(\"%s\", err.buf);\n \t\t\tret = \"failed to delete\";\n \t\t} else {\ndiff --git a/builtin/update-ref.c b/builtin/update-ref.c\nindex 6dce1cd663..0ef9c38d8d 100644\n--- a/builtin/update-ref.c\n+++ b/builtin/update-ref.c\n@@ -293,7 +293,7 @@ static void parse_cmd_delete(struct ref_transaction *transaction,\n \n \tif (ref_transaction_delete(transaction, refname,\n \t\t\t\t   have_old ? &old_oid : NULL,\n-\t\t\t\t   update_flags, msg, &err))\n+\t\t\t\t   update_flags, NULL, msg, &err))\n \t\tdie(\"%s\", err.buf);\n \n \tupdate_flags = default_flags;\n@@ -301,6 +301,36 @@ static void parse_cmd_delete(struct ref_transaction *transaction,\n \tstrbuf_release(&err);\n }\n \n+\n+static void parse_cmd_symref_delete(struct ref_transaction *transaction,\n+\t\t\t\t    const char *next, const char *end)\n+{\n+\tstruct strbuf err = STRBUF_INIT;\n+\tchar *refname, *old_target;\n+\n+\tif (!(update_flags & REF_NO_DEREF))\n+\t\tdie(\"symref-delete: cannot operate with deref mode\");\n+\n+\trefname = parse_refname(&next);\n+\tif (!refname)\n+\t\tdie(\"symref-delete: missing <ref>\");\n+\n+\told_target = parse_next_refname(&next);\n+\n+\tif (*next != line_termination)\n+\t\tdie(\"symref-delete %s: extra input: %s\", refname, next);\n+\n+\tif (ref_transaction_delete(transaction, refname, NULL,\n+\t\t\t\t   update_flags, old_target, msg, &err))\n+\t\tdie(\"%s\", err.buf);\n+\n+\tupdate_flags = default_flags;\n+\tfree(refname);\n+\tfree(old_target);\n+\tstrbuf_release(&err);\n+}\n+\n+\n static void parse_cmd_verify(struct ref_transaction *transaction,\n \t\t\t     const char *next, const char *end)\n {\n@@ -443,6 +473,7 @@ static const struct parse_cmd {\n \t{ \"create\",        parse_cmd_create,        2, UPDATE_REFS_OPEN },\n \t{ \"delete\",        parse_cmd_delete,        2, UPDATE_REFS_OPEN },\n \t{ \"verify\",        parse_cmd_verify,        2, UPDATE_REFS_OPEN },\n+\t{ \"symref-delete\", parse_cmd_symref_delete, 2, UPDATE_REFS_OPEN },\n \t{ \"symref-verify\", parse_cmd_symref_verify, 2, UPDATE_REFS_OPEN },\n \t{ \"option\",        parse_cmd_option,        1, UPDATE_REFS_OPEN },\n \t{ \"start\",         parse_cmd_start,         0, UPDATE_REFS_STARTED },\ndiff --git a/refs.c b/refs.c\nindex 865264d487..3afebfcb63 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -979,7 +979,7 @@ int refs_delete_ref(struct ref_store *refs, const char *msg,\n \ttransaction = ref_store_transaction_begin(refs, &err);\n \tif (!transaction ||\n \t    ref_transaction_delete(transaction, refname, old_oid,\n-\t\t\t\t   flags, msg, &err) ||\n+\t\t\t\t   flags, NULL, msg, &err) ||\n \t    ref_transaction_commit(transaction, &err)) {\n \t\terror(\"%s\", err.buf);\n \t\tref_transaction_free(transaction);\n@@ -1317,14 +1317,20 @@ int ref_transaction_create(struct ref_transaction *transaction,\n int ref_transaction_delete(struct ref_transaction *transaction,\n \t\t\t   const char *refname,\n \t\t\t   const struct object_id *old_oid,\n-\t\t\t   unsigned int flags, const char *msg,\n+\t\t\t   unsigned int flags,\n+\t\t\t   const char *old_target,\n+\t\t\t   const char *msg,\n \t\t\t   struct strbuf *err)\n {\n \tif (old_oid && is_null_oid(old_oid))\n \t\tBUG(\"delete called with old_oid set to zeros\");\n+\tif (old_oid && old_target)\n+\t\tBUG(\"delete called with both old_oid and old_target set\");\n+\tif (old_target && !(flags & REF_NO_DEREF))\n+\t\tBUG(\"delete cannot operate on symrefs with deref mode\");\n \treturn ref_transaction_update(transaction, refname,\n \t\t\t\t      null_oid(), old_oid,\n-\t\t\t\t      NULL, NULL, flags,\n+\t\t\t\t      NULL, old_target, flags,\n \t\t\t\t      msg, err);\n }\n \n@@ -2767,7 +2773,7 @@ int refs_delete_refs(struct ref_store *refs, const char *logmsg,\n \n \tfor_each_string_list_item(item, refnames) {\n \t\tret = ref_transaction_delete(transaction, item->string,\n-\t\t\t\t\t     NULL, flags, msg, &err);\n+\t\t\t\t\t     NULL, flags, NULL, msg, &err);\n \t\tif (ret) {\n \t\t\twarning(_(\"could not delete reference %s: %s\"),\n \t\t\t\titem->string, err.buf);\ndiff --git a/refs.h b/refs.h\nindex 48cec1ba72..053c382f0b 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -767,7 +767,9 @@ int ref_transaction_create(struct ref_transaction *transaction,\n int ref_transaction_delete(struct ref_transaction *transaction,\n \t\t\t   const char *refname,\n \t\t\t   const struct object_id *old_oid,\n-\t\t\t   unsigned int flags, const char *msg,\n+\t\t\t   unsigned int flags,\n+\t\t\t   const char *old_target,\n+\t\t\t   const char *msg,\n \t\t\t   struct strbuf *err);\n \n /*\ndiff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\nindex 07e111b063..22d560f0bf 100755\n--- a/t/t1400-update-ref.sh\n+++ b/t/t1400-update-ref.sh\n@@ -1729,6 +1729,60 @@ do\n \t\ttest_cmp expect actual\n \t'\n \n+\ttest_expect_success \"stdin $type symref-delete fails without --no-deref\" '\n+\t\tgit symbolic-ref refs/heads/symref $a &&\n+\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$a\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n+\t\tgrep \"fatal: symref-delete: cannot operate with deref mode\" err\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-delete fails with no ref\" '\n+\t\tformat_command $type \"symref-delete \" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n+\t\tgrep \"fatal: symref-delete: missing <ref>\" err\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-delete fails with too many arguments\" '\n+\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$a\" \"$a\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n+\t\tif test \"$type\" = \"-z\"\n+\t\tthen\n+\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\telse\n+\t\t\tgrep \"fatal: symref-delete refs/heads/symref: extra input:  $a\" err\n+\t\tfi\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-delete fails with wrong old value\" '\n+\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$m\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n+\t\tgrep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected refs/heads/main\" err &&\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\techo $a >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-delete works with right old value\" '\n+\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\ttest_must_fail git rev-parse --verify -q refs/heads/symref\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-delete works with empty old value\" '\n+\t\tgit symbolic-ref refs/heads/symref $a >stdin &&\n+\t\tformat_command $type \"symref-delete refs/heads/symref\" \"\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\ttest_must_fail git rev-parse --verify -q $b\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-delete succeeds for dangling reference\" '\n+\t\ttest_must_fail git symbolic-ref refs/heads/nonexistent &&\n+\t\tgit symbolic-ref refs/heads/symref2 refs/heads/nonexistent &&\n+\t\tformat_command $type \"symref-delete refs/heads/symref2\" \"refs/heads/nonexistent\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\ttest_must_fail git symbolic-ref -d refs/heads/symref2\n+\t'\n+\n done\n \n test_done\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex fd58b902f4..ccde1b944b 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -162,6 +162,7 @@ test_expect_success 'hook gets all queued symref updates' '\n \n \tgit update-ref refs/heads/branch $POST_OID &&\n \tgit symbolic-ref refs/heads/symref refs/heads/main &&\n+\tgit symbolic-ref refs/heads/symrefd refs/heads/main &&\n \n \ttest_hook reference-transaction <<-\\EOF &&\n \techo \"$*\" >>actual\n@@ -171,16 +172,32 @@ test_expect_success 'hook gets all queued symref updates' '\n \tdone >>actual\n \tEOF\n \n-\tcat >expect <<-EOF &&\n+\t# In the files backend, \"delete\" also triggers an additional transaction\n+\t# update on the packed-refs backend, which constitutes additional reflog\n+\t# entries.\n+\tif test_have_prereq REFFILES\n+\tthen\n+\t\tcat >expect <<-EOF\n+\t\taborted\n+\t\t$ZERO_OID $ZERO_OID refs/heads/symrefd\n+\t\tEOF\n+\telse\n+\t\t>expect\n+\tfi &&\n+\n+\tcat >>expect <<-EOF &&\n \tprepared\n \tref:refs/heads/main $ZERO_OID refs/heads/symref\n+\tref:refs/heads/main $ZERO_OID refs/heads/symrefd\n \tcommitted\n \tref:refs/heads/main $ZERO_OID refs/heads/symref\n+\tref:refs/heads/main $ZERO_OID refs/heads/symrefd\n \tEOF\n \n \tgit update-ref --no-deref --stdin <<-EOF &&\n \tstart\n \tsymref-verify refs/heads/symref refs/heads/main\n+\tsymref-delete refs/heads/symrefd refs/heads/main\n \tprepare\n \tcommit\n \tEOF\n-- \n2.43.GIT\n\n"},{"id":"495289","messageId":"20240522090326.1268326-5-knayak@gitlab.com","threadId":"61469","inReplyTo":"20240522090326.1268326-1-knayak@gitlab.com","subject":"[PATCH v2 4/6] update-ref: add support for 'symref-create' command","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-22T09:03:24Z","receivedAt":"2024-05-22T09:03:35Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"From: Karthik Nayak <karthik.188@gmail.com>\n\nAdd 'symref-create' command to the '--stdin' mode 'git-update-ref' to\nallow creation of symbolic refs in a transaction. The 'symref-create'\ncommand takes in a <new-target>, which the created <ref> will point to.\n\nAlso, support the 'core.prefersymlinkrefs' config, wherein if the config\nis set and the filesystem supports symlinks, we create the symbolic ref\nas a symlink. We fallback to creating a regular symref if creating the\nsymlink is unsuccessful.\n\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n Documentation/git-update-ref.txt |  6 +++\n builtin/clone.c                  |  2 +-\n builtin/update-ref.c             | 32 +++++++++++++++-\n refs.c                           |  9 +++--\n refs.h                           |  1 +\n t/t0600-reffiles-backend.sh      | 32 ++++++++++++++++\n t/t1400-update-ref.sh            | 65 ++++++++++++++++++++++++++++++++\n t/t1416-ref-transaction-hooks.sh |  3 ++\n t/t5605-clone-local.sh           |  2 +-\n 9 files changed, 146 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/git-update-ref.txt b/Documentation/git-update-ref.txt\nindex 16e02f6979..364ef78af1 100644\n--- a/Documentation/git-update-ref.txt\n+++ b/Documentation/git-update-ref.txt\n@@ -65,6 +65,7 @@ performs all modifications together.  Specify commands of the form:\n \tcreate SP <ref> SP <new-oid> LF\n \tdelete SP <ref> [SP <old-oid>] LF\n \tverify SP <ref> [SP <old-oid>] LF\n+\tsymref-create SP <ref> SP <new-target> LF\n \tsymref-delete SP <ref> [SP <old-target>] LF\n \tsymref-verify SP <ref> [SP <old-target>] LF\n \toption SP <opt> LF\n@@ -88,6 +89,7 @@ quoting:\n \tcreate SP <ref> NUL <new-oid> NUL\n \tdelete SP <ref> NUL [<old-oid>] NUL\n \tverify SP <ref> NUL [<old-oid>] NUL\n+\tsymref-create SP <ref> NUL <new-target> NUL\n \tsymref-delete SP <ref> [NUL <old-target>] NUL\n \tsymref-verify SP <ref> [NUL <old-target>] NUL\n \toption SP <opt> NUL\n@@ -121,6 +123,10 @@ verify::\n \tVerify <ref> against <old-oid> but do not change it.  If\n \t<old-oid> is zero or missing, the ref must not exist.\n \n+symref-create:\n+\tCreate symbolic ref <ref> with <new-target> after verifying\n+\tit does not exist.\n+\n symref-delete::\n \tDelete <ref> after verifying it exists with <old-target>, if given.\n \ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 93fdfc945a..ac1e131d8b 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -546,7 +546,7 @@ static void write_remote_refs(const struct ref *local_refs)\n \t\tif (!r->peer_ref)\n \t\t\tcontinue;\n \t\tif (ref_transaction_create(t, r->peer_ref->name, &r->old_oid,\n-\t\t\t\t\t   0, NULL, &err))\n+\t\t\t\t\t   NULL, 0, NULL, &err))\n \t\t\tdie(\"%s\", err.buf);\n \t}\n \ndiff --git a/builtin/update-ref.c b/builtin/update-ref.c\nindex 0ef9c38d8d..16d184603b 100644\n--- a/builtin/update-ref.c\n+++ b/builtin/update-ref.c\n@@ -257,7 +257,7 @@ static void parse_cmd_create(struct ref_transaction *transaction,\n \tif (*next != line_termination)\n \t\tdie(\"create %s: extra input: %s\", refname, next);\n \n-\tif (ref_transaction_create(transaction, refname, &new_oid,\n+\tif (ref_transaction_create(transaction, refname, &new_oid, NULL,\n \t\t\t\t   update_flags | create_reflog_flag,\n \t\t\t\t   msg, &err))\n \t\tdie(\"%s\", err.buf);\n@@ -267,6 +267,35 @@ static void parse_cmd_create(struct ref_transaction *transaction,\n \tstrbuf_release(&err);\n }\n \n+\n+static void parse_cmd_symref_create(struct ref_transaction *transaction,\n+\t\t\t\t    const char *next, const char *end)\n+{\n+\tstruct strbuf err = STRBUF_INIT;\n+\tchar *refname, *new_target;\n+\n+\trefname = parse_refname(&next);\n+\tif (!refname)\n+\t\tdie(\"symref-create: missing <ref>\");\n+\n+\tnew_target = parse_next_refname(&next);\n+\tif (!new_target)\n+\t\tdie(\"symref-create %s: missing <new-target>\", refname);\n+\n+\tif (*next != line_termination)\n+\t\tdie(\"symref-create %s: extra input: %s\", refname, next);\n+\n+\tif (ref_transaction_create(transaction, refname, NULL, new_target,\n+\t\t\t\t   update_flags | create_reflog_flag,\n+\t\t\t\t   msg, &err))\n+\t\tdie(\"%s\", err.buf);\n+\n+\tupdate_flags = default_flags;\n+\tfree(refname);\n+\tfree(new_target);\n+\tstrbuf_release(&err);\n+}\n+\n static void parse_cmd_delete(struct ref_transaction *transaction,\n \t\t\t     const char *next, const char *end)\n {\n@@ -473,6 +502,7 @@ static const struct parse_cmd {\n \t{ \"create\",        parse_cmd_create,        2, UPDATE_REFS_OPEN },\n \t{ \"delete\",        parse_cmd_delete,        2, UPDATE_REFS_OPEN },\n \t{ \"verify\",        parse_cmd_verify,        2, UPDATE_REFS_OPEN },\n+\t{ \"symref-create\", parse_cmd_symref_create, 2, UPDATE_REFS_OPEN },\n \t{ \"symref-delete\", parse_cmd_symref_delete, 2, UPDATE_REFS_OPEN },\n \t{ \"symref-verify\", parse_cmd_symref_verify, 2, UPDATE_REFS_OPEN },\n \t{ \"option\",        parse_cmd_option,        1, UPDATE_REFS_OPEN },\ndiff --git a/refs.c b/refs.c\nindex 3afebfcb63..7172f31bf2 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1302,15 +1302,18 @@ int ref_transaction_update(struct ref_transaction *transaction,\n int ref_transaction_create(struct ref_transaction *transaction,\n \t\t\t   const char *refname,\n \t\t\t   const struct object_id *new_oid,\n+\t\t\t   const char *new_target,\n \t\t\t   unsigned int flags, const char *msg,\n \t\t\t   struct strbuf *err)\n {\n-\tif (!new_oid || is_null_oid(new_oid)) {\n-\t\tstrbuf_addf(err, \"'%s' has a null OID\", refname);\n+\tif (new_oid && new_target)\n+\t\tBUG(\"create called with both new_oid and new_target set\");\n+\tif ((!new_oid || is_null_oid(new_oid)) && !new_target) {\n+\t\tstrbuf_addf(err, \"'%s' has neither a valid OID nor a target\", refname);\n \t\treturn 1;\n \t}\n \treturn ref_transaction_update(transaction, refname, new_oid,\n-\t\t\t\t      null_oid(), NULL, NULL, flags,\n+\t\t\t\t      null_oid(), new_target, NULL, flags,\n \t\t\t\t      msg, err);\n }\n \ndiff --git a/refs.h b/refs.h\nindex 053c382f0b..055cc9173b 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -753,6 +753,7 @@ int ref_transaction_update(struct ref_transaction *transaction,\n int ref_transaction_create(struct ref_transaction *transaction,\n \t\t\t   const char *refname,\n \t\t\t   const struct object_id *new_oid,\n+\t\t\t   const char *new_target,\n \t\t\t   unsigned int flags, const char *msg,\n \t\t\t   struct strbuf *err);\n \ndiff --git a/t/t0600-reffiles-backend.sh b/t/t0600-reffiles-backend.sh\nindex a390cffc80..7cfb5e8c6c 100755\n--- a/t/t0600-reffiles-backend.sh\n+++ b/t/t0600-reffiles-backend.sh\n@@ -468,4 +468,36 @@ test_expect_success POSIXPERM 'git reflog expire honors core.sharedRepository' '\n \tesac\n '\n \n+test_expect_success SYMLINKS 'symref transaction supports symlinks' '\n+\ttest_when_finished \"git symbolic-ref -d TEST_SYMREF_HEAD\" &&\n+\tgit update-ref refs/heads/new @ &&\n+\ttest_config core.prefersymlinkrefs true &&\n+\tcat >stdin <<-EOF &&\n+\tstart\n+\tsymref-create TEST_SYMREF_HEAD refs/heads/new\n+\tprepare\n+\tcommit\n+\tEOF\n+\tgit update-ref --no-deref --stdin <stdin &&\n+\ttest_path_is_symlink .git/TEST_SYMREF_HEAD &&\n+\ttest \"$(test_readlink .git/TEST_SYMREF_HEAD)\" = refs/heads/new\n+'\n+\n+test_expect_success 'symref transaction supports false symlink config' '\n+\ttest_when_finished \"git symbolic-ref -d TEST_SYMREF_HEAD\" &&\n+\tgit update-ref refs/heads/new @ &&\n+\ttest_config core.prefersymlinkrefs false &&\n+\tcat >stdin <<-EOF &&\n+\tstart\n+\tsymref-create TEST_SYMREF_HEAD refs/heads/new\n+\tprepare\n+\tcommit\n+\tEOF\n+\tgit update-ref --no-deref --stdin <stdin &&\n+\ttest_path_is_file .git/TEST_SYMREF_HEAD &&\n+\tgit symbolic-ref TEST_SYMREF_HEAD >actual &&\n+\techo refs/heads/new >expect &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\ndiff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\nindex 22d560f0bf..78001f879a 100755\n--- a/t/t1400-update-ref.sh\n+++ b/t/t1400-update-ref.sh\n@@ -1783,6 +1783,71 @@ do\n \t\ttest_must_fail git symbolic-ref -d refs/heads/symref2\n \t'\n \n+\ttest_expect_success \"stdin $type symref-create fails with too many arguments\" '\n+\t\tformat_command $type \"symref-create refs/heads/symref\" \"$a\" \"$a\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n+\t\tif test \"$type\" = \"-z\"\n+\t\tthen\n+\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\telse\n+\t\t\tgrep \"fatal: symref-create refs/heads/symref: extra input:  $a\" err\n+\t\tfi\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-create fails with no target\" '\n+\t\tformat_command $type \"symref-create refs/heads/symref\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-create fails with empty target\" '\n+\t\tformat_command $type \"symref-create refs/heads/symref\" \"\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-create works\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tformat_command $type \"symref-create refs/heads/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\techo $a >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-create works with --no-deref\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tformat_command $type \"symref-create refs/heads/symref\" \"$a\" &&\n+\t\tgit update-ref --stdin $type <stdin 2>err\n+\t'\n+\n+\ttest_expect_success \"stdin $type create dangling symref ref works\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tformat_command $type \"symref-create refs/heads/symref\" \"refs/heads/unkown\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\techo refs/heads/unkown >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-create does not create reflogs by default\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/symref\" &&\n+\t\tformat_command $type \"symref-create refs/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\tgit symbolic-ref refs/symref >expect &&\n+\t\techo $a >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest_must_fail git reflog exists refs/symref\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-create reflogs with --create-reflog\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tformat_command $type \"symref-create refs/heads/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref --create-reflog --stdin $type --no-deref <stdin &&\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\techo $a >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\tgit reflog exists refs/heads/symref\n+\t'\n+\n done\n \n test_done\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex ccde1b944b..ff77dcca6b 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -189,15 +189,18 @@ test_expect_success 'hook gets all queued symref updates' '\n \tprepared\n \tref:refs/heads/main $ZERO_OID refs/heads/symref\n \tref:refs/heads/main $ZERO_OID refs/heads/symrefd\n+\t$ZERO_OID ref:refs/heads/main refs/heads/symrefc\n \tcommitted\n \tref:refs/heads/main $ZERO_OID refs/heads/symref\n \tref:refs/heads/main $ZERO_OID refs/heads/symrefd\n+\t$ZERO_OID ref:refs/heads/main refs/heads/symrefc\n \tEOF\n \n \tgit update-ref --no-deref --stdin <<-EOF &&\n \tstart\n \tsymref-verify refs/heads/symref refs/heads/main\n \tsymref-delete refs/heads/symrefd refs/heads/main\n+\tsymref-create refs/heads/symrefc refs/heads/main\n \tprepare\n \tcommit\n \tEOF\ndiff --git a/t/t5605-clone-local.sh b/t/t5605-clone-local.sh\nindex a3055869bc..339d8c786f 100755\n--- a/t/t5605-clone-local.sh\n+++ b/t/t5605-clone-local.sh\n@@ -163,7 +163,7 @@ test_expect_success REFFILES 'local clone from repo with corrupt refs fails grac\n \techo a >corrupt/.git/refs/heads/topic &&\n \n \ttest_must_fail git clone corrupt working 2>err &&\n-\tgrep \"has a null OID\" err\n+\tgrep \"has neither a valid OID nor a target\" err\n '\n \n test_done\n-- \n2.43.GIT\n\n"},{"id":"495290","messageId":"20240522090326.1268326-6-knayak@gitlab.com","threadId":"61469","inReplyTo":"20240522090326.1268326-1-knayak@gitlab.com","subject":"[PATCH v2 5/6] reftable: pick either 'oid' or 'target' for new updates","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-22T09:03:25Z","receivedAt":"2024-05-22T09:03:36Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"From: Karthik Nayak <karthik.188@gmail.com>\n\nWhen creating a reference transaction update, we can provide the old/new\noid/target for the update. We have checks in place to ensure that for\neach old/new, either oid or target is set and not both.\n\nIn the reftable backend, when dealing with updates without the\n`REF_NO_DEREF` flag, we don't selectively propagate data as needed.\nSince there are no active users of the path, this is not caught. As we\nwant to introduce the 'symref-update' command in the upcoming commit,\nwhich would use this flow, correct it.\n\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n refs/reftable-backend.c | 5 +++--\n 1 file changed, 3 insertions(+), 2 deletions(-)\n\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex dc7aa7f274..8582f2ff2f 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -896,8 +896,9 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n \t\t\t\t */\n \t\t\t\tnew_update = ref_transaction_add_update(\n \t\t\t\t\ttransaction, referent.buf, new_flags,\n-\t\t\t\t\t&u->new_oid, &u->old_oid, u->new_target,\n-\t\t\t\t\tu->old_target, u->msg);\n+\t\t\t\t\tu->new_target ? NULL : &u->new_oid,\n+\t\t\t\t\tu->old_target ? NULL : &u->old_oid,\n+\t\t\t\t\tu->new_target, u->old_target, u->msg);\n \n \t\t\t\tnew_update->parent_update = u;\n \n-- \n2.43.GIT\n\n"},{"id":"495291","messageId":"20240522090326.1268326-7-knayak@gitlab.com","threadId":"61469","inReplyTo":"20240522090326.1268326-1-knayak@gitlab.com","subject":"[PATCH v2 6/6] update-ref: add support for 'symref-update' command","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-22T09:03:26Z","receivedAt":"2024-05-22T09:03:38Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"From: Karthik Nayak <karthik.188@gmail.com>\n\nAdd 'symref-update' command to the '--stdin' mode of 'git-update-ref' to\nallow updates of symbolic refs. The 'symref-update' command takes in a\n<new-target>, which the <ref> will be updated to. If the <ref> doesn't\nexist it will be created.\n\nIt also optionally takes either an `ref <old-target>` or `oid\n<old-oid>`. If the <old-target> is provided, it checks to see if the\n<ref> targets the <old-target> before the update. If <old-oid> is provided\nit checks <ref> to ensure that it is a regular ref and <old-oid> is the\nOID before the update. This by extension also means that this when a\nzero <old-oid> is provided, it ensures that the ref didn't exist before.\n\nThe divergence in syntax from the regular `update` command is because if\nwe don't use a `(ref | oid)` prefix for the old_value, then there is\nambiguity around if the value provided should be treated as an oid or a\nreference. This is more so the reason, because we allow anything\ncommittish to be provided as an oid.\n\nThe command allows users to perform symbolic ref updates within a\ntransaction. This provides atomicity and allows users to perform a set\nof operations together.\n\nThis command supports deref mode, to ensure that we can update\ndereferenced regular refs to symrefs.\n\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n Documentation/git-update-ref.txt |   7 ++\n builtin/update-ref.c             |  90 ++++++++++++++\n t/t1400-update-ref.sh            | 203 +++++++++++++++++++++++++++++++\n t/t1416-ref-transaction-hooks.sh |   4 +\n 4 files changed, 304 insertions(+)\n\ndiff --git a/Documentation/git-update-ref.txt b/Documentation/git-update-ref.txt\nindex 364ef78af1..afcf33cf60 100644\n--- a/Documentation/git-update-ref.txt\n+++ b/Documentation/git-update-ref.txt\n@@ -65,6 +65,7 @@ performs all modifications together.  Specify commands of the form:\n \tcreate SP <ref> SP <new-oid> LF\n \tdelete SP <ref> [SP <old-oid>] LF\n \tverify SP <ref> [SP <old-oid>] LF\n+\tsymref-update SP <ref> SP <new-target> [SP (ref SP <old-target> | oid SP <old-oid>)] LF\n \tsymref-create SP <ref> SP <new-target> LF\n \tsymref-delete SP <ref> [SP <old-target>] LF\n \tsymref-verify SP <ref> [SP <old-target>] LF\n@@ -89,6 +90,7 @@ quoting:\n \tcreate SP <ref> NUL <new-oid> NUL\n \tdelete SP <ref> NUL [<old-oid>] NUL\n \tverify SP <ref> NUL [<old-oid>] NUL\n+\tsymref-update SP <ref> NUL <new-target> [NUL (ref NUL <old-target> | oid NUL <old-oid>)] NUL\n \tsymref-create SP <ref> NUL <new-target> NUL\n \tsymref-delete SP <ref> [NUL <old-target>] NUL\n \tsymref-verify SP <ref> [NUL <old-target>] NUL\n@@ -119,6 +121,11 @@ delete::\n \tDelete <ref> after verifying it exists with <old-oid>, if\n \tgiven.  If given, <old-oid> may not be zero.\n \n+symref-update::\n+\tSet <ref> to <new-target> after verifying <old-target> or <old-oid>,\n+\tif given. Specify a zero <old-oid> to ensure that the ref does not\n+\texist before the update.\n+\n verify::\n \tVerify <ref> against <old-oid> but do not change it.  If\n \t<old-oid> is zero or missing, the ref must not exist.\ndiff --git a/builtin/update-ref.c b/builtin/update-ref.c\nindex 16d184603b..bda37c161d 100644\n--- a/builtin/update-ref.c\n+++ b/builtin/update-ref.c\n@@ -98,6 +98,40 @@ static char *parse_next_refname(const char **next)\n \treturn parse_refname(next);\n }\n \n+/*\n+ * Wrapper around parse_arg which skips the next delimiter.\n+ */\n+static char *parse_next_arg(const char **next)\n+{\n+\tstruct strbuf arg = STRBUF_INIT;\n+\n+\tif (line_termination) {\n+\t\t/* Without -z, consume SP and use next argument */\n+\t\tif (!**next || **next == line_termination)\n+\t\t\treturn NULL;\n+\t\tif (**next != ' ')\n+\t\t\tdie(\"expected SP but got: %s\", *next);\n+\t} else {\n+\t\t/* With -z, read the next NUL-terminated line */\n+\t\tif (**next)\n+\t\t\treturn NULL;\n+\t}\n+\t/* Skip the delimiter */\n+\t(*next)++;\n+\n+\tif (line_termination) {\n+\t\t/* Without -z, use the next argument */\n+\t\t*next = parse_arg(*next, &arg);\n+\t} else {\n+\t\t/* With -z, use everything up to the next NUL */\n+\t\tstrbuf_addstr(&arg, *next);\n+\t\t*next += arg.len;\n+\t}\n+\n+\tif (arg.len)\n+\t\treturn strbuf_detach(&arg, NULL);\n+\treturn NULL;\n+}\n \n /*\n  * The value being parsed is <old-oid> (as opposed to <new-oid>; the\n@@ -237,6 +271,61 @@ static void parse_cmd_update(struct ref_transaction *transaction,\n \tstrbuf_release(&err);\n }\n \n+static void parse_cmd_symref_update(struct ref_transaction *transaction,\n+\t\t\t\t    const char *next, const char *end)\n+{\n+\tchar *refname, *new_target, *old_arg;\n+\tchar *old_target = NULL;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tstruct object_id old_oid;\n+\tint have_old_oid = 0;\n+\n+\trefname = parse_refname(&next);\n+\tif (!refname)\n+\t\tdie(\"symref-update: missing <ref>\");\n+\n+\tnew_target = parse_next_refname(&next);\n+\tif (!new_target)\n+\t\tdie(\"symref-update %s: missing <new-target>\", refname);\n+\n+\told_arg = parse_next_arg(&next);\n+\tif (old_arg) {\n+\t\told_target = parse_next_arg(&next);\n+\t\tif (!old_target)\n+\t\t\tdie(\"symref-update %s: expected old value\", refname);\n+\n+\t\tif (!strcmp(old_arg, \"oid\")) {\n+\t\t\tif (repo_get_oid(the_repository, old_target, &old_oid))\n+\t\t\t\tdie(\"symref-update %s: invalid oid: %s\", refname, old_target);\n+\n+\t\t\told_target = NULL;\n+\t\t\thave_old_oid = 1;\n+\t\t} else if (!strcmp(old_arg, \"ref\")) {\n+\t\t\tif (check_refname_format(old_target, REFNAME_ALLOW_ONELEVEL))\n+\t\t\t\tdie(\"symref-update %s: invalid ref: %s\", refname, old_target);\n+\t\t} else {\n+\t\t\tdie(\"symref-update %s: invalid arg '%s' for old value\", refname, old_arg);\n+\t\t}\n+\t}\n+\n+\tif (*next != line_termination)\n+\t\tdie(\"symref-update %s: extra input: %s\", refname, next);\n+\n+\tif (ref_transaction_update(transaction, refname, NULL,\n+\t\t\t\t   have_old_oid ? &old_oid : NULL,\n+\t\t\t\t   new_target, old_target,\n+\t\t\t\t   update_flags | create_reflog_flag,\n+\t\t\t\t   msg, &err))\n+\t\tdie(\"%s\", err.buf);\n+\n+\tupdate_flags = default_flags;\n+\tfree(refname);\n+\tfree(old_arg);\n+\tfree(old_target);\n+\tfree(new_target);\n+\tstrbuf_release(&err);\n+}\n+\n static void parse_cmd_create(struct ref_transaction *transaction,\n \t\t\t     const char *next, const char *end)\n {\n@@ -502,6 +591,7 @@ static const struct parse_cmd {\n \t{ \"create\",        parse_cmd_create,        2, UPDATE_REFS_OPEN },\n \t{ \"delete\",        parse_cmd_delete,        2, UPDATE_REFS_OPEN },\n \t{ \"verify\",        parse_cmd_verify,        2, UPDATE_REFS_OPEN },\n+\t{ \"symref-update\", parse_cmd_symref_update, 4, UPDATE_REFS_OPEN },\n \t{ \"symref-create\", parse_cmd_symref_create, 2, UPDATE_REFS_OPEN },\n \t{ \"symref-delete\", parse_cmd_symref_delete, 2, UPDATE_REFS_OPEN },\n \t{ \"symref-verify\", parse_cmd_symref_verify, 2, UPDATE_REFS_OPEN },\ndiff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\nindex 78001f879a..68afe7c228 100755\n--- a/t/t1400-update-ref.sh\n+++ b/t/t1400-update-ref.sh\n@@ -1360,6 +1360,7 @@ test_expect_success 'fails with duplicate HEAD update' '\n '\n \n test_expect_success 'fails with duplicate ref update via symref' '\n+\ttest_when_finished \"git symbolic-ref -d refs/heads/symref2\" &&\n \tgit branch target2 $A &&\n \tgit symbolic-ref refs/heads/symref2 refs/heads/target2 &&\n \tcat >stdin <<-EOF &&\n@@ -1848,6 +1849,208 @@ do\n \t\tgit reflog exists refs/heads/symref\n \t'\n \n+\ttest_expect_success \"stdin $type symref-update fails with too many arguments\" '\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"ref\" \"$a\" \"$a\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n+\t\tif test \"$type\" = \"-z\"\n+\t\tthen\n+\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\telse\n+\t\t\tgrep \"fatal: symref-update refs/heads/symref: extra input:  $a\" err\n+\t\tfi\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update fails with wrong old value argument\" '\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"foo\" \"$a\" \"$a\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n+\t\tgrep \"fatal: symref-update refs/heads/symref: invalid arg ${SQ}foo${SQ} for old value\" err\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update creates with zero old value\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"oid\" \"$Z\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update creates with no old value\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update creates dangling\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\ttest_must_fail git rev-parse refs/heads/nonexistent &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"refs/heads/nonexistent\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\techo refs/heads/nonexistent >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update fails with wrong old value\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tgit symbolic-ref refs/heads/symref $a &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$m\" \"ref\" \"$b\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n+\t\tgrep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected $b\" err &&\n+\t\ttest_must_fail git rev-parse --verify -q $c\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update updates dangling ref\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\ttest_must_fail git rev-parse refs/heads/nonexistent &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/nonexistent &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update updates dangling ref with old value\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\ttest_must_fail git rev-parse refs/heads/nonexistent &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/nonexistent &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"ref\" \"refs/heads/nonexistent\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update fails update dangling ref with wrong old value\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\ttest_must_fail git rev-parse refs/heads/nonexistent &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/nonexistent &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"ref\" \"refs/heads/wrongref\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin &&\n+\t\techo refs/heads/nonexistent >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update works with right old value\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tgit symbolic-ref refs/heads/symref $a &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$m\" \"ref\" \"$a\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\techo $m >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update works with no old value\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tgit symbolic-ref refs/heads/symref $a &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$m\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\techo $m >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update fails with empty old ref-target\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tgit symbolic-ref refs/heads/symref $a &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$m\" \"ref\" \"\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update creates (with deref)\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref --stdin $type <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref --no-recurse refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >actual &&\n+\t\tgrep \"$Z $(git rev-parse $a)\" actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update regular ref to symref with correct old-oid\" '\n+\t\ttest_when_finished \"git symbolic-ref -d --no-recurse refs/heads/regularref\" &&\n+\t\tgit update-ref --no-deref refs/heads/regularref $a &&\n+\t\tformat_command $type \"symref-update refs/heads/regularref\" \"$a\" \"oid\" \"$(git rev-parse $a)\" >stdin &&\n+\t\tgit update-ref --stdin $type <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref --no-recurse refs/heads/regularref >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/regularref >actual &&\n+\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update regular ref to symref fails with wrong old-oid\" '\n+\t\ttest_when_finished \"git update-ref -d refs/heads/regularref\" &&\n+\t\tgit update-ref --no-deref refs/heads/regularref $a &&\n+\t\tformat_command $type \"symref-update refs/heads/regularref\" \"$a\" \"oid\" \"$(git rev-parse refs/heads/target2)\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n+\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: is at $(git rev-parse $a) but expected $(git rev-parse refs/heads/target2)\" err &&\n+\t\techo $(git rev-parse $a) >expect &&\n+\t\tgit rev-parse refs/heads/regularref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update regular ref to symref fails with invalid old-oid\" '\n+\t\ttest_when_finished \"git update-ref -d refs/heads/regularref\" &&\n+\t\tgit update-ref --no-deref refs/heads/regularref $a &&\n+\t\tformat_command $type \"symref-update refs/heads/regularref\" \"$a\" \"oid\" \"not-a-ref-oid\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n+\t\tgrep \"fatal: symref-update refs/heads/regularref: invalid oid: not-a-ref-oid\" err &&\n+\t\techo $(git rev-parse $a) >expect &&\n+\t\tgit rev-parse refs/heads/regularref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update existing symref with zero old-oid\" '\n+\t\ttest_when_finished \"git symbolic-ref -d --no-recurse refs/heads/symref\" &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/target2 &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"oid\" \"$Z\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n+\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/symref${SQ}: reference already exists\" err &&\n+\t\techo refs/heads/target2 >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update regular ref to symref (with deref)\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\ttest_when_finished \"git update-ref -d --no-deref refs/heads/symref2\" &&\n+\t\tgit update-ref refs/heads/symref2 $a &&\n+\t\tgit symbolic-ref --no-recurse refs/heads/symref refs/heads/symref2 &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref $type --stdin <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref --no-recurse refs/heads/symref2 >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\techo refs/heads/symref2 >expect &&\n+\t\tgit symbolic-ref --no-recurse refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >actual &&\n+\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update regular ref to symref\" '\n+\t\ttest_when_finished \"git symbolic-ref -d --no-recurse refs/heads/regularref\" &&\n+\t\tgit update-ref --no-deref refs/heads/regularref $a &&\n+\t\tformat_command $type \"symref-update refs/heads/regularref\" \"$a\" >stdin &&\n+\t\tgit update-ref $type --stdin <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref --no-recurse refs/heads/regularref >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/regularref >actual &&\n+\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n+\t'\n+\n done\n \n test_done\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex ff77dcca6b..5a812ca3c0 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -163,6 +163,7 @@ test_expect_success 'hook gets all queued symref updates' '\n \tgit update-ref refs/heads/branch $POST_OID &&\n \tgit symbolic-ref refs/heads/symref refs/heads/main &&\n \tgit symbolic-ref refs/heads/symrefd refs/heads/main &&\n+\tgit symbolic-ref refs/heads/symrefu refs/heads/main &&\n \n \ttest_hook reference-transaction <<-\\EOF &&\n \techo \"$*\" >>actual\n@@ -190,10 +191,12 @@ test_expect_success 'hook gets all queued symref updates' '\n \tref:refs/heads/main $ZERO_OID refs/heads/symref\n \tref:refs/heads/main $ZERO_OID refs/heads/symrefd\n \t$ZERO_OID ref:refs/heads/main refs/heads/symrefc\n+\tref:refs/heads/main ref:refs/heads/branch refs/heads/symrefu\n \tcommitted\n \tref:refs/heads/main $ZERO_OID refs/heads/symref\n \tref:refs/heads/main $ZERO_OID refs/heads/symrefd\n \t$ZERO_OID ref:refs/heads/main refs/heads/symrefc\n+\tref:refs/heads/main ref:refs/heads/branch refs/heads/symrefu\n \tEOF\n \n \tgit update-ref --no-deref --stdin <<-EOF &&\n@@ -201,6 +204,7 @@ test_expect_success 'hook gets all queued symref updates' '\n \tsymref-verify refs/heads/symref refs/heads/main\n \tsymref-delete refs/heads/symrefd refs/heads/main\n \tsymref-create refs/heads/symrefc refs/heads/main\n+\tsymref-update refs/heads/symrefu refs/heads/branch ref refs/heads/main\n \tprepare\n \tcommit\n \tEOF\n-- \n2.43.GIT\n\n"},{"id":"495408","messageId":"xmqqbk4wy3ji.fsf@gitster.g","threadId":"61469","inReplyTo":"20240522090326.1268326-1-knayak@gitlab.com","subject":"Re: [PATCH v2 0/6] update-ref: add symref support for --stdin","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-23T15:02:41Z","receivedAt":"2024-05-23T15:02:48Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Karthik Nayak <karthik.188@gmail.com> writes:\n\n> The patches 1, 5 fix small issues in the reference backends. The other\n> patches 2, 3, 4 & 6, each add one of the new sub-commands.\n>\n> The series is based off master, with 'kn/ref-transaction-symref' merged\n> in. There seem to be no conflicts with 'next' or 'seen'.\n\nWait.  There is something fishy going on.\n\n> Range diff vs v1:\n>  1:  1bc4cc3fc4 =  1:  1bc4cc3fc4 refs: accept symref values in `ref_transaction_update()`\n>  2:  57d0b1e2ea =  2:  57d0b1e2ea files-backend: extract out `create_symref_lock()`\n>  3:  a8ae923f85 =  3:  a8ae923f85 refs: support symrefs in 'reference-transaction' hook\n>  4:  e9965ba477 =  4:  e9965ba477 refs: move `original_update_refname` to 'refs.c'\n>  5:  644daf7785 =  5:  644daf7785 refs: add support for transactional symref updates\n>  6:  300b38e46f =  6:  300b38e46f refs: use transaction in `refs_create_symref()`\n>  7:  f151dfe3c9 =  7:  f151dfe3c9 refs: rename `refs_create_symref()` to `refs_update_symref()`\n>  8:  4865707bda =  8:  4865707bda refs: remove `create_symref` and associated dead code\n>  9:  4cb67dce7c !  9:  2bbdeff798 refs: create and use `ref_update_ref_must_exist()`\n\n4865707bda has been part of 'next' since 0a7119f2 (Merge branch\n'kn/ref-transaction-symref' into next, 2024-05-11) and was merged to\n'master' with 4beb7a3b (Merge branch 'kn/ref-transaction-symref',\n2024-05-20).\n\nI am confused why we are seeing a total reroll of such an old topic.\n\nAlso you have one more patch at the end.  Neither the before or\nafter version of 9/9.\n\nIs this actually a single patch submission of 9/9 alone?  Patches\n1-8/9 are all old ones that are in 'master' already.\n\nPuzzled...\n\n"},{"id":"495414","messageId":"CAOLa=ZS31CuL8kDODNfM5L8gXN6RUOG5uEP5Q9JzEuaxtLEZ8g@mail.gmail.com","threadId":"61469","inReplyTo":"xmqqbk4wy3ji.fsf@gitster.g","subject":"Re: [PATCH v2 0/6] update-ref: add symref support for --stdin","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-23T15:52:32Z","receivedAt":"2024-05-23T15:52:35Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Karthik Nayak <karthik.188@gmail.com> writes:\n>\n>> The patches 1, 5 fix small issues in the reference backends. The other\n>> patches 2, 3, 4 & 6, each add one of the new sub-commands.\n>>\n>> The series is based off master, with 'kn/ref-transaction-symref' merged\n>> in. There seem to be no conflicts with 'next' or 'seen'.\n>\n> Wait.  There is something fishy going on.\n>\n>> Range diff vs v1:\n>>  1:  1bc4cc3fc4 =  1:  1bc4cc3fc4 refs: accept symref values in `ref_transaction_update()`\n>>  2:  57d0b1e2ea =  2:  57d0b1e2ea files-backend: extract out `create_symref_lock()`\n>>  3:  a8ae923f85 =  3:  a8ae923f85 refs: support symrefs in 'reference-transaction' hook\n>>  4:  e9965ba477 =  4:  e9965ba477 refs: move `original_update_refname` to 'refs.c'\n>>  5:  644daf7785 =  5:  644daf7785 refs: add support for transactional symref updates\n>>  6:  300b38e46f =  6:  300b38e46f refs: use transaction in `refs_create_symref()`\n>>  7:  f151dfe3c9 =  7:  f151dfe3c9 refs: rename `refs_create_symref()` to `refs_update_symref()`\n>>  8:  4865707bda =  8:  4865707bda refs: remove `create_symref` and associated dead code\n>>  9:  4cb67dce7c !  9:  2bbdeff798 refs: create and use `ref_update_ref_must_exist()`\n>\n> 4865707bda has been part of 'next' since 0a7119f2 (Merge branch\n> 'kn/ref-transaction-symref' into next, 2024-05-11) and was merged to\n> 'master' with 4beb7a3b (Merge branch 'kn/ref-transaction-symref',\n> 2024-05-20).\n>\n> I am confused why we are seeing a total reroll of such an old topic.\n>\n> Also you have one more patch at the end.  Neither the before or\n> after version of 9/9.\n>\n> Is this actually a single patch submission of 9/9 alone?  Patches\n> 1-8/9 are all old ones that are in 'master' already.\n>\n> Puzzled...\n\nI think this is just a mess up in the range diff, I haven't changed\nanything locally. So adding the correct range diff here:\n\nRange diff agains v1:\n\n1:  4cb67dce7c ! 1:  2bbdeff798 refs: create and use\n`ref_update_ref_must_exist()`\n    @@ Metadata\n     Author: Karthik Nayak <karthik.188@gmail.com>\n\n      ## Commit message ##\n    -    refs: create and use `ref_update_ref_must_exist()`\n    +    refs: create and use `ref_update_expects_existing_old_ref()`\n\n         The files and reftable backend, need to check if a ref must exist, so\n         that the required validation can be done. A ref must exist\nonly when the\n    @@ Commit message\n         path. As we introduce the 'symref-verify' command in the upcoming\n         commits, it is important to fix this.\n\n    -    So let's export this to a function called `ref_update_ref_must_exist()`\n    -    and expose it internally via 'refs-internal.h'.\n    +    So let's export this to a function called\n    +    `ref_update_expects_existing_old_ref()` and expose it internally via\n    +    'refs-internal.h'.\n\n         Signed-off-by: Karthik Nayak <karthik.188@gmail.com>\n\n    @@ refs.c: int ref_update_check_old_target(const char *referent,\nstruct ref_update\n      \treturn -1;\n      }\n     +\n    -+int ref_update_ref_must_exist(struct ref_update *update)\n    ++int ref_update_expects_existing_old_ref(struct ref_update *update)\n     +{\n     +\treturn (update->flags & REF_HAVE_OLD) &&\n     +\t\t(!is_null_oid(&update->old_oid) || update->old_target);\n    @@ refs/files-backend.c: static int lock_ref_for_update(struct\nfiles_ref_store *ref\n      \tstruct strbuf referent = STRBUF_INIT;\n     -\tint mustexist = (update->flags & REF_HAVE_OLD) &&\n     -\t\t!is_null_oid(&update->old_oid);\n    -+\tint mustexist = ref_update_ref_must_exist(update);\n    ++\tint mustexist = ref_update_expects_existing_old_ref(update);\n      \tint ret = 0;\n      \tstruct ref_lock *lock;\n\n    @@ refs/refs-internal.h: int ref_update_has_null_new_value(struct\nref_update *updat\n     + * Check if the ref must exist, this means that the old_oid or\n     + * old_target is non NULL.\n     + */\n    -+int ref_update_ref_must_exist(struct ref_update *update);\n    ++int ref_update_expects_existing_old_ref(struct ref_update *update);\n     +\n      #endif /* REFS_REFS_INTERNAL_H */\n\n    @@ refs/reftable-backend.c: static int\nreftable_be_transaction_prepare(struct ref_s\n      \t\tif (ret < 0)\n      \t\t\tgoto done;\n     -\t\tif (ret > 0 && (!(u->flags & REF_HAVE_OLD) ||\nis_null_oid(&u->old_oid))) {\n    -+\t\tif (ret > 0 && !ref_update_ref_must_exist(u)) {\n    ++\t\tif (ret > 0 && !ref_update_expects_existing_old_ref(u)) {\n      \t\t\t/*\n      \t\t\t * The reference does not exist, and we either have no\n      \t\t\t * old object ID or expect the reference to not exist.\n2:  9fda13b468 ! 2:  f509066cab update-ref: add support for\n'symref-verify' command\n    @@ Commit message\n         point to an object and not a ref and the regular 'verify'\ncommand can be\n         used in such situations.\n\n    -    Add required tests for symref support in 'verify' while also adding\n    -    reflog checks for the pre-existing 'verify' tests.\n    +    Add required tests for symref support in 'verify'. Since we're here,\n    +    also add reflog checks for the pre-existing 'verify' tests, there is no\n    +    divergence from behavior, but we never tested to ensure that reflog\n    +    wasn't affected by the 'verify' command.\n\n         Signed-off-by: Karthik Nayak <karthik.188@gmail.com>\n\n    @@ refs.c: int ref_transaction_delete(struct ref_transaction *transaction,\n     -\t\tBUG(\"verify called with old_oid set to NULL\");\n     +\tif (!old_target && !old_oid)\n     +\t\tBUG(\"verify called with old_oid and old_target set to NULL\");\n    ++\tif (old_oid && old_target)\n    ++\t\tBUG(\"verify called with both old_oid and old_target set\");\n     +\tif (old_target && !(flags & REF_NO_DEREF))\n     +\t\tBUG(\"verify cannot operate on symrefs with deref mode\");\n      \treturn ref_transaction_update(transaction, refname,\n    @@ t/t1400-update-ref.sh: test_expect_success PIPE 'transaction\nflushes status upda\n      \ttest_cmp expected actual\n      '\n\n    -+create_stdin_buf () {\n    ++format_command () {\n     +\tif test \"$1\" = \"-z\"\n     +\tthen\n     +\t\tshift\n    -+\t\tprintf \"$F\" \"$@\" >stdin\n    ++\t\tprintf \"$F\" \"$@\"\n     +\telse\n    -+\t\techo \"$@\" >stdin\n    ++\t\techo \"$@\"\n     +\tfi\n     +}\n     +\n     +for type in \"\" \"-z\"\n     +do\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-verify fails without\n--no-deref\" '\n    ++\ttest_expect_success \"stdin $type symref-verify fails without\n--no-deref\" '\n     +\t\tgit symbolic-ref refs/heads/symref $a &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref\" \"$a\" &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} <stdin 2>err &&\n    ++\t\tformat_command $type \"symref-verify refs/heads/symref\" \"$a\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n     +\t\tgrep \"fatal: symref-verify: cannot operate with deref mode\" err\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-verify fails with too\nmany arguments\" '\n    -+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref\" \"$a\" \"$a\" &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref\n<stdin 2>err  &&\n    ++\ttest_expect_success \"stdin $type symref-verify fails with too\nmany arguments\" '\n    ++\t\tformat_command $type \"symref-verify refs/heads/symref\" \"$a\"\n\"$a\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err  &&\n     +\t\tif test \"$type\" = \"-z\"\n     +\t\tthen\n     +\t\t\tgrep \"fatal: unknown command: $a\" err\n    @@ t/t1400-update-ref.sh: test_expect_success PIPE 'transaction\nflushes status upda\n     +\t\tfi\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-verify succeeds for\ncorrect value\" '\n    ++\ttest_expect_success \"stdin $type symref-verify succeeds for\ncorrect value\" '\n     +\t\tgit symbolic-ref refs/heads/symref >expect &&\n     +\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref\n>before &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref\" \"$a\" &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-verify refs/heads/symref\" \"$a\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual &&\n     +\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >after &&\n     +\t\ttest_cmp before after\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-verify no value is\ntreated as zero value\" '\n    ++\ttest_expect_success \"stdin $type symref-verify fails with no value\" '\n     +\t\tgit symbolic-ref refs/heads/symref >expect &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref\" \"\" &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin\n    ++\t\tformat_command $type \"symref-verify refs/heads/symref\" \"\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-verify succeeds for\ndangling reference\" '\n    ++\ttest_expect_success \"stdin $type symref-verify succeeds for\ndangling reference\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref2\" &&\n     +\t\ttest_must_fail git symbolic-ref refs/heads/nonexistent &&\n     +\t\tgit symbolic-ref refs/heads/symref2 refs/heads/nonexistent &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref2\"\n\"refs/heads/nonexistent\" &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin\n    ++\t\tformat_command $type \"symref-verify refs/heads/symref2\"\n\"refs/heads/nonexistent\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-verify fails for\nmissing reference\" '\n    ++\ttest_expect_success \"stdin $type symref-verify fails for\nmissing reference\" '\n     +\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref\n>before &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/missing\"\n\"refs/heads/unknown\" &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n    ++\t\tformat_command $type \"symref-verify refs/heads/missing\"\n\"refs/heads/unknown\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n     +\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/missing${SQ}:\nunable to resolve reference ${SQ}refs/heads/missing${SQ}\" err &&\n     +\t\ttest_must_fail git rev-parse --verify -q refs/heads/missing &&\n     +\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >after &&\n     +\t\ttest_cmp before after\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-verify fails for wrong value\" '\n    ++\ttest_expect_success \"stdin $type symref-verify fails for wrong value\" '\n     +\t\tgit symbolic-ref refs/heads/symref >expect &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref\" \"$b\" &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-verify refs/heads/symref\" \"$b\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-verify fails for\nmistaken null value\" '\n    ++\ttest_expect_success \"stdin $type symref-verify fails for\nmistaken null value\" '\n     +\t\tgit symbolic-ref refs/heads/symref >expect &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref\" \"$Z\" &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-verify refs/heads/symref\" \"$Z\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n3:  d07031827b ! 3:  a11f4c1e48 update-ref: add support for\n'symref-delete' command\n    @@ refs.c: int ref_transaction_create(struct ref_transaction *transaction,\n      {\n      \tif (old_oid && is_null_oid(old_oid))\n      \t\tBUG(\"delete called with old_oid set to zeros\");\n    ++\tif (old_oid && old_target)\n    ++\t\tBUG(\"delete called with both old_oid and old_target set\");\n     +\tif (old_target && !(flags & REF_NO_DEREF))\n     +\t\tBUG(\"delete cannot operate on symrefs with deref mode\");\n      \treturn ref_transaction_update(transaction, refname,\n    @@ refs.h: int ref_transaction_create(struct ref_transaction *transaction,\n      /*\n\n      ## t/t1400-update-ref.sh ##\n    -@@ t/t1400-update-ref.sh: do\n    - \t\ttest_cmp before after\n    - \t'\n    -\n    --\ttest_expect_success \"stdin ${type} symref-verify no value is\ntreated as zero value\" '\n    -+\ttest_expect_success \"stdin ${type} symref-verify fails with no value\" '\n    - \t\tgit symbolic-ref refs/heads/symref >expect &&\n    - \t\tcreate_stdin_buf ${type} \"symref-verify refs/heads/symref\" \"\" &&\n    - \t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin\n     @@ t/t1400-update-ref.sh: do\n      \t\ttest_cmp expect actual\n      \t'\n\n    -+\ttest_expect_success \"stdin ${type} symref-delete fails without\n--no-deref\" '\n    ++\ttest_expect_success \"stdin $type symref-delete fails without\n--no-deref\" '\n     +\t\tgit symbolic-ref refs/heads/symref $a &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-delete refs/heads/symref\" \"$a\" &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} <stdin 2>err &&\n    ++\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$a\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n     +\t\tgrep \"fatal: symref-delete: cannot operate with deref mode\" err\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-delete fails with no ref\" '\n    -+\t\tcreate_stdin_buf ${type} \"symref-delete \" &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n    ++\ttest_expect_success \"stdin $type symref-delete fails with no ref\" '\n    ++\t\tformat_command $type \"symref-delete \" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n     +\t\tgrep \"fatal: symref-delete: missing <ref>\" err\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-delete fails with too\nmany arguments\" '\n    -+\t\tcreate_stdin_buf ${type} \"symref-delete refs/heads/symref\" \"$a\" \"$a\" &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n    ++\ttest_expect_success \"stdin $type symref-delete fails with too\nmany arguments\" '\n    ++\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$a\"\n\"$a\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n     +\t\tif test \"$type\" = \"-z\"\n     +\t\tthen\n     +\t\t\tgrep \"fatal: unknown command: $a\" err\n    @@ t/t1400-update-ref.sh: do\n     +\t\tfi\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-delete fails with\nwrong old value\" '\n    -+\t\tcreate_stdin_buf ${type} \"symref-delete refs/heads/symref\" \"$m\" &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n    ++\ttest_expect_success \"stdin $type symref-delete fails with wrong\nold value\" '\n    ++\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$m\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n     +\t\tgrep \"fatal: verifying symref target:\n${SQ}refs/heads/symref${SQ}: is at $a but expected refs/heads/main\"\nerr &&\n     +\t\tgit symbolic-ref refs/heads/symref >expect &&\n     +\t\techo $a >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-delete works with\nright old value\" '\n    -+\t\tcreate_stdin_buf ${type} \"symref-delete refs/heads/symref\" \"$a\" &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\ttest_expect_success \"stdin $type symref-delete works with right\nold value\" '\n    ++\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$a\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\ttest_must_fail git rev-parse --verify -q refs/heads/symref\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-delete works with\nempty old value\" '\n    -+\t\tgit symbolic-ref refs/heads/symref $a &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-delete refs/heads/symref\" \"\" &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\ttest_expect_success \"stdin $type symref-delete works with empty\nold value\" '\n    ++\t\tgit symbolic-ref refs/heads/symref $a >stdin &&\n    ++\t\tformat_command $type \"symref-delete refs/heads/symref\" \"\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\ttest_must_fail git rev-parse --verify -q $b\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-delete succeeds for\ndangling reference\" '\n    ++\ttest_expect_success \"stdin $type symref-delete succeeds for\ndangling reference\" '\n     +\t\ttest_must_fail git symbolic-ref refs/heads/nonexistent &&\n     +\t\tgit symbolic-ref refs/heads/symref2 refs/heads/nonexistent &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-delete refs/heads/symref2\"\n\"refs/heads/nonexistent\" &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-delete refs/heads/symref2\"\n\"refs/heads/nonexistent\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\ttest_must_fail git symbolic-ref -d refs/heads/symref2\n     +\t'\n     +\n4:  1038e96a44 ! 4:  9b71c9e07b update-ref: add support for\n'symref-create' command\n    @@ refs.c: int ref_transaction_update(struct ref_transaction *transaction,\n      {\n     -\tif (!new_oid || is_null_oid(new_oid)) {\n     -\t\tstrbuf_addf(err, \"'%s' has a null OID\", refname);\n    ++\tif (new_oid && new_target)\n    ++\t\tBUG(\"create called with both new_oid and new_target set\");\n     +\tif ((!new_oid || is_null_oid(new_oid)) && !new_target) {\n    -+\t\tstrbuf_addf(err, \"'%s' has a null OID or no new target\", refname);\n    ++\t\tstrbuf_addf(err, \"'%s' has neither a valid OID nor a target\", refname);\n      \t\treturn 1;\n      \t}\n      \treturn ref_transaction_update(transaction, refname, new_oid,\n    @@ t/t0600-reffiles-backend.sh: test_expect_success POSIXPERM 'git\nreflog expire ho\n      '\n\n     +test_expect_success SYMLINKS 'symref transaction supports symlinks' '\n    -+\ttest_when_finished \"git symbolic-ref -d TESTSYMREFONE\" &&\n    ++\ttest_when_finished \"git symbolic-ref -d TEST_SYMREF_HEAD\" &&\n     +\tgit update-ref refs/heads/new @ &&\n     +\ttest_config core.prefersymlinkrefs true &&\n     +\tcat >stdin <<-EOF &&\n     +\tstart\n    -+\tsymref-create TESTSYMREFONE refs/heads/new\n    ++\tsymref-create TEST_SYMREF_HEAD refs/heads/new\n     +\tprepare\n     +\tcommit\n     +\tEOF\n     +\tgit update-ref --no-deref --stdin <stdin &&\n    -+\ttest_path_is_symlink .git/TESTSYMREFONE &&\n    -+\ttest \"$(test_readlink .git/TESTSYMREFONE)\" = refs/heads/new\n    ++\ttest_path_is_symlink .git/TEST_SYMREF_HEAD &&\n    ++\ttest \"$(test_readlink .git/TEST_SYMREF_HEAD)\" = refs/heads/new\n     +'\n     +\n     +test_expect_success 'symref transaction supports false symlink config' '\n    -+\ttest_when_finished \"git symbolic-ref -d TESTSYMREFONE\" &&\n    ++\ttest_when_finished \"git symbolic-ref -d TEST_SYMREF_HEAD\" &&\n     +\tgit update-ref refs/heads/new @ &&\n     +\ttest_config core.prefersymlinkrefs false &&\n     +\tcat >stdin <<-EOF &&\n     +\tstart\n    -+\tsymref-create TESTSYMREFONE refs/heads/new\n    ++\tsymref-create TEST_SYMREF_HEAD refs/heads/new\n     +\tprepare\n     +\tcommit\n     +\tEOF\n     +\tgit update-ref --no-deref --stdin <stdin &&\n    -+\ttest_path_is_file .git/TESTSYMREFONE &&\n    -+\tgit symbolic-ref TESTSYMREFONE >actual &&\n    ++\ttest_path_is_file .git/TEST_SYMREF_HEAD &&\n    ++\tgit symbolic-ref TEST_SYMREF_HEAD >actual &&\n     +\techo refs/heads/new >expect &&\n     +\ttest_cmp expect actual\n     +'\n    @@ t/t1400-update-ref.sh: do\n      \t\ttest_must_fail git symbolic-ref -d refs/heads/symref2\n      \t'\n\n    -+\ttest_expect_success \"stdin ${type} symref-create fails with too\nmany arguments\" '\n    -+\t\tcreate_stdin_buf ${type} \"symref-create refs/heads/symref\"\n\"$a\" \"$a\" >stdin &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n    ++\ttest_expect_success \"stdin $type symref-create fails with too\nmany arguments\" '\n    ++\t\tformat_command $type \"symref-create refs/heads/symref\" \"$a\"\n\"$a\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n     +\t\tif test \"$type\" = \"-z\"\n     +\t\tthen\n     +\t\t\tgrep \"fatal: unknown command: $a\" err\n    @@ t/t1400-update-ref.sh: do\n     +\t\tfi\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-create fails with no target\" '\n    -+\t\tcreate_stdin_buf ${type} \"symref-create refs/heads/symref\" >stdin &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin\n    ++\ttest_expect_success \"stdin $type symref-create fails with no target\" '\n    ++\t\tformat_command $type \"symref-create refs/heads/symref\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-create fails with\nempty target\" '\n    -+\t\tcreate_stdin_buf ${type} \"symref-create refs/heads/symref\" \"\" >stdin &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin\n    ++\ttest_expect_success \"stdin $type symref-create fails with empty target\" '\n    ++\t\tformat_command $type \"symref-create refs/heads/symref\" \"\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-create works\" '\n    ++\ttest_expect_success \"stdin $type symref-create works\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-create refs/heads/symref\"\n\"$a\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-create refs/heads/symref\" \"$a\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\tgit symbolic-ref refs/heads/symref >expect &&\n     +\t\techo $a >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-create works with --no-deref\" '\n    ++\ttest_expect_success \"stdin $type symref-create works with --no-deref\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-create refs/heads/symref\" \"$a\" &&\n    -+\t\tgit update-ref --stdin ${type} <stdin 2>err\n    ++\t\tformat_command $type \"symref-create refs/heads/symref\" \"$a\" &&\n    ++\t\tgit update-ref --stdin $type <stdin 2>err\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} create dangling symref ref works\" '\n    ++\ttest_expect_success \"stdin $type create dangling symref ref works\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-create refs/heads/symref\"\n\"refs/heads/unkown\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-create refs/heads/symref\"\n\"refs/heads/unkown\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\tgit symbolic-ref refs/heads/symref >expect &&\n     +\t\techo refs/heads/unkown >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-create does not\ncreate reflogs by default\" '\n    ++\ttest_expect_success \"stdin $type symref-create does not create\nreflogs by default\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/symref\" &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-create refs/symref\" \"$a\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-create refs/symref\" \"$a\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\tgit symbolic-ref refs/symref >expect &&\n     +\t\techo $a >actual &&\n     +\t\ttest_cmp expect actual &&\n     +\t\ttest_must_fail git reflog exists refs/symref\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-create reflogs with\n--create-reflog\" '\n    ++\ttest_expect_success \"stdin $type symref-create reflogs with\n--create-reflog\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-create refs/heads/symref\"\n\"$a\" >stdin &&\n    -+\t\tgit update-ref --create-reflog --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-create refs/heads/symref\" \"$a\" >stdin &&\n    ++\t\tgit update-ref --create-reflog --stdin $type --no-deref <stdin &&\n     +\t\tgit symbolic-ref refs/heads/symref >expect &&\n     +\t\techo $a >actual &&\n     +\t\ttest_cmp expect actual &&\n    @@ t/t1416-ref-transaction-hooks.sh: test_expect_success 'hook\ngets all queued symr\n      \tprepare\n      \tcommit\n      \tEOF\n    +\n    + ## t/t5605-clone-local.sh ##\n    +@@ t/t5605-clone-local.sh: test_expect_success REFFILES 'local\nclone from repo with corrupt refs fails grac\n    + \techo a >corrupt/.git/refs/heads/topic &&\n    +\n    + \ttest_must_fail git clone corrupt working 2>err &&\n    +-\tgrep \"has a null OID\" err\n    ++\tgrep \"has neither a valid OID nor a target\" err\n    + '\n    +\n    + test_done\n5:  78dd51b65f = 5:  a9b1a31756 reftable: pick either 'oid' or\n'target' for new updates\n6:  562e061063 ! 6:  1bbbe86743 update-ref: add support for\n'symref-update' command\n    @@ Commit message\n         OID before the update. This by extension also means that this when a\n         zero <old-oid> is provided, it ensures that the ref didn't\nexist before.\n\n    +    The divergence in syntax from the regular `update` command is\nbecause if\n    +    we don't use a `(ref | oid)` prefix for the old_value, then there is\n    +    ambiguity around if the value provided should be treated as an oid or a\n    +    reference. This is more so the reason, because we allow anything\n    +    committish to be provided as an oid.\n    +\n         The command allows users to perform symbolic ref updates within a\n         transaction. This provides atomicity and allows users to perform a set\n         of operations together.\n\n    -    This command will also support deref mode, to ensure that we can update\n    +    This command supports deref mode, to ensure that we can update\n         dereferenced regular refs to symrefs.\n\n         Signed-off-by: Karthik Nayak <karthik.188@gmail.com>\n    @@ builtin/update-ref.c: static char *parse_next_refname(const char **next)\n     +\t\treturn strbuf_detach(&arg, NULL);\n     +\treturn NULL;\n     +}\n    -+\n\n      /*\n       * The value being parsed is <old-oid> (as opposed to <new-oid>; the\n    @@ builtin/update-ref.c: static void parse_cmd_update(struct\nref_transaction *trans\n     +\tchar *old_target = NULL;\n     +\tstruct strbuf err = STRBUF_INIT;\n     +\tstruct object_id old_oid;\n    -+\tint have_old = 0;\n    ++\tint have_old_oid = 0;\n     +\n     +\trefname = parse_refname(&next);\n     +\tif (!refname)\n    @@ builtin/update-ref.c: static void parse_cmd_update(struct\nref_transaction *trans\n     +\n     +\told_arg = parse_next_arg(&next);\n     +\tif (old_arg) {\n    -+\t\told_target = parse_next_refname(&next);\n    ++\t\told_target = parse_next_arg(&next);\n     +\t\tif (!old_target)\n     +\t\t\tdie(\"symref-update %s: expected old value\", refname);\n     +\n    -+\t\tif (!strcmp(old_arg, \"oid\") &&\n    -+\t\t    !repo_get_oid(the_repository, old_target, &old_oid)) {\n    ++\t\tif (!strcmp(old_arg, \"oid\")) {\n    ++\t\t\tif (repo_get_oid(the_repository, old_target, &old_oid))\n    ++\t\t\t\tdie(\"symref-update %s: invalid oid: %s\", refname, old_target);\n    ++\n     +\t\t\told_target = NULL;\n    -+\t\t\thave_old = 1;\n    -+\t\t} else if (strcmp(old_arg, \"ref\"))\n    ++\t\t\thave_old_oid = 1;\n    ++\t\t} else if (!strcmp(old_arg, \"ref\")) {\n    ++\t\t\tif (check_refname_format(old_target, REFNAME_ALLOW_ONELEVEL))\n    ++\t\t\t\tdie(\"symref-update %s: invalid ref: %s\", refname, old_target);\n    ++\t\t} else {\n     +\t\t\tdie(\"symref-update %s: invalid arg '%s' for old value\",\nrefname, old_arg);\n    ++\t\t}\n     +\t}\n     +\n     +\tif (*next != line_termination)\n     +\t\tdie(\"symref-update %s: extra input: %s\", refname, next);\n     +\n     +\tif (ref_transaction_update(transaction, refname, NULL,\n    -+\t\t\t\t   have_old ? &old_oid : NULL,\n    ++\t\t\t\t   have_old_oid ? &old_oid : NULL,\n     +\t\t\t\t   new_target, old_target,\n    -+\t\t\t\t   update_flags |= create_reflog_flag,\n    ++\t\t\t\t   update_flags | create_reflog_flag,\n     +\t\t\t\t   msg, &err))\n     +\t\tdie(\"%s\", err.buf);\n     +\n    @@ t/t1400-update-ref.sh: do\n      \t\tgit reflog exists refs/heads/symref\n      \t'\n\n    -+\ttest_expect_success \"stdin ${type} symref-update fails with too\nmany arguments\" '\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\"\n\"$a\" \"ref\" \"$a\" \"$a\" >stdin &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n    ++\ttest_expect_success \"stdin $type symref-update fails with too\nmany arguments\" '\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\"\n\"ref\" \"$a\" \"$a\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n     +\t\tif test \"$type\" = \"-z\"\n     +\t\tthen\n     +\t\t\tgrep \"fatal: unknown command: $a\" err\n    @@ t/t1400-update-ref.sh: do\n     +\t\tfi\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update fails with\nwrong old value argument\" '\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\"\n\"$a\" \"foo\" \"$a\" \"$a\" >stdin &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n    ++\ttest_expect_success \"stdin $type symref-update fails with wrong\nold value argument\" '\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\"\n\"foo\" \"$a\" \"$a\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n     +\t\tgrep \"fatal: symref-update refs/heads/symref: invalid arg\n${SQ}foo${SQ} for old value\" err\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update creates with\nzero old value\" '\n    ++\ttest_expect_success \"stdin $type symref-update creates with\nzero old value\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\"\n\"$a\" \"oid\" \"$Z\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\"\n\"oid\" \"$Z\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\techo $a >expect &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update creates with\nno old value\" '\n    ++\ttest_expect_success \"stdin $type symref-update creates with no\nold value\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\"\n\"$a\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\techo $a >expect &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update creates dangling\" '\n    ++\ttest_expect_success \"stdin $type symref-update creates dangling\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n     +\t\ttest_must_fail git rev-parse refs/heads/nonexistent &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\"\n\"refs/heads/nonexistent\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\"\n\"refs/heads/nonexistent\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\techo refs/heads/nonexistent >expect &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update fails with\nwrong old value\" '\n    ++\ttest_expect_success \"stdin $type symref-update fails with wrong\nold value\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n     +\t\tgit symbolic-ref refs/heads/symref $a &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\"\n\"$m\" \"ref\" \"$b\" >stdin &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$m\"\n\"ref\" \"$b\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n     +\t\tgrep \"fatal: verifying symref target:\n${SQ}refs/heads/symref${SQ}: is at $a but expected $b\" err &&\n     +\t\ttest_must_fail git rev-parse --verify -q $c\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update updates dangling ref\" '\n    ++\ttest_expect_success \"stdin $type symref-update updates dangling ref\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n     +\t\ttest_must_fail git rev-parse refs/heads/nonexistent &&\n     +\t\tgit symbolic-ref refs/heads/symref refs/heads/nonexistent &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\"\n\"$a\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\techo $a >expect &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update updates\ndangling ref with old value\" '\n    ++\ttest_expect_success \"stdin $type symref-update updates dangling\nref with old value\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n     +\t\ttest_must_fail git rev-parse refs/heads/nonexistent &&\n     +\t\tgit symbolic-ref refs/heads/symref refs/heads/nonexistent &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\"\n\"$a\" \"ref\" \"refs/heads/nonexistent\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\"\n\"ref\" \"refs/heads/nonexistent\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\techo $a >expect &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update fails update\ndangling ref with wrong old value\" '\n    ++\ttest_expect_success \"stdin $type symref-update fails update\ndangling ref with wrong old value\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n     +\t\ttest_must_fail git rev-parse refs/heads/nonexistent &&\n     +\t\tgit symbolic-ref refs/heads/symref refs/heads/nonexistent &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\"\n\"$a\" \"ref\" \"refs/heads/wrongref\" >stdin &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\"\n\"ref\" \"refs/heads/wrongref\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin &&\n     +\t\techo refs/heads/nonexistent >expect &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update works with\nright old value\" '\n    ++\ttest_expect_success \"stdin $type symref-update works with right\nold value\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n     +\t\tgit symbolic-ref refs/heads/symref $a &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\"\n\"$m\" \"ref\" \"$a\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$m\"\n\"ref\" \"$a\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\techo $m >expect &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update works with no\nold value\" '\n    ++\ttest_expect_success \"stdin $type symref-update works with no old value\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n     +\t\tgit symbolic-ref refs/heads/symref $a &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\"\n\"$m\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$m\" >stdin &&\n    ++\t\tgit update-ref --stdin $type --no-deref <stdin &&\n     +\t\techo $m >expect &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update fails with\nempty old ref-target\" '\n    ++\ttest_expect_success \"stdin $type symref-update fails with empty\nold ref-target\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n     +\t\tgit symbolic-ref refs/heads/symref $a &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\"\n\"$m\" \"ref\" \"\" >stdin &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} --no-deref <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$m\"\n\"ref\" \"\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin &&\n     +\t\techo $a >expect &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update creates (with deref)\" '\n    ++\ttest_expect_success \"stdin $type symref-update creates (with deref)\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\"\n\"$a\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n    ++\t\tgit update-ref --stdin $type <stdin &&\n     +\t\techo $a >expect &&\n     +\t\tgit symbolic-ref --no-recurse refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual &&\n    @@ t/t1400-update-ref.sh: do\n     +\t\tgrep \"$Z $(git rev-parse $a)\" actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update regular ref to\nsymref with correct old-oid\" '\n    ++\ttest_expect_success \"stdin $type symref-update regular ref to\nsymref with correct old-oid\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d --no-recurse\nrefs/heads/regularref\" &&\n     +\t\tgit update-ref --no-deref refs/heads/regularref $a &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/regularref\"\n\"$a\" \"oid\" \"$(git rev-parse $a)\" >stdin &&\n    -+\t\tgit update-ref --stdin ${type} <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/regularref\"\n\"$a\" \"oid\" \"$(git rev-parse $a)\" >stdin &&\n    ++\t\tgit update-ref --stdin $type <stdin &&\n     +\t\techo $a >expect &&\n     +\t\tgit symbolic-ref --no-recurse refs/heads/regularref >actual &&\n     +\t\ttest_cmp expect actual &&\n    @@ t/t1400-update-ref.sh: do\n     +\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update regular ref to\nsymref fails with wrong old-oid\" '\n    ++\ttest_expect_success \"stdin $type symref-update regular ref to\nsymref fails with wrong old-oid\" '\n    ++\t\ttest_when_finished \"git update-ref -d refs/heads/regularref\" &&\n    ++\t\tgit update-ref --no-deref refs/heads/regularref $a &&\n    ++\t\tformat_command $type \"symref-update refs/heads/regularref\"\n\"$a\" \"oid\" \"$(git rev-parse refs/heads/target2)\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n    ++\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}:\nis at $(git rev-parse $a) but expected $(git rev-parse\nrefs/heads/target2)\" err &&\n    ++\t\techo $(git rev-parse $a) >expect &&\n    ++\t\tgit rev-parse refs/heads/regularref >actual &&\n    ++\t\ttest_cmp expect actual\n    ++\t'\n    ++\n    ++\ttest_expect_success \"stdin $type symref-update regular ref to\nsymref fails with invalid old-oid\" '\n     +\t\ttest_when_finished \"git update-ref -d refs/heads/regularref\" &&\n     +\t\tgit update-ref --no-deref refs/heads/regularref $a &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/regularref\"\n\"$a\" \"oid\" \"$(git rev-parse refs/heads/target2)\" >stdin &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} <stdin 2>err &&\n    ++\t\tformat_command $type \"symref-update refs/heads/regularref\"\n\"$a\" \"oid\" \"not-a-ref-oid\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n    ++\t\tgrep \"fatal: symref-update refs/heads/regularref: invalid oid:\nnot-a-ref-oid\" err &&\n     +\t\techo $(git rev-parse $a) >expect &&\n     +\t\tgit rev-parse refs/heads/regularref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update existing\nsymref with zero old-oid\" '\n    ++\ttest_expect_success \"stdin $type symref-update existing symref\nwith zero old-oid\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d --no-recurse\nrefs/heads/symref\" &&\n     +\t\tgit symbolic-ref refs/heads/symref refs/heads/target2 &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\"\n\"$a\" \"oid\" \"$Z\" >stdin &&\n    -+\t\ttest_must_fail git update-ref --stdin ${type} <stdin 2>err &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\"\n\"oid\" \"$Z\" >stdin &&\n    ++\t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n     +\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/symref${SQ}:\nreference already exists\" err &&\n     +\t\techo refs/heads/target2 >expect &&\n     +\t\tgit symbolic-ref refs/heads/symref >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update regular ref to\nsymref (with deref)\" '\n    ++\ttest_expect_success \"stdin $type symref-update regular ref to\nsymref (with deref)\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n     +\t\ttest_when_finished \"git update-ref -d --no-deref refs/heads/symref2\" &&\n     +\t\tgit update-ref refs/heads/symref2 $a &&\n     +\t\tgit symbolic-ref --no-recurse refs/heads/symref refs/heads/symref2 &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/symref\"\n\"$a\" >stdin &&\n    -+\t\tgit update-ref ${type} --stdin <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n    ++\t\tgit update-ref $type --stdin <stdin &&\n     +\t\techo $a >expect &&\n     +\t\tgit symbolic-ref --no-recurse refs/heads/symref2 >actual &&\n     +\t\ttest_cmp expect actual &&\n    @@ t/t1400-update-ref.sh: do\n     +\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n     +\t'\n     +\n    -+\ttest_expect_success \"stdin ${type} symref-update regular ref to symref\" '\n    ++\ttest_expect_success \"stdin $type symref-update regular ref to symref\" '\n     +\t\ttest_when_finished \"git symbolic-ref -d --no-recurse\nrefs/heads/regularref\" &&\n     +\t\tgit update-ref --no-deref refs/heads/regularref $a &&\n    -+\t\tcreate_stdin_buf ${type} \"symref-update refs/heads/regularref\"\n\"$a\" >stdin &&\n    -+\t\tgit update-ref ${type} --stdin <stdin &&\n    ++\t\tformat_command $type \"symref-update refs/heads/regularref\"\n\"$a\" >stdin &&\n    ++\t\tgit update-ref $type --stdin <stdin &&\n     +\t\techo $a >expect &&\n     +\t\tgit symbolic-ref --no-recurse refs/heads/regularref >actual &&\n     +\t\ttest_cmp expect actual &&\n"},{"id":"495416","messageId":"xmqq1q5swm52.fsf@gitster.g","threadId":"61469","inReplyTo":"xmqqbk4wy3ji.fsf@gitster.g","subject":"Re: [PATCH v2 0/6] update-ref: add symref support for --stdin","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-23T16:03:53Z","receivedAt":"2024-05-23T16:03:59Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Wait.  There is something fishy going on.\n>\n>> Range diff vs v1:\n>>  1:  1bc4cc3fc4 =  1:  1bc4cc3fc4 refs: accept symref values in `ref_transac...\n>>...\n>>  8:  4865707bda =  8:  4865707bda refs: remove `create_symref` and associated dead code\n>>  9:  4cb67dce7c !  9:  2bbdeff798 refs: create and use `ref_update_ref_must_exist()`\n> ...\n> I am confused why we are seeing a total reroll of such an old topic.\n>\n> Also you have one more patch at the end.  Neither the before or\n> after version of 9/9.\n>\n> Is this actually a single patch submission of 9/9 alone?  Patches\n> 1-8/9 are all old ones that are in 'master' already.\n\nAnd then there is a mystery of this v2 being a 6-patch series.\nPerhpas a wrong range-diff was pasted into it?  If this were truly a\ntotal reroll of the previous 8-patch series with an extra step\nappended to the end, it would have been a 9-patch series, not 6.\n\nEven puzzled...\n"},{"id":"495418","messageId":"xmqqr0dsv6e8.fsf@gitster.g","threadId":"61469","inReplyTo":"CAOLa=ZS31CuL8kDODNfM5L8gXN6RUOG5uEP5Q9JzEuaxtLEZ8g@mail.gmail.com","subject":"Re: [PATCH v2 0/6] update-ref: add symref support for --stdin","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-23T16:29:19Z","receivedAt":"2024-05-23T16:29:22Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Karthik Nayak <karthik.188@gmail.com> writes:\n\n> Junio C Hamano <gitster@pobox.com> writes:\n>\n>> Karthik Nayak <karthik.188@gmail.com> writes:\n>>\n>>> The patches 1, 5 fix small issues in the reference backends. The other\n>>> patches 2, 3, 4 & 6, each add one of the new sub-commands.\n>>>\n>>> The series is based off master, with 'kn/ref-transaction-symref' merged\n>>> in. There seem to be no conflicts with 'next' or 'seen'.\n>>\n>> Wait.  There is something fishy going on.\n>> ...\n>> Is this actually a single patch submission of 9/9 alone?  Patches\n>> 1-8/9 are all old ones that are in 'master' already.\n>>\n>> Puzzled...\n>\n> I think this is just a mess up in the range diff, I haven't changed\n> anything locally. So adding the correct range diff here:\n\nQuite honestly, I care much less about the range-diff that is almost\nunintelligible than the actual patches.  Your title line says 0/6,\nyour updated range-diff presumably have 1: to 6:?  As a sanity check\nmechanism, the list of commits and the overall diffstat is a more\nuseful part in the cover letter message so that I (or any other\nrecipients) can use to compare against the list of messages that\nappeared on the list.\n\nWe may want to teach \"format-patch --range-diff\" to place the output\nof range-diff _below_ the list of commits and the overall diffstat\nin the cover letter (and at the end of the patch for a single patch\ntopic).\n\nI'll ignore the range-diff in the original cover letter and see if\nthe rest makes sense.\n\nThanks.\n\n\n\n"},{"id":"495437","messageId":"CAOLa=ZQq_ce8thcjxMitDt8s0aiqVUWt8A43ziU0Zd5F2Joorg@mail.gmail.com","threadId":"61469","inReplyTo":"xmqqr0dsv6e8.fsf@gitster.g","subject":"Re: [PATCH v2 0/6] update-ref: add symref support for --stdin","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-23T17:50:18Z","receivedAt":"2024-05-23T17:50:23Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Karthik Nayak <karthik.188@gmail.com> writes:\n>\n>> Junio C Hamano <gitster@pobox.com> writes:\n>>\n>>> Karthik Nayak <karthik.188@gmail.com> writes:\n>>>\n>>>> The patches 1, 5 fix small issues in the reference backends. The other\n>>>> patches 2, 3, 4 & 6, each add one of the new sub-commands.\n>>>>\n>>>> The series is based off master, with 'kn/ref-transaction-symref' merged\n>>>> in. There seem to be no conflicts with 'next' or 'seen'.\n>>>\n>>> Wait.  There is something fishy going on.\n>>> ...\n>>> Is this actually a single patch submission of 9/9 alone?  Patches\n>>> 1-8/9 are all old ones that are in 'master' already.\n>>>\n>>> Puzzled...\n>>\n>> I think this is just a mess up in the range diff, I haven't changed\n>> anything locally. So adding the correct range diff here:\n>\n> Quite honestly, I care much less about the range-diff that is almost\n> unintelligible than the actual patches.  Your title line says 0/6,\n> your updated range-diff presumably have 1: to 6:?  As a sanity check\n> mechanism, the list of commits and the overall diffstat is a more\n> useful part in the cover letter message so that I (or any other\n> recipients) can use to compare against the list of messages that\n> appeared on the list.\n>\n> We may want to teach \"format-patch --range-diff\" to place the output\n> of range-diff _below_ the list of commits and the overall diffstat\n> in the cover letter (and at the end of the patch for a single patch\n> topic).\n>\n\nI usually manually add in the range-diff, which is probably where the\nerror came from. I didn't even know about \"format-patch --range-diff\".\n\n> I'll ignore the range-diff in the original cover letter and see if\n> the rest makes sense.\n>\n> Thanks.\n\nIt does use the same base as the previous revision, I rebased in place\nusing 'rebase -i' and amended for fixes from the first review.\n\n> Junio C Hamano <gitster@pobox.com> writes:\n>\n>> Wait.  There is something fishy going on.\n>>\n>>> Range diff vs v1:\n>>>  1:  1bc4cc3fc4 =  1:  1bc4cc3fc4 refs: accept symref values in `ref_transac...\n>>>...\n>>>  8:  4865707bda =  8:  4865707bda refs: remove `create_symref` and associated dead code\n>>>  9:  4cb67dce7c !  9:  2bbdeff798 refs: create and use `ref_update_ref_must_exist()`\n>> ...\n>> I am confused why we are seeing a total reroll of such an old topic.\n>>\n>> Also you have one more patch at the end.  Neither the before or\n>> after version of 9/9.\n>>\n>> Is this actually a single patch submission of 9/9 alone?  Patches\n>> 1-8/9 are all old ones that are in 'master' already.\n>\n> And then there is a mystery of this v2 being a 6-patch series.\n> Perhpas a wrong range-diff was pasted into it?  If this were truly a\n> total reroll of the previous 8-patch series with an extra step\n> appended to the end, it would have been a 9-patch series, not 6.\n>\n> Even puzzled...\n\nThe v1 of this series is also a 6-patch series, this is not a re-roll of\nthe earlier series 'kn/ref-transaction-symref' (which is already in\nnext). This is based on top of it.\n\nSorry for the confusion though.\n"},{"id":"495441","messageId":"CAPig+cT6_j80vh_HEjg6HWKXpkv-huggudShh_RgzLSKvV_bOA@mail.gmail.com","threadId":"61469","inReplyTo":"xmqqr0dsv6e8.fsf@gitster.g","subject":"Re: [PATCH v2 0/6] update-ref: add symref support for --stdin","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2024-05-23T17:59:43Z","receivedAt":"2024-05-23T17:59:55Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Thu, May 23, 2024 at 12:29 PM Junio C Hamano <gitster@pobox.com> wrote:\n> We may want to teach \"format-patch --range-diff\" to place the output\n> of range-diff _below_ the list of commits and the overall diffstat\n> in the cover letter\n\nPlacing the range-diff below the list of commits and the overall\ndiffstat in the cover letter is how `format-patch --range-diff`\nalready works.\n\n> (and at the end of the patch for a single patch topic).\n\nThis could indeed lead to less visual clutter for the single-patch topic.\n"},{"id":"495442","messageId":"xmqqo78wtn7y.fsf@gitster.g","threadId":"61469","inReplyTo":"CAPig+cT6_j80vh_HEjg6HWKXpkv-huggudShh_RgzLSKvV_bOA@mail.gmail.com","subject":"Re: [PATCH v2 0/6] update-ref: add symref support for --stdin","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-23T18:08:49Z","receivedAt":"2024-05-23T18:08:55Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Eric Sunshine <sunshine@sunshineco.com> writes:\n\n> On Thu, May 23, 2024 at 12:29 PM Junio C Hamano <gitster@pobox.com> wrote:\n>> We may want to teach \"format-patch --range-diff\" to place the output\n>> of range-diff _below_ the list of commits and the overall diffstat\n>> in the cover letter\n>\n> Placing the range-diff below the list of commits and the overall\n> diffstat in the cover letter is how `format-patch --range-diff`\n> already works.\n\nHeh, so what Karthik was manually doing made the cover letter harder\nto read?\n\nIn any case, that is a great news.\n\n>> (and at the end of the patch for a single patch topic).\n>\n> This could indeed lead to less visual clutter for the single-patch topic.\n"},{"id":"495446","messageId":"CAPig+cT71R1qYTkshvQNpOepOskz3LNDCc4eiih-8H9LDJmf-A@mail.gmail.com","threadId":"61469","inReplyTo":"xmqqo78wtn7y.fsf@gitster.g","subject":"Re: [PATCH v2 0/6] update-ref: add symref support for --stdin","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2024-05-23T18:50:03Z","receivedAt":"2024-05-23T18:50:15Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Thu, May 23, 2024 at 2:08 PM Junio C Hamano <gitster@pobox.com> wrote:\n> Eric Sunshine <sunshine@sunshineco.com> writes:\n> > On Thu, May 23, 2024 at 12:29 PM Junio C Hamano <gitster@pobox.com> wrote:\n> >> We may want to teach \"format-patch --range-diff\" to place the output\n> >> of range-diff _below_ the list of commits and the overall diffstat\n> >> in the cover letter\n> >\n> > Placing the range-diff below the list of commits and the overall\n> > diffstat in the cover letter is how `format-patch --range-diff`\n> > already works.\n>\n> Heh, so what Karthik was manually doing made the cover letter harder\n> to read?\n\nApparently so.\n\n> In any case, that is a great news.\n\nPlacement of the range-diff in the cover-letter was a deliberate choice[1].\n\n> >> (and at the end of the patch for a single patch topic).\n> >\n> > This could indeed lead to less visual clutter for the single-patch topic.\n\nRegarding your experiment[2] to place the range-diff at the end of a\nsingle-patch, apparently that idea had been considered, as well, but\nit was noted[3,4] that, by default, some MUIs hide everything after\nthe \"--\" line.\n\n[1]: https://lore.kernel.org/git/CAPig+cT9zCcNxn1+DPMQWqJ-hfxb7gE7rKyfbqHjTC+FDNY_mw@mail.gmail.com/\n[2]: https://lore.kernel.org/git/xmqqh6ep1pwz.fsf_-_@gitster.g/\n[3]: https://lore.kernel.org/git/xmqqmuupogei.fsf@gitster-ct.c.googlers.com/\n[4]: https://lore.kernel.org/git/CAPig+cRx5-2TYOm_8oayFfbKGpmTJf=M0cNR3L5UJGGC6vHPDQ@mail.gmail.com/\n"},{"id":"495447","messageId":"xmqq4jaotkkf.fsf@gitster.g","threadId":"61469","inReplyTo":"CAPig+cT71R1qYTkshvQNpOepOskz3LNDCc4eiih-8H9LDJmf-A@mail.gmail.com","subject":"Re: [PATCH v2 0/6] update-ref: add symref support for --stdin","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-23T19:06:08Z","receivedAt":"2024-05-23T19:06:11Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Eric Sunshine <sunshine@sunshineco.com> writes:\n\n> Placement of the range-diff in the cover-letter was a deliberate choice[1].\n\nOK, and the reasoning in [1] still makes sense.  Short-and-sweet\nstuff with denser information contents first, before a bulky \"diff\nof diff\" we add as an auxiliary piece of information.\n\n> Regarding your experiment[2] to place the range-diff at the end of a\n> single-patch, apparently that idea had been considered, as well, but\n> it was noted[3,4] that, by default, some MUIs hide everything after\n> the \"--\" line.\n\nThat is different from what [3] noted, though.  It was \"the stuff\nafter '-- ' not included in a reply/reply-all when responding\".\n\n (1) As we'd rather want to see the actual patch, not a part of\n     range-diff in the cover letter, responded, placing it under the\n     \"-- \" mark and excluded from the response is actually a\n     feature.\n\n (2) If we wanted to, we can show the log message, \"---\", output of\n     \"git diff --stat -p\", output of \"git range-diff\" and finally\n     the \"-- \" signature mark.  Receiving end will not mistake the\n     range-diff output as part of the last hunk of the last patch.\n\nSo, I do not see it as a reason to refrain from placing the range\ndiff output after the main patch.\n\nThanks.\n\n"},{"id":"495481","messageId":"xmqqbk4wrykv.fsf@gitster.g","threadId":"61469","inReplyTo":"CAPig+cT6_j80vh_HEjg6HWKXpkv-huggudShh_RgzLSKvV_bOA@mail.gmail.com","subject":"Re: [PATCH v2 0/6] update-ref: add symref support for --stdin","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-23T21:46:24Z","receivedAt":"2024-05-23T21:46:35Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Eric Sunshine <sunshine@sunshineco.com> writes:\n\n>> (and at the end of the patch for a single patch topic).\n>\n> This could indeed lead to less visual clutter for the single-patch topic.\n\nSo here is how such a change looks like.  I actually have this as a\ntwo-patch series in my tree, but here is in squashed-into-one form.\n\nThe log-tree.c:show_log() function has a logic to create inter/range\ndiff at its end.  This function is called early by log_tree_diff(),\nwhich is responsible for showing a single commit (log message,\nauxiliary info like diffstat, and the patch, right before the\nsignature mark \"-- \" which is given by the format-patch itself).\n\nWe move that inter/range logic out into a helper function and call\nit at the original place (which is [1/2] step of the two patch\nseries), which is a no-op refactoring.\n\nIn the second step, we remove the call out of show_log(), and\ninstead call it at the end of the log_tree_commit() after\nlog_tree_diff() did its thing.  This removes the inter/range diff\nout of the \"auxiliary info\" section between \"---\" and the patch and\nmoves it at the end of the patch text, still before the signature\nmark \"-- \".  As this makes inter/range diff no longer part of the\nruns of \"commentary block\"s, calls to next_commentary_block() is\nremoved from the show_diff_of_diff() helper.\n\nAs expected, this requires adjustment to t/t4014-format-patch.sh but\nthe fallout is surprisingly small.  It may be either an indication\nthat our test coverage for the feature is sketchy, or the tests were\nwritten robustly, anticipating that somebody someday may want to\nmove things around in the output this way.\n\n\n log-tree.c              |  7 +++----\n t/t4014-format-patch.sh | 17 +++++++++++------\n 2 files changed, 14 insertions(+), 10 deletions(-)\n\ndiff --git c/log-tree.c w/log-tree.c\nindex e7cd2c491f..f28c4d0bb0 100644\n--- c/log-tree.c\n+++ w/log-tree.c\n@@ -684,7 +684,6 @@ static void show_diff_of_diff(struct rev_info *opt)\n \t\tmemcpy(&dq, &diff_queued_diff, sizeof(diff_queued_diff));\n \t\tDIFF_QUEUE_CLEAR(&diff_queued_diff);\n \n-\t\tnext_commentary_block(opt, NULL);\n \t\tfprintf_ln(opt->diffopt.file, \"%s\", opt->idiff_title);\n \t\tshow_interdiff(opt->idiff_oid1, opt->idiff_oid2, 2,\n \t\t\t       &opt->diffopt);\n@@ -704,7 +703,6 @@ static void show_diff_of_diff(struct rev_info *opt)\n \t\tmemcpy(&dq, &diff_queued_diff, sizeof(diff_queued_diff));\n \t\tDIFF_QUEUE_CLEAR(&diff_queued_diff);\n \n-\t\tnext_commentary_block(opt, NULL);\n \t\tfprintf_ln(opt->diffopt.file, \"%s\", opt->rdiff_title);\n \t\t/*\n \t\t * Pass minimum required diff-options to range-diff; others\n@@ -903,8 +901,6 @@ void show_log(struct rev_info *opt)\n \tstrbuf_release(&msgbuf);\n \tfree(ctx.notes_message);\n \tfree(ctx.after_subject);\n-\n-\tshow_diff_of_diff(opt);\n }\n \n int log_tree_diff_flush(struct rev_info *opt)\n@@ -1176,6 +1172,9 @@ int log_tree_commit(struct rev_info *opt, struct commit *commit)\n \topt->loginfo = NULL;\n \tmaybe_flush_or_die(opt->diffopt.file, \"stdout\");\n \topt->diffopt.no_free = no_free;\n+\tif (shown)\n+\t\tshow_diff_of_diff(opt);\n+\n \tdiff_free(&opt->diffopt);\n \treturn shown;\n }\ndiff --git c/t/t4014-format-patch.sh w/t/t4014-format-patch.sh\nindex ba85b582c5..c0c5eccb7c 100755\n--- c/t/t4014-format-patch.sh\n+++ w/t/t4014-format-patch.sh\n@@ -2482,13 +2482,18 @@ test_expect_success 'interdiff: reroll-count with a integer' '\n '\n \n test_expect_success 'interdiff: solo-patch' '\n-\tcat >expect <<-\\EOF &&\n-\t  +fleep\n-\n-\tEOF\n \tgit format-patch --interdiff=boop~2 -1 boop &&\n-\ttest_grep \"^Interdiff:$\" 0001-fleep.patch &&\n-\tsed \"1,/^  @@ /d; /^$/q\" 0001-fleep.patch >actual &&\n+\n+\t# remove up to the last \"patch\" output line,\n+\t# and remove everything below the signature mark.\n+\tsed -e \"1,/^+fleep\\$/d\" -e \"/^-- /,\\$d\" 0001-fleep.patch >actual &&\n+\n+\t# fabricate Interdiff output.\n+\tgit diff boop~2 boop >inter &&\n+\t{\n+\t\techo \"Interdiff:\" &&\n+\t\tsed -e \"s/^/  /\" inter\n+\t} >expect &&\n \ttest_cmp expect actual\n '\n \n"},{"id":"495641","messageId":"xmqqplt9h4yq.fsf@gitster.g","threadId":"61469","inReplyTo":"20240522090326.1268326-7-knayak@gitlab.com","subject":"Re: [PATCH v2 6/6] update-ref: add support for 'symref-update' command","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-25T23:00:45Z","receivedAt":"2024-05-25T23:00:48Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Karthik Nayak <karthik.188@gmail.com> writes:\n\n> +static void parse_cmd_symref_update(struct ref_transaction *transaction,\n> +\t\t\t\t    const char *next, const char *end)\n> +{\n> +\tchar *refname, *new_target, *old_arg;\n> +\tchar *old_target = NULL;\n> + ...\n> +\told_arg = parse_next_arg(&next);\n\n> +\tif (old_arg) {\n> +\t\told_target = parse_next_arg(&next);\n\nNow we have an allocated memory we are responsible for freeing in\nold_target, obtained from parse_next_arg() ...\n\n> +\t\tif (!old_target)\n> +\t\t\tdie(\"symref-update %s: expected old value\", refname);\n\n... and here we know it is not NULL.  We use it to grab the object\nname ...\n\n> +\t\tif (!strcmp(old_arg, \"oid\")) {\n> +\t\t\tif (repo_get_oid(the_repository, old_target, &old_oid))\n> +\t\t\t\tdie(\"symref-update %s: invalid oid: %s\", refname, old_target);\n> +\n> +\t\t\told_target = NULL;\n\n... and then we overwritten the variable, losing the last reference\nto the piece of memory without freeing.\n\nPerhaps squashing this in is sufficient to plug this leak, but there\nprobably are other new leaks around this code.  I ran out of time so\nI'll let you take care of the rest ;-)\n\nThanks.\n\n builtin/update-ref.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git c/builtin/update-ref.c w/builtin/update-ref.c\nindex 76d20ca0f1..7d2a419230 100644\n--- c/builtin/update-ref.c\n+++ w/builtin/update-ref.c\n@@ -297,7 +297,7 @@ static void parse_cmd_symref_update(struct ref_transaction *transaction,\n \n \t\tif (!strcmp(old_arg, \"oid\") &&\n \t\t    !repo_get_oid(the_repository, old_target, &old_oid)) {\n-\t\t\told_target = NULL;\n+\t\t\tFREE_AND_NULL(old_target);\n \t\t\thave_old = 1;\n \t\t} else if (strcmp(old_arg, \"ref\"))\n \t\t\tdie(\"symref-update %s: invalid arg '%s' for old value\", refname, old_arg);\n"},{"id":"495797","messageId":"CAOLa=ZSMox99eZ=QmkucN+M+SxzFkQ3ivEzOfa-SZ=kKtEtcMw@mail.gmail.com","threadId":"61469","inReplyTo":"xmqqplt9h4yq.fsf@gitster.g","subject":"Re: [PATCH v2 6/6] update-ref: add support for 'symref-update' command","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-29T08:29:21Z","receivedAt":"2024-05-29T08:29:27Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Karthik Nayak <karthik.188@gmail.com> writes:\n>\n>> +static void parse_cmd_symref_update(struct ref_transaction *transaction,\n>> +\t\t\t\t    const char *next, const char *end)\n>> +{\n>> +\tchar *refname, *new_target, *old_arg;\n>> +\tchar *old_target = NULL;\n>> + ...\n>> +\told_arg = parse_next_arg(&next);\n>\n>> +\tif (old_arg) {\n>> +\t\told_target = parse_next_arg(&next);\n>\n> Now we have an allocated memory we are responsible for freeing in\n> old_target, obtained from parse_next_arg() ...\n>\n>> +\t\tif (!old_target)\n>> +\t\t\tdie(\"symref-update %s: expected old value\", refname);\n>\n> ... and here we know it is not NULL.  We use it to grab the object\n> name ...\n>\n>> +\t\tif (!strcmp(old_arg, \"oid\")) {\n>> +\t\t\tif (repo_get_oid(the_repository, old_target, &old_oid))\n>> +\t\t\t\tdie(\"symref-update %s: invalid oid: %s\", refname, old_target);\n>> +\n>> +\t\t\told_target = NULL;\n>\n> ... and then we overwritten the variable, losing the last reference\n> to the piece of memory without freeing.\n>\n> Perhaps squashing this in is sufficient to plug this leak, but there\n> probably are other new leaks around this code.  I ran out of time so\n> I'll let you take care of the rest ;-)\n>\n\nYeah this indeed was missed, I think having the\n`TEST_PASSES_SANITIZE_LEAK` flag set, helped catch this leak and another\naround strbuf.\n\n> Thanks.\n>\n>  builtin/update-ref.c | 2 +-\n>  1 file changed, 1 insertion(+), 1 deletion(-)\n>\n> diff --git c/builtin/update-ref.c w/builtin/update-ref.c\n> index 76d20ca0f1..7d2a419230 100644\n> --- c/builtin/update-ref.c\n> +++ w/builtin/update-ref.c\n> @@ -297,7 +297,7 @@ static void parse_cmd_symref_update(struct ref_transaction *transaction,\n>\n>  \t\tif (!strcmp(old_arg, \"oid\") &&\n>  \t\t    !repo_get_oid(the_repository, old_target, &old_oid)) {\n> -\t\t\told_target = NULL;\n> +\t\t\tFREE_AND_NULL(old_target);\n>  \t\t\thave_old = 1;\n>  \t\t} else if (strcmp(old_arg, \"ref\"))\n>  \t\t\tdie(\"symref-update %s: invalid arg '%s' for old value\", refname, old_arg);\n\nThis definitely works, but keeping it consistent with the rest of the\ncode in this file, I think we can do:\n\ndiff --git a/builtin/update-ref.c b/builtin/update-ref.c\nindex bda37c161d..82f461d6f8 100644\n--- a/builtin/update-ref.c\n+++ b/builtin/update-ref.c\n@@ -298,7 +300,6 @@ static void parse_cmd_symref_update(struct\nref_transaction *transaction,\n \t\t\tif (repo_get_oid(the_repository, old_target, &old_oid))\n \t\t\t\tdie(\"symref-update %s: invalid oid: %s\", refname, old_target);\n\n-\t\t\told_target = NULL;\n \t\t\thave_old_oid = 1;\n \t\t} else if (!strcmp(old_arg, \"ref\")) {\n \t\t\tif (check_refname_format(old_target, REFNAME_ALLOW_ONELEVEL))\n@@ -313,7 +314,8 @@ static void parse_cmd_symref_update(struct\nref_transaction *transaction,\n\n \tif (ref_transaction_update(transaction, refname, NULL,\n \t\t\t\t   have_old_oid ? &old_oid : NULL,\n-\t\t\t\t   new_target, old_target,\n+\t\t\t\t   new_target,\n+\t\t\t\t   have_old_oid ? NULL : old_target,\n \t\t\t\t   update_flags | create_reflog_flag,\n \t\t\t\t   msg, &err))\n \t\tdie(\"%s\", err.buf);\n\nThis works, since we anyways do a `free(old_target)` at the end of this\nfunction.\n"},{"id":"495925","messageId":"20240530120940.456817-1-knayak@gitlab.com","threadId":"61469","inReplyTo":"20240514124411.1037019-1-knayak@gitlab.com","subject":"[PATCH v3 0/6] update-ref: add symref support for --stdin","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-30T12:09:34Z","receivedAt":"2024-05-30T12:09:48Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"From: Karthik Nayak <karthik.188@gmail.com>\n\nThe 'update-ref' command is used to update refs using transactions. The\ncommand allows users to also utilize a '--stdin' mode to provide a\nbatch of sub-commands which can be processed in a transaction.\n\nCurrently, the sub-commands involve {verify, delete, create, update}\nand they allow users to work with regular refs in the repository. To\nwork with symrefs, users only have the option of using\n'git-symbolic-ref', which doesn't provide transaction support to the\nusers eventhough it uses the same behind the hood.\n\nRecently, we modified the reference backend to add symref support,\nfollowing which, 'git-symbolic-ref' also uses the transaction backend.\nBut, it doesn't expose this to the user. To allow users to work with\nsymrefs via transaction, this series adds support for new sub-commands\n{symrer-verify, symref-delete, symref-create, symref-update} to the\n'--stdin' mode of update-ref. These complement the existing\nsub-commands.\n\nThe patches 1, 5 fix small issues in the reference backends. The other\npatches 2, 3, 4 & 6, each add one of the new sub-commands.\n\nThe series is based off master, with 'kn/ref-transaction-symref' merged\nin. There seem to be no conflicts with 'next' or 'seen'.\n\nThere was some discussion [1] also about adding `old_target` support to\nthe existing `update` command. I think its worthwhile to do this with\nsome tests cleanup, will follow that up as a separate series.\n\nChanges since v2:\n* Based off 'ps/fixleaks' (commit: ebdbefa4fe9f618347124b37d44e517e0c6a3e4c)\nwhich brought to light two leaks, which have been fixed.\n* Adding credit where it's due.\n\n[1]: https://lore.kernel.org/r/CAOLa=ZQW-cCV5BP_fCvuZimfkjwAzjEiqXYRPft1Wf9kAX=_bw@mail.gmail.com\n\nI used the '--range-diff' flag for 'git-format-patch(1)' this time.\nAlso I'm on vacation at the moment, so my responses are a bit slower than usual.\n\nKarthik Nayak (6):\n  refs: create and use `ref_update_expects_existing_old_ref()`\n  update-ref: add support for 'symref-verify' command\n  update-ref: add support for 'symref-delete' command\n  update-ref: add support for 'symref-create' command\n  reftable: pick either 'oid' or 'target' for new updates\n  update-ref: add support for 'symref-update' command\n\n Documentation/git-update-ref.txt |  25 ++\n builtin/clone.c                  |   2 +-\n builtin/fetch.c                  |   2 +-\n builtin/receive-pack.c           |   3 +-\n builtin/update-ref.c             | 237 +++++++++++++++++-\n refs.c                           |  40 ++-\n refs.h                           |   6 +-\n refs/files-backend.c             |   3 +-\n refs/refs-internal.h             |   6 +\n refs/reftable-backend.c          |   7 +-\n t/t0600-reffiles-backend.sh      |  32 +++\n t/t1400-update-ref.sh            | 416 ++++++++++++++++++++++++++++++-\n t/t1416-ref-transaction-hooks.sh |  54 ++++\n t/t5605-clone-local.sh           |   2 +-\n 14 files changed, 801 insertions(+), 34 deletions(-)\n\nRange-diff against v2:\n1:  2bbdeff798 ! 1:  cab5265c3c refs: create and use `ref_update_expects_existing_old_ref()`\n    @@ Commit message\n         `ref_update_expects_existing_old_ref()` and expose it internally via\n         'refs-internal.h'.\n     \n    +    Helped-by: Patrick Steinhardt <ps@pks.im>\n         Signed-off-by: Karthik Nayak <karthik.188@gmail.com>\n     \n      ## refs.c ##\n2:  f509066cab ! 2:  ed54b0dfb9 update-ref: add support for 'symref-verify' command\n    @@ Commit message\n         divergence from behavior, but we never tested to ensure that reflog\n         wasn't affected by the 'verify' command.\n     \n    +    Helped-by: Patrick Steinhardt <ps@pks.im>\n         Signed-off-by: Karthik Nayak <karthik.188@gmail.com>\n     \n      ## Documentation/git-update-ref.txt ##\n3:  a11f4c1e48 ! 3:  b82b86ff40 update-ref: add support for 'symref-delete' command\n    @@ Commit message\n         within a transaction, which promises atomicity of the operation and can\n         be batched with other commands.\n     \n    +    Helped-by: Patrick Steinhardt <ps@pks.im>\n         Signed-off-by: Karthik Nayak <karthik.188@gmail.com>\n     \n      ## Documentation/git-update-ref.txt ##\n4:  9b71c9e07b ! 4:  ae127f7d52 update-ref: add support for 'symref-create' command\n    @@ Commit message\n         as a symlink. We fallback to creating a regular symref if creating the\n         symlink is unsuccessful.\n     \n    +    Helped-by: Patrick Steinhardt <ps@pks.im>\n         Signed-off-by: Karthik Nayak <karthik.188@gmail.com>\n     \n      ## Documentation/git-update-ref.txt ##\n5:  a9b1a31756 ! 5:  8889dcbf40 reftable: pick either 'oid' or 'target' for new updates\n    @@ Commit message\n         want to introduce the 'symref-update' command in the upcoming commit,\n         which would use this flow, correct it.\n     \n    +    Helped-by: Patrick Steinhardt <ps@pks.im>\n         Signed-off-by: Karthik Nayak <karthik.188@gmail.com>\n     \n      ## refs/reftable-backend.c ##\n6:  1bbbe86743 ! 6:  19d85d56c4 update-ref: add support for 'symref-update' command\n    @@ Commit message\n         This command supports deref mode, to ensure that we can update\n         dereferenced regular refs to symrefs.\n     \n    +    Helped-by: Patrick Steinhardt <ps@pks.im>\n    +    Helped-by: Junio C Hamano <gitster@pobox.com>\n         Signed-off-by: Karthik Nayak <karthik.188@gmail.com>\n     \n      ## Documentation/git-update-ref.txt ##\n    @@ builtin/update-ref.c: static char *parse_next_refname(const char **next)\n     +\n     +\tif (arg.len)\n     +\t\treturn strbuf_detach(&arg, NULL);\n    ++\n    ++\tstrbuf_release(&arg);\n     +\treturn NULL;\n     +}\n      \n    @@ builtin/update-ref.c: static void parse_cmd_update(struct ref_transaction *trans\n     +\t\t\tif (repo_get_oid(the_repository, old_target, &old_oid))\n     +\t\t\t\tdie(\"symref-update %s: invalid oid: %s\", refname, old_target);\n     +\n    -+\t\t\told_target = NULL;\n     +\t\t\thave_old_oid = 1;\n     +\t\t} else if (!strcmp(old_arg, \"ref\")) {\n     +\t\t\tif (check_refname_format(old_target, REFNAME_ALLOW_ONELEVEL))\n    @@ builtin/update-ref.c: static void parse_cmd_update(struct ref_transaction *trans\n     +\n     +\tif (ref_transaction_update(transaction, refname, NULL,\n     +\t\t\t\t   have_old_oid ? &old_oid : NULL,\n    -+\t\t\t\t   new_target, old_target,\n    ++\t\t\t\t   new_target,\n    ++\t\t\t\t   have_old_oid ? NULL : old_target,\n     +\t\t\t\t   update_flags | create_reflog_flag,\n     +\t\t\t\t   msg, &err))\n     +\t\tdie(\"%s\", err.buf);\n-- \n2.43.GIT\n\n"},{"id":"495926","messageId":"20240530120940.456817-2-knayak@gitlab.com","threadId":"61469","inReplyTo":"20240514124411.1037019-1-knayak@gitlab.com","subject":"[PATCH v3 1/6] refs: create and use `ref_update_expects_existing_old_ref()`","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-30T12:09:35Z","receivedAt":"2024-05-30T12:09:49Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"From: Karthik Nayak <karthik.188@gmail.com>\n\nThe files and reftable backend, need to check if a ref must exist, so\nthat the required validation can be done. A ref must exist only when the\n`old_oid` value of the update has been explicitly set and it is not the\n`null_oid` value.\n\nSince we also support symrefs now, we need to ensure that even when\n`old_target` is set a ref must exist. While this was missed when we\nadded symref support in transactions, there are no active users of this\npath. As we introduce the 'symref-verify' command in the upcoming\ncommits, it is important to fix this.\n\nSo let's export this to a function called\n`ref_update_expects_existing_old_ref()` and expose it internally via\n'refs-internal.h'.\n\nHelped-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n refs.c                  | 6 ++++++\n refs/files-backend.c    | 3 +--\n refs/refs-internal.h    | 6 ++++++\n refs/reftable-backend.c | 2 +-\n 4 files changed, 14 insertions(+), 3 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex 8260c27cde..50d8d7d777 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2679,3 +2679,9 @@ int ref_update_check_old_target(const char *referent, struct ref_update *update,\n \t\t\t    referent, update->old_target);\n \treturn -1;\n }\n+\n+int ref_update_expects_existing_old_ref(struct ref_update *update)\n+{\n+\treturn (update->flags & REF_HAVE_OLD) &&\n+\t\t(!is_null_oid(&update->old_oid) || update->old_target);\n+}\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 5f3089d947..194e74eb4d 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2412,8 +2412,7 @@ static int lock_ref_for_update(struct files_ref_store *refs,\n \t\t\t       struct strbuf *err)\n {\n \tstruct strbuf referent = STRBUF_INIT;\n-\tint mustexist = (update->flags & REF_HAVE_OLD) &&\n-\t\t!is_null_oid(&update->old_oid);\n+\tint mustexist = ref_update_expects_existing_old_ref(update);\n \tint ret = 0;\n \tstruct ref_lock *lock;\n \ndiff --git a/refs/refs-internal.h b/refs/refs-internal.h\nindex 53a6c5d842..ee298ec0d5 100644\n--- a/refs/refs-internal.h\n+++ b/refs/refs-internal.h\n@@ -765,4 +765,10 @@ int ref_update_has_null_new_value(struct ref_update *update);\n int ref_update_check_old_target(const char *referent, struct ref_update *update,\n \t\t\t\tstruct strbuf *err);\n \n+/*\n+ * Check if the ref must exist, this means that the old_oid or\n+ * old_target is non NULL.\n+ */\n+int ref_update_expects_existing_old_ref(struct ref_update *update);\n+\n #endif /* REFS_REFS_INTERNAL_H */\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 1af86bbdec..b838cf8f00 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -824,7 +824,7 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n \t\t\t\t\t      &current_oid, &referent, &u->type);\n \t\tif (ret < 0)\n \t\t\tgoto done;\n-\t\tif (ret > 0 && (!(u->flags & REF_HAVE_OLD) || is_null_oid(&u->old_oid))) {\n+\t\tif (ret > 0 && !ref_update_expects_existing_old_ref(u)) {\n \t\t\t/*\n \t\t\t * The reference does not exist, and we either have no\n \t\t\t * old object ID or expect the reference to not exist.\n-- \n2.43.GIT\n\n"},{"id":"495927","messageId":"20240530120940.456817-3-knayak@gitlab.com","threadId":"61469","inReplyTo":"20240514124411.1037019-1-knayak@gitlab.com","subject":"[PATCH v3 2/6] update-ref: add support for 'symref-verify' command","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-30T12:09:36Z","receivedAt":"2024-05-30T12:09:52Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"From: Karthik Nayak <karthik.188@gmail.com>\n\nThe 'symref-verify' command allows users to verify if a provided <ref>\ncontains the provided <old-target> without changing the <ref>. If\n<old-target> is not provided, the command will verify that the <ref>\ndoesn't exist.\n\nThe command allows users to verify symbolic refs within a transaction,\nand this means users can perform a set of changes in a transaction only\nwhen the verification holds good.\n\nSince we're checking for symbolic refs, this command will only work with\nthe 'no-deref' mode. This is because any dereferenced symbolic ref will\npoint to an object and not a ref and the regular 'verify' command can be\nused in such situations.\n\nAdd required tests for symref support in 'verify'. Since we're here,\nalso add reflog checks for the pre-existing 'verify' tests, there is no\ndivergence from behavior, but we never tested to ensure that reflog\nwasn't affected by the 'verify' command.\n\nHelped-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n Documentation/git-update-ref.txt |  7 +++\n builtin/update-ref.c             | 80 +++++++++++++++++++++++----\n refs.c                           | 11 +++-\n refs.h                           |  1 +\n t/t1400-update-ref.sh            | 94 +++++++++++++++++++++++++++++++-\n t/t1416-ref-transaction-hooks.sh | 30 ++++++++++\n 6 files changed, 208 insertions(+), 15 deletions(-)\n\ndiff --git a/Documentation/git-update-ref.txt b/Documentation/git-update-ref.txt\nindex 374a2ebd2b..9fe78b3501 100644\n--- a/Documentation/git-update-ref.txt\n+++ b/Documentation/git-update-ref.txt\n@@ -65,6 +65,7 @@ performs all modifications together.  Specify commands of the form:\n \tcreate SP <ref> SP <new-oid> LF\n \tdelete SP <ref> [SP <old-oid>] LF\n \tverify SP <ref> [SP <old-oid>] LF\n+\tsymref-verify SP <ref> [SP <old-target>] LF\n \toption SP <opt> LF\n \tstart LF\n \tprepare LF\n@@ -86,6 +87,7 @@ quoting:\n \tcreate SP <ref> NUL <new-oid> NUL\n \tdelete SP <ref> NUL [<old-oid>] NUL\n \tverify SP <ref> NUL [<old-oid>] NUL\n+\tsymref-verify SP <ref> [NUL <old-target>] NUL\n \toption SP <opt> NUL\n \tstart NUL\n \tprepare NUL\n@@ -117,6 +119,11 @@ verify::\n \tVerify <ref> against <old-oid> but do not change it.  If\n \t<old-oid> is zero or missing, the ref must not exist.\n \n+symref-verify::\n+\tVerify symbolic <ref> against <old-target> but do not change it.\n+\tIf <old-target> is missing, the ref must not exist.  Can only be\n+\tused in `no-deref` mode.\n+\n option::\n \tModify the behavior of the next command naming a <ref>.\n \tThe only valid option is `no-deref` to avoid dereferencing\ndiff --git a/builtin/update-ref.c b/builtin/update-ref.c\nindex 6cda1c08aa..50f5472160 100644\n--- a/builtin/update-ref.c\n+++ b/builtin/update-ref.c\n@@ -76,6 +76,29 @@ static char *parse_refname(const char **next)\n \treturn strbuf_detach(&ref, NULL);\n }\n \n+/*\n+ * Wrapper around parse_refname which skips the next delimiter.\n+ */\n+static char *parse_next_refname(const char **next)\n+{\n+\tif (line_termination) {\n+\t\t/* Without -z, consume SP and use next argument */\n+\t\tif (!**next || **next == line_termination)\n+\t\t\treturn NULL;\n+\t\tif (**next != ' ')\n+\t\t\tdie(\"expected SP but got: %s\", *next);\n+\t} else {\n+\t\t/* With -z, read the next NUL-terminated line */\n+\t\tif (**next)\n+\t\t\treturn NULL;\n+\t}\n+\t/* Skip the delimiter */\n+\t(*next)++;\n+\n+\treturn parse_refname(next);\n+}\n+\n+\n /*\n  * The value being parsed is <old-oid> (as opposed to <new-oid>; the\n  * difference affects which error messages are generated):\n@@ -297,11 +320,47 @@ static void parse_cmd_verify(struct ref_transaction *transaction,\n \t\tdie(\"verify %s: extra input: %s\", refname, next);\n \n \tif (ref_transaction_verify(transaction, refname, &old_oid,\n-\t\t\t\t   update_flags, &err))\n+\t\t\t\t   NULL, update_flags, &err))\n+\t\tdie(\"%s\", err.buf);\n+\n+\tupdate_flags = default_flags;\n+\tfree(refname);\n+\tstrbuf_release(&err);\n+}\n+\n+static void parse_cmd_symref_verify(struct ref_transaction *transaction,\n+\t\t\t\t    const char *next, const char *end)\n+{\n+\tstruct strbuf err = STRBUF_INIT;\n+\tstruct object_id old_oid;\n+\tchar *refname, *old_target;\n+\n+\tif (!(update_flags & REF_NO_DEREF))\n+\t\tdie(\"symref-verify: cannot operate with deref mode\");\n+\n+\trefname = parse_refname(&next);\n+\tif (!refname)\n+\t\tdie(\"symref-verify: missing <ref>\");\n+\n+\t/*\n+\t * old_ref is optional, if not provided, we need to ensure that the\n+\t * ref doesn't exist.\n+\t */\n+\told_target = parse_next_refname(&next);\n+\tif (!old_target)\n+\t\toidcpy(&old_oid, null_oid());\n+\n+\tif (*next != line_termination)\n+\t\tdie(\"symref-verify %s: extra input: %s\", refname, next);\n+\n+\tif (ref_transaction_verify(transaction, refname,\n+\t\t\t\t   old_target ? NULL : &old_oid,\n+\t\t\t\t   old_target, update_flags, &err))\n \t\tdie(\"%s\", err.buf);\n \n \tupdate_flags = default_flags;\n \tfree(refname);\n+\tfree(old_target);\n \tstrbuf_release(&err);\n }\n \n@@ -380,15 +439,16 @@ static const struct parse_cmd {\n \tunsigned args;\n \tenum update_refs_state state;\n } command[] = {\n-\t{ \"update\",  parse_cmd_update,  3, UPDATE_REFS_OPEN },\n-\t{ \"create\",  parse_cmd_create,  2, UPDATE_REFS_OPEN },\n-\t{ \"delete\",  parse_cmd_delete,  2, UPDATE_REFS_OPEN },\n-\t{ \"verify\",  parse_cmd_verify,  2, UPDATE_REFS_OPEN },\n-\t{ \"option\",  parse_cmd_option,  1, UPDATE_REFS_OPEN },\n-\t{ \"start\",   parse_cmd_start,   0, UPDATE_REFS_STARTED },\n-\t{ \"prepare\", parse_cmd_prepare, 0, UPDATE_REFS_PREPARED },\n-\t{ \"abort\",   parse_cmd_abort,   0, UPDATE_REFS_CLOSED },\n-\t{ \"commit\",  parse_cmd_commit,  0, UPDATE_REFS_CLOSED },\n+\t{ \"update\",        parse_cmd_update,        3, UPDATE_REFS_OPEN },\n+\t{ \"create\",        parse_cmd_create,        2, UPDATE_REFS_OPEN },\n+\t{ \"delete\",        parse_cmd_delete,        2, UPDATE_REFS_OPEN },\n+\t{ \"verify\",        parse_cmd_verify,        2, UPDATE_REFS_OPEN },\n+\t{ \"symref-verify\", parse_cmd_symref_verify, 2, UPDATE_REFS_OPEN },\n+\t{ \"option\",        parse_cmd_option,        1, UPDATE_REFS_OPEN },\n+\t{ \"start\",         parse_cmd_start,         0, UPDATE_REFS_STARTED },\n+\t{ \"prepare\",       parse_cmd_prepare,       0, UPDATE_REFS_PREPARED },\n+\t{ \"abort\",         parse_cmd_abort,         0, UPDATE_REFS_CLOSED },\n+\t{ \"commit\",        parse_cmd_commit,        0, UPDATE_REFS_CLOSED },\n };\n \n static void update_refs_stdin(void)\ndiff --git a/refs.c b/refs.c\nindex 50d8d7d777..cdc4d25557 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1297,14 +1297,19 @@ int ref_transaction_delete(struct ref_transaction *transaction,\n int ref_transaction_verify(struct ref_transaction *transaction,\n \t\t\t   const char *refname,\n \t\t\t   const struct object_id *old_oid,\n+\t\t\t   const char *old_target,\n \t\t\t   unsigned int flags,\n \t\t\t   struct strbuf *err)\n {\n-\tif (!old_oid)\n-\t\tBUG(\"verify called with old_oid set to NULL\");\n+\tif (!old_target && !old_oid)\n+\t\tBUG(\"verify called with old_oid and old_target set to NULL\");\n+\tif (old_oid && old_target)\n+\t\tBUG(\"verify called with both old_oid and old_target set\");\n+\tif (old_target && !(flags & REF_NO_DEREF))\n+\t\tBUG(\"verify cannot operate on symrefs with deref mode\");\n \treturn ref_transaction_update(transaction, refname,\n \t\t\t\t      NULL, old_oid,\n-\t\t\t\t      NULL, NULL,\n+\t\t\t\t      NULL, old_target,\n \t\t\t\t      flags, NULL, err);\n }\n \ndiff --git a/refs.h b/refs.h\nindex 34568ee1fb..906299351c 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -736,6 +736,7 @@ int ref_transaction_delete(struct ref_transaction *transaction,\n int ref_transaction_verify(struct ref_transaction *transaction,\n \t\t\t   const char *refname,\n \t\t\t   const struct object_id *old_oid,\n+\t\t\t   const char *old_target,\n \t\t\t   unsigned int flags,\n \t\t\t   struct strbuf *err);\n \ndiff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\nindex bbee2783ab..52801be07d 100755\n--- a/t/t1400-update-ref.sh\n+++ b/t/t1400-update-ref.sh\n@@ -892,17 +892,23 @@ test_expect_success 'stdin update/create/verify combination works' '\n '\n \n test_expect_success 'stdin verify succeeds for correct value' '\n+\ttest-tool ref-store main for-each-reflog-ent $m >before &&\n \tgit rev-parse $m >expect &&\n \techo \"verify $m $m\" >stdin &&\n \tgit update-ref --stdin <stdin &&\n \tgit rev-parse $m >actual &&\n-\ttest_cmp expect actual\n+\ttest_cmp expect actual &&\n+\ttest-tool ref-store main for-each-reflog-ent $m >after &&\n+\ttest_cmp before after\n '\n \n test_expect_success 'stdin verify succeeds for missing reference' '\n+\ttest-tool ref-store main for-each-reflog-ent $m >before &&\n \techo \"verify refs/heads/missing $Z\" >stdin &&\n \tgit update-ref --stdin <stdin &&\n-\ttest_must_fail git rev-parse --verify -q refs/heads/missing\n+\ttest_must_fail git rev-parse --verify -q refs/heads/missing &&\n+\ttest-tool ref-store main for-each-reflog-ent $m >after &&\n+\ttest_cmp before after\n '\n \n test_expect_success 'stdin verify treats no value as missing' '\n@@ -1643,4 +1649,88 @@ test_expect_success PIPE 'transaction flushes status updates' '\n \ttest_cmp expected actual\n '\n \n+format_command () {\n+\tif test \"$1\" = \"-z\"\n+\tthen\n+\t\tshift\n+\t\tprintf \"$F\" \"$@\"\n+\telse\n+\t\techo \"$@\"\n+\tfi\n+}\n+\n+for type in \"\" \"-z\"\n+do\n+\n+\ttest_expect_success \"stdin $type symref-verify fails without --no-deref\" '\n+\t\tgit symbolic-ref refs/heads/symref $a &&\n+\t\tformat_command $type \"symref-verify refs/heads/symref\" \"$a\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n+\t\tgrep \"fatal: symref-verify: cannot operate with deref mode\" err\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-verify fails with too many arguments\" '\n+\t\tformat_command $type \"symref-verify refs/heads/symref\" \"$a\" \"$a\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err  &&\n+\t\tif test \"$type\" = \"-z\"\n+\t\tthen\n+\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\telse\n+\t\t\tgrep \"fatal: symref-verify refs/heads/symref: extra input:  $a\" err\n+\t\tfi\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-verify succeeds for correct value\" '\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >before &&\n+\t\tformat_command $type \"symref-verify refs/heads/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >after &&\n+\t\ttest_cmp before after\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-verify fails with no value\" '\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\tformat_command $type \"symref-verify refs/heads/symref\" \"\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-verify succeeds for dangling reference\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref2\" &&\n+\t\ttest_must_fail git symbolic-ref refs/heads/nonexistent &&\n+\t\tgit symbolic-ref refs/heads/symref2 refs/heads/nonexistent &&\n+\t\tformat_command $type \"symref-verify refs/heads/symref2\" \"refs/heads/nonexistent\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-verify fails for missing reference\" '\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >before &&\n+\t\tformat_command $type \"symref-verify refs/heads/missing\" \"refs/heads/unknown\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n+\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/missing${SQ}: unable to resolve reference ${SQ}refs/heads/missing${SQ}\" err &&\n+\t\ttest_must_fail git rev-parse --verify -q refs/heads/missing &&\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >after &&\n+\t\ttest_cmp before after\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-verify fails for wrong value\" '\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\tformat_command $type \"symref-verify refs/heads/symref\" \"$b\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-verify fails for mistaken null value\" '\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\tformat_command $type \"symref-verify refs/heads/symref\" \"$Z\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+done\n+\n test_done\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex 067fd57290..fd58b902f4 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -157,4 +157,34 @@ test_expect_success 'hook captures git-symbolic-ref updates' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'hook gets all queued symref updates' '\n+\ttest_when_finished \"rm actual\" &&\n+\n+\tgit update-ref refs/heads/branch $POST_OID &&\n+\tgit symbolic-ref refs/heads/symref refs/heads/main &&\n+\n+\ttest_hook reference-transaction <<-\\EOF &&\n+\techo \"$*\" >>actual\n+\twhile read -r line\n+\tdo\n+\t\tprintf \"%s\\n\" \"$line\"\n+\tdone >>actual\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\tprepared\n+\tref:refs/heads/main $ZERO_OID refs/heads/symref\n+\tcommitted\n+\tref:refs/heads/main $ZERO_OID refs/heads/symref\n+\tEOF\n+\n+\tgit update-ref --no-deref --stdin <<-EOF &&\n+\tstart\n+\tsymref-verify refs/heads/symref refs/heads/main\n+\tprepare\n+\tcommit\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \n2.43.GIT\n\n"},{"id":"495928","messageId":"20240530120940.456817-4-knayak@gitlab.com","threadId":"61469","inReplyTo":"20240514124411.1037019-1-knayak@gitlab.com","subject":"[PATCH v3 3/6] update-ref: add support for 'symref-delete' command","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-30T12:09:37Z","receivedAt":"2024-05-30T12:09:54Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"From: Karthik Nayak <karthik.188@gmail.com>\n\nAdd a new command 'symref-delete' to allow deletions of symbolic refs in\na transaction via the '--stdin' mode of the 'git-update-ref' command.\nThe 'symref-delete' command can, when given an <old-target>, delete the\nprovided <ref> only when it points to <old-target>.\n\nThis command is only compatible with the 'no-deref' mode because we\noptionally want to check the 'old_target' of the ref being deleted.\nDe-referencing a symbolic ref would provide a regular ref and we already\nhave the 'delete' command for regular refs.\n\nWhile users can also use 'git symbolic-ref -d' to delete symbolic refs,\nthe 'symref-delete' command in 'git-update-ref' allows users to do so\nwithin a transaction, which promises atomicity of the operation and can\nbe batched with other commands.\n\nHelped-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n Documentation/git-update-ref.txt |  5 +++\n builtin/fetch.c                  |  2 +-\n builtin/receive-pack.c           |  3 +-\n builtin/update-ref.c             | 33 ++++++++++++++++++-\n refs.c                           | 14 ++++++---\n refs.h                           |  4 ++-\n t/t1400-update-ref.sh            | 54 ++++++++++++++++++++++++++++++++\n t/t1416-ref-transaction-hooks.sh | 19 ++++++++++-\n 8 files changed, 125 insertions(+), 9 deletions(-)\n\ndiff --git a/Documentation/git-update-ref.txt b/Documentation/git-update-ref.txt\nindex 9fe78b3501..16e02f6979 100644\n--- a/Documentation/git-update-ref.txt\n+++ b/Documentation/git-update-ref.txt\n@@ -65,6 +65,7 @@ performs all modifications together.  Specify commands of the form:\n \tcreate SP <ref> SP <new-oid> LF\n \tdelete SP <ref> [SP <old-oid>] LF\n \tverify SP <ref> [SP <old-oid>] LF\n+\tsymref-delete SP <ref> [SP <old-target>] LF\n \tsymref-verify SP <ref> [SP <old-target>] LF\n \toption SP <opt> LF\n \tstart LF\n@@ -87,6 +88,7 @@ quoting:\n \tcreate SP <ref> NUL <new-oid> NUL\n \tdelete SP <ref> NUL [<old-oid>] NUL\n \tverify SP <ref> NUL [<old-oid>] NUL\n+\tsymref-delete SP <ref> [NUL <old-target>] NUL\n \tsymref-verify SP <ref> [NUL <old-target>] NUL\n \toption SP <opt> NUL\n \tstart NUL\n@@ -119,6 +121,9 @@ verify::\n \tVerify <ref> against <old-oid> but do not change it.  If\n \t<old-oid> is zero or missing, the ref must not exist.\n \n+symref-delete::\n+\tDelete <ref> after verifying it exists with <old-target>, if given.\n+\n symref-verify::\n \tVerify symbolic <ref> against <old-target> but do not change it.\n \tIf <old-target> is missing, the ref must not exist.  Can only be\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 75255dc600..90a80cf016 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1387,7 +1387,7 @@ static int prune_refs(struct display_state *display_state,\n \t\tif (transaction) {\n \t\t\tfor (ref = stale_refs; ref; ref = ref->next) {\n \t\t\t\tresult = ref_transaction_delete(transaction, ref->name, NULL, 0,\n-\t\t\t\t\t\t\t\t\"fetch: prune\", &err);\n+\t\t\t\t\t\t\t\tNULL, \"fetch: prune\", &err);\n \t\t\t\tif (result)\n \t\t\t\t\tgoto cleanup;\n \t\t\t}\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 01c1f04ece..9430a50fde 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1576,7 +1576,8 @@ static const char *update(struct command *cmd, struct shallow_info *si)\n \t\tif (ref_transaction_delete(transaction,\n \t\t\t\t\t   namespaced_name,\n \t\t\t\t\t   old_oid,\n-\t\t\t\t\t   0, \"push\", &err)) {\n+\t\t\t\t\t   0, NULL,\n+\t\t\t\t\t   \"push\", &err)) {\n \t\t\trp_error(\"%s\", err.buf);\n \t\t\tret = \"failed to delete\";\n \t\t} else {\ndiff --git a/builtin/update-ref.c b/builtin/update-ref.c\nindex 50f5472160..a78c2f9c85 100644\n--- a/builtin/update-ref.c\n+++ b/builtin/update-ref.c\n@@ -293,7 +293,7 @@ static void parse_cmd_delete(struct ref_transaction *transaction,\n \n \tif (ref_transaction_delete(transaction, refname,\n \t\t\t\t   have_old ? &old_oid : NULL,\n-\t\t\t\t   update_flags, msg, &err))\n+\t\t\t\t   update_flags, NULL, msg, &err))\n \t\tdie(\"%s\", err.buf);\n \n \tupdate_flags = default_flags;\n@@ -301,6 +301,36 @@ static void parse_cmd_delete(struct ref_transaction *transaction,\n \tstrbuf_release(&err);\n }\n \n+\n+static void parse_cmd_symref_delete(struct ref_transaction *transaction,\n+\t\t\t\t    const char *next, const char *end)\n+{\n+\tstruct strbuf err = STRBUF_INIT;\n+\tchar *refname, *old_target;\n+\n+\tif (!(update_flags & REF_NO_DEREF))\n+\t\tdie(\"symref-delete: cannot operate with deref mode\");\n+\n+\trefname = parse_refname(&next);\n+\tif (!refname)\n+\t\tdie(\"symref-delete: missing <ref>\");\n+\n+\told_target = parse_next_refname(&next);\n+\n+\tif (*next != line_termination)\n+\t\tdie(\"symref-delete %s: extra input: %s\", refname, next);\n+\n+\tif (ref_transaction_delete(transaction, refname, NULL,\n+\t\t\t\t   update_flags, old_target, msg, &err))\n+\t\tdie(\"%s\", err.buf);\n+\n+\tupdate_flags = default_flags;\n+\tfree(refname);\n+\tfree(old_target);\n+\tstrbuf_release(&err);\n+}\n+\n+\n static void parse_cmd_verify(struct ref_transaction *transaction,\n \t\t\t     const char *next, const char *end)\n {\n@@ -443,6 +473,7 @@ static const struct parse_cmd {\n \t{ \"create\",        parse_cmd_create,        2, UPDATE_REFS_OPEN },\n \t{ \"delete\",        parse_cmd_delete,        2, UPDATE_REFS_OPEN },\n \t{ \"verify\",        parse_cmd_verify,        2, UPDATE_REFS_OPEN },\n+\t{ \"symref-delete\", parse_cmd_symref_delete, 2, UPDATE_REFS_OPEN },\n \t{ \"symref-verify\", parse_cmd_symref_verify, 2, UPDATE_REFS_OPEN },\n \t{ \"option\",        parse_cmd_option,        1, UPDATE_REFS_OPEN },\n \t{ \"start\",         parse_cmd_start,         0, UPDATE_REFS_STARTED },\ndiff --git a/refs.c b/refs.c\nindex cdc4d25557..e29abebe1d 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -950,7 +950,7 @@ int refs_delete_ref(struct ref_store *refs, const char *msg,\n \ttransaction = ref_store_transaction_begin(refs, &err);\n \tif (!transaction ||\n \t    ref_transaction_delete(transaction, refname, old_oid,\n-\t\t\t\t   flags, msg, &err) ||\n+\t\t\t\t   flags, NULL, msg, &err) ||\n \t    ref_transaction_commit(transaction, &err)) {\n \t\terror(\"%s\", err.buf);\n \t\tref_transaction_free(transaction);\n@@ -1283,14 +1283,20 @@ int ref_transaction_create(struct ref_transaction *transaction,\n int ref_transaction_delete(struct ref_transaction *transaction,\n \t\t\t   const char *refname,\n \t\t\t   const struct object_id *old_oid,\n-\t\t\t   unsigned int flags, const char *msg,\n+\t\t\t   unsigned int flags,\n+\t\t\t   const char *old_target,\n+\t\t\t   const char *msg,\n \t\t\t   struct strbuf *err)\n {\n \tif (old_oid && is_null_oid(old_oid))\n \t\tBUG(\"delete called with old_oid set to zeros\");\n+\tif (old_oid && old_target)\n+\t\tBUG(\"delete called with both old_oid and old_target set\");\n+\tif (old_target && !(flags & REF_NO_DEREF))\n+\t\tBUG(\"delete cannot operate on symrefs with deref mode\");\n \treturn ref_transaction_update(transaction, refname,\n \t\t\t\t      null_oid(), old_oid,\n-\t\t\t\t      NULL, NULL, flags,\n+\t\t\t\t      NULL, old_target, flags,\n \t\t\t\t      msg, err);\n }\n \n@@ -2599,7 +2605,7 @@ int refs_delete_refs(struct ref_store *refs, const char *logmsg,\n \n \tfor_each_string_list_item(item, refnames) {\n \t\tret = ref_transaction_delete(transaction, item->string,\n-\t\t\t\t\t     NULL, flags, msg, &err);\n+\t\t\t\t\t     NULL, flags, NULL, msg, &err);\n \t\tif (ret) {\n \t\t\twarning(_(\"could not delete reference %s: %s\"),\n \t\t\t\titem->string, err.buf);\ndiff --git a/refs.h b/refs.h\nindex 906299351c..a054a4f998 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -722,7 +722,9 @@ int ref_transaction_create(struct ref_transaction *transaction,\n int ref_transaction_delete(struct ref_transaction *transaction,\n \t\t\t   const char *refname,\n \t\t\t   const struct object_id *old_oid,\n-\t\t\t   unsigned int flags, const char *msg,\n+\t\t\t   unsigned int flags,\n+\t\t\t   const char *old_target,\n+\t\t\t   const char *msg,\n \t\t\t   struct strbuf *err);\n \n /*\ndiff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\nindex 52801be07d..848d6fc42e 100755\n--- a/t/t1400-update-ref.sh\n+++ b/t/t1400-update-ref.sh\n@@ -1731,6 +1731,60 @@ do\n \t\ttest_cmp expect actual\n \t'\n \n+\ttest_expect_success \"stdin $type symref-delete fails without --no-deref\" '\n+\t\tgit symbolic-ref refs/heads/symref $a &&\n+\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$a\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n+\t\tgrep \"fatal: symref-delete: cannot operate with deref mode\" err\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-delete fails with no ref\" '\n+\t\tformat_command $type \"symref-delete \" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n+\t\tgrep \"fatal: symref-delete: missing <ref>\" err\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-delete fails with too many arguments\" '\n+\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$a\" \"$a\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n+\t\tif test \"$type\" = \"-z\"\n+\t\tthen\n+\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\telse\n+\t\t\tgrep \"fatal: symref-delete refs/heads/symref: extra input:  $a\" err\n+\t\tfi\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-delete fails with wrong old value\" '\n+\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$m\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n+\t\tgrep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected refs/heads/main\" err &&\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\techo $a >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-delete works with right old value\" '\n+\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\ttest_must_fail git rev-parse --verify -q refs/heads/symref\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-delete works with empty old value\" '\n+\t\tgit symbolic-ref refs/heads/symref $a >stdin &&\n+\t\tformat_command $type \"symref-delete refs/heads/symref\" \"\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\ttest_must_fail git rev-parse --verify -q $b\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-delete succeeds for dangling reference\" '\n+\t\ttest_must_fail git symbolic-ref refs/heads/nonexistent &&\n+\t\tgit symbolic-ref refs/heads/symref2 refs/heads/nonexistent &&\n+\t\tformat_command $type \"symref-delete refs/heads/symref2\" \"refs/heads/nonexistent\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\ttest_must_fail git symbolic-ref -d refs/heads/symref2\n+\t'\n+\n done\n \n test_done\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex fd58b902f4..ccde1b944b 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -162,6 +162,7 @@ test_expect_success 'hook gets all queued symref updates' '\n \n \tgit update-ref refs/heads/branch $POST_OID &&\n \tgit symbolic-ref refs/heads/symref refs/heads/main &&\n+\tgit symbolic-ref refs/heads/symrefd refs/heads/main &&\n \n \ttest_hook reference-transaction <<-\\EOF &&\n \techo \"$*\" >>actual\n@@ -171,16 +172,32 @@ test_expect_success 'hook gets all queued symref updates' '\n \tdone >>actual\n \tEOF\n \n-\tcat >expect <<-EOF &&\n+\t# In the files backend, \"delete\" also triggers an additional transaction\n+\t# update on the packed-refs backend, which constitutes additional reflog\n+\t# entries.\n+\tif test_have_prereq REFFILES\n+\tthen\n+\t\tcat >expect <<-EOF\n+\t\taborted\n+\t\t$ZERO_OID $ZERO_OID refs/heads/symrefd\n+\t\tEOF\n+\telse\n+\t\t>expect\n+\tfi &&\n+\n+\tcat >>expect <<-EOF &&\n \tprepared\n \tref:refs/heads/main $ZERO_OID refs/heads/symref\n+\tref:refs/heads/main $ZERO_OID refs/heads/symrefd\n \tcommitted\n \tref:refs/heads/main $ZERO_OID refs/heads/symref\n+\tref:refs/heads/main $ZERO_OID refs/heads/symrefd\n \tEOF\n \n \tgit update-ref --no-deref --stdin <<-EOF &&\n \tstart\n \tsymref-verify refs/heads/symref refs/heads/main\n+\tsymref-delete refs/heads/symrefd refs/heads/main\n \tprepare\n \tcommit\n \tEOF\n-- \n2.43.GIT\n\n"},{"id":"495929","messageId":"20240530120940.456817-5-knayak@gitlab.com","threadId":"61469","inReplyTo":"20240514124411.1037019-1-knayak@gitlab.com","subject":"[PATCH v3 4/6] update-ref: add support for 'symref-create' command","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-30T12:09:38Z","receivedAt":"2024-05-30T12:09:55Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"From: Karthik Nayak <karthik.188@gmail.com>\n\nAdd 'symref-create' command to the '--stdin' mode 'git-update-ref' to\nallow creation of symbolic refs in a transaction. The 'symref-create'\ncommand takes in a <new-target>, which the created <ref> will point to.\n\nAlso, support the 'core.prefersymlinkrefs' config, wherein if the config\nis set and the filesystem supports symlinks, we create the symbolic ref\nas a symlink. We fallback to creating a regular symref if creating the\nsymlink is unsuccessful.\n\nHelped-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n Documentation/git-update-ref.txt |  6 +++\n builtin/clone.c                  |  2 +-\n builtin/update-ref.c             | 32 +++++++++++++++-\n refs.c                           |  9 +++--\n refs.h                           |  1 +\n t/t0600-reffiles-backend.sh      | 32 ++++++++++++++++\n t/t1400-update-ref.sh            | 65 ++++++++++++++++++++++++++++++++\n t/t1416-ref-transaction-hooks.sh |  3 ++\n t/t5605-clone-local.sh           |  2 +-\n 9 files changed, 146 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/git-update-ref.txt b/Documentation/git-update-ref.txt\nindex 16e02f6979..364ef78af1 100644\n--- a/Documentation/git-update-ref.txt\n+++ b/Documentation/git-update-ref.txt\n@@ -65,6 +65,7 @@ performs all modifications together.  Specify commands of the form:\n \tcreate SP <ref> SP <new-oid> LF\n \tdelete SP <ref> [SP <old-oid>] LF\n \tverify SP <ref> [SP <old-oid>] LF\n+\tsymref-create SP <ref> SP <new-target> LF\n \tsymref-delete SP <ref> [SP <old-target>] LF\n \tsymref-verify SP <ref> [SP <old-target>] LF\n \toption SP <opt> LF\n@@ -88,6 +89,7 @@ quoting:\n \tcreate SP <ref> NUL <new-oid> NUL\n \tdelete SP <ref> NUL [<old-oid>] NUL\n \tverify SP <ref> NUL [<old-oid>] NUL\n+\tsymref-create SP <ref> NUL <new-target> NUL\n \tsymref-delete SP <ref> [NUL <old-target>] NUL\n \tsymref-verify SP <ref> [NUL <old-target>] NUL\n \toption SP <opt> NUL\n@@ -121,6 +123,10 @@ verify::\n \tVerify <ref> against <old-oid> but do not change it.  If\n \t<old-oid> is zero or missing, the ref must not exist.\n \n+symref-create:\n+\tCreate symbolic ref <ref> with <new-target> after verifying\n+\tit does not exist.\n+\n symref-delete::\n \tDelete <ref> after verifying it exists with <old-target>, if given.\n \ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 23993b905b..6ddb3084e6 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -576,7 +576,7 @@ static void write_remote_refs(const struct ref *local_refs)\n \t\tif (!r->peer_ref)\n \t\t\tcontinue;\n \t\tif (ref_transaction_create(t, r->peer_ref->name, &r->old_oid,\n-\t\t\t\t\t   0, NULL, &err))\n+\t\t\t\t\t   NULL, 0, NULL, &err))\n \t\t\tdie(\"%s\", err.buf);\n \t}\n \ndiff --git a/builtin/update-ref.c b/builtin/update-ref.c\nindex a78c2f9c85..5ebfecf617 100644\n--- a/builtin/update-ref.c\n+++ b/builtin/update-ref.c\n@@ -257,7 +257,7 @@ static void parse_cmd_create(struct ref_transaction *transaction,\n \tif (*next != line_termination)\n \t\tdie(\"create %s: extra input: %s\", refname, next);\n \n-\tif (ref_transaction_create(transaction, refname, &new_oid,\n+\tif (ref_transaction_create(transaction, refname, &new_oid, NULL,\n \t\t\t\t   update_flags | create_reflog_flag,\n \t\t\t\t   msg, &err))\n \t\tdie(\"%s\", err.buf);\n@@ -267,6 +267,35 @@ static void parse_cmd_create(struct ref_transaction *transaction,\n \tstrbuf_release(&err);\n }\n \n+\n+static void parse_cmd_symref_create(struct ref_transaction *transaction,\n+\t\t\t\t    const char *next, const char *end)\n+{\n+\tstruct strbuf err = STRBUF_INIT;\n+\tchar *refname, *new_target;\n+\n+\trefname = parse_refname(&next);\n+\tif (!refname)\n+\t\tdie(\"symref-create: missing <ref>\");\n+\n+\tnew_target = parse_next_refname(&next);\n+\tif (!new_target)\n+\t\tdie(\"symref-create %s: missing <new-target>\", refname);\n+\n+\tif (*next != line_termination)\n+\t\tdie(\"symref-create %s: extra input: %s\", refname, next);\n+\n+\tif (ref_transaction_create(transaction, refname, NULL, new_target,\n+\t\t\t\t   update_flags | create_reflog_flag,\n+\t\t\t\t   msg, &err))\n+\t\tdie(\"%s\", err.buf);\n+\n+\tupdate_flags = default_flags;\n+\tfree(refname);\n+\tfree(new_target);\n+\tstrbuf_release(&err);\n+}\n+\n static void parse_cmd_delete(struct ref_transaction *transaction,\n \t\t\t     const char *next, const char *end)\n {\n@@ -473,6 +502,7 @@ static const struct parse_cmd {\n \t{ \"create\",        parse_cmd_create,        2, UPDATE_REFS_OPEN },\n \t{ \"delete\",        parse_cmd_delete,        2, UPDATE_REFS_OPEN },\n \t{ \"verify\",        parse_cmd_verify,        2, UPDATE_REFS_OPEN },\n+\t{ \"symref-create\", parse_cmd_symref_create, 2, UPDATE_REFS_OPEN },\n \t{ \"symref-delete\", parse_cmd_symref_delete, 2, UPDATE_REFS_OPEN },\n \t{ \"symref-verify\", parse_cmd_symref_verify, 2, UPDATE_REFS_OPEN },\n \t{ \"option\",        parse_cmd_option,        1, UPDATE_REFS_OPEN },\ndiff --git a/refs.c b/refs.c\nindex e29abebe1d..e2b8d9a6c8 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1268,15 +1268,18 @@ int ref_transaction_update(struct ref_transaction *transaction,\n int ref_transaction_create(struct ref_transaction *transaction,\n \t\t\t   const char *refname,\n \t\t\t   const struct object_id *new_oid,\n+\t\t\t   const char *new_target,\n \t\t\t   unsigned int flags, const char *msg,\n \t\t\t   struct strbuf *err)\n {\n-\tif (!new_oid || is_null_oid(new_oid)) {\n-\t\tstrbuf_addf(err, \"'%s' has a null OID\", refname);\n+\tif (new_oid && new_target)\n+\t\tBUG(\"create called with both new_oid and new_target set\");\n+\tif ((!new_oid || is_null_oid(new_oid)) && !new_target) {\n+\t\tstrbuf_addf(err, \"'%s' has neither a valid OID nor a target\", refname);\n \t\treturn 1;\n \t}\n \treturn ref_transaction_update(transaction, refname, new_oid,\n-\t\t\t\t      null_oid(), NULL, NULL, flags,\n+\t\t\t\t      null_oid(), new_target, NULL, flags,\n \t\t\t\t      msg, err);\n }\n \ndiff --git a/refs.h b/refs.h\nindex a054a4f998..1a58764298 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -708,6 +708,7 @@ int ref_transaction_update(struct ref_transaction *transaction,\n int ref_transaction_create(struct ref_transaction *transaction,\n \t\t\t   const char *refname,\n \t\t\t   const struct object_id *new_oid,\n+\t\t\t   const char *new_target,\n \t\t\t   unsigned int flags, const char *msg,\n \t\t\t   struct strbuf *err);\n \ndiff --git a/t/t0600-reffiles-backend.sh b/t/t0600-reffiles-backend.sh\nindex 92f570313d..b2a771ff2b 100755\n--- a/t/t0600-reffiles-backend.sh\n+++ b/t/t0600-reffiles-backend.sh\n@@ -468,4 +468,36 @@ test_expect_success POSIXPERM 'git reflog expire honors core.sharedRepository' '\n \tesac\n '\n \n+test_expect_success SYMLINKS 'symref transaction supports symlinks' '\n+\ttest_when_finished \"git symbolic-ref -d TEST_SYMREF_HEAD\" &&\n+\tgit update-ref refs/heads/new @ &&\n+\ttest_config core.prefersymlinkrefs true &&\n+\tcat >stdin <<-EOF &&\n+\tstart\n+\tsymref-create TEST_SYMREF_HEAD refs/heads/new\n+\tprepare\n+\tcommit\n+\tEOF\n+\tgit update-ref --no-deref --stdin <stdin &&\n+\ttest_path_is_symlink .git/TEST_SYMREF_HEAD &&\n+\ttest \"$(test_readlink .git/TEST_SYMREF_HEAD)\" = refs/heads/new\n+'\n+\n+test_expect_success 'symref transaction supports false symlink config' '\n+\ttest_when_finished \"git symbolic-ref -d TEST_SYMREF_HEAD\" &&\n+\tgit update-ref refs/heads/new @ &&\n+\ttest_config core.prefersymlinkrefs false &&\n+\tcat >stdin <<-EOF &&\n+\tstart\n+\tsymref-create TEST_SYMREF_HEAD refs/heads/new\n+\tprepare\n+\tcommit\n+\tEOF\n+\tgit update-ref --no-deref --stdin <stdin &&\n+\ttest_path_is_file .git/TEST_SYMREF_HEAD &&\n+\tgit symbolic-ref TEST_SYMREF_HEAD >actual &&\n+\techo refs/heads/new >expect &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\ndiff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\nindex 848d6fc42e..3d5f00ffb8 100755\n--- a/t/t1400-update-ref.sh\n+++ b/t/t1400-update-ref.sh\n@@ -1785,6 +1785,71 @@ do\n \t\ttest_must_fail git symbolic-ref -d refs/heads/symref2\n \t'\n \n+\ttest_expect_success \"stdin $type symref-create fails with too many arguments\" '\n+\t\tformat_command $type \"symref-create refs/heads/symref\" \"$a\" \"$a\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n+\t\tif test \"$type\" = \"-z\"\n+\t\tthen\n+\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\telse\n+\t\t\tgrep \"fatal: symref-create refs/heads/symref: extra input:  $a\" err\n+\t\tfi\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-create fails with no target\" '\n+\t\tformat_command $type \"symref-create refs/heads/symref\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-create fails with empty target\" '\n+\t\tformat_command $type \"symref-create refs/heads/symref\" \"\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-create works\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tformat_command $type \"symref-create refs/heads/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\techo $a >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-create works with --no-deref\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tformat_command $type \"symref-create refs/heads/symref\" \"$a\" &&\n+\t\tgit update-ref --stdin $type <stdin 2>err\n+\t'\n+\n+\ttest_expect_success \"stdin $type create dangling symref ref works\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tformat_command $type \"symref-create refs/heads/symref\" \"refs/heads/unkown\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\techo refs/heads/unkown >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-create does not create reflogs by default\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/symref\" &&\n+\t\tformat_command $type \"symref-create refs/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\tgit symbolic-ref refs/symref >expect &&\n+\t\techo $a >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest_must_fail git reflog exists refs/symref\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-create reflogs with --create-reflog\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tformat_command $type \"symref-create refs/heads/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref --create-reflog --stdin $type --no-deref <stdin &&\n+\t\tgit symbolic-ref refs/heads/symref >expect &&\n+\t\techo $a >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\tgit reflog exists refs/heads/symref\n+\t'\n+\n done\n \n test_done\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex ccde1b944b..ff77dcca6b 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -189,15 +189,18 @@ test_expect_success 'hook gets all queued symref updates' '\n \tprepared\n \tref:refs/heads/main $ZERO_OID refs/heads/symref\n \tref:refs/heads/main $ZERO_OID refs/heads/symrefd\n+\t$ZERO_OID ref:refs/heads/main refs/heads/symrefc\n \tcommitted\n \tref:refs/heads/main $ZERO_OID refs/heads/symref\n \tref:refs/heads/main $ZERO_OID refs/heads/symrefd\n+\t$ZERO_OID ref:refs/heads/main refs/heads/symrefc\n \tEOF\n \n \tgit update-ref --no-deref --stdin <<-EOF &&\n \tstart\n \tsymref-verify refs/heads/symref refs/heads/main\n \tsymref-delete refs/heads/symrefd refs/heads/main\n+\tsymref-create refs/heads/symrefc refs/heads/main\n \tprepare\n \tcommit\n \tEOF\ndiff --git a/t/t5605-clone-local.sh b/t/t5605-clone-local.sh\nindex a3055869bc..339d8c786f 100755\n--- a/t/t5605-clone-local.sh\n+++ b/t/t5605-clone-local.sh\n@@ -163,7 +163,7 @@ test_expect_success REFFILES 'local clone from repo with corrupt refs fails grac\n \techo a >corrupt/.git/refs/heads/topic &&\n \n \ttest_must_fail git clone corrupt working 2>err &&\n-\tgrep \"has a null OID\" err\n+\tgrep \"has neither a valid OID nor a target\" err\n '\n \n test_done\n-- \n2.43.GIT\n\n"},{"id":"495930","messageId":"20240530120940.456817-6-knayak@gitlab.com","threadId":"61469","inReplyTo":"20240514124411.1037019-1-knayak@gitlab.com","subject":"[PATCH v3 5/6] reftable: pick either 'oid' or 'target' for new updates","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-30T12:09:39Z","receivedAt":"2024-05-30T12:09:56Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"From: Karthik Nayak <karthik.188@gmail.com>\n\nWhen creating a reference transaction update, we can provide the old/new\noid/target for the update. We have checks in place to ensure that for\neach old/new, either oid or target is set and not both.\n\nIn the reftable backend, when dealing with updates without the\n`REF_NO_DEREF` flag, we don't selectively propagate data as needed.\nSince there are no active users of the path, this is not caught. As we\nwant to introduce the 'symref-update' command in the upcoming commit,\nwhich would use this flow, correct it.\n\nHelped-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n refs/reftable-backend.c | 5 +++--\n 1 file changed, 3 insertions(+), 2 deletions(-)\n\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex b838cf8f00..3cbf95525f 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -895,8 +895,9 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n \t\t\t\t */\n \t\t\t\tnew_update = ref_transaction_add_update(\n \t\t\t\t\ttransaction, referent.buf, new_flags,\n-\t\t\t\t\t&u->new_oid, &u->old_oid, u->new_target,\n-\t\t\t\t\tu->old_target, u->msg);\n+\t\t\t\t\tu->new_target ? NULL : &u->new_oid,\n+\t\t\t\t\tu->old_target ? NULL : &u->old_oid,\n+\t\t\t\t\tu->new_target, u->old_target, u->msg);\n \n \t\t\t\tnew_update->parent_update = u;\n \n-- \n2.43.GIT\n\n"},{"id":"495931","messageId":"20240530120940.456817-7-knayak@gitlab.com","threadId":"61469","inReplyTo":"20240514124411.1037019-1-knayak@gitlab.com","subject":"[PATCH v3 6/6] update-ref: add support for 'symref-update' command","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-30T12:09:40Z","receivedAt":"2024-05-30T12:09:58Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"From: Karthik Nayak <karthik.188@gmail.com>\n\nAdd 'symref-update' command to the '--stdin' mode of 'git-update-ref' to\nallow updates of symbolic refs. The 'symref-update' command takes in a\n<new-target>, which the <ref> will be updated to. If the <ref> doesn't\nexist it will be created.\n\nIt also optionally takes either an `ref <old-target>` or `oid\n<old-oid>`. If the <old-target> is provided, it checks to see if the\n<ref> targets the <old-target> before the update. If <old-oid> is provided\nit checks <ref> to ensure that it is a regular ref and <old-oid> is the\nOID before the update. This by extension also means that this when a\nzero <old-oid> is provided, it ensures that the ref didn't exist before.\n\nThe divergence in syntax from the regular `update` command is because if\nwe don't use a `(ref | oid)` prefix for the old_value, then there is\nambiguity around if the value provided should be treated as an oid or a\nreference. This is more so the reason, because we allow anything\ncommittish to be provided as an oid.\n\nThe command allows users to perform symbolic ref updates within a\ntransaction. This provides atomicity and allows users to perform a set\nof operations together.\n\nThis command supports deref mode, to ensure that we can update\ndereferenced regular refs to symrefs.\n\nHelped-by: Patrick Steinhardt <ps@pks.im>\nHelped-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n Documentation/git-update-ref.txt |   7 ++\n builtin/update-ref.c             |  92 ++++++++++++++\n t/t1400-update-ref.sh            | 203 +++++++++++++++++++++++++++++++\n t/t1416-ref-transaction-hooks.sh |   4 +\n 4 files changed, 306 insertions(+)\n\ndiff --git a/Documentation/git-update-ref.txt b/Documentation/git-update-ref.txt\nindex 364ef78af1..afcf33cf60 100644\n--- a/Documentation/git-update-ref.txt\n+++ b/Documentation/git-update-ref.txt\n@@ -65,6 +65,7 @@ performs all modifications together.  Specify commands of the form:\n \tcreate SP <ref> SP <new-oid> LF\n \tdelete SP <ref> [SP <old-oid>] LF\n \tverify SP <ref> [SP <old-oid>] LF\n+\tsymref-update SP <ref> SP <new-target> [SP (ref SP <old-target> | oid SP <old-oid>)] LF\n \tsymref-create SP <ref> SP <new-target> LF\n \tsymref-delete SP <ref> [SP <old-target>] LF\n \tsymref-verify SP <ref> [SP <old-target>] LF\n@@ -89,6 +90,7 @@ quoting:\n \tcreate SP <ref> NUL <new-oid> NUL\n \tdelete SP <ref> NUL [<old-oid>] NUL\n \tverify SP <ref> NUL [<old-oid>] NUL\n+\tsymref-update SP <ref> NUL <new-target> [NUL (ref NUL <old-target> | oid NUL <old-oid>)] NUL\n \tsymref-create SP <ref> NUL <new-target> NUL\n \tsymref-delete SP <ref> [NUL <old-target>] NUL\n \tsymref-verify SP <ref> [NUL <old-target>] NUL\n@@ -119,6 +121,11 @@ delete::\n \tDelete <ref> after verifying it exists with <old-oid>, if\n \tgiven.  If given, <old-oid> may not be zero.\n \n+symref-update::\n+\tSet <ref> to <new-target> after verifying <old-target> or <old-oid>,\n+\tif given. Specify a zero <old-oid> to ensure that the ref does not\n+\texist before the update.\n+\n verify::\n \tVerify <ref> against <old-oid> but do not change it.  If\n \t<old-oid> is zero or missing, the ref must not exist.\ndiff --git a/builtin/update-ref.c b/builtin/update-ref.c\nindex 5ebfecf617..82f461d6f8 100644\n--- a/builtin/update-ref.c\n+++ b/builtin/update-ref.c\n@@ -98,6 +98,42 @@ static char *parse_next_refname(const char **next)\n \treturn parse_refname(next);\n }\n \n+/*\n+ * Wrapper around parse_arg which skips the next delimiter.\n+ */\n+static char *parse_next_arg(const char **next)\n+{\n+\tstruct strbuf arg = STRBUF_INIT;\n+\n+\tif (line_termination) {\n+\t\t/* Without -z, consume SP and use next argument */\n+\t\tif (!**next || **next == line_termination)\n+\t\t\treturn NULL;\n+\t\tif (**next != ' ')\n+\t\t\tdie(\"expected SP but got: %s\", *next);\n+\t} else {\n+\t\t/* With -z, read the next NUL-terminated line */\n+\t\tif (**next)\n+\t\t\treturn NULL;\n+\t}\n+\t/* Skip the delimiter */\n+\t(*next)++;\n+\n+\tif (line_termination) {\n+\t\t/* Without -z, use the next argument */\n+\t\t*next = parse_arg(*next, &arg);\n+\t} else {\n+\t\t/* With -z, use everything up to the next NUL */\n+\t\tstrbuf_addstr(&arg, *next);\n+\t\t*next += arg.len;\n+\t}\n+\n+\tif (arg.len)\n+\t\treturn strbuf_detach(&arg, NULL);\n+\n+\tstrbuf_release(&arg);\n+\treturn NULL;\n+}\n \n /*\n  * The value being parsed is <old-oid> (as opposed to <new-oid>; the\n@@ -237,6 +273,61 @@ static void parse_cmd_update(struct ref_transaction *transaction,\n \tstrbuf_release(&err);\n }\n \n+static void parse_cmd_symref_update(struct ref_transaction *transaction,\n+\t\t\t\t    const char *next, const char *end)\n+{\n+\tchar *refname, *new_target, *old_arg;\n+\tchar *old_target = NULL;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tstruct object_id old_oid;\n+\tint have_old_oid = 0;\n+\n+\trefname = parse_refname(&next);\n+\tif (!refname)\n+\t\tdie(\"symref-update: missing <ref>\");\n+\n+\tnew_target = parse_next_refname(&next);\n+\tif (!new_target)\n+\t\tdie(\"symref-update %s: missing <new-target>\", refname);\n+\n+\told_arg = parse_next_arg(&next);\n+\tif (old_arg) {\n+\t\told_target = parse_next_arg(&next);\n+\t\tif (!old_target)\n+\t\t\tdie(\"symref-update %s: expected old value\", refname);\n+\n+\t\tif (!strcmp(old_arg, \"oid\")) {\n+\t\t\tif (repo_get_oid(the_repository, old_target, &old_oid))\n+\t\t\t\tdie(\"symref-update %s: invalid oid: %s\", refname, old_target);\n+\n+\t\t\thave_old_oid = 1;\n+\t\t} else if (!strcmp(old_arg, \"ref\")) {\n+\t\t\tif (check_refname_format(old_target, REFNAME_ALLOW_ONELEVEL))\n+\t\t\t\tdie(\"symref-update %s: invalid ref: %s\", refname, old_target);\n+\t\t} else {\n+\t\t\tdie(\"symref-update %s: invalid arg '%s' for old value\", refname, old_arg);\n+\t\t}\n+\t}\n+\n+\tif (*next != line_termination)\n+\t\tdie(\"symref-update %s: extra input: %s\", refname, next);\n+\n+\tif (ref_transaction_update(transaction, refname, NULL,\n+\t\t\t\t   have_old_oid ? &old_oid : NULL,\n+\t\t\t\t   new_target,\n+\t\t\t\t   have_old_oid ? NULL : old_target,\n+\t\t\t\t   update_flags | create_reflog_flag,\n+\t\t\t\t   msg, &err))\n+\t\tdie(\"%s\", err.buf);\n+\n+\tupdate_flags = default_flags;\n+\tfree(refname);\n+\tfree(old_arg);\n+\tfree(old_target);\n+\tfree(new_target);\n+\tstrbuf_release(&err);\n+}\n+\n static void parse_cmd_create(struct ref_transaction *transaction,\n \t\t\t     const char *next, const char *end)\n {\n@@ -502,6 +593,7 @@ static const struct parse_cmd {\n \t{ \"create\",        parse_cmd_create,        2, UPDATE_REFS_OPEN },\n \t{ \"delete\",        parse_cmd_delete,        2, UPDATE_REFS_OPEN },\n \t{ \"verify\",        parse_cmd_verify,        2, UPDATE_REFS_OPEN },\n+\t{ \"symref-update\", parse_cmd_symref_update, 4, UPDATE_REFS_OPEN },\n \t{ \"symref-create\", parse_cmd_symref_create, 2, UPDATE_REFS_OPEN },\n \t{ \"symref-delete\", parse_cmd_symref_delete, 2, UPDATE_REFS_OPEN },\n \t{ \"symref-verify\", parse_cmd_symref_verify, 2, UPDATE_REFS_OPEN },\ndiff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\nindex 3d5f00ffb8..c208cf777e 100755\n--- a/t/t1400-update-ref.sh\n+++ b/t/t1400-update-ref.sh\n@@ -1362,6 +1362,7 @@ test_expect_success 'fails with duplicate HEAD update' '\n '\n \n test_expect_success 'fails with duplicate ref update via symref' '\n+\ttest_when_finished \"git symbolic-ref -d refs/heads/symref2\" &&\n \tgit branch target2 $A &&\n \tgit symbolic-ref refs/heads/symref2 refs/heads/target2 &&\n \tcat >stdin <<-EOF &&\n@@ -1850,6 +1851,208 @@ do\n \t\tgit reflog exists refs/heads/symref\n \t'\n \n+\ttest_expect_success \"stdin $type symref-update fails with too many arguments\" '\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"ref\" \"$a\" \"$a\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n+\t\tif test \"$type\" = \"-z\"\n+\t\tthen\n+\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\telse\n+\t\t\tgrep \"fatal: symref-update refs/heads/symref: extra input:  $a\" err\n+\t\tfi\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update fails with wrong old value argument\" '\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"foo\" \"$a\" \"$a\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n+\t\tgrep \"fatal: symref-update refs/heads/symref: invalid arg ${SQ}foo${SQ} for old value\" err\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update creates with zero old value\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"oid\" \"$Z\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update creates with no old value\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update creates dangling\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\ttest_must_fail git rev-parse refs/heads/nonexistent &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"refs/heads/nonexistent\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\techo refs/heads/nonexistent >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update fails with wrong old value\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tgit symbolic-ref refs/heads/symref $a &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$m\" \"ref\" \"$b\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n+\t\tgrep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected $b\" err &&\n+\t\ttest_must_fail git rev-parse --verify -q $c\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update updates dangling ref\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\ttest_must_fail git rev-parse refs/heads/nonexistent &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/nonexistent &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update updates dangling ref with old value\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\ttest_must_fail git rev-parse refs/heads/nonexistent &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/nonexistent &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"ref\" \"refs/heads/nonexistent\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update fails update dangling ref with wrong old value\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\ttest_must_fail git rev-parse refs/heads/nonexistent &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/nonexistent &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"ref\" \"refs/heads/wrongref\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin &&\n+\t\techo refs/heads/nonexistent >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update works with right old value\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tgit symbolic-ref refs/heads/symref $a &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$m\" \"ref\" \"$a\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\techo $m >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update works with no old value\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tgit symbolic-ref refs/heads/symref $a &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$m\" >stdin &&\n+\t\tgit update-ref --stdin $type --no-deref <stdin &&\n+\t\techo $m >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update fails with empty old ref-target\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tgit symbolic-ref refs/heads/symref $a &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$m\" \"ref\" \"\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update creates (with deref)\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref --stdin $type <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref --no-recurse refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >actual &&\n+\t\tgrep \"$Z $(git rev-parse $a)\" actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update regular ref to symref with correct old-oid\" '\n+\t\ttest_when_finished \"git symbolic-ref -d --no-recurse refs/heads/regularref\" &&\n+\t\tgit update-ref --no-deref refs/heads/regularref $a &&\n+\t\tformat_command $type \"symref-update refs/heads/regularref\" \"$a\" \"oid\" \"$(git rev-parse $a)\" >stdin &&\n+\t\tgit update-ref --stdin $type <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref --no-recurse refs/heads/regularref >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/regularref >actual &&\n+\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update regular ref to symref fails with wrong old-oid\" '\n+\t\ttest_when_finished \"git update-ref -d refs/heads/regularref\" &&\n+\t\tgit update-ref --no-deref refs/heads/regularref $a &&\n+\t\tformat_command $type \"symref-update refs/heads/regularref\" \"$a\" \"oid\" \"$(git rev-parse refs/heads/target2)\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n+\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: is at $(git rev-parse $a) but expected $(git rev-parse refs/heads/target2)\" err &&\n+\t\techo $(git rev-parse $a) >expect &&\n+\t\tgit rev-parse refs/heads/regularref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update regular ref to symref fails with invalid old-oid\" '\n+\t\ttest_when_finished \"git update-ref -d refs/heads/regularref\" &&\n+\t\tgit update-ref --no-deref refs/heads/regularref $a &&\n+\t\tformat_command $type \"symref-update refs/heads/regularref\" \"$a\" \"oid\" \"not-a-ref-oid\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n+\t\tgrep \"fatal: symref-update refs/heads/regularref: invalid oid: not-a-ref-oid\" err &&\n+\t\techo $(git rev-parse $a) >expect &&\n+\t\tgit rev-parse refs/heads/regularref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update existing symref with zero old-oid\" '\n+\t\ttest_when_finished \"git symbolic-ref -d --no-recurse refs/heads/symref\" &&\n+\t\tgit symbolic-ref refs/heads/symref refs/heads/target2 &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"oid\" \"$Z\" >stdin &&\n+\t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n+\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/symref${SQ}: reference already exists\" err &&\n+\t\techo refs/heads/target2 >expect &&\n+\t\tgit symbolic-ref refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update regular ref to symref (with deref)\" '\n+\t\ttest_when_finished \"git symbolic-ref -d refs/heads/symref\" &&\n+\t\ttest_when_finished \"git update-ref -d --no-deref refs/heads/symref2\" &&\n+\t\tgit update-ref refs/heads/symref2 $a &&\n+\t\tgit symbolic-ref --no-recurse refs/heads/symref refs/heads/symref2 &&\n+\t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" >stdin &&\n+\t\tgit update-ref $type --stdin <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref --no-recurse refs/heads/symref2 >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\techo refs/heads/symref2 >expect &&\n+\t\tgit symbolic-ref --no-recurse refs/heads/symref >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >actual &&\n+\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n+\t'\n+\n+\ttest_expect_success \"stdin $type symref-update regular ref to symref\" '\n+\t\ttest_when_finished \"git symbolic-ref -d --no-recurse refs/heads/regularref\" &&\n+\t\tgit update-ref --no-deref refs/heads/regularref $a &&\n+\t\tformat_command $type \"symref-update refs/heads/regularref\" \"$a\" >stdin &&\n+\t\tgit update-ref $type --stdin <stdin &&\n+\t\techo $a >expect &&\n+\t\tgit symbolic-ref --no-recurse refs/heads/regularref >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest-tool ref-store main for-each-reflog-ent refs/heads/regularref >actual &&\n+\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n+\t'\n+\n done\n \n test_done\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex ff77dcca6b..5a812ca3c0 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -163,6 +163,7 @@ test_expect_success 'hook gets all queued symref updates' '\n \tgit update-ref refs/heads/branch $POST_OID &&\n \tgit symbolic-ref refs/heads/symref refs/heads/main &&\n \tgit symbolic-ref refs/heads/symrefd refs/heads/main &&\n+\tgit symbolic-ref refs/heads/symrefu refs/heads/main &&\n \n \ttest_hook reference-transaction <<-\\EOF &&\n \techo \"$*\" >>actual\n@@ -190,10 +191,12 @@ test_expect_success 'hook gets all queued symref updates' '\n \tref:refs/heads/main $ZERO_OID refs/heads/symref\n \tref:refs/heads/main $ZERO_OID refs/heads/symrefd\n \t$ZERO_OID ref:refs/heads/main refs/heads/symrefc\n+\tref:refs/heads/main ref:refs/heads/branch refs/heads/symrefu\n \tcommitted\n \tref:refs/heads/main $ZERO_OID refs/heads/symref\n \tref:refs/heads/main $ZERO_OID refs/heads/symrefd\n \t$ZERO_OID ref:refs/heads/main refs/heads/symrefc\n+\tref:refs/heads/main ref:refs/heads/branch refs/heads/symrefu\n \tEOF\n \n \tgit update-ref --no-deref --stdin <<-EOF &&\n@@ -201,6 +204,7 @@ test_expect_success 'hook gets all queued symref updates' '\n \tsymref-verify refs/heads/symref refs/heads/main\n \tsymref-delete refs/heads/symrefd refs/heads/main\n \tsymref-create refs/heads/symrefc refs/heads/main\n+\tsymref-update refs/heads/symrefu refs/heads/branch ref refs/heads/main\n \tprepare\n \tcommit\n \tEOF\n-- \n2.43.GIT\n\n"},{"id":"496337","messageId":"ZmAbgGfNpZzU76pd@tanuki","threadId":"61469","inReplyTo":"20240530120940.456817-4-knayak@gitlab.com","subject":"Re: [PATCH v3 3/6] update-ref: add support for 'symref-delete' command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-06-05T08:02:08Z","receivedAt":"2024-06-05T08:02:15Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, May 30, 2024 at 03:09:37PM +0300, Karthik Nayak wrote:\n> diff --git a/refs.c b/refs.c\n> index cdc4d25557..e29abebe1d 100644\n> --- a/refs.c\n> +++ b/refs.c\n> @@ -950,7 +950,7 @@ int refs_delete_ref(struct ref_store *refs, const char *msg,\n>  \ttransaction = ref_store_transaction_begin(refs, &err);\n>  \tif (!transaction ||\n>  \t    ref_transaction_delete(transaction, refname, old_oid,\n> -\t\t\t\t   flags, msg, &err) ||\n> +\t\t\t\t   flags, NULL, msg, &err) ||\n>  \t    ref_transaction_commit(transaction, &err)) {\n>  \t\terror(\"%s\", err.buf);\n>  \t\tref_transaction_free(transaction);\n> @@ -1283,14 +1283,20 @@ int ref_transaction_create(struct ref_transaction *transaction,\n>  int ref_transaction_delete(struct ref_transaction *transaction,\n>  \t\t\t   const char *refname,\n>  \t\t\t   const struct object_id *old_oid,\n> -\t\t\t   unsigned int flags, const char *msg,\n> +\t\t\t   unsigned int flags,\n> +\t\t\t   const char *old_target,\n\nThe old target is somewhat weirdly placed here, as I'd have expected it\nto live next to `old_oid`. That's only a minor nit, nothing that's worth\na reroll in my opinion.\n\n> diff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\n> index 52801be07d..848d6fc42e 100755\n> --- a/t/t1400-update-ref.sh\n> +++ b/t/t1400-update-ref.sh\n> @@ -1731,6 +1731,60 @@ do\n>  \t\ttest_cmp expect actual\n>  \t'\n>  \n> +\ttest_expect_success \"stdin $type symref-delete fails without --no-deref\" '\n> +\t\tgit symbolic-ref refs/heads/symref $a &&\n> +\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$a\" >stdin &&\n> +\t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n> +\t\tgrep \"fatal: symref-delete: cannot operate with deref mode\" err\n> +\t'\n> +\n> +\ttest_expect_success \"stdin $type symref-delete fails with no ref\" '\n> +\t\tformat_command $type \"symref-delete \" >stdin &&\n> +\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n> +\t\tgrep \"fatal: symref-delete: missing <ref>\" err\n> +\t'\n> +\n> +\ttest_expect_success \"stdin $type symref-delete fails with too many arguments\" '\n> +\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$a\" \"$a\" >stdin &&\n> +\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n> +\t\tif test \"$type\" = \"-z\"\n> +\t\tthen\n> +\t\t\tgrep \"fatal: unknown command: $a\" err\n> +\t\telse\n> +\t\t\tgrep \"fatal: symref-delete refs/heads/symref: extra input:  $a\" err\n> +\t\tfi\n> +\t'\n> +\n> +\ttest_expect_success \"stdin $type symref-delete fails with wrong old value\" '\n> +\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$m\" >stdin &&\n> +\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n> +\t\tgrep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected refs/heads/main\" err &&\n> +\t\tgit symbolic-ref refs/heads/symref >expect &&\n> +\t\techo $a >actual &&\n> +\t\ttest_cmp expect actual\n> +\t'\n> +\n> +\ttest_expect_success \"stdin $type symref-delete works with right old value\" '\n> +\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$a\" >stdin &&\n> +\t\tgit update-ref --stdin $type --no-deref <stdin &&\n> +\t\ttest_must_fail git rev-parse --verify -q refs/heads/symref\n> +\t'\n> +\n> +\ttest_expect_success \"stdin $type symref-delete works with empty old value\" '\n> +\t\tgit symbolic-ref refs/heads/symref $a >stdin &&\n> +\t\tformat_command $type \"symref-delete refs/heads/symref\" \"\" >stdin &&\n> +\t\tgit update-ref --stdin $type --no-deref <stdin &&\n> +\t\ttest_must_fail git rev-parse --verify -q $b\n> +\t'\n> +\n> +\ttest_expect_success \"stdin $type symref-delete succeeds for dangling reference\" '\n> +\t\ttest_must_fail git symbolic-ref refs/heads/nonexistent &&\n> +\t\tgit symbolic-ref refs/heads/symref2 refs/heads/nonexistent &&\n> +\t\tformat_command $type \"symref-delete refs/heads/symref2\" \"refs/heads/nonexistent\" >stdin &&\n> +\t\tgit update-ref --stdin $type --no-deref <stdin &&\n> +\t\ttest_must_fail git symbolic-ref -d refs/heads/symref2\n> +\t'\n> +\n\nNot sure whether I overlooked it, but one test I missed was when trying\nto delete a non-symbolic-ref.\n\nPatrick\n"},{"id":"496338","messageId":"ZmAbhTN0mbGO5t8x@tanuki","threadId":"61469","inReplyTo":"20240530120940.456817-5-knayak@gitlab.com","subject":"Re: [PATCH v3 4/6] update-ref: add support for 'symref-create' command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-06-05T08:02:13Z","receivedAt":"2024-06-05T08:02:18Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, May 30, 2024 at 03:09:38PM +0300, Karthik Nayak wrote:\n> From: Karthik Nayak <karthik.188@gmail.com>\n> diff --git a/refs.h b/refs.h\n> index a054a4f998..1a58764298 100644\n> --- a/refs.h\n> +++ b/refs.h\n> @@ -708,6 +708,7 @@ int ref_transaction_update(struct ref_transaction *transaction,\n>  int ref_transaction_create(struct ref_transaction *transaction,\n>  \t\t\t   const char *refname,\n>  \t\t\t   const struct object_id *new_oid,\n> +\t\t\t   const char *new_target,\n>  \t\t\t   unsigned int flags, const char *msg,\n>  \t\t\t   struct strbuf *err);\n\nHere, the `new_target` is in the expected place right next to `new_oid`.\n\nPatrick\n"},{"id":"496339","messageId":"ZmAbi6RtD7EFkKJm@tanuki","threadId":"61469","inReplyTo":"20240530120940.456817-7-knayak@gitlab.com","subject":"Re: [PATCH v3 6/6] update-ref: add support for 'symref-update' command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-06-05T08:02:19Z","receivedAt":"2024-06-05T08:02:23Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, May 30, 2024 at 03:09:40PM +0300, Karthik Nayak wrote:\n> From: Karthik Nayak <karthik.188@gmail.com>\n> \n> Add 'symref-update' command to the '--stdin' mode of 'git-update-ref' to\n> allow updates of symbolic refs. The 'symref-update' command takes in a\n> <new-target>, which the <ref> will be updated to. If the <ref> doesn't\n> exist it will be created.\n> \n> It also optionally takes either an `ref <old-target>` or `oid\n> <old-oid>`. If the <old-target> is provided, it checks to see if the\n> <ref> targets the <old-target> before the update. If <old-oid> is provided\n> it checks <ref> to ensure that it is a regular ref and <old-oid> is the\n> OID before the update. This by extension also means that this when a\n> zero <old-oid> is provided, it ensures that the ref didn't exist before.\n> \n> The divergence in syntax from the regular `update` command is because if\n> we don't use a `(ref | oid)` prefix for the old_value, then there is\n> ambiguity around if the value provided should be treated as an oid or a\n> reference. This is more so the reason, because we allow anything\n> committish to be provided as an oid.\n\nIt would be nice to explain why this isn't needed for symref-delete and\nsymref-create, which also both have an old value.\n\nPatrick\n"},{"id":"496340","messageId":"ZmAbkVtgfriYsOdX@tanuki","threadId":"61469","inReplyTo":"20240530120940.456817-1-knayak@gitlab.com","subject":"Re: [PATCH v3 0/6] update-ref: add symref support for --stdin","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-06-05T08:02:25Z","receivedAt":"2024-06-05T08:02:29Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, May 30, 2024 at 03:09:34PM +0300, Karthik Nayak wrote:\n> From: Karthik Nayak <karthik.188@gmail.com>\n> \n> The 'update-ref' command is used to update refs using transactions. The\n> command allows users to also utilize a '--stdin' mode to provide a\n> batch of sub-commands which can be processed in a transaction.\n> \n> Currently, the sub-commands involve {verify, delete, create, update}\n> and they allow users to work with regular refs in the repository. To\n> work with symrefs, users only have the option of using\n> 'git-symbolic-ref', which doesn't provide transaction support to the\n> users eventhough it uses the same behind the hood.\n> \n> Recently, we modified the reference backend to add symref support,\n> following which, 'git-symbolic-ref' also uses the transaction backend.\n> But, it doesn't expose this to the user. To allow users to work with\n> symrefs via transaction, this series adds support for new sub-commands\n> {symrer-verify, symref-delete, symref-create, symref-update} to the\n> '--stdin' mode of update-ref. These complement the existing\n> sub-commands.\n> \n> The patches 1, 5 fix small issues in the reference backends. The other\n> patches 2, 3, 4 & 6, each add one of the new sub-commands.\n> \n> The series is based off master, with 'kn/ref-transaction-symref' merged\n> in. There seem to be no conflicts with 'next' or 'seen'.\n> \n> There was some discussion [1] also about adding `old_target` support to\n> the existing `update` command. I think its worthwhile to do this with\n> some tests cleanup, will follow that up as a separate series.\n> \n> Changes since v2:\n> * Based off 'ps/fixleaks' (commit: ebdbefa4fe9f618347124b37d44e517e0c6a3e4c)\n> which brought to light two leaks, which have been fixed.\n> * Adding credit where it's due.\n> \n> [1]: https://lore.kernel.org/r/CAOLa=ZQW-cCV5BP_fCvuZimfkjwAzjEiqXYRPft1Wf9kAX=_bw@mail.gmail.com\n> \n> I used the '--range-diff' flag for 'git-format-patch(1)' this time.\n> Also I'm on vacation at the moment, so my responses are a bit slower than usual.\n\nThanks, overall this looks good to me. I've got a small couple of nits,\nnone of which are mandatory to be addressed from my point of view.\n\nPatrick\n"},{"id":"496353","messageId":"CAOLa=ZSDRcsmBxje-EgdKNyXCh1OYc4vtzJZKmoHSs94fMxPtw@mail.gmail.com","threadId":"61469","inReplyTo":"ZmAbgGfNpZzU76pd@tanuki","subject":"Re: [PATCH v3 3/6] update-ref: add support for 'symref-delete' command","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-06-05T09:31:45Z","receivedAt":"2024-06-05T09:31:47Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Thu, May 30, 2024 at 03:09:37PM +0300, Karthik Nayak wrote:\n>> diff --git a/refs.c b/refs.c\n>> index cdc4d25557..e29abebe1d 100644\n>> --- a/refs.c\n>> +++ b/refs.c\n>> @@ -950,7 +950,7 @@ int refs_delete_ref(struct ref_store *refs, const char *msg,\n>>  \ttransaction = ref_store_transaction_begin(refs, &err);\n>>  \tif (!transaction ||\n>>  \t    ref_transaction_delete(transaction, refname, old_oid,\n>> -\t\t\t\t   flags, msg, &err) ||\n>> +\t\t\t\t   flags, NULL, msg, &err) ||\n>>  \t    ref_transaction_commit(transaction, &err)) {\n>>  \t\terror(\"%s\", err.buf);\n>>  \t\tref_transaction_free(transaction);\n>> @@ -1283,14 +1283,20 @@ int ref_transaction_create(struct ref_transaction *transaction,\n>>  int ref_transaction_delete(struct ref_transaction *transaction,\n>>  \t\t\t   const char *refname,\n>>  \t\t\t   const struct object_id *old_oid,\n>> -\t\t\t   unsigned int flags, const char *msg,\n>> +\t\t\t   unsigned int flags,\n>> +\t\t\t   const char *old_target,\n>\n> The old target is somewhat weirdly placed here, as I'd have expected it\n> to live next to `old_oid`. That's only a minor nit, nothing that's worth\n> a reroll in my opinion.\n>\n\nThis is a good point.\n\n>> diff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\n>> index 52801be07d..848d6fc42e 100755\n>> --- a/t/t1400-update-ref.sh\n>> +++ b/t/t1400-update-ref.sh\n>> @@ -1731,6 +1731,60 @@ do\n>>  \t\ttest_cmp expect actual\n>>  \t'\n>>\n>> +\ttest_expect_success \"stdin $type symref-delete fails without --no-deref\" '\n>> +\t\tgit symbolic-ref refs/heads/symref $a &&\n>> +\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$a\" >stdin &&\n>> +\t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n>> +\t\tgrep \"fatal: symref-delete: cannot operate with deref mode\" err\n>> +\t'\n>> +\n>> +\ttest_expect_success \"stdin $type symref-delete fails with no ref\" '\n>> +\t\tformat_command $type \"symref-delete \" >stdin &&\n>> +\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n>> +\t\tgrep \"fatal: symref-delete: missing <ref>\" err\n>> +\t'\n>> +\n>> +\ttest_expect_success \"stdin $type symref-delete fails with too many arguments\" '\n>> +\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$a\" \"$a\" >stdin &&\n>> +\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n>> +\t\tif test \"$type\" = \"-z\"\n>> +\t\tthen\n>> +\t\t\tgrep \"fatal: unknown command: $a\" err\n>> +\t\telse\n>> +\t\t\tgrep \"fatal: symref-delete refs/heads/symref: extra input:  $a\" err\n>> +\t\tfi\n>> +\t'\n>> +\n>> +\ttest_expect_success \"stdin $type symref-delete fails with wrong old value\" '\n>> +\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$m\" >stdin &&\n>> +\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n>> +\t\tgrep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected refs/heads/main\" err &&\n>> +\t\tgit symbolic-ref refs/heads/symref >expect &&\n>> +\t\techo $a >actual &&\n>> +\t\ttest_cmp expect actual\n>> +\t'\n>> +\n>> +\ttest_expect_success \"stdin $type symref-delete works with right old value\" '\n>> +\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$a\" >stdin &&\n>> +\t\tgit update-ref --stdin $type --no-deref <stdin &&\n>> +\t\ttest_must_fail git rev-parse --verify -q refs/heads/symref\n>> +\t'\n>> +\n>> +\ttest_expect_success \"stdin $type symref-delete works with empty old value\" '\n>> +\t\tgit symbolic-ref refs/heads/symref $a >stdin &&\n>> +\t\tformat_command $type \"symref-delete refs/heads/symref\" \"\" >stdin &&\n>> +\t\tgit update-ref --stdin $type --no-deref <stdin &&\n>> +\t\ttest_must_fail git rev-parse --verify -q $b\n>> +\t'\n>> +\n>> +\ttest_expect_success \"stdin $type symref-delete succeeds for dangling reference\" '\n>> +\t\ttest_must_fail git symbolic-ref refs/heads/nonexistent &&\n>> +\t\tgit symbolic-ref refs/heads/symref2 refs/heads/nonexistent &&\n>> +\t\tformat_command $type \"symref-delete refs/heads/symref2\" \"refs/heads/nonexistent\" >stdin &&\n>> +\t\tgit update-ref --stdin $type --no-deref <stdin &&\n>> +\t\ttest_must_fail git symbolic-ref -d refs/heads/symref2\n>> +\t'\n>> +\n>\n> Not sure whether I overlooked it, but one test I missed was when trying\n> to delete a non-symbolic-ref.\n>\n> Patrick\n\nI think this point is worthwhile of a reroll, not because of the test\nitself, but because while testing it, I realized that currently when we\ntry to delete a regular ref using `symref-delete` and providing an\n`old_target`, the error message is quite generic. I've added a new\ncommit to make this more specific and will send in a new version today.\n"},{"id":"496375","messageId":"xmqqv82nuzpv.fsf@gitster.g","threadId":"61469","inReplyTo":"ZmAbgGfNpZzU76pd@tanuki","subject":"Re: [PATCH v3 3/6] update-ref: add support for 'symref-delete' command","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-06-05T16:22:04Z","receivedAt":"2024-06-05T16:22:08Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n>> +\ttest_expect_success \"stdin $type symref-delete fails with wrong old value\" '\n>> +\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$m\" >stdin &&\n>> +\t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n>> +\t\tgrep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected refs/heads/main\" err &&\n>> +\t\tgit symbolic-ref refs/heads/symref >expect &&\n>> +\t\techo $a >actual &&\n>> +\t\ttest_cmp expect actual\n>> +\t'\n>> +\n>> +\ttest_expect_success \"stdin $type symref-delete works with right old value\" '\n>> +\t\tformat_command $type \"symref-delete refs/heads/symref\" \"$a\" >stdin &&\n>> +\t\tgit update-ref --stdin $type --no-deref <stdin &&\n>> +\t\ttest_must_fail git rev-parse --verify -q refs/heads/symref\n>> +\t'\n>> +\n>> +\ttest_expect_success \"stdin $type symref-delete works with empty old value\" '\n>> +\t\tgit symbolic-ref refs/heads/symref $a >stdin &&\n>> +\t\tformat_command $type \"symref-delete refs/heads/symref\" \"\" >stdin &&\n>> +\t\tgit update-ref --stdin $type --no-deref <stdin &&\n>> +\t\ttest_must_fail git rev-parse --verify -q $b\n>> +\t'\n>> +\n>> +\ttest_expect_success \"stdin $type symref-delete succeeds for dangling reference\" '\n>> +\t\ttest_must_fail git symbolic-ref refs/heads/nonexistent &&\n>> +\t\tgit symbolic-ref refs/heads/symref2 refs/heads/nonexistent &&\n>> +\t\tformat_command $type \"symref-delete refs/heads/symref2\" \"refs/heads/nonexistent\" >stdin &&\n>> +\t\tgit update-ref --stdin $type --no-deref <stdin &&\n>> +\t\ttest_must_fail git symbolic-ref -d refs/heads/symref2\n>> +\t'\n>> +\n>\n> Not sure whether I overlooked it, but one test I missed was when trying\n> to delete a non-symbolic-ref.\n\nHmph, so we want to see an attempt to run symref-delete of\nrefs/heads/main when refs/heads/main is *not* a symref to fail?\nThat is a reasonable test to have.\n\nWhen you invent a shiny new toy, it is hard to make sure you covered\ncases where it should not kick in or positively diagnose a failure.\nA review with a sharp eye to spot missing tests is very much\nappreciated.\n\nTHanks.\n"}]}