{"thread":{"id":"58240","subject":"[PATCH 0/9] Fix issues of reference-transaction hook for various git commands","startedAt":"2022-07-29T10:12:57Z","lastAt":"2022-08-19T03:22:29Z","messageCount":27,"participants":["Jiang Xin","Eric Sunshine","Michael Heemskerk","Patrick Steinhardt"],"isPatch":true,"patchVersion":1,"patchTotal":9},"messages":[{"id":"460194","messageId":"20220729101245.6469-1-worldhello.net@gmail.com","threadId":"58240","inReplyTo":null,"subject":"[PATCH 0/9] Fix issues of reference-transaction hook for various git commands","fromName":"Jiang Xin","fromEmail":"worldhello.net@gmail.com","sentAt":"2022-07-29T10:12:36Z","receivedAt":"2022-07-29T10:12:57Z","isPatch":true,"sender":{"key":"worldhello.net@gmail.com","avatar":"https://avatars.githubusercontent.com/u/183860?v=4"},"body":"From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n\nBefore Patrick introduced the reference-transaction hook in git 2.28,\nWe had implemented some internal transaction hooks \"pre-txn\" and\n\"post-txn\" for our internal git infrastructure. The \"pre-txn\" hook is\nused to check our site-wide lockfile to disable write services for\nthe repository. The \"post-txn\" hook is used to update the timestamp\nand checksum of the repository. Recently I wanted to migrate our\ninternal implementation to use ref-txn hooks, but I ran into the same\nissues reported by Michael and Bryan:\n\n * https://lore.kernel.org/git/CAGyf7-GYYA2DOnAVYW--=QEc2WjSHzUhp2OQyuyOr3EOtFDm6g@mail.gmail.com/\n * https://lore.kernel.org/git/pull.1228.git.1651676435634.gitgitgadget@gmail.com/\n\nThis patch series try to fix the issues I encountered.\n\nPatch 1/9 adds more testcases in t1416:\n * Some commands failed because the expected \"<old-oid>\" was\n * \"<zero-oid>\".\n   Such as: \"git update-ref -d <ref>\".\n * Some commands failed because the \"reference-transaction committed\"\n   command was repeated multiple times for the same changes. Such as:\n   \"git update-ref -d <ref>\" and \"git tag -d <tag>\".\n * Some commands should not trigger the \"reference-transaction\" hook\n   because no real changes have occurred to the repository. Such as:\n   \"git pack-refs --all\" and \"git gc\".\n * Some commands did not execute the \"reference-transaction\" hook at\n * all.\n   Such as: \"git branch -c <src> <dest>\", \"git branch -m <old> <new>\"\n\nPatch 2/9 propagates real old-oid found in lockfile to the update entry\nfor txn, so we can get proper old-oid in \"prepared\" and \"committed\"\nstage\nfor ref-txn hook.\n\nPatch 3/9 adds a new flag in transaction, and we can turn off running\nthe\nentire hook for git-pack-refs in patch 4/9. We can also turn off the\n\"committed\" and \"aborted\" stages of the ref-txn hook for\n\"packed-ref-store\",\nbut we can still run \"prepared\" stage of the hook. See patch 5/9.\n\nPatch 6/9 and 7/9 create an extended function\n\"ref_transaction_update_extended()\"\nto be used in patch 8/9 to reimplement branch copy and rename.\n\nPatch 9/9 reimplents \"files_delete_refs()\" to fix failed testcases for\n\"git branch -d\" and \"git tag -d\".\n\n--\n\nJiang Xin (9):\n  t1416: more testcases for reference-transaction hook\n  refs: update missing old-oid in transaction from lockfile\n  refs: add new field in transaction for running transaction hook\n  refs: do not run transaction hook for git-pack-refs\n  refs: avoid duplicate running of the reference-transaction hook\n  refs: add reflog_info to hold more fields for reflog entry\n  refs: get error message via refs_update_ref_extended()\n  refs: reimplement files_copy_or_rename_ref() to run hook\n  refs: reimplement refs_delete_refs() and run hook once\n\n refs.c                           |   99 ++-\n refs.h                           |   23 +\n refs/debug.c                     |    2 +-\n refs/files-backend.c             |  158 ++---\n refs/packed-backend.c            |   51 +-\n refs/refs-internal.h             |   25 +-\n t/t1416-ref-transaction-hooks.sh | 1072 +++++++++++++++++++++++++++++-\n t/t5510-fetch.sh                 |   17 +\n 8 files changed, 1264 insertions(+), 183 deletions(-)\n\n-- \n2.36.1.25.gc87d5ad63a.dirty\n\n"},{"id":"460195","messageId":"20220729101245.6469-3-worldhello.net@gmail.com","threadId":"58240","inReplyTo":"20220729101245.6469-1-worldhello.net@gmail.com","subject":"[PATCH 2/9] refs: update missing old-oid in transaction from lockfile","fromName":"Jiang Xin","fromEmail":"worldhello.net@gmail.com","sentAt":"2022-07-29T10:12:38Z","receivedAt":"2022-07-29T10:12:59Z","isPatch":true,"sender":{"key":"worldhello.net@gmail.com","avatar":"https://avatars.githubusercontent.com/u/183860?v=4"},"body":"From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n\nFor commands that update a reference without providing an old-oid, the\n\"reference-transaction\" hook will receive a zero-oid instead of the\ncorrect old-oid.\n\nIn order to provide the \"reference-transaction\" hook with a real old-oid\nin any case, get proper old_oid from the lock file and propagate it to\nthe corresponding update entry of a transaction.\n\nThe behavior of the following git commands and four testcases have been\nfixed in t1416:\n\n * git branch [-f] <ref> <new-oid>              # update branch\n * git cherry-pick <oid>\n * git rebase\n * git tag -d <tag>\n * git update-ref --stdin                       # update refs\n * git update-ref -d <ref>\n * git update-ref <ref> <new-oid> [<old-oid>]   # update ref\n\nSigned-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n---\n refs/files-backend.c             |  9 +++++++++\n t/t1416-ref-transaction-hooks.sh | 12 ++++++------\n 2 files changed, 15 insertions(+), 6 deletions(-)\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 8db7882aac..957ebe08c0 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2565,6 +2565,15 @@ static int lock_ref_for_update(struct files_ref_store *refs,\n \t\t\tgoto out;\n \t\t}\n \n+\t\t/*\n+\t\t * Propagate old_oid from the lock to the update entry, so we can\n+\t\t * provide a real old-oid of to the \"reference-transaction\" hook.\n+\t\t */\n+\t\tif (!(update->flags & REF_HAVE_OLD)) {\n+\t\t\toidcpy(&update->old_oid, &lock->old_oid);\n+\t\t\tupdate->flags |= REF_HAVE_OLD;\n+\t\t}\n+\n \t\t/*\n \t\t * If this update is happening indirectly because of a\n \t\t * symref update, record the old OID in the parent\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex a84621516c..eef7489b5b 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -53,7 +53,7 @@ test_expect_success 'hook gets all queued updates in prepared state' '\n \tEOF\n \tcat >expect <<-EOF &&\n \t\t$ZERO_OID $POST_OID HEAD\n-\t\t$ZERO_OID $POST_OID refs/heads/main\n+\t\t$PRE_OID $POST_OID refs/heads/main\n \tEOF\n \tgit update-ref HEAD POST <<-EOF &&\n \t\tupdate HEAD $ZERO_OID $POST_OID\n@@ -76,7 +76,7 @@ test_expect_success 'hook gets all queued updates in committed state' '\n \tEOF\n \tcat >expect <<-EOF &&\n \t\t$ZERO_OID $POST_OID HEAD\n-\t\t$ZERO_OID $POST_OID refs/heads/main\n+\t\t$PRE_OID $POST_OID refs/heads/main\n \tEOF\n \tgit update-ref HEAD POST &&\n \ttest_cmp expect actual\n@@ -321,7 +321,7 @@ test_expect_success \"update-ref: create new refs\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-test_expect_failure \"update-ref: update HEAD, a symbolic-ref\" '\n+test_expect_success \"update-ref: update HEAD, a symbolic-ref\" '\n \ttest_when_finished \"git switch main; rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-EOF &&\n@@ -365,7 +365,7 @@ test_expect_failure \"update-ref: call git-pack-refs to create packed_ref_store\"\n \ttest_path_is_missing $HOOK_OUTPUT\n '\n \n-test_expect_failure \"update-ref: update refs already packed to .git/packed-refs\" '\n+test_expect_success \"update-ref: update refs already packed to .git/packed-refs\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-EOF &&\n@@ -485,7 +485,7 @@ test_expect_success \"update-ref --stdin: create new refs\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-test_expect_failure \"update-ref --stdin: update refs\" '\n+test_expect_success \"update-ref --stdin: update refs\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-EOF &&\n@@ -639,7 +639,7 @@ test_expect_failure \"branch: call git-gc to create packed_ref_store\" '\n \ttest_path_is_missing $HOOK_OUTPUT\n '\n \n-test_expect_failure \"branch: update refs to create loose refs\" '\n+test_expect_success \"branch: update refs to create loose refs\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-EOF &&\n-- \n2.36.1.25.gc87d5ad63a.dirty\n\n"},{"id":"460196","messageId":"20220729101245.6469-4-worldhello.net@gmail.com","threadId":"58240","inReplyTo":"20220729101245.6469-1-worldhello.net@gmail.com","subject":"[PATCH 3/9] refs: add new field in transaction for running transaction hook","fromName":"Jiang Xin","fromEmail":"worldhello.net@gmail.com","sentAt":"2022-07-29T10:12:39Z","receivedAt":"2022-07-29T10:13:04Z","isPatch":true,"sender":{"key":"worldhello.net@gmail.com","avatar":"https://avatars.githubusercontent.com/u/183860?v=4"},"body":"From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n\nAdd a new field \"hook_flags\" in transaction, and only run the\n\"reference-transaction\" hook if the specific flag is turned on.\n\nThe \"reference-transaction\" hook has three states: prepared, committed,\nand aborted. To update a reference, git may create two seperate\ntransactions, one for loose reference and one for packed ref-store. This\nmay cause duplicate running of the hook for same references. The new\nfield \"hook_flags\" in the transaction can turn off running of a specific\ntransaction. In some scenarios, we may only want to turn off certain\nstates of a transaction, such as \"committed\" and \"aborted\", but want to\nturn on the \"prepared\" state of the hook to do some pre-checks, so the\n\"hook_flags\" field has three bits to control running of the three states\nof the hook.\n\nBy calling the \"ref_store_transaction_begin()\" function, all the flags\nof the \"hook_flags\" field for the new initialized transaction will be\nturned on. The new function \"ref_store_transaction_begin_extended()\"\nwill be used in later commits to custom the \"hook_flags\" field for a\nnew initialized transaction.\n\nSigned-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n---\n refs.c               | 25 +++++++++++++++++++++++--\n refs.h               |  3 +++\n refs/refs-internal.h |  8 ++++++++\n 3 files changed, 34 insertions(+), 2 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex 90bcb27168..48b69460e2 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -998,17 +998,27 @@ int read_ref_at(struct ref_store *refs, const char *refname,\n \treturn 1;\n }\n \n-struct ref_transaction *ref_store_transaction_begin(struct ref_store *refs,\n-\t\t\t\t\t\t    struct strbuf *err)\n+struct ref_transaction *ref_store_transaction_begin_extended(struct ref_store *refs,\n+\t\t\t\t\t\t\t     unsigned int hook_flags,\n+\t\t\t\t\t\t\t     struct strbuf *err)\n {\n \tstruct ref_transaction *tr;\n \tassert(err);\n \n \tCALLOC_ARRAY(tr, 1);\n \ttr->ref_store = refs;\n+\ttr->hook_flags = hook_flags;\n \treturn tr;\n }\n \n+struct ref_transaction *ref_store_transaction_begin(struct ref_store *refs,\n+\t\t\t\t\t\t    struct strbuf *err)\n+{\n+\treturn ref_store_transaction_begin_extended(refs,\n+\t\t\t\t\t\t    REF_TRANSACTION_RUN_ALL_HOOKS,\n+\t\t\t\t\t\t    err);\n+}\n+\n struct ref_transaction *ref_transaction_begin(struct strbuf *err)\n {\n \treturn ref_store_transaction_begin(get_main_ref_store(the_repository), err);\n@@ -2074,6 +2084,17 @@ static int run_transaction_hook(struct ref_transaction *transaction,\n \tconst char *hook;\n \tint ret = 0, i;\n \n+\tif (!strcmp(state, \"prepared\")) {\n+\t\tif (!(transaction->hook_flags & REF_TRANSACTION_RUN_PREPARED_HOOK))\n+\t\t\treturn 0;\n+\t} else if (!strcmp(state, \"committed\")) {\n+\t\tif (!(transaction->hook_flags & REF_TRANSACTION_RUN_COMMITTED_HOOK))\n+\t\t\treturn 0;\n+\t} else if (!strcmp(state, \"aborted\")) {\n+\t\tif (!(transaction->hook_flags & REF_TRANSACTION_RUN_ABORTED_HOOK))\n+\t\t\treturn 0;\n+\t}\n+\n \thook = find_hook(\"reference-transaction\");\n \tif (!hook)\n \t\treturn ret;\ndiff --git a/refs.h b/refs.h\nindex 47cb9edbaa..715127ab58 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -570,6 +570,9 @@ enum action_on_err {\n  * Begin a reference transaction.  The reference transaction must\n  * be freed by calling ref_transaction_free().\n  */\n+struct ref_transaction *ref_store_transaction_begin_extended(struct ref_store *refs,\n+\t\t\t\t\t\t\t     unsigned int hook_flags,\n+\t\t\t\t\t\t\t     struct strbuf *err);\n struct ref_transaction *ref_store_transaction_begin(struct ref_store *refs,\n \t\t\t\t\t\t    struct strbuf *err);\n struct ref_transaction *ref_transaction_begin(struct strbuf *err);\ndiff --git a/refs/refs-internal.h b/refs/refs-internal.h\nindex 69f93b0e2a..5220d1980d 100644\n--- a/refs/refs-internal.h\n+++ b/refs/refs-internal.h\n@@ -201,6 +201,13 @@ enum ref_transaction_state {\n \tREF_TRANSACTION_CLOSED   = 2\n };\n \n+#define REF_TRANSACTION_RUN_PREPARED_HOOK (1 << 0)\n+#define REF_TRANSACTION_RUN_COMMITTED_HOOK (1 << 1)\n+#define REF_TRANSACTION_RUN_ABORTED_HOOK (1 << 2)\n+#define REF_TRANSACTION_RUN_ALL_HOOKS         \\\n+\t(REF_TRANSACTION_RUN_PREPARED_HOOK  | \\\n+\t REF_TRANSACTION_RUN_COMMITTED_HOOK | \\\n+\t REF_TRANSACTION_RUN_ABORTED_HOOK)\n /*\n  * Data structure for holding a reference transaction, which can\n  * consist of checks and updates to multiple references, carried out\n@@ -212,6 +219,7 @@ struct ref_transaction {\n \tsize_t alloc;\n \tsize_t nr;\n \tenum ref_transaction_state state;\n+\tunsigned int hook_flags;\n \tvoid *backend_data;\n };\n \n-- \n2.36.1.25.gc87d5ad63a.dirty\n\n"},{"id":"460198","messageId":"20220729101245.6469-2-worldhello.net@gmail.com","threadId":"58240","inReplyTo":"20220729101245.6469-1-worldhello.net@gmail.com","subject":"[PATCH 1/9] t1416: more testcases for reference-transaction hook","fromName":"Jiang Xin","fromEmail":"worldhello.net@gmail.com","sentAt":"2022-07-29T10:12:37Z","receivedAt":"2022-07-29T10:13:05Z","isPatch":true,"sender":{"key":"worldhello.net@gmail.com","avatar":"https://avatars.githubusercontent.com/u/183860?v=4"},"body":"From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n\nAppend more testcases in t1416 for various git commands that may trigger\nthe \"reference-transaction\" hook.\n\nIn order to create a common \"reference-transaction\" hook, we define the\ncommon hook path using config variable \"core.hooksPath\", and create the\n\"reference-transaction\" hook in the path.\n\nSome commands trigger the \"reference-transaction\" hook properly and get\nthe expected output. E.g.:\n\n * git branch <new-branch> <oid>\t\t# create new branch\n * git clone\n * git commit [--amend]\n * git fetch\n * git merge\n * git push [--atomic]\n * git reset --hard\n * git tag [-f] <tag> <oid>\t\t\t# update tag\n * git tag <new-tag> <oid>\t\t\t# create new tag\n * git update-ref --stdin\t\t\t# create new refs\n * git update-ref <ref> <oid>\t\t\t# create new ref\n\nBut 16 testcases failed.\n\nSome commands failed because the expected \"<old-oid>\" became\n\"<zero-oid>\". E.g.:\n\n * git branch [-f] <ref> <new-oid>\t\t# update branch\n * git cherry-pick <oid>\n * git rebase\n * git tag -d <tag>\n * git update-ref --stdin\t\t\t# update/delete refs\n * git update-ref -d <ref>\n * git update-ref <ref> <new-oid> [<old-oid>]\t# update ref\n\nSome commands failed because the \"reference-transaction committed\"\ncommand was repeated multiple times for the same changes. E.g.:\n\n * git cherry-pick\n * git rebase\n * git revert\n * git tag -d <tag>\n * git update-ref -d <ref>\n * git update-ref --stdin\t\t\t# delete refs\n\nSome commands should not trigger the \"reference-transaction\" hook\nbecause no real changes have occurred to the repository. E.g.:\n\n * git gc\n * git pack-refs --all\n\nSome commands did not execute the \"reference-transaction\" hook at all.\nE.g.:\n\n * git branch -c <src> <dest>\t\t\t# copy branch\n * git branch -m <old> <new>\t\t\t# rename branch\n\nSome commands ran unexpected command \"reference-transaction aborted\".\nE.g.:\n\n * git branch -d <branch>\t\t\t# delete branch\n * git branch -m <old> <new>\t\t\t# rename branch\n * git cherr-pick <oid>\n * git rebase\n * git revert\n * git tag -d <tag>\t\t\t\t# delete tag\n * git update-ref -d <ref> \t\t\t# delete ref\n\nWe will fix the failed testcases in later commits.\n\nSigned-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n---\n t/t1416-ref-transaction-hooks.sh | 1068 ++++++++++++++++++++++++++++++\n 1 file changed, 1068 insertions(+)\n\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex 27731722a5..a84621516c 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -133,4 +133,1072 @@ test_expect_success 'interleaving hook calls succeed' '\n \ttest_cmp expect target-repo.git/actual\n '\n \n+HOOK_OUTPUT=hook-output\n+\n+# Create commits in <repo> and assign each commit's oid to shell variables\n+# given in the arguments (A, B, and C). E.g.:\n+#\n+#     create_commits_in <repo> A B C\n+#\n+# NOTE: Never calling this function from a subshell since variable\n+# assignments will disappear when subshell exits.\n+create_commits_in () {\n+\trepo=\"$1\" && test -d \"$repo\" ||\n+\terror \"Repository $repo does not exist.\"\n+\tshift &&\n+\twhile test $# -gt 0\n+\tdo\n+\t\tname=$1 &&\n+\t\tshift &&\n+\t\ttest_commit -C \"$repo\" --no-tag \"$name\" &&\n+\t\teval $name=$(git -C \"$repo\" rev-parse HEAD)\n+\tdone\n+}\n+\n+get_abbrev_oid () {\n+\toid=$1 &&\n+\tsuffix=${oid#???????} &&\n+\toid=${oid%$suffix} &&\n+\tif test -n \"$oid\"\n+\tthen\n+\t\techo \"$oid\"\n+\telse\n+\t\techo \"undefined-oid\"\n+\tfi\n+}\n+\n+# Format the output of git-push, git-show-ref and other commands to make a\n+# user-friendly and stable text.  We can easily prepare the expect text\n+# without having to worry about future changes of the commit ID.\n+make_user_friendly_and_stable_output () {\n+\tsed \\\n+\t\t-e \"s/$(get_abbrev_oid $A)[0-9a-f]*/<COMMIT-A>/g\" \\\n+\t\t-e \"s/$(get_abbrev_oid $B)[0-9a-f]*/<COMMIT-B>/g\" \\\n+\t\t-e \"s/$(get_abbrev_oid $C)[0-9a-f]*/<COMMIT-C>/g\" \\\n+\t\t-e \"s/$(get_abbrev_oid $D)[0-9a-f]*/<COMMIT-D>/g\" \\\n+\t\t-e \"s/$(get_abbrev_oid $E)[0-9a-f]*/<COMMIT-E>/g\" \\\n+\t\t-e \"s/$(get_abbrev_oid $F)[0-9a-f]*/<COMMIT-F>/g\" \\\n+\t\t-e \"s/$(get_abbrev_oid $G)[0-9a-f]*/<COMMIT-G>/g\" \\\n+\t\t-e \"s/$(get_abbrev_oid $H)[0-9a-f]*/<COMMIT-H>/g\" \\\n+\t\t-e \"s/$(get_abbrev_oid $I)[0-9a-f]*/<COMMIT-I>/g\" \\\n+\t\t-e \"s/$ZERO_OID/<ZERO-OID>/g\"\n+}\n+\n+test_cmp_heads_and_tags () {\n+\tindir= &&\n+\twhile test $# != 0\n+\tdo\n+\t\tcase \"$1\" in\n+\t\t-C)\n+\t\t\tindir=\"$2\"\n+\t\t\tshift\n+\t\t\t;;\n+\t\t*)\n+\t\t\tbreak\n+\t\t\t;;\n+\t\tesac\n+\t\tshift\n+\tdone &&\n+\texpect=${1:-expect} &&\n+\tactual=${2:-actual-heads-and-tags} &&\n+\tindir=${indir:+\"$indir\"/} &&\n+\ttest_path_is_file \"$expect\" &&\n+\ttest_when_finished \"rm -f \\\"$actual\\\"\" &&\n+\tgit ${indir:+ -C \"$indir\"} show-ref --heads --tags | \\\n+\t\tmake_user_friendly_and_stable_output >\"$actual\" &&\n+\ttest_cmp \"$expect\" \"$actual\"\n+}\n+\n+test_expect_success 'setup git config and common reference-transaction hook' '\n+\tgit config --global \\\n+\t\tcore.hooksPath \"$HOME/test-hooks\" &&\n+\tgit config --global core.abbrev 7 &&\n+\tmkdir \"test-hooks\" &&\n+\twrite_script \"test-hooks/reference-transaction\" <<-EOF\n+\t\texec >>\"$HOME/$HOOK_OUTPUT\"\n+\t\tprintf \"## Call hook: reference-transaction %9s ##\\n\" \"\\$@\"\n+\t\twhile read -r line\n+\t\tdo\n+\t\t    printf \"%s\\n\" \"\\$line\"\n+\t\tdone\n+\tEOF\n+'\n+\n+test_expect_success \"setup base repository\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> HEAD\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/main\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> HEAD\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/main\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <COMMIT-B> HEAD\n+\t\t<COMMIT-A> <COMMIT-B> refs/heads/main\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <COMMIT-B> HEAD\n+\t\t<COMMIT-A> <COMMIT-B> refs/heads/main\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-B> <COMMIT-C> HEAD\n+\t\t<COMMIT-B> <COMMIT-C> refs/heads/main\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-B> <COMMIT-C> HEAD\n+\t\t<COMMIT-B> <COMMIT-C> refs/heads/main\n+\tEOF\n+\n+\tgit init base &&\n+\tcreate_commits_in base A B C &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\tEOF\n+\ttest_cmp_heads_and_tags -C base expect\n+'\n+\n+test_expect_success \"update-ref: setup workdir using git-clone\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/remotes/origin/main\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/remotes/origin/main\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> HEAD\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/main\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> HEAD\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/main\n+\tEOF\n+\n+\tgit clone base workdir &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t<COMMIT-C> refs/heads/main\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_success \"update-ref: create new refs\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic1\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic1\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic2\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic2\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic3\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic3\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit update-ref refs/heads/topic1 $A &&\n+\t\tgit update-ref refs/heads/topic2 $A &&\n+\t\tgit update-ref refs/heads/topic3 $A\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-A> refs/heads/topic1\n+\t\t<COMMIT-A> refs/heads/topic2\n+\t\t<COMMIT-A> refs/heads/topic3\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_failure \"update-ref: update HEAD, a symbolic-ref\" '\n+\ttest_when_finished \"git switch main; rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <COMMIT-B> HEAD\n+\t\t<COMMIT-A> <COMMIT-B> refs/heads/topic1\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <COMMIT-B> HEAD\n+\t\t<COMMIT-A> <COMMIT-B> refs/heads/topic1\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> HEAD\n+\t\t<COMMIT-B> <COMMIT-A> refs/heads/topic1\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> HEAD\n+\t\t<COMMIT-B> <COMMIT-A> refs/heads/topic1\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit switch topic1 &&\n+\t\tgit update-ref HEAD $B $A &&\n+\t\tgit update-ref HEAD $A &&\n+\t\tgit switch main\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-A> refs/heads/topic1\n+\t\t<COMMIT-A> refs/heads/topic2\n+\t\t<COMMIT-A> refs/heads/topic3\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_failure \"update-ref: call git-pack-refs to create packed_ref_store\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\tgit -C workdir pack-refs --all &&\n+\ttest_path_is_file workdir/.git/packed-refs &&\n+\ttest_path_is_missing $HOOK_OUTPUT\n+'\n+\n+test_expect_failure \"update-ref: update refs already packed to .git/packed-refs\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <COMMIT-B> refs/heads/topic2\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <COMMIT-B> refs/heads/topic2\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <COMMIT-C> refs/heads/topic3\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <COMMIT-C> refs/heads/topic3\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic4\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic4\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <COMMIT-C> refs/heads/topic4\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <COMMIT-C> refs/heads/topic4\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit update-ref refs/heads/topic2 $B $A &&\n+\t\tgit update-ref refs/heads/topic3 $C &&\n+\t\tgit update-ref refs/heads/topic4 $A &&\n+\t\tgit update-ref refs/heads/topic4 $C\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-A> refs/heads/topic1\n+\t\t<COMMIT-B> refs/heads/topic2\n+\t\t<COMMIT-C> refs/heads/topic3\n+\t\t<COMMIT-C> refs/heads/topic4\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_failure \"update-ref: remove refs with mixed ref_stores\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <ZERO-OID> refs/heads/topic1\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <ZERO-OID> refs/heads/topic1\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <ZERO-OID> refs/heads/topic1\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <ZERO-OID> refs/heads/topic2\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-B> <ZERO-OID> refs/heads/topic2\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-B> <ZERO-OID> refs/heads/topic2\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <ZERO-OID> refs/heads/topic3\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic3\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic3\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic4\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic4\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit update-ref -d refs/heads/topic1 $A &&\n+\t\tgit update-ref -d refs/heads/topic2 $B &&\n+\t\tgit update-ref -d refs/heads/topic3 &&\n+\t\tgit update-ref -d refs/heads/topic4\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_success \"update-ref --stdin: create new refs\" '\n+\ttest_when_finished \"git -C workdir pack-refs --all; rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic1\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic2\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic3\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic1\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic2\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic3\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit update-ref --stdin <<-EOF\n+\t\tcreate refs/heads/topic1 $A\n+\t\tcreate refs/heads/topic2 $A\n+\t\tcreate refs/heads/topic3 $A\n+\t\tEOF\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-A> refs/heads/topic1\n+\t\t<COMMIT-A> refs/heads/topic2\n+\t\t<COMMIT-A> refs/heads/topic3\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_failure \"update-ref --stdin: update refs\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <COMMIT-B> refs/heads/topic2\n+\t\t<COMMIT-A> <COMMIT-C> refs/heads/topic3\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic4\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <COMMIT-B> refs/heads/topic2\n+\t\t<COMMIT-A> <COMMIT-C> refs/heads/topic3\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic4\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit update-ref --stdin <<-EOF\n+\t\tstart\n+\t\tupdate refs/heads/topic2 $B $A\n+\t\tupdate refs/heads/topic3 $C\n+\t\tcreate refs/heads/topic4 $C\n+\t\tprepare\n+\t\tcommit\n+\t\tEOF\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-A> refs/heads/topic1\n+\t\t<COMMIT-B> refs/heads/topic2\n+\t\t<COMMIT-C> refs/heads/topic3\n+\t\t<COMMIT-C> refs/heads/topic4\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_failure \"update-ref --stdin: delete refs\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <ZERO-OID> refs/heads/topic1\n+\t\t<ZERO-OID> <ZERO-OID> refs/heads/topic2\n+\t\t<ZERO-OID> <ZERO-OID> refs/heads/topic3\n+\t\t<ZERO-OID> <ZERO-OID> refs/heads/topic4\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <ZERO-OID> refs/heads/topic1\n+\t\t<COMMIT-B> <ZERO-OID> refs/heads/topic2\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic3\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic4\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <ZERO-OID> refs/heads/topic1\n+\t\t<COMMIT-B> <ZERO-OID> refs/heads/topic2\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic3\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic4\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit update-ref --stdin <<-EOF\n+\t\tstart\n+\t\tdelete refs/heads/topic1\n+\t\tdelete refs/heads/topic2 $B\n+\t\tdelete refs/heads/topic3\n+\t\tdelete refs/heads/topic4\n+\t\tprepare\n+\t\tcommit\n+\t\tEOF\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_success \"branch: setup workdir using git-fetch\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/remotes/origin/main\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/remotes/origin/main\n+\tEOF\n+\n+\trm -rf workdir &&\n+\tgit init workdir &&\n+\tgit -C workdir remote add origin ../base &&\n+\tgit -C workdir fetch origin &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/main\n+\t\t<ZERO-OID> <COMMIT-C> HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/main\n+\t\t<ZERO-OID> <COMMIT-C> HEAD\n+\tEOF\n+\n+\trm $HOOK_OUTPUT &&\n+\tgit -C workdir switch -c main origin/main &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t<COMMIT-C> refs/heads/main\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_success \"branch: create new branches\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic1\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic1\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic2\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic2\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit branch topic1 $A &&\n+\t\tgit branch topic2 $A\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-A> refs/heads/topic1\n+\t\t<COMMIT-A> refs/heads/topic2\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_failure \"branch: call git-gc to create packed_ref_store\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\tgit -C workdir gc &&\n+\ttest_path_is_file workdir/.git/packed-refs &&\n+\ttest_path_is_missing $HOOK_OUTPUT\n+'\n+\n+test_expect_failure \"branch: update refs to create loose refs\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <COMMIT-B> refs/heads/topic2\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <COMMIT-B> refs/heads/topic2\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic3\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic3\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit branch -f topic2 $B &&\n+\t\tgit branch topic3 $C\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-A> refs/heads/topic1\n+\t\t<COMMIT-B> refs/heads/topic2\n+\t\t<COMMIT-C> refs/heads/topic3\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_failure \"branch: copy branches\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-B> refs/heads/topic4\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-B> refs/heads/topic4\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic5\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic5\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit branch -c topic2 topic4 &&\n+\t\tgit branch -c topic3 topic5\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-A> refs/heads/topic1\n+\t\t<COMMIT-B> refs/heads/topic2\n+\t\t<COMMIT-C> refs/heads/topic3\n+\t\t<COMMIT-B> refs/heads/topic4\n+\t\t<COMMIT-C> refs/heads/topic5\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_failure \"branch: rename branches\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-B> <ZERO-OID> refs/heads/topic4\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-B> <ZERO-OID> refs/heads/topic4\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-B> refs/heads/topic6\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-B> refs/heads/topic6\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic5\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic5\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic7\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic7\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit branch -m topic4 topic6 &&\n+\t\tgit branch -m topic5 topic7\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-A> refs/heads/topic1\n+\t\t<COMMIT-B> refs/heads/topic2\n+\t\t<COMMIT-C> refs/heads/topic3\n+\t\t<COMMIT-B> refs/heads/topic6\n+\t\t<COMMIT-C> refs/heads/topic7\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_failure \"branch: remove branches\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <ZERO-OID> refs/heads/topic1\n+\t\t<ZERO-OID> <ZERO-OID> refs/heads/topic2\n+\t\t<ZERO-OID> <ZERO-OID> refs/heads/topic3\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <ZERO-OID> refs/heads/topic1\n+\t\t<COMMIT-B> <ZERO-OID> refs/heads/topic2\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic3\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <ZERO-OID> refs/heads/topic1\n+\t\t<COMMIT-B> <ZERO-OID> refs/heads/topic2\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic3\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit branch -d topic1 topic2 topic3\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-B> refs/heads/topic6\n+\t\t<COMMIT-C> refs/heads/topic7\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_success \"tag: setup workdir using git-push\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/main\n+\t\t<ZERO-OID> <COMMIT-C> HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/main\n+\t\t<ZERO-OID> <COMMIT-C> HEAD\n+\tEOF\n+\n+\trm -rf workdir &&\n+\tgit init workdir &&\n+\tgit -C workdir config receive.denyCurrentBranch ignore &&\n+\tgit -C base push ../workdir \"+refs/heads/*:refs/heads/*\" &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect &&\n+\n+\tgit -C workdir restore --staged -- . &&\n+\tgit -C workdir restore -- .\n+'\n+\n+test_expect_success \"tag: create new tags\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/tags/v1\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/tags/v1\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/tags/v2\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/tags/v2\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit tag v1 $A &&\n+\t\tgit tag v2 $A\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-A> refs/tags/v1\n+\t\t<COMMIT-A> refs/tags/v2\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_failure \"tag: call git-pack-refs to create packed_ref_store\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\tgit -C workdir pack-refs --all &&\n+\ttest_path_is_file workdir/.git/packed-refs &&\n+\ttest_path_is_missing $HOOK_OUTPUT\n+'\n+\n+test_expect_success \"tag: update refs to create loose refs\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <COMMIT-B> refs/tags/v2\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <COMMIT-B> refs/tags/v2\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/tags/v3\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/tags/v3\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit tag -f v2 $B &&\n+\t\tgit tag v3 $C\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-A> refs/tags/v1\n+\t\t<COMMIT-B> refs/tags/v2\n+\t\t<COMMIT-C> refs/tags/v3\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_failure \"tag: remove tags with mixed ref_stores\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <ZERO-OID> refs/tags/v1\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <ZERO-OID> refs/tags/v1\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <ZERO-OID> refs/tags/v1\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <ZERO-OID> refs/tags/v2\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-B> <ZERO-OID> refs/tags/v2\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-B> <ZERO-OID> refs/tags/v2\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-C> <ZERO-OID> refs/tags/v3\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-C> <ZERO-OID> refs/tags/v3\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit tag -d v1 &&\n+\t\tgit tag -d v2 &&\n+\t\tgit tag -d v3\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_success \"worktree: setup workdir using push --atomic\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/main\n+\t\t<ZERO-OID> <COMMIT-C> HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/main\n+\t\t<ZERO-OID> <COMMIT-C> HEAD\n+\tEOF\n+\n+\trm -rf workdir &&\n+\tgit init --bare repo.git &&\n+\tgit -C base push --atomic --mirror ../repo.git &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\trm $HOOK_OUTPUT &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/remotes/origin/main\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/remotes/origin/main\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> HEAD\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/main\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> HEAD\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/main\n+\tEOF\n+\n+\tgit clone --no-local repo.git workdir &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_success \"worktree: topic1: commit --amend\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic1\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic1\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-C> <COMMIT-D> HEAD\n+\t\t<COMMIT-C> <COMMIT-D> refs/heads/topic1\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-C> <COMMIT-D> HEAD\n+\t\t<COMMIT-C> <COMMIT-D> refs/heads/topic1\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit checkout -b topic1 &&\n+\t\tgit commit --amend -m \"C (amend)\"\n+\t) &&\n+\tD=$(git -C workdir rev-parse HEAD) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-D> refs/heads/topic1\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_success \"worktree: topic2: merge\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic2\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic2\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> ORIG_HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> ORIG_HEAD\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <COMMIT-E> HEAD\n+\t\t<COMMIT-A> <COMMIT-E> refs/heads/topic2\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <COMMIT-E> HEAD\n+\t\t<COMMIT-A> <COMMIT-E> refs/heads/topic2\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit checkout -b topic2 $A &&\n+\t\tgit merge --no-ff main &&\n+\t\ttest_path_is_file B.t &&\n+\t\ttest_path_is_file C.t\n+\t) &&\n+\tE=$(git -C workdir rev-parse HEAD) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-D> refs/heads/topic1\n+\t\t<COMMIT-E> refs/heads/topic2\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_failure \"worktree: topic3: cherry-pick\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic3\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic3\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> CHERRY_PICK_HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> CHERRY_PICK_HEAD\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <COMMIT-F> HEAD\n+\t\t<COMMIT-A> <COMMIT-F> refs/heads/topic3\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <COMMIT-F> HEAD\n+\t\t<COMMIT-A> <COMMIT-F> refs/heads/topic3\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-C> <ZERO-OID> CHERRY_PICK_HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-C> <ZERO-OID> CHERRY_PICK_HEAD\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit checkout -b topic3 $A &&\n+\t\tgit cherry-pick $C &&\n+\t\ttest_path_is_file C.t &&\n+\t\ttest_path_is_missing B.t\n+\t) &&\n+\tF=$(git -C workdir rev-parse HEAD) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-D> refs/heads/topic1\n+\t\t<COMMIT-E> refs/heads/topic2\n+\t\t<COMMIT-F> refs/heads/topic3\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_failure \"worktree: topic4: rebase\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <COMMIT-G> ORIG_HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <COMMIT-G> ORIG_HEAD\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-G> <COMMIT-C> HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-G> <COMMIT-C> HEAD\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <ZERO-OID> REBASE_HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <ZERO-OID> REBASE_HEAD\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-G> CHERRY_PICK_HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-G> CHERRY_PICK_HEAD\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-C> <COMMIT-H> HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-C> <COMMIT-H> HEAD\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-G> <ZERO-OID> CHERRY_PICK_HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-G> <ZERO-OID> CHERRY_PICK_HEAD\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-G> <COMMIT-H> refs/heads/topic4\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-G> <COMMIT-H> refs/heads/topic4\n+\tEOF\n+\n+\tgit -C workdir checkout -b topic4 $A &&\n+\tcreate_commits_in workdir G &&\n+\trm $HOOK_OUTPUT &&\n+\tgit -C workdir rebase main &&\n+\tH=$(git -C workdir rev-parse HEAD) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-D> refs/heads/topic1\n+\t\t<COMMIT-E> refs/heads/topic2\n+\t\t<COMMIT-F> refs/heads/topic3\n+\t\t<COMMIT-H> refs/heads/topic4\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_failure \"worktree: topic5: revert\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic5\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic5\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-C> <COMMIT-I> HEAD\n+\t\t<COMMIT-C> <COMMIT-I> refs/heads/topic5\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-C> <COMMIT-I> HEAD\n+\t\t<COMMIT-C> <COMMIT-I> refs/heads/topic5\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <ZERO-OID> CHERRY_PICK_HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <ZERO-OID> CHERRY_PICK_HEAD\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit checkout -b topic5 $C &&\n+\t\tgit revert HEAD &&\n+\t\ttest_path_is_file B.t &&\n+\t\ttest_path_is_missing C.t\n+\t) &&\n+\tI=$(git -C workdir rev-parse HEAD) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-D> refs/heads/topic1\n+\t\t<COMMIT-E> refs/heads/topic2\n+\t\t<COMMIT-F> refs/heads/topic3\n+\t\t<COMMIT-H> refs/heads/topic4\n+\t\t<COMMIT-I> refs/heads/topic5\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_success \"worktree: topic6: reset\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic6\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic6\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-G> <COMMIT-C> ORIG_HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-G> <COMMIT-C> ORIG_HEAD\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-C> <COMMIT-B> HEAD\n+\t\t<COMMIT-C> <COMMIT-B> refs/heads/topic6\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-C> <COMMIT-B> HEAD\n+\t\t<COMMIT-C> <COMMIT-B> refs/heads/topic6\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit checkout -b topic6 $C &&\n+\t\tgit reset --hard $B\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-D> refs/heads/topic1\n+\t\t<COMMIT-E> refs/heads/topic2\n+\t\t<COMMIT-F> refs/heads/topic3\n+\t\t<COMMIT-H> refs/heads/topic4\n+\t\t<COMMIT-I> refs/heads/topic5\n+\t\t<COMMIT-B> refs/heads/topic6\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n test_done\n-- \n2.36.1.25.gc87d5ad63a.dirty\n\n"},{"id":"460197","messageId":"20220729101245.6469-5-worldhello.net@gmail.com","threadId":"58240","inReplyTo":"20220729101245.6469-1-worldhello.net@gmail.com","subject":"[PATCH 4/9] refs: do not run transaction hook for git-pack-refs","fromName":"Jiang Xin","fromEmail":"worldhello.net@gmail.com","sentAt":"2022-07-29T10:12:40Z","receivedAt":"2022-07-29T10:13:06Z","isPatch":true,"sender":{"key":"worldhello.net@gmail.com","avatar":"https://avatars.githubusercontent.com/u/183860?v=4"},"body":"From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n\ngit-pack-refs will call \"files_pack_refs()\" to pack loose references\ninto the \"packed-refs\" file, and there are no real changes to the\nrepository. Therefore, by initializing a transaction with an empty\n\"hook_flags\" field, the \"reference-transaction\" hook will not run.\n\nThe \"prune_refs()\" and \"prune_ref()\" functions are called from\n\"file_pack_refs()\", and they are used to prune loose refereces which\nhave already been packed into \"packed-refs\". The transaction instance\nin \"prune_ref()\" should also be initialized with an empty \"hook_flags\"\nfield.\n\nThe behavior of the following git commands and three testcases have been\nfixed in t1416:\n\n * git gc\n * git pack-refs\n\nSigned-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n---\n refs/files-backend.c             | 10 ++++++++--\n t/t1416-ref-transaction-hooks.sh |  6 +++---\n 2 files changed, 11 insertions(+), 5 deletions(-)\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 957ebe08c0..3be317d524 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -1136,7 +1136,8 @@ static void prune_ref(struct files_ref_store *refs, struct ref_to_prune *r)\n \tif (check_refname_format(r->name, 0))\n \t\treturn;\n \n-\ttransaction = ref_store_transaction_begin(&refs->base, &err);\n+\t/* Called by \"files_pack_refs()\" and should not run any hooks. */\n+\ttransaction = ref_store_transaction_begin_extended(&refs->base, 0, &err);\n \tif (!transaction)\n \t\tgoto cleanup;\n \tref_transaction_add_update(\n@@ -1207,7 +1208,12 @@ static int files_pack_refs(struct ref_store *ref_store, unsigned int flags)\n \tstruct strbuf err = STRBUF_INIT;\n \tstruct ref_transaction *transaction;\n \n-\ttransaction = ref_store_transaction_begin(refs->packed_ref_store, &err);\n+\t/*\n+\t * No real changes have occurred to the repository and no hooks\n+\t * should be run for this transaction.\n+\t */\n+\ttransaction = ref_store_transaction_begin_extended(refs->packed_ref_store,\n+\t\t\t\t\t\t\t   0, &err);\n \tif (!transaction)\n \t\treturn -1;\n \ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex eef7489b5b..7f8fd012e8 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -358,7 +358,7 @@ test_expect_success \"update-ref: update HEAD, a symbolic-ref\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-test_expect_failure \"update-ref: call git-pack-refs to create packed_ref_store\" '\n+test_expect_success \"update-ref: call git-pack-refs to create packed_ref_store\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \tgit -C workdir pack-refs --all &&\n \ttest_path_is_file workdir/.git/packed-refs &&\n@@ -632,7 +632,7 @@ test_expect_success \"branch: create new branches\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-test_expect_failure \"branch: call git-gc to create packed_ref_store\" '\n+test_expect_success \"branch: call git-gc to create packed_ref_store\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \tgit -C workdir gc &&\n \ttest_path_is_file workdir/.git/packed-refs &&\n@@ -835,7 +835,7 @@ test_expect_success \"tag: create new tags\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-test_expect_failure \"tag: call git-pack-refs to create packed_ref_store\" '\n+test_expect_success \"tag: call git-pack-refs to create packed_ref_store\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \tgit -C workdir pack-refs --all &&\n \ttest_path_is_file workdir/.git/packed-refs &&\n-- \n2.36.1.25.gc87d5ad63a.dirty\n\n"},{"id":"460199","messageId":"20220729101245.6469-6-worldhello.net@gmail.com","threadId":"58240","inReplyTo":"20220729101245.6469-1-worldhello.net@gmail.com","subject":"[PATCH 5/9] refs: avoid duplicate running of the reference-transaction hook","fromName":"Jiang Xin","fromEmail":"worldhello.net@gmail.com","sentAt":"2022-07-29T10:12:41Z","receivedAt":"2022-07-29T10:13:08Z","isPatch":true,"sender":{"key":"worldhello.net@gmail.com","avatar":"https://avatars.githubusercontent.com/u/183860?v=4"},"body":"From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n\nIf there are references to be deleted in a transaction, we should remove\neach reference from both loose references and the \"packed-refs\" file.\nThe \"reference-transaction\" hook will run twice, once for the primary\nref-store (loose references), and another for the second ref-store (i.e.\npacked ref-store).\n\nTo avoid duplicate running of the \"reference-trancaction\" hook, we pass\na special \"hook-flags\" parameter to initialize the second ref-store.\nThe \"REF_TRANSACTION_RUN_PREPARED_HOOK\" bit is preserved for the\ntransaction of the second ref-store because we may still want to call\ncommand \"reference-trancaction prepared\" for some pre-checks, such as\nterminate unwanted transaction for the \"packed-refs\" file.\n\nThe behavior of the following git commands and five testcases have been\nfixed in t1416:\n\n * git cherry-pick\n * git rebase\n * git revert\n * git update-ref -d <ref>\n * git update-ref --stdin\t# delete refs\n\nSigned-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n---\n refs/files-backend.c             |  7 +++++--\n t/t1416-ref-transaction-hooks.sh | 10 +++++-----\n 2 files changed, 10 insertions(+), 7 deletions(-)\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 3be317d524..e8d12cb343 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2788,8 +2788,11 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n \t\t\t * packed-refs if it exists there.\n \t\t\t */\n \t\t\tif (!packed_transaction) {\n-\t\t\t\tpacked_transaction = ref_store_transaction_begin(\n-\t\t\t\t\t\trefs->packed_ref_store, err);\n+\t\t\t\tpacked_transaction = ref_store_transaction_begin_extended(\n+\t\t\t\t\t\trefs->packed_ref_store,\n+\t\t\t\t\t\ttransaction->hook_flags &\n+\t\t\t\t\t\t\tREF_TRANSACTION_RUN_PREPARED_HOOK,\n+\t\t\t\t\t\terr);\n \t\t\t\tif (!packed_transaction) {\n \t\t\t\t\tret = TRANSACTION_GENERIC_ERROR;\n \t\t\t\t\tgoto cleanup;\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex 7f8fd012e8..21d5326575 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -407,7 +407,7 @@ test_expect_success \"update-ref: update refs already packed to .git/packed-refs\"\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-test_expect_failure \"update-ref: remove refs with mixed ref_stores\" '\n+test_expect_success \"update-ref: remove refs with mixed ref_stores\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-EOF &&\n@@ -523,7 +523,7 @@ test_expect_success \"update-ref --stdin: update refs\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-test_expect_failure \"update-ref --stdin: delete refs\" '\n+test_expect_success \"update-ref --stdin: delete refs\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-EOF &&\n@@ -1023,7 +1023,7 @@ test_expect_success \"worktree: topic2: merge\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-test_expect_failure \"worktree: topic3: cherry-pick\" '\n+test_expect_success \"worktree: topic3: cherry-pick\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-EOF &&\n@@ -1067,7 +1067,7 @@ test_expect_failure \"worktree: topic3: cherry-pick\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-test_expect_failure \"worktree: topic4: rebase\" '\n+test_expect_success \"worktree: topic4: rebase\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-EOF &&\n@@ -1119,7 +1119,7 @@ test_expect_failure \"worktree: topic4: rebase\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-test_expect_failure \"worktree: topic5: revert\" '\n+test_expect_success \"worktree: topic5: revert\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-EOF &&\n-- \n2.36.1.25.gc87d5ad63a.dirty\n\n"},{"id":"460200","messageId":"20220729101245.6469-7-worldhello.net@gmail.com","threadId":"58240","inReplyTo":"20220729101245.6469-1-worldhello.net@gmail.com","subject":"[PATCH 6/9] refs: add reflog_info to hold more fields for reflog entry","fromName":"Jiang Xin","fromEmail":"worldhello.net@gmail.com","sentAt":"2022-07-29T10:12:42Z","receivedAt":"2022-07-29T10:13:10Z","isPatch":true,"sender":{"key":"worldhello.net@gmail.com","avatar":"https://avatars.githubusercontent.com/u/183860?v=4"},"body":"From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n\nThe parameter \"msg\" of the functions \"ref_transaction_add_update()\" and\n\"refs_update_ref()\" is used as a comment for creating a new reflog\nentry. For some cases, like copying or renaming a branch, we may need\nmore custom fields for the new reflog entry, such as old-oid which is\ndifferent from the oid we get from the lock file. Therefore, we create\na new structure \"reflog_info\" to hold more custom fields for the new\nreflog entry, and add two additional extended version functions.\n\nWe will use this extension in a later commit to reimplement\n\"files_copy_or_rename_ref()\" using \"refs_update_ref_extended()\" to\ncreate new reference in a transaction and add proper reflog entry.\n\nSigned-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n---\n refs.c               | 54 +++++++++++++++++++++++++++++++++++++++-----\n refs.h               | 20 ++++++++++++++++\n refs/debug.c         |  2 +-\n refs/files-backend.c | 14 ++++++++----\n refs/refs-internal.h | 17 ++++++++++++--\n 5 files changed, 94 insertions(+), 13 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex 48b69460e2..e53f011e6b 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1045,7 +1045,11 @@ void ref_transaction_free(struct ref_transaction *transaction)\n \t}\n \n \tfor (i = 0; i < transaction->nr; i++) {\n-\t\tfree(transaction->updates[i]->msg);\n+\t\tif (transaction->updates[i]->reflog_info) {\n+\t\t\tfree(transaction->updates[i]->reflog_info->msg);\n+\t\t\tfree(transaction->updates[i]->reflog_info->old_oid);\n+\t\t\tfree(transaction->updates[i]->reflog_info);\n+\t\t}\n \t\tfree(transaction->updates[i]);\n \t}\n \tfree(transaction->updates);\n@@ -1057,7 +1061,7 @@ struct ref_update *ref_transaction_add_update(\n \t\tconst char *refname, unsigned int flags,\n \t\tconst struct object_id *new_oid,\n \t\tconst struct object_id *old_oid,\n-\t\tconst char *msg)\n+\t\tconst struct reflog_info *reflog_info)\n {\n \tstruct ref_update *update;\n \n@@ -1074,7 +1078,12 @@ struct ref_update *ref_transaction_add_update(\n \t\toidcpy(&update->new_oid, new_oid);\n \tif (flags & REF_HAVE_OLD)\n \t\toidcpy(&update->old_oid, old_oid);\n-\tupdate->msg = normalize_reflog_message(msg);\n+\tif (reflog_info) {\n+\t\tupdate->reflog_info = xmalloc(sizeof(*reflog_info));\n+\t\tupdate->reflog_info->msg = normalize_reflog_message(reflog_info->msg);\n+\t\tif (reflog_info->old_oid)\n+\t\t\tupdate->reflog_info->old_oid = oiddup(reflog_info->old_oid);\n+\t}\n \treturn update;\n }\n \n@@ -1084,6 +1093,23 @@ int ref_transaction_update(struct ref_transaction *transaction,\n \t\t\t   const struct object_id *old_oid,\n \t\t\t   unsigned int flags, const char *msg,\n \t\t\t   struct strbuf *err)\n+{\n+\tstruct reflog_info reflog_info;\n+\n+\treflog_info.msg = (char *)msg;\n+\treflog_info.old_oid = NULL;\n+\treturn ref_transaction_update_extended(transaction,\n+\t\t\t\t\t       refname, new_oid, old_oid,\n+\t\t\t\t\t       flags, &reflog_info, err);\n+}\n+\n+int ref_transaction_update_extended(struct ref_transaction *transaction,\n+\t\t\t\t    const char *refname,\n+\t\t\t\t    const struct object_id *new_oid,\n+\t\t\t\t    const struct object_id *old_oid,\n+\t\t\t\t    unsigned int flags,\n+\t\t\t\t    const struct reflog_info *reflog_info,\n+\t\t\t\t    struct strbuf *err)\n {\n \tassert(err);\n \n@@ -1109,7 +1135,7 @@ int ref_transaction_update(struct ref_transaction *transaction,\n \tflags |= (new_oid ? REF_HAVE_NEW : 0) | (old_oid ? REF_HAVE_OLD : 0);\n \n \tref_transaction_add_update(transaction, refname, flags,\n-\t\t\t\t   new_oid, old_oid, msg);\n+\t\t\t\t   new_oid, old_oid, reflog_info);\n \treturn 0;\n }\n \n@@ -1157,6 +1183,22 @@ int refs_update_ref(struct ref_store *refs, const char *msg,\n \t\t    const char *refname, const struct object_id *new_oid,\n \t\t    const struct object_id *old_oid, unsigned int flags,\n \t\t    enum action_on_err onerr)\n+{\n+\tstruct reflog_info reflog_info;\n+\n+\treflog_info.msg = (char *)msg;\n+\treflog_info.old_oid = NULL;\n+\treturn refs_update_ref_extended(refs, refname, new_oid, old_oid,\n+\t\t\t\t\tflags, &reflog_info, onerr);\n+}\n+\n+int refs_update_ref_extended(struct ref_store *refs,\n+\t\t\t     const char *refname,\n+\t\t\t     const struct object_id *new_oid,\n+\t\t\t     const struct object_id *old_oid,\n+\t\t\t     unsigned int flags,\n+\t\t\t     const struct reflog_info *reflog_info,\n+\t\t\t     enum action_on_err onerr)\n {\n \tstruct ref_transaction *t = NULL;\n \tstruct strbuf err = STRBUF_INIT;\n@@ -1164,8 +1206,8 @@ int refs_update_ref(struct ref_store *refs, const char *msg,\n \n \tt = ref_store_transaction_begin(refs, &err);\n \tif (!t ||\n-\t    ref_transaction_update(t, refname, new_oid, old_oid, flags, msg,\n-\t\t\t\t   &err) ||\n+\t    ref_transaction_update_extended(t, refname, new_oid, old_oid,\n+\t\t\t\t\t    flags, reflog_info, &err) ||\n \t    ref_transaction_commit(t, &err)) {\n \t\tret = 1;\n \t\tref_transaction_free(t);\ndiff --git a/refs.h b/refs.h\nindex 715127ab58..0f21ba259f 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -10,6 +10,7 @@ struct strbuf;\n struct string_list;\n struct string_list_item;\n struct worktree;\n+struct reflog_info;\n \n /*\n  * Resolve a reference, recursively following symbolic refererences.\n@@ -677,6 +678,18 @@ int ref_transaction_update(struct ref_transaction *transaction,\n \t\t\t   const struct object_id *old_oid,\n \t\t\t   unsigned int flags, const char *msg,\n \t\t\t   struct strbuf *err);\n+/*\n+ * Extended version of ref_transaction_update() that allows us to\n+ * provide more fields (in reflog_info) to custom reflog, such\n+ * as msg and old_oid.\n+ */\n+int ref_transaction_update_extended(struct ref_transaction *transaction,\n+\t\t\t\t    const char *refname,\n+\t\t\t\t    const struct object_id *new_oid,\n+\t\t\t\t    const struct object_id *old_oid,\n+\t\t\t\t    unsigned int flags,\n+\t\t\t\t    const struct reflog_info *reflog_info,\n+\t\t\t\t    struct strbuf *err);\n \n /*\n  * Add a reference creation to transaction. new_oid is the value that\n@@ -806,6 +819,13 @@ void ref_transaction_free(struct ref_transaction *transaction);\n int refs_update_ref(struct ref_store *refs, const char *msg, const char *refname,\n \t\t    const struct object_id *new_oid, const struct object_id *old_oid,\n \t\t    unsigned int flags, enum action_on_err onerr);\n+int refs_update_ref_extended(struct ref_store *refs,\n+\t\t\t     const char *refname,\n+\t\t\t     const struct object_id *new_oid,\n+\t\t\t     const struct object_id *old_oid,\n+\t\t\t     unsigned int flags,\n+\t\t\t     const struct reflog_info *reflog_info,\n+\t\t\t     enum action_on_err onerr);\n int update_ref(const char *msg, const char *refname,\n \t       const struct object_id *new_oid, const struct object_id *old_oid,\n \t       unsigned int flags, enum action_on_err onerr);\ndiff --git a/refs/debug.c b/refs/debug.c\nindex eed8bc94b0..1e60507249 100644\n--- a/refs/debug.c\n+++ b/refs/debug.c\n@@ -79,7 +79,7 @@ static void print_transaction(struct ref_transaction *transaction)\n \tfor (i = 0; i < transaction->nr; i++) {\n \t\tstruct ref_update *u = transaction->updates[i];\n \t\tprint_update(i, u->refname, &u->old_oid, &u->new_oid, u->flags,\n-\t\t\t     u->type, u->msg);\n+\t\t\t     u->type, u->reflog_info? u->reflog_info->msg : NULL);\n \t}\n \ttrace_printf_key(&trace_refs, \"}\\n\");\n }\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex e8d12cb343..e2eabe9d8e 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2340,7 +2340,7 @@ static int split_head_update(struct ref_update *update,\n \t\t\ttransaction, \"HEAD\",\n \t\t\tupdate->flags | REF_LOG_ONLY | REF_NO_DEREF,\n \t\t\t&update->new_oid, &update->old_oid,\n-\t\t\tupdate->msg);\n+\t\t\tupdate->reflog_info);\n \n \t/*\n \t * Add \"HEAD\". This insertion is O(N) in the transaction\n@@ -2403,7 +2403,7 @@ static int split_symref_update(struct ref_update *update,\n \tnew_update = ref_transaction_add_update(\n \t\t\ttransaction, referent, new_flags,\n \t\t\t&update->new_oid, &update->old_oid,\n-\t\t\tupdate->msg);\n+\t\t\tupdate->reflog_info);\n \n \tnew_update->parent_update = update;\n \n@@ -2893,9 +2893,15 @@ static int files_transaction_finish(struct ref_store *ref_store,\n \t\t    update->flags & REF_LOG_ONLY) {\n \t\t\tif (files_log_ref_write(refs,\n \t\t\t\t\t\tlock->ref_name,\n-\t\t\t\t\t\t&lock->old_oid,\n+\t\t\t\t\t\tupdate->reflog_info &&\n+\t\t\t\t\t\tupdate->reflog_info->old_oid ?\n+\t\t\t\t\t\t\tupdate->reflog_info->old_oid :\n+\t\t\t\t\t\t\t&lock->old_oid,\n \t\t\t\t\t\t&update->new_oid,\n-\t\t\t\t\t\tupdate->msg, update->flags,\n+\t\t\t\t\t\tupdate->reflog_info ?\n+\t\t\t\t\t\t\tupdate->reflog_info->msg :\n+\t\t\t\t\t\t\tNULL,\n+\t\t\t\t\t\tupdate->flags,\n \t\t\t\t\t\terr)) {\n \t\t\t\tchar *old_msg = strbuf_detach(err, NULL);\n \ndiff --git a/refs/refs-internal.h b/refs/refs-internal.h\nindex 5220d1980d..782cf5fa78 100644\n--- a/refs/refs-internal.h\n+++ b/refs/refs-internal.h\n@@ -104,6 +104,19 @@ enum peel_status {\n  */\n enum peel_status peel_object(const struct object_id *name, struct object_id *oid);\n \n+/*\n+ * When using refs_update_ref() to copy or rename a branch, the old-oid\n+ * for the new created branch is null_oid, but the old_oid for the new\n+ * appended log entry for the reflog file which is copied from the\n+ * original reflog should be the same as the new_oid for the target\n+ * branch. Use \"reflog_info\" to hold log message and old_oid for the\n+ * new reflog entry.\n+ */\n+struct reflog_info {\n+\tstruct object_id *old_oid;\n+\tchar *msg;\n+};\n+\n /**\n  * Information needed for a single ref update. Set new_oid to the new\n  * value or to null_oid to delete the ref. To check the old value\n@@ -133,7 +146,7 @@ struct ref_update {\n \n \tvoid *backend_data;\n \tunsigned int type;\n-\tchar *msg;\n+\tstruct reflog_info *reflog_info;\n \n \t/*\n \t * If this ref_update was split off of a symref update via\n@@ -174,7 +187,7 @@ struct ref_update *ref_transaction_add_update(\n \t\tconst char *refname, unsigned int flags,\n \t\tconst struct object_id *new_oid,\n \t\tconst struct object_id *old_oid,\n-\t\tconst char *msg);\n+\t\tconst struct reflog_info *reflog_info);\n \n /*\n  * Transaction states.\n-- \n2.36.1.25.gc87d5ad63a.dirty\n\n"},{"id":"460201","messageId":"20220729101245.6469-8-worldhello.net@gmail.com","threadId":"58240","inReplyTo":"20220729101245.6469-1-worldhello.net@gmail.com","subject":"[PATCH 7/9] refs: get error message via refs_update_ref_extended()","fromName":"Jiang Xin","fromEmail":"worldhello.net@gmail.com","sentAt":"2022-07-29T10:12:43Z","receivedAt":"2022-07-29T10:13:12Z","isPatch":true,"sender":{"key":"worldhello.net@gmail.com","avatar":"https://avatars.githubusercontent.com/u/183860?v=4"},"body":"From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n\nThe last parameter for \"refs_update_ref_extended()\" is an enum\n\"action_on_err\", and we can not use this function to get the specific\nerror message. Extend this function again to get error message.\n\nWe will use the function \"refs_update_ref_extended()\" to reimplement\nthe function \"files_copy_or_rename_ref()\" in later commit.\n\nSigned-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n---\n refs.c | 50 +++++++++++++++++++++++++-------------------------\n refs.h |  2 +-\n 2 files changed, 26 insertions(+), 26 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex e53f011e6b..60c741a584 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1185,11 +1185,29 @@ int refs_update_ref(struct ref_store *refs, const char *msg,\n \t\t    enum action_on_err onerr)\n {\n \tstruct reflog_info reflog_info;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tint ret;\n \n \treflog_info.msg = (char *)msg;\n \treflog_info.old_oid = NULL;\n-\treturn refs_update_ref_extended(refs, refname, new_oid, old_oid,\n-\t\t\t\t\tflags, &reflog_info, onerr);\n+\tret = refs_update_ref_extended(refs, refname, new_oid, old_oid,\n+\t\t\t\t       flags, &reflog_info, &err);\n+\tif (ret) {\n+\t\tconst char *str = _(\"update_ref failed for ref '%s': %s\");\n+\n+\t\tswitch (onerr) {\n+\t\tcase UPDATE_REFS_MSG_ON_ERR:\n+\t\t\terror(str, refname, err.buf);\n+\t\t\tbreak;\n+\t\tcase UPDATE_REFS_DIE_ON_ERR:\n+\t\t\tdie(str, refname, err.buf);\n+\t\t\tbreak;\n+\t\tcase UPDATE_REFS_QUIET_ON_ERR:\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\tstrbuf_release(&err);\n+\treturn ret;\n }\n \n int refs_update_ref_extended(struct ref_store *refs,\n@@ -1198,37 +1216,19 @@ int refs_update_ref_extended(struct ref_store *refs,\n \t\t\t     const struct object_id *old_oid,\n \t\t\t     unsigned int flags,\n \t\t\t     const struct reflog_info *reflog_info,\n-\t\t\t     enum action_on_err onerr)\n+\t\t\t     struct strbuf *err)\n {\n \tstruct ref_transaction *t = NULL;\n-\tstruct strbuf err = STRBUF_INIT;\n-\tint ret = 0;\n \n-\tt = ref_store_transaction_begin(refs, &err);\n+\tt = ref_store_transaction_begin(refs, err);\n \tif (!t ||\n \t    ref_transaction_update_extended(t, refname, new_oid, old_oid,\n-\t\t\t\t\t    flags, reflog_info, &err) ||\n-\t    ref_transaction_commit(t, &err)) {\n-\t\tret = 1;\n+\t\t\t\t\t    flags, reflog_info, err) ||\n+\t    ref_transaction_commit(t, err)) {\n \t\tref_transaction_free(t);\n-\t}\n-\tif (ret) {\n-\t\tconst char *str = _(\"update_ref failed for ref '%s': %s\");\n-\n-\t\tswitch (onerr) {\n-\t\tcase UPDATE_REFS_MSG_ON_ERR:\n-\t\t\terror(str, refname, err.buf);\n-\t\t\tbreak;\n-\t\tcase UPDATE_REFS_DIE_ON_ERR:\n-\t\t\tdie(str, refname, err.buf);\n-\t\t\tbreak;\n-\t\tcase UPDATE_REFS_QUIET_ON_ERR:\n-\t\t\tbreak;\n-\t\t}\n-\t\tstrbuf_release(&err);\n \t\treturn 1;\n \t}\n-\tstrbuf_release(&err);\n+\n \tif (t)\n \t\tref_transaction_free(t);\n \treturn 0;\ndiff --git a/refs.h b/refs.h\nindex 0f21ba259f..85832c4863 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -825,7 +825,7 @@ int refs_update_ref_extended(struct ref_store *refs,\n \t\t\t     const struct object_id *old_oid,\n \t\t\t     unsigned int flags,\n \t\t\t     const struct reflog_info *reflog_info,\n-\t\t\t     enum action_on_err onerr);\n+\t\t\t     struct strbuf *err);\n int update_ref(const char *msg, const char *refname,\n \t       const struct object_id *new_oid, const struct object_id *old_oid,\n \t       unsigned int flags, enum action_on_err onerr);\n-- \n2.36.1.25.gc87d5ad63a.dirty\n\n"},{"id":"460202","messageId":"20220729101245.6469-9-worldhello.net@gmail.com","threadId":"58240","inReplyTo":"20220729101245.6469-1-worldhello.net@gmail.com","subject":"[PATCH 8/9] refs: reimplement files_copy_or_rename_ref() to run hook","fromName":"Jiang Xin","fromEmail":"worldhello.net@gmail.com","sentAt":"2022-07-29T10:12:44Z","receivedAt":"2022-07-29T10:13:15Z","isPatch":true,"sender":{"key":"worldhello.net@gmail.com","avatar":"https://avatars.githubusercontent.com/u/183860?v=4"},"body":"From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n\nWhen copying or renaming a branch, the \"reference-transaction\" hook is\nnot executed. This is because we used to reinvent the wheel in function\n\"files_copy_or_rename_ref()\". By calling \"refs_update_ref_extended()\" to\nreimplement \"files_copy_or_rename_ref()\", the \"reference-transaction\"\nhook will run correctly.\n\nThe behavior of the following git commands and two testcases have been\nfixed in t1416:\n\n * git branch -c <src> <dest>\t# copy branch\n * git branch -m <old> <new>\t# rename branch\n\nSigned-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n---\n refs/files-backend.c             | 97 +++-----------------------------\n t/t1416-ref-transaction-hooks.sh |  4 +-\n 2 files changed, 9 insertions(+), 92 deletions(-)\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex e2eabe9d8e..8baea66e58 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -1376,10 +1376,6 @@ static int rename_tmp_log(struct files_ref_store *refs, const char *newrefname)\n static int write_ref_to_lockfile(struct ref_lock *lock,\n \t\t\t\t const struct object_id *oid,\n \t\t\t\t int skip_oid_verification, struct strbuf *err);\n-static int commit_ref_update(struct files_ref_store *refs,\n-\t\t\t     struct ref_lock *lock,\n-\t\t\t     const struct object_id *oid, const char *logmsg,\n-\t\t\t     struct strbuf *err);\n \n /*\n  * Emit a better error message than lockfile.c's\n@@ -1418,13 +1414,13 @@ static int files_copy_or_rename_ref(struct ref_store *ref_store,\n \t\tfiles_downcast(ref_store, REF_STORE_WRITE, \"rename_ref\");\n \tstruct object_id orig_oid;\n \tint flag = 0, logmoved = 0;\n-\tstruct ref_lock *lock;\n \tstruct stat loginfo;\n \tstruct strbuf sb_oldref = STRBUF_INIT;\n \tstruct strbuf sb_newref = STRBUF_INIT;\n \tstruct strbuf tmp_renamed_log = STRBUF_INIT;\n \tint log, ret;\n \tstruct strbuf err = STRBUF_INIT;\n+\tstruct reflog_info reflog_info;\n \n \tfiles_reflog_path(refs, &sb_oldref, oldrefname);\n \tfiles_reflog_path(refs, &sb_newref, newrefname);\n@@ -1510,8 +1506,10 @@ static int files_copy_or_rename_ref(struct ref_store *ref_store,\n \n \tlogmoved = log;\n \n-\tlock = lock_ref_oid_basic(refs, newrefname, &err);\n-\tif (!lock) {\n+\treflog_info.msg = (char *)logmsg;\n+\treflog_info.old_oid = &orig_oid;\n+\tif (refs_update_ref_extended(ref_store, newrefname, &orig_oid, NULL,\n+\t\t\t\t     REF_NO_DEREF, &reflog_info, &err)) {\n \t\tif (copy)\n \t\t\terror(\"unable to copy '%s' to '%s': %s\", oldrefname, newrefname, err.buf);\n \t\telse\n@@ -1519,36 +1517,20 @@ static int files_copy_or_rename_ref(struct ref_store *ref_store,\n \t\tstrbuf_release(&err);\n \t\tgoto rollback;\n \t}\n-\toidcpy(&lock->old_oid, &orig_oid);\n-\n-\tif (write_ref_to_lockfile(lock, &orig_oid, 0, &err) ||\n-\t    commit_ref_update(refs, lock, &orig_oid, logmsg, &err)) {\n-\t\terror(\"unable to write current sha1 into %s: %s\", newrefname, err.buf);\n-\t\tstrbuf_release(&err);\n-\t\tgoto rollback;\n-\t}\n \n \tret = 0;\n \tgoto out;\n \n  rollback:\n-\tlock = lock_ref_oid_basic(refs, oldrefname, &err);\n-\tif (!lock) {\n-\t\terror(\"unable to lock %s for rollback: %s\", oldrefname, err.buf);\n-\t\tstrbuf_release(&err);\n-\t\tgoto rollbacklog;\n-\t}\n-\n \tflag = log_all_ref_updates;\n \tlog_all_ref_updates = LOG_REFS_NONE;\n-\tif (write_ref_to_lockfile(lock, &orig_oid, 0, &err) ||\n-\t    commit_ref_update(refs, lock, &orig_oid, NULL, &err)) {\n+\tif (refs_update_ref_extended(ref_store, oldrefname, &orig_oid, NULL,\n+\t\t\t\t     REF_NO_DEREF, NULL, &err)) {\n \t\terror(\"unable to write current sha1 into %s: %s\", oldrefname, err.buf);\n \t\tstrbuf_release(&err);\n \t}\n \tlog_all_ref_updates = flag;\n \n- rollbacklog:\n \tif (logmoved && rename(sb_newref.buf, sb_oldref.buf))\n \t\terror(\"unable to restore logfile %s from %s: %s\",\n \t\t\toldrefname, newrefname, strerror(errno));\n@@ -1815,71 +1797,6 @@ static int write_ref_to_lockfile(struct ref_lock *lock,\n \treturn 0;\n }\n \n-/*\n- * Commit a change to a loose reference that has already been written\n- * to the loose reference lockfile. Also update the reflogs if\n- * necessary, using the specified lockmsg (which can be NULL).\n- */\n-static int commit_ref_update(struct files_ref_store *refs,\n-\t\t\t     struct ref_lock *lock,\n-\t\t\t     const struct object_id *oid, const char *logmsg,\n-\t\t\t     struct strbuf *err)\n-{\n-\tfiles_assert_main_repository(refs, \"commit_ref_update\");\n-\n-\tclear_loose_ref_cache(refs);\n-\tif (files_log_ref_write(refs, lock->ref_name,\n-\t\t\t\t&lock->old_oid, oid,\n-\t\t\t\tlogmsg, 0, err)) {\n-\t\tchar *old_msg = strbuf_detach(err, NULL);\n-\t\tstrbuf_addf(err, \"cannot update the ref '%s': %s\",\n-\t\t\t    lock->ref_name, old_msg);\n-\t\tfree(old_msg);\n-\t\tunlock_ref(lock);\n-\t\treturn -1;\n-\t}\n-\n-\tif (strcmp(lock->ref_name, \"HEAD\") != 0) {\n-\t\t/*\n-\t\t * Special hack: If a branch is updated directly and HEAD\n-\t\t * points to it (may happen on the remote side of a push\n-\t\t * for example) then logically the HEAD reflog should be\n-\t\t * updated too.\n-\t\t * A generic solution implies reverse symref information,\n-\t\t * but finding all symrefs pointing to the given branch\n-\t\t * would be rather costly for this rare event (the direct\n-\t\t * update of a branch) to be worth it.  So let's cheat and\n-\t\t * check with HEAD only which should cover 99% of all usage\n-\t\t * scenarios (even 100% of the default ones).\n-\t\t */\n-\t\tint head_flag;\n-\t\tconst char *head_ref;\n-\n-\t\thead_ref = refs_resolve_ref_unsafe(&refs->base, \"HEAD\",\n-\t\t\t\t\t\t   RESOLVE_REF_READING,\n-\t\t\t\t\t\t   NULL, &head_flag);\n-\t\tif (head_ref && (head_flag & REF_ISSYMREF) &&\n-\t\t    !strcmp(head_ref, lock->ref_name)) {\n-\t\t\tstruct strbuf log_err = STRBUF_INIT;\n-\t\t\tif (files_log_ref_write(refs, \"HEAD\",\n-\t\t\t\t\t\t&lock->old_oid, oid,\n-\t\t\t\t\t\tlogmsg, 0, &log_err)) {\n-\t\t\t\terror(\"%s\", log_err.buf);\n-\t\t\t\tstrbuf_release(&log_err);\n-\t\t\t}\n-\t\t}\n-\t}\n-\n-\tif (commit_ref(lock)) {\n-\t\tstrbuf_addf(err, \"couldn't set '%s'\", lock->ref_name);\n-\t\tunlock_ref(lock);\n-\t\treturn -1;\n-\t}\n-\n-\tunlock_ref(lock);\n-\treturn 0;\n-}\n-\n static int create_ref_symlink(struct ref_lock *lock, const char *target)\n {\n \tint ret = -1;\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex 21d5326575..df75e5727c 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -670,7 +670,7 @@ test_expect_success \"branch: update refs to create loose refs\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-test_expect_failure \"branch: copy branches\" '\n+test_expect_success \"branch: copy branches\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-EOF &&\n@@ -703,7 +703,7 @@ test_expect_failure \"branch: copy branches\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-test_expect_failure \"branch: rename branches\" '\n+test_expect_success \"branch: rename branches\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-EOF &&\n-- \n2.36.1.25.gc87d5ad63a.dirty\n\n"},{"id":"460203","messageId":"20220729101245.6469-10-worldhello.net@gmail.com","threadId":"58240","inReplyTo":"20220729101245.6469-1-worldhello.net@gmail.com","subject":"[PATCH 9/9] refs: reimplement refs_delete_refs() and run hook once","fromName":"Jiang Xin","fromEmail":"worldhello.net@gmail.com","sentAt":"2022-07-29T10:12:45Z","receivedAt":"2022-07-29T10:13:16Z","isPatch":true,"sender":{"key":"worldhello.net@gmail.com","avatar":"https://avatars.githubusercontent.com/u/183860?v=4"},"body":"From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n\nWhen delete references using \"git branch -d\" or \"git tag -d\", there will\nbe duplicate call of \"reference-transaction committed\" for same refs.\nThis is because \"refs_delete_refs()\" is called twice, once for\nfiles-backend and once for packed-backend, and we used to reinvented the\nwheel in \"files_delete_refs()\" and \"packed_delete_refs()\". By removing\n\"packed_delete_refs()\" and reimplement \"files_delete_refs()\", the\n\"reference-transaction\" hook will run only once for deleted branches and\ntags.\n\nThe behavior of the following git commands and the last two testcases\nhave been fixed in t1416:\n\n * git branch -d <branch>\n * git tag -d <tag>\n\nA testcase in t5510 is broken because we used to call the function\n\"packed_refs_lock()\", but it is not necessary if the deleted reference\nis not in the \"packed-refs\" file.\n\nSigned-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n---\n refs/files-backend.c             | 21 ++++++-------\n refs/packed-backend.c            | 51 +-------------------------------\n t/t1416-ref-transaction-hooks.sh |  4 +--\n t/t5510-fetch.sh                 | 17 +++++++++++\n 4 files changed, 29 insertions(+), 64 deletions(-)\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 8baea66e58..21426efaae 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -1268,31 +1268,27 @@ static int files_pack_refs(struct ref_store *ref_store, unsigned int flags)\n static int files_delete_refs(struct ref_store *ref_store, const char *msg,\n \t\t\t     struct string_list *refnames, unsigned int flags)\n {\n-\tstruct files_ref_store *refs =\n-\t\tfiles_downcast(ref_store, REF_STORE_WRITE, \"delete_refs\");\n+\tstruct ref_transaction *transaction;\n \tstruct strbuf err = STRBUF_INIT;\n \tint i, result = 0;\n \n \tif (!refnames->nr)\n \t\treturn 0;\n \n-\tif (packed_refs_lock(refs->packed_ref_store, 0, &err))\n-\t\tgoto error;\n-\n-\tif (refs_delete_refs(refs->packed_ref_store, msg, refnames, flags)) {\n-\t\tpacked_refs_unlock(refs->packed_ref_store);\n+\ttransaction = ref_store_transaction_begin(ref_store, &err);\n+\tif (!transaction)\n \t\tgoto error;\n-\t}\n-\n-\tpacked_refs_unlock(refs->packed_ref_store);\n \n \tfor (i = 0; i < refnames->nr; i++) {\n \t\tconst char *refname = refnames->items[i].string;\n-\n-\t\tif (refs_delete_ref(&refs->base, msg, refname, NULL, flags))\n+\t\tif (ref_transaction_delete(transaction, refname, NULL,\n+\t\t\t\t\t   flags, msg, &err))\n \t\t\tresult |= error(_(\"could not remove reference %s\"), refname);\n \t}\n+\tif (ref_transaction_commit(transaction, &err))\n+\t\tgoto error;\n \n+\tref_transaction_free(transaction);\n \tstrbuf_release(&err);\n \treturn result;\n \n@@ -1309,6 +1305,7 @@ static int files_delete_refs(struct ref_store *ref_store, const char *msg,\n \telse\n \t\terror(_(\"could not delete references: %s\"), err.buf);\n \n+\tref_transaction_free(transaction);\n \tstrbuf_release(&err);\n \treturn -1;\n }\ndiff --git a/refs/packed-backend.c b/refs/packed-backend.c\nindex 97b6837767..fdb7a0a52c 100644\n--- a/refs/packed-backend.c\n+++ b/refs/packed-backend.c\n@@ -1519,55 +1519,6 @@ static int packed_initial_transaction_commit(struct ref_store *ref_store,\n \treturn ref_transaction_commit(transaction, err);\n }\n \n-static int packed_delete_refs(struct ref_store *ref_store, const char *msg,\n-\t\t\t     struct string_list *refnames, unsigned int flags)\n-{\n-\tstruct packed_ref_store *refs =\n-\t\tpacked_downcast(ref_store, REF_STORE_WRITE, \"delete_refs\");\n-\tstruct strbuf err = STRBUF_INIT;\n-\tstruct ref_transaction *transaction;\n-\tstruct string_list_item *item;\n-\tint ret;\n-\n-\t(void)refs; /* We need the check above, but don't use the variable */\n-\n-\tif (!refnames->nr)\n-\t\treturn 0;\n-\n-\t/*\n-\t * Since we don't check the references' old_oids, the\n-\t * individual updates can't fail, so we can pack all of the\n-\t * updates into a single transaction.\n-\t */\n-\n-\ttransaction = ref_store_transaction_begin(ref_store, &err);\n-\tif (!transaction)\n-\t\treturn -1;\n-\n-\tfor_each_string_list_item(item, refnames) {\n-\t\tif (ref_transaction_delete(transaction, item->string, NULL,\n-\t\t\t\t\t   flags, msg, &err)) {\n-\t\t\twarning(_(\"could not delete reference %s: %s\"),\n-\t\t\t\titem->string, err.buf);\n-\t\t\tstrbuf_reset(&err);\n-\t\t}\n-\t}\n-\n-\tret = ref_transaction_commit(transaction, &err);\n-\n-\tif (ret) {\n-\t\tif (refnames->nr == 1)\n-\t\t\terror(_(\"could not delete reference %s: %s\"),\n-\t\t\t      refnames->items[0].string, err.buf);\n-\t\telse\n-\t\t\terror(_(\"could not delete references: %s\"), err.buf);\n-\t}\n-\n-\tref_transaction_free(transaction);\n-\tstrbuf_release(&err);\n-\treturn ret;\n-}\n-\n static int packed_pack_refs(struct ref_store *ref_store, unsigned int flags)\n {\n \t/*\n@@ -1595,7 +1546,7 @@ struct ref_storage_be refs_be_packed = {\n \n \t.pack_refs = packed_pack_refs,\n \t.create_symref = NULL,\n-\t.delete_refs = packed_delete_refs,\n+\t.delete_refs = NULL,\n \t.rename_ref = NULL,\n \t.copy_ref = NULL,\n \ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex df75e5727c..f64166f9d7 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -744,7 +744,7 @@ test_expect_success \"branch: rename branches\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-test_expect_failure \"branch: remove branches\" '\n+test_expect_success \"branch: remove branches\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-EOF &&\n@@ -873,7 +873,7 @@ test_expect_success \"tag: update refs to create loose refs\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-test_expect_failure \"tag: remove tags with mixed ref_stores\" '\n+test_expect_success \"tag: remove tags with mixed ref_stores\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-EOF &&\ndiff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\nindex b45879a760..22de7ac9ec 100755\n--- a/t/t5510-fetch.sh\n+++ b/t/t5510-fetch.sh\n@@ -168,6 +168,8 @@ test_expect_success REFFILES 'fetch --prune fails to delete branches' '\n \tcd \"$D\" &&\n \tgit clone . prune-fail &&\n \tcd prune-fail &&\n+\tgit update-ref refs/remotes/origin/extrabranch main~ &&\n+\tgit pack-refs --all &&\n \tgit update-ref refs/remotes/origin/extrabranch main &&\n \t: this will prevent --prune from locking packed-refs for deleting refs, but adding loose refs still succeeds  &&\n \t>.git/packed-refs.new &&\n@@ -175,6 +177,21 @@ test_expect_success REFFILES 'fetch --prune fails to delete branches' '\n \ttest_must_fail git fetch --prune origin\n '\n \n+test_expect_success REFFILES 'fetch --prune ok for loose refs not in locked packed-refs' '\n+\ttest_when_finished \"cd \\\"$D\\\"; rm -rf \\\"prune-ok-ref-not-packed\\\"\" &&\n+\tcd \"$D\" &&\n+\tgit clone . prune-ok-ref-not-packed &&\n+\t(\n+\t\tcd prune-ok-ref-not-packed &&\n+\t\tgit update-ref refs/remotes/origin/extrabranch main &&\n+\t\t: for loose refs not in packed-refs, we can delete them even the packed-refs is locked &&\n+\t\t:>.git/packed-refs.new &&\n+\n+\t\tgit fetch --prune origin &&\n+\t\ttest_must_fail git rev-parse refs/remotes/origin/extrabranch --\n+\t)\n+'\n+\n test_expect_success 'fetch --atomic works with a single branch' '\n \ttest_when_finished \"rm -rf \\\"$D\\\"/atomic\" &&\n \n-- \n2.36.1.25.gc87d5ad63a.dirty\n\n"},{"id":"460300","messageId":"CAPig+cQyW4Bz1kL5MriXeU6Zd93oYQU8ZuA-1gaEmAERpbTaDA@mail.gmail.com","threadId":"58240","inReplyTo":"20220729101245.6469-2-worldhello.net@gmail.com","subject":"Re: [PATCH 1/9] t1416: more testcases for reference-transaction hook","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2022-07-30T06:44:29Z","receivedAt":"2022-07-30T06:44:44Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Fri, Jul 29, 2022 at 6:21 AM Jiang Xin <worldhello.net@gmail.com> wrote:\n> Append more testcases in t1416 for various git commands that may trigger\n> the \"reference-transaction\" hook.\n> [...]\n> Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n> ---\n> diff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\n> @@ -133,4 +133,1072 @@ test_expect_success 'interleaving hook calls succeed' '\n> +# Create commits in <repo> and assign each commit's oid to shell variables\n> +# given in the arguments (A, B, and C). E.g.:\n> +#\n> +#     create_commits_in <repo> A B C\n> +#\n> +# NOTE: Never calling this function from a subshell since variable\n> +# assignments will disappear when subshell exits.\n> +create_commits_in () {\n> +       repo=\"$1\" && test -d \"$repo\" ||\n> +       error \"Repository $repo does not exist.\"\n> +       shift &&\n> +       while test $# -gt 0\n> +       do\n> +               name=$1 &&\n> +               shift &&\n> +               test_commit -C \"$repo\" --no-tag \"$name\" &&\n> +               eval $name=$(git -C \"$repo\" rev-parse HEAD)\n> +       done\n> +}\n\nSince tests call this function within an &&-chain, we should make sure\nthat &&-chain inside the function itself does the right thing. There\nare a couple important and one (somewhat optional) minor fix needed\nfor this function. First, the function should manually break from the\nloop and indicate failure (using `|| return 1`) if any command inside\nthe loop fails. Second, the `eval` is always going to return success\neven if the embedded `git rev-parse` command fails. Finally, the minor\nfix is that the `test ... || error ...` could be difficult for an\n&&-chain linter to grok if we ever start linting function bodies. To\nfix all these problems, you could perhaps write the function like\nthis:\n\n    create_commits_in () {\n        local repo=\"$1\" &&\n        if ! test -d \"$repo\"\n        then\n            error \"Repository $repo does not exist.\"\n        fi &&\n        shift &&\n        while test $# -gt 0\n        do\n            local name=$1 &&\n            shift &&\n            test_commit -C \"$repo\" --no-tag \"$name\" &&\n            local rev=$(git -C \"$repo\" rev-parse HEAD) &&\n            eval \"$name=$rev\" || return 1\n        done\n    }\n\nNow that the function breaks out of the loop properly with `|| return\n1` upon failure, it's no longer necessary to perform the directory\ncheck at the top of the function since the call to test_commit() will\ncorrectly fail if the directory does not exist. So, the function can\nbe shortened to:\n\n    create_commits_in () {\n        local repo=\"$1\" &&\n        shift &&\n        while test $# -gt 0\n        do\n            local name=$1 &&\n            shift &&\n            test_commit -C \"$repo\" --no-tag \"$name\" &&\n            local rev=$(git -C \"$repo\" rev-parse HEAD) &&\n            eval $name=$rev || return 1\n        done\n    }\n\nHaving said all that, it almost seems overkill to build the loop into\nthis function considering that it sets only four shell variables in\nthe entire test script, so it might be simpler to drop the loop\naltogether:\n\n    create_commits_in () {\n        local repo=\"$1\" name=\"$2\" &&\n        test_commit -C \"$repo\" --no-tag \"$name\" &&\n        local rev=$(git -C \"$repo\" rev-parse HEAD) &&\n        echo $rev\n    }\n\nand change the callers to invoke it individually for each variable:\n\n    A=$(create_commits_in base A) &&\n    B=$(create_commits_in base B) &&\n    C=$(create_commits_in base C) &&\n\nor even drop the function entirely:\n\n    test_commit -C base --no-tag A &&\n    A=$(git -C base rev-parse HEAD) &&\n    test_commit -C base --no-tag B &&\n    B=$(git -C base rev-parse HEAD) &&\n    test_commit -C base --no-tag C &&\n    C=$(git -C base rev-parse HEAD) &&\n\nthough, it's a matter of taste whether that's better.\n\n> +test_cmp_heads_and_tags () {\n> +       indir= &&\n> +       while test $# != 0\n> +       do\n> +               case \"$1\" in\n> +               -C)\n> +                       indir=\"$2\"\n> +                       shift\n> +                       ;;\n\nIt wouldn't hurt to keep the &&-chain intact here in case the &&-chain\nlinter is some day updated to check function bodies, so:\n\n    indir=\"$2\" &&\n    shift\n\n> +               *)\n> +                       break\n> +                       ;;\n> +               esac\n> +               shift\n\nSame here:\n\n    esac &&\n    shift\n\n> +       done &&\n> +       expect=${1:-expect} &&\n> +       actual=${2:-actual-heads-and-tags} &&\n> +       indir=${indir:+\"$indir\"/} &&\n> +       test_path_is_file \"$expect\" &&\n> +       test_when_finished \"rm -f \\\"$actual\\\"\" &&\n> +       git ${indir:+ -C \"$indir\"} show-ref --heads --tags | \\\n> +               make_user_friendly_and_stable_output >\"$actual\" &&\n\nThe exit code from `git show-ref` is being lost down the pipe. You\nalso don't need the `\\` after `|`.\n\n> +       test_cmp \"$expect\" \"$actual\"\n> +}\n> +\n> +test_expect_success 'setup git config and common reference-transaction hook' '\n> +       git config --global \\\n> +               core.hooksPath \"$HOME/test-hooks\" &&\n\nNit: This would fit nicely on a single line; no need for the line splicing.\n\n> +       git config --global core.abbrev 7 &&\n> +       mkdir \"test-hooks\" &&\n> +       write_script \"test-hooks/reference-transaction\" <<-EOF\n> +               exec >>\"$HOME/$HOOK_OUTPUT\"\n> +               printf \"## Call hook: reference-transaction %9s ##\\n\" \"\\$@\"\n> +               while read -r line\n> +               do\n> +                   printf \"%s\\n\" \"\\$line\"\n\nNit This is the same as:\n\n    echo \"\\$line\"\n\n> +               done\n> +       EOF\n> +'\n> +\n> +test_expect_success \"update-ref: create new refs\" '\n> +       test_when_finished \"rm -f $HOOK_OUTPUT\" &&\n> +\n> +       cat >expect <<-EOF &&\n> +               ## Call hook: reference-transaction  prepared ##\n\nThis and a bunch of other here-doc tags in subsequent tests are\nmissing the backslash:\n\n    cat >expect <<-\\EOF &&\n"},{"id":"460324","messageId":"CANYiYbHcd3UNF2vq6YLQifEWW6eSLhB=7d23907TAbUeum2w0A@mail.gmail.com","threadId":"58240","inReplyTo":"CAPig+cQyW4Bz1kL5MriXeU6Zd93oYQU8ZuA-1gaEmAERpbTaDA@mail.gmail.com","subject":"Re: [PATCH 1/9] t1416: more testcases for reference-transaction hook","fromName":"Jiang Xin","fromEmail":"worldhello.net@gmail.com","sentAt":"2022-07-31T03:25:05Z","receivedAt":"2022-07-31T03:25:23Z","isPatch":true,"sender":{"key":"worldhello.net@gmail.com","avatar":"https://avatars.githubusercontent.com/u/183860?v=4"},"body":"On Sat, Jul 30, 2022 at 2:44 PM Eric Sunshine <sunshine@sunshineco.com> wrote:\n\n> Now that the function breaks out of the loop properly with `|| return\n> 1` upon failure, it's no longer necessary to perform the directory\n> check at the top of the function since the call to test_commit() will\n> correctly fail if the directory does not exist. So, the function can\n> be shortened to:\n>\n>     create_commits_in () {\n>         local repo=\"$1\" &&\n>         shift &&\n>         while test $# -gt 0\n>         do\n>             local name=$1 &&\n>             shift &&\n>             test_commit -C \"$repo\" --no-tag \"$name\" &&\n>             local rev=$(git -C \"$repo\" rev-parse HEAD) &&\n>             eval $name=$rev || return 1\n>         done\n>     }\n\nThanks, Eric. Appreciate for all suggestions. I prefer this version\nand will use it in reroll v2.\n\n> > +       done &&\n> > +       expect=${1:-expect} &&\n> > +       actual=${2:-actual-heads-and-tags} &&\n> > +       indir=${indir:+\"$indir\"/} &&\n> > +       test_path_is_file \"$expect\" &&\n> > +       test_when_finished \"rm -f \\\"$actual\\\"\" &&\n> > +       git ${indir:+ -C \"$indir\"} show-ref --heads --tags | \\\n> > +               make_user_friendly_and_stable_output >\"$actual\" &&\n>\n> The exit code from `git show-ref` is being lost down the pipe. You\n> also don't need the `\\` after `|`.\n\nWill remove unnecessary backslash before EOL.\n\n> > +       test_cmp \"$expect\" \"$actual\"\n> > +}\n> > +\n> > +test_expect_success 'setup git config and common reference-transaction hook' '\n> > +       git config --global \\\n> > +               core.hooksPath \"$HOME/test-hooks\" &&\n>\n> Nit: This would fit nicely on a single line; no need for the line splicing.\n\nWill write in oneline.\n\n> > +       git config --global core.abbrev 7 &&\n> > +       mkdir \"test-hooks\" &&\n> > +       write_script \"test-hooks/reference-transaction\" <<-EOF\n> > +               exec >>\"$HOME/$HOOK_OUTPUT\"\n> > +               printf \"## Call hook: reference-transaction %9s ##\\n\" \"\\$@\"\n> > +               while read -r line\n> > +               do\n> > +                   printf \"%s\\n\" \"\\$line\"\n>\n> Nit This is the same as:\n>\n>     echo \"\\$line\"\n\nWill do.\n\n> > +               done\n> > +       EOF\n> > +'\n> > +\n> > +test_expect_success \"update-ref: create new refs\" '\n> > +       test_when_finished \"rm -f $HOOK_OUTPUT\" &&\n> > +\n> > +       cat >expect <<-EOF &&\n> > +               ## Call hook: reference-transaction  prepared ##\n>\n> This and a bunch of other here-doc tags in subsequent tests are\n> missing the backslash:\n>\n>     cat >expect <<-\\EOF &&\n\nWill add backslash unless there are variables need to be expanded.\n"},{"id":"460340","messageId":"CANYiYbG28jAZ8Kk17h0gkmmU300b1n917oPtBM_WeFX3QN=VXQ@mail.gmail.com","threadId":"58240","inReplyTo":"20220729101245.6469-7-worldhello.net@gmail.com","subject":"Re: [PATCH 6/9] refs: add reflog_info to hold more fields for reflog entry","fromName":"Jiang Xin","fromEmail":"worldhello.net@gmail.com","sentAt":"2022-08-01T11:32:44Z","receivedAt":"2022-08-01T11:33:00Z","isPatch":true,"sender":{"key":"worldhello.net@gmail.com","avatar":"https://avatars.githubusercontent.com/u/183860?v=4"},"body":"On Fri, Jul 29, 2022 at 6:12 PM Jiang Xin <worldhello.net@gmail.com> wrote:\n>\n> From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n>\n> The parameter \"msg\" of the functions \"ref_transaction_add_update()\" and\n> \"refs_update_ref()\" is used as a comment for creating a new reflog\n> entry. For some cases, like copying or renaming a branch, we may need\n> more custom fields for the new reflog entry, such as old-oid which is\n> different from the oid we get from the lock file. Therefore, we create\n> a new structure \"reflog_info\" to hold more custom fields for the new\n> reflog entry, and add two additional extended version functions.\n>\n> We will use this extension in a later commit to reimplement\n> \"files_copy_or_rename_ref()\" using \"refs_update_ref_extended()\" to\n> create new reference in a transaction and add proper reflog entry.\n>\n> Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n> ---\n>  refs.c               | 54 +++++++++++++++++++++++++++++++++++++++-----\n>  refs.h               | 20 ++++++++++++++++\n>  refs/debug.c         |  2 +-\n>  refs/files-backend.c | 14 ++++++++----\n>  refs/refs-internal.h | 17 ++++++++++++--\n>  5 files changed, 94 insertions(+), 13 deletions(-)\n>\n> diff --git a/refs.c b/refs.c\n> index 48b69460e2..e53f011e6b 100644\n> --- a/refs.c\n> +++ b/refs.c\n\n> @@ -1074,7 +1078,12 @@ struct ref_update *ref_transaction_add_update(\n>                 oidcpy(&update->new_oid, new_oid);\n>         if (flags & REF_HAVE_OLD)\n>                 oidcpy(&update->old_oid, old_oid);\n> -       update->msg = normalize_reflog_message(msg);\n> +       if (reflog_info) {\n> +               update->reflog_info = xmalloc(sizeof(*reflog_info));\n\nShould be:\n        update->reflog_info = xcalloc(1, sizeof(*reflog_info));\n\nWill fix in v2.\n\n> +               update->reflog_info->msg = normalize_reflog_message(reflog_info->msg);\n> +               if (reflog_info->old_oid)\n> +                       update->reflog_info->old_oid = oiddup(reflog_info->old_oid);\n> +       }\n>         return update;\n>  }\n"},{"id":"460410","messageId":"CAJDSCnOFSnhOU06UHF6T9RT31Gq56BhJn5fQ4_ys2ERrN1stSw@mail.gmail.com","threadId":"58240","inReplyTo":"20220729101245.6469-6-worldhello.net@gmail.com","subject":"Re: [PATCH 5/9] refs: avoid duplicate running of the reference-transaction hook","fromName":"Michael Heemskerk","fromEmail":"mheemskerk@atlassian.com","sentAt":"2022-08-02T12:18:33Z","receivedAt":"2022-08-02T12:19:12Z","isPatch":true,"sender":{"key":"mheemskerk@atlassian.com","avatar":"https://gravatar.com/avatar/c3ebe086c3c55529246291e9879e5ef7af3c0ffcadbd81b5508b5fcdf66aa065?d=mp&s=160"},"body":"On Fri, Jul 29, 2022 at 12:20 PM Jiang Xin <worldhello.net@gmail.com> wrote:\n>\n> From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n>\n> If there are references to be deleted in a transaction, we should remove\n> each reference from both loose references and the \"packed-refs\" file.\n> The \"reference-transaction\" hook will run twice, once for the primary\n> ref-store (loose references), and another for the second ref-store (i.e.\n> packed ref-store).\n>\n> To avoid duplicate running of the \"reference-trancaction\" hook, we pass\n> a special \"hook-flags\" parameter to initialize the second ref-store.\n> The \"REF_TRANSACTION_RUN_PREPARED_HOOK\" bit is preserved for the\n> transaction of the second ref-store because we may still want to call\n> command \"reference-trancaction prepared\" for some pre-checks, such as\n> terminate unwanted transaction for the \"packed-refs\" file.\n\nCan you elaborate on the rationale for continuing to invoke the \"prepared\"\nreference-transaction hook for the \"packed-refs\" file? Did you have a specific\ntype of check in mind?\n\nAs far as I can tell, any ref update included in this 'extra' callback is also\nprovided to the \"prepared\" callback for the \"files\" backend, and that \"files\"\ncallback always happens - even when deleting a ref that only exists in\npacked-refs. As a result, it _should_ be safe to also suppress the \"prepared\"\ncallback for the \"packed-refs\" backend. Patrick has a patch series that does\nexactly that (suppress all \"packed-refs\" reference-transaction callbacks).\n\nYour patch series will achieve the same (and more) if you change\n\n> +                               packed_transaction = ref_store_transaction_begin_extended(\n> +                                               refs->packed_ref_store,\n> +                                               transaction->hook_flags &\n> +                                                       REF_TRANSACTION_RUN_PREPARED_HOOK,\n> +                                               err);\n\nto\n\n> +                               packed_transaction = ref_store_transaction_begin_extended(\n> +                                               refs->packed_ref_store,\n> +                                               0,\n> +                                               err);\n\nFor illustration, here are some callbacks I see with your patches applied\n(without the change I suggested above):\n\ndeleting a ref that only exists as a loose ref:\n  1. prepared (for 'files')\n  d6edcbf924697ab811a867421dab60d954ccad99\n0000000000000000000000000000000000000000 refs/heads/basic_branching\n  2. committed (for 'files')\n  d6edcbf924697ab811a867421dab60d954ccad99\n0000000000000000000000000000000000000000 refs/heads/basic_branching\n\ndeleting a ref that only exists in packed-refs:\n  1. prepared (for 'packed-refs')\n  0000000000000000000000000000000000000000\n0000000000000000000000000000000000000000 refs/heads/basic_branching\n  2. prepared (for 'files')\n  d6edcbf924697ab811a867421dab60d954ccad99\n0000000000000000000000000000000000000000 refs/heads/basic_branching\n  3. committed (for 'files')\n  d6edcbf924697ab811a867421dab60d954ccad99\n0000000000000000000000000000000000000000 refs/heads/basic_branching\n\ndeleting a ref that only exists both in packed-refs and as a loose ref:\n  1. prepared (for 'packed-refs')\n  0000000000000000000000000000000000000000\n0000000000000000000000000000000000000000 refs/heads/basic_branching\n  2. prepared (for 'files')\n  d6edcbf924697ab811a867421dab60d954ccad99\n0000000000000000000000000000000000000000 refs/heads/basic_branching\n  3. committed (for 'files')\n  d6edcbf924697ab811a867421dab60d954ccad99\n0000000000000000000000000000000000000000 refs/heads/basic_branching\n\na mixed update, containing some adds, deletes and updates where some\nrefs existed in packed-refs:\n  1. prepared (for 'packed-refs')\n  0000000000000000000000000000000000000000\n0000000000000000000000000000000000000000 refs/heads/branch_mod_merge\n  0000000000000000000000000000000000000000\n0000000000000000000000000000000000000000 refs/heads/basic_branching\n  2. prepared (for 'files')\n  6053a1eaa1c009dd11092d09a72f3c41af1b59ad\n0000000000000000000000000000000000000000 refs/heads/branch_mod_merge\n  d6edcbf924697ab811a867421dab60d954ccad99\n0000000000000000000000000000000000000000 refs/heads/basic_branching\n  2e10dd2d1d5eea9291b296e78312e8a703964a95\n9fbc34a0d905950131d73f352abe68520c6db2a3\nrefs/heads/out_of_order_branch\n  0000000000000000000000000000000000000000\n9b4e781dea0769888fe270e06ad76675f73851b1 refs/tags/retagged_2\n  2. committed (for 'files')\n  6053a1eaa1c009dd11092d09a72f3c41af1b59ad\n0000000000000000000000000000000000000000 refs/heads/branch_mod_merge\n  d6edcbf924697ab811a867421dab60d954ccad99\n0000000000000000000000000000000000000000 refs/heads/basic_branching\n  2e10dd2d1d5eea9291b296e78312e8a703964a95\n9fbc34a0d905950131d73f352abe68520c6db2a3\nrefs/heads/out_of_order_branch\n  0000000000000000000000000000000000000000\n9b4e781dea0769888fe270e06ad76675f73851b1 refs/tags/retagged_2\n\nIn all of these cases, the \"prepared (for 'packed-refs')\" callback\nseems unnecessary\noverhead?\n\n> The behavior of the following git commands and five testcases have been\n> fixed in t1416:\n>\n>  * git cherry-pick\n>  * git rebase\n>  * git revert\n>  * git update-ref -d <ref>\n>  * git update-ref --stdin       # delete refs\n\nThanks for adding (and fixing) these test cases! We have a similar set of test\ncases in Bitbucket Server to validate that we're interpreting the\nreference-transaction\ncallbacks correctly in various scenarios. These tests still pass\nagainst git with your patch\nseries applied, so from our end they look good.\n\nCheers,\nMichael Heemskerk\n"},{"id":"460418","messageId":"CAJDSCnMHHdYGeyXKj=ztUKBv2vRTn5BEXUR_7fAfATJxn_uwww@mail.gmail.com","threadId":"58240","inReplyTo":"20220729101245.6469-10-worldhello.net@gmail.com","subject":"Re: [PATCH 9/9] refs: reimplement refs_delete_refs() and run hook once","fromName":"Michael Heemskerk","fromEmail":"mheemskerk@atlassian.com","sentAt":"2022-08-02T12:42:01Z","receivedAt":"2022-08-02T12:42:16Z","isPatch":true,"sender":{"key":"mheemskerk@atlassian.com","avatar":"https://gravatar.com/avatar/c3ebe086c3c55529246291e9879e5ef7af3c0ffcadbd81b5508b5fcdf66aa065?d=mp&s=160"},"body":"Let me re-share some questions/suggestions/objections I got on a patch I\nshared with similar changes:\nhttps://lore.kernel.org/git/pull.1228.git.1651676435634.gitgitgadget@gmail.com/\n\nThere's a lot to like about the change; it fixes the incorrect invocation of\nthe reference-transaction hooks when (bulk) deleting refs, but there is a\ndown-side that Patrick pointed out. We never got to a satisfactory solution,\nso let me reshare his feedback to pick up the discussion.\n\nPatrick:\n> I really like these changes given that they simplify things, but I\n> wonder whether we can do them. In the preimage we're eagerly removing\n> loose refs: any error encountered when deleting a reference is recorded,\n> but we keep on trying to remove the other refs, as well. With the new\n> behaviour we now create a single transaction for all refs and try to\n> commit it. This also means that we'll abort the transaction when locking\n> any of the refs fails, which is a change in behaviour.\n>\n> The current behaviour is explicitly documented in `refs.h:refs_delete_refs()`:\n>\n>     /*\n>      * Delete the specified references. If there are any problems, emit\n>      * errors but attempt to keep going (i.e., the deletes are not done in\n>      * an all-or-nothing transaction). msg and flags are passed through to\n>      * ref_transaction_delete().\n>      */\n>    int refs_delete_refs(struct ref_store *refs, const char *msg,\n>                          struct string_list *refnames, unsigned int flags);\n>\n> There are multiple callsites of this function via `delete_refs()`. Now\n> honestly, most of these callsites look somewhat broken:\n>\n>     - `bisect.c` simply does its best to clean up bisect state. This\n>       usecase looks fine to me.\n>\n>    - `builtin/branch.c` reports the branches as deleted even if\n>       `delete_refs()` failed.\n>\n>     - `builtin/remote.c` also misreports the deleted branches for the\n>       `prune` verb. The `rm` verb looks alright: if deletion of any\n>       branch failed then it doesn't prune the remote's config in the end\n>       and reports an error.\n>\n>     - `builtin/fetch.c` also misreports deleted branches with `--prune`.\n>\n> So most of these commands incorrectly handle the case where only a\n> subset of branches has been deleted. This raises the question whether\n> the interface provided by `refs_delete_refs()` is actually sensible if\n> it's so easy to get wrong. It doesn't even report which branches could\n> be removed and which couldn't. Furthermore, the question is whether new\n> backends like the reftable backend which write all refs into a single\n> slice would actually even be in a position to efficiently retain\n> semantics of this function.\n>\n> I'm torn. There are valid usecases for eagerly deleting refs even if a\n> subset of deletions failed, making this change a tough sell, but most of\n> the callsites don't actually handle this correctly in the first place.\n\nAt the time, the only solution I could see was to switch to\ntransaction-per-ref semantics, but this results in bad performance when\ndeleting tens of thousands of refs.\n\nOne option might be to optimistically try to delete the refs in a single\ntransaction. If that fails for whatever reason and multiple ref deletions are\nrequested, we could fall back to a transaction-per-ref approach. That'd keep\nthe common case fast, and still provide best effort deletes.\n\nThoughts?\n\nCheers,\nMichael Heemskerk\n\nOn Fri, Jul 29, 2022 at 12:13 PM Jiang Xin <worldhello.net@gmail.com> wrote:\n>\n> From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n>\n> When delete references using \"git branch -d\" or \"git tag -d\", there will\n> be duplicate call of \"reference-transaction committed\" for same refs.\n> This is because \"refs_delete_refs()\" is called twice, once for\n> files-backend and once for packed-backend, and we used to reinvented the\n> wheel in \"files_delete_refs()\" and \"packed_delete_refs()\". By removing\n> \"packed_delete_refs()\" and reimplement \"files_delete_refs()\", the\n> \"reference-transaction\" hook will run only once for deleted branches and\n> tags.\n>\n> The behavior of the following git commands and the last two testcases\n> have been fixed in t1416:\n>\n>  * git branch -d <branch>\n>  * git tag -d <tag>\n>\n> A testcase in t5510 is broken because we used to call the function\n> \"packed_refs_lock()\", but it is not necessary if the deleted reference\n> is not in the \"packed-refs\" file.\n>\n> Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n> ---\n>  refs/files-backend.c             | 21 ++++++-------\n>  refs/packed-backend.c            | 51 +-------------------------------\n>  t/t1416-ref-transaction-hooks.sh |  4 +--\n>  t/t5510-fetch.sh                 | 17 +++++++++++\n>  4 files changed, 29 insertions(+), 64 deletions(-)\n>\n> diff --git a/refs/files-backend.c b/refs/files-backend.c\n> index 8baea66e58..21426efaae 100644\n> --- a/refs/files-backend.c\n> +++ b/refs/files-backend.c\n> @@ -1268,31 +1268,27 @@ static int files_pack_refs(struct ref_store *ref_store, unsigned int flags)\n>  static int files_delete_refs(struct ref_store *ref_store, const char *msg,\n>                              struct string_list *refnames, unsigned int flags)\n>  {\n> -       struct files_ref_store *refs =\n> -               files_downcast(ref_store, REF_STORE_WRITE, \"delete_refs\");\n> +       struct ref_transaction *transaction;\n>         struct strbuf err = STRBUF_INIT;\n>         int i, result = 0;\n>\n>         if (!refnames->nr)\n>                 return 0;\n>\n> -       if (packed_refs_lock(refs->packed_ref_store, 0, &err))\n> -               goto error;\n> -\n> -       if (refs_delete_refs(refs->packed_ref_store, msg, refnames, flags)) {\n> -               packed_refs_unlock(refs->packed_ref_store);\n> +       transaction = ref_store_transaction_begin(ref_store, &err);\n> +       if (!transaction)\n>                 goto error;\n> -       }\n> -\n> -       packed_refs_unlock(refs->packed_ref_store);\n>\n>         for (i = 0; i < refnames->nr; i++) {\n>                 const char *refname = refnames->items[i].string;\n> -\n> -               if (refs_delete_ref(&refs->base, msg, refname, NULL, flags))\n> +               if (ref_transaction_delete(transaction, refname, NULL,\n> +                                          flags, msg, &err))\n>                         result |= error(_(\"could not remove reference %s\"), refname);\n>         }\n> +       if (ref_transaction_commit(transaction, &err))\n> +               goto error;\n>\n> +       ref_transaction_free(transaction);\n>         strbuf_release(&err);\n>         return result;\n>\n> @@ -1309,6 +1305,7 @@ static int files_delete_refs(struct ref_store *ref_store, const char *msg,\n>         else\n>                 error(_(\"could not delete references: %s\"), err.buf);\n>\n> +       ref_transaction_free(transaction);\n>         strbuf_release(&err);\n>         return -1;\n>  }\n> diff --git a/refs/packed-backend.c b/refs/packed-backend.c\n> index 97b6837767..fdb7a0a52c 100644\n> --- a/refs/packed-backend.c\n> +++ b/refs/packed-backend.c\n> @@ -1519,55 +1519,6 @@ static int packed_initial_transaction_commit(struct ref_store *ref_store,\n>         return ref_transaction_commit(transaction, err);\n>  }\n>\n> -static int packed_delete_refs(struct ref_store *ref_store, const char *msg,\n> -                            struct string_list *refnames, unsigned int flags)\n> -{\n> -       struct packed_ref_store *refs =\n> -               packed_downcast(ref_store, REF_STORE_WRITE, \"delete_refs\");\n> -       struct strbuf err = STRBUF_INIT;\n> -       struct ref_transaction *transaction;\n> -       struct string_list_item *item;\n> -       int ret;\n> -\n> -       (void)refs; /* We need the check above, but don't use the variable */\n> -\n> -       if (!refnames->nr)\n> -               return 0;\n> -\n> -       /*\n> -        * Since we don't check the references' old_oids, the\n> -        * individual updates can't fail, so we can pack all of the\n> -        * updates into a single transaction.\n> -        */\n> -\n> -       transaction = ref_store_transaction_begin(ref_store, &err);\n> -       if (!transaction)\n> -               return -1;\n> -\n> -       for_each_string_list_item(item, refnames) {\n> -               if (ref_transaction_delete(transaction, item->string, NULL,\n> -                                          flags, msg, &err)) {\n> -                       warning(_(\"could not delete reference %s: %s\"),\n> -                               item->string, err.buf);\n> -                       strbuf_reset(&err);\n> -               }\n> -       }\n> -\n> -       ret = ref_transaction_commit(transaction, &err);\n> -\n> -       if (ret) {\n> -               if (refnames->nr == 1)\n> -                       error(_(\"could not delete reference %s: %s\"),\n> -                             refnames->items[0].string, err.buf);\n> -               else\n> -                       error(_(\"could not delete references: %s\"), err.buf);\n> -       }\n> -\n> -       ref_transaction_free(transaction);\n> -       strbuf_release(&err);\n> -       return ret;\n> -}\n> -\n>  static int packed_pack_refs(struct ref_store *ref_store, unsigned int flags)\n>  {\n>         /*\n> @@ -1595,7 +1546,7 @@ struct ref_storage_be refs_be_packed = {\n>\n>         .pack_refs = packed_pack_refs,\n>         .create_symref = NULL,\n> -       .delete_refs = packed_delete_refs,\n> +       .delete_refs = NULL,\n>         .rename_ref = NULL,\n>         .copy_ref = NULL,\n>\n> diff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\n> index df75e5727c..f64166f9d7 100755\n> --- a/t/t1416-ref-transaction-hooks.sh\n> +++ b/t/t1416-ref-transaction-hooks.sh\n> @@ -744,7 +744,7 @@ test_expect_success \"branch: rename branches\" '\n>         test_cmp_heads_and_tags -C workdir expect\n>  '\n>\n> -test_expect_failure \"branch: remove branches\" '\n> +test_expect_success \"branch: remove branches\" '\n>         test_when_finished \"rm -f $HOOK_OUTPUT\" &&\n>\n>         cat >expect <<-EOF &&\n> @@ -873,7 +873,7 @@ test_expect_success \"tag: update refs to create loose refs\" '\n>         test_cmp_heads_and_tags -C workdir expect\n>  '\n>\n> -test_expect_failure \"tag: remove tags with mixed ref_stores\" '\n> +test_expect_success \"tag: remove tags with mixed ref_stores\" '\n>         test_when_finished \"rm -f $HOOK_OUTPUT\" &&\n>\n>         cat >expect <<-EOF &&\n> diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\n> index b45879a760..22de7ac9ec 100755\n> --- a/t/t5510-fetch.sh\n> +++ b/t/t5510-fetch.sh\n> @@ -168,6 +168,8 @@ test_expect_success REFFILES 'fetch --prune fails to delete branches' '\n>         cd \"$D\" &&\n>         git clone . prune-fail &&\n>         cd prune-fail &&\n> +       git update-ref refs/remotes/origin/extrabranch main~ &&\n> +       git pack-refs --all &&\n>         git update-ref refs/remotes/origin/extrabranch main &&\n>         : this will prevent --prune from locking packed-refs for deleting refs, but adding loose refs still succeeds  &&\n>         >.git/packed-refs.new &&\n> @@ -175,6 +177,21 @@ test_expect_success REFFILES 'fetch --prune fails to delete branches' '\n>         test_must_fail git fetch --prune origin\n>  '\n>\n> +test_expect_success REFFILES 'fetch --prune ok for loose refs not in locked packed-refs' '\n> +       test_when_finished \"cd \\\"$D\\\"; rm -rf \\\"prune-ok-ref-not-packed\\\"\" &&\n> +       cd \"$D\" &&\n> +       git clone . prune-ok-ref-not-packed &&\n> +       (\n> +               cd prune-ok-ref-not-packed &&\n> +               git update-ref refs/remotes/origin/extrabranch main &&\n> +               : for loose refs not in packed-refs, we can delete them even the packed-refs is locked &&\n> +               :>.git/packed-refs.new &&\n> +\n> +               git fetch --prune origin &&\n> +               test_must_fail git rev-parse refs/remotes/origin/extrabranch --\n> +       )\n> +'\n> +\n>  test_expect_success 'fetch --atomic works with a single branch' '\n>         test_when_finished \"rm -rf \\\"$D\\\"/atomic\" &&\n>\n> --\n> 2.36.1.25.gc87d5ad63a.dirty\n>\n"},{"id":"460684","messageId":"CANYiYbFw71bX827akAG87RSKOozPk313Hoe573O9dQ65_U6sLQ@mail.gmail.com","threadId":"58240","inReplyTo":"CAJDSCnOFSnhOU06UHF6T9RT31Gq56BhJn5fQ4_ys2ERrN1stSw@mail.gmail.com","subject":"Re: [PATCH 5/9] refs: avoid duplicate running of the reference-transaction hook","fromName":"Jiang Xin","fromEmail":"worldhello.net@gmail.com","sentAt":"2022-08-05T01:41:42Z","receivedAt":"2022-08-05T01:41:56Z","isPatch":true,"sender":{"key":"worldhello.net@gmail.com","avatar":"https://avatars.githubusercontent.com/u/183860?v=4"},"body":"On Tue, Aug 2, 2022 at 8:18 PM Michael Heemskerk\n<mheemskerk@atlassian.com> wrote:\n>\n> On Fri, Jul 29, 2022 at 12:20 PM Jiang Xin <worldhello.net@gmail.com> wrote:\n> >\n> > From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n> >\n> > If there are references to be deleted in a transaction, we should remove\n> > each reference from both loose references and the \"packed-refs\" file.\n> > The \"reference-transaction\" hook will run twice, once for the primary\n> > ref-store (loose references), and another for the second ref-store (i.e.\n> > packed ref-store).\n> >\n> > To avoid duplicate running of the \"reference-trancaction\" hook, we pass\n> > a special \"hook-flags\" parameter to initialize the second ref-store.\n> > The \"REF_TRANSACTION_RUN_PREPARED_HOOK\" bit is preserved for the\n> > transaction of the second ref-store because we may still want to call\n> > command \"reference-trancaction prepared\" for some pre-checks, such as\n> > terminate unwanted transaction for the \"packed-refs\" file.\n>\n> Can you elaborate on the rationale for continuing to invoke the \"prepared\"\n> reference-transaction hook for the \"packed-refs\" file? Did you have a specific\n> type of check in mind?\n\nUse \"reference-transaction prepared\" hook command, we can implement\nwrite lock for repositories. We can create a lock file (e.g. \"site.lock\") inside\na repository or in a parent directory to disable write operations for one\nrepository, a group of repositories or all repositories. The magic is in the\n\"reference-transaction prepared\" hook command. As a transaction may\nbe triggered twice, one for packed-refs, and one for loose-refs, we should\nmake sure the \"reference-transaction prepared\" command must run for\nboth transactions on different ref-stores.\n\nBTW, I'm on vacation for the next few days and I will pick up this\ntopic next week.\n\n--\nJiang Xin\n"},{"id":"460887","messageId":"YvI/ipxGZ3rPZ/OQ@ncase","threadId":"58240","inReplyTo":"CAJDSCnMHHdYGeyXKj=ztUKBv2vRTn5BEXUR_7fAfATJxn_uwww@mail.gmail.com","subject":"Re: [PATCH 9/9] refs: reimplement refs_delete_refs() and run hook once","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2022-08-09T11:05:46Z","receivedAt":"2022-08-09T11:06:02Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Aug 02, 2022 at 02:42:01PM +0200, Michael Heemskerk wrote:\n> Let me re-share some questions/suggestions/objections I got on a patch I\n> shared with similar changes:\n> https://lore.kernel.org/git/pull.1228.git.1651676435634.gitgitgadget@gmail.com/\n> \n> There's a lot to like about the change; it fixes the incorrect invocation of\n> the reference-transaction hooks when (bulk) deleting refs, but there is a\n> down-side that Patrick pointed out. We never got to a satisfactory solution,\n> so let me reshare his feedback to pick up the discussion.\n> \n> Patrick:\n> > I really like these changes given that they simplify things, but I\n> > wonder whether we can do them. In the preimage we're eagerly removing\n> > loose refs: any error encountered when deleting a reference is recorded,\n> > but we keep on trying to remove the other refs, as well. With the new\n> > behaviour we now create a single transaction for all refs and try to\n> > commit it. This also means that we'll abort the transaction when locking\n> > any of the refs fails, which is a change in behaviour.\n> >\n> > The current behaviour is explicitly documented in `refs.h:refs_delete_refs()`:\n> >\n> >     /*\n> >      * Delete the specified references. If there are any problems, emit\n> >      * errors but attempt to keep going (i.e., the deletes are not done in\n> >      * an all-or-nothing transaction). msg and flags are passed through to\n> >      * ref_transaction_delete().\n> >      */\n> >    int refs_delete_refs(struct ref_store *refs, const char *msg,\n> >                          struct string_list *refnames, unsigned int flags);\n> >\n> > There are multiple callsites of this function via `delete_refs()`. Now\n> > honestly, most of these callsites look somewhat broken:\n> >\n> >     - `bisect.c` simply does its best to clean up bisect state. This\n> >       usecase looks fine to me.\n> >\n> >    - `builtin/branch.c` reports the branches as deleted even if\n> >       `delete_refs()` failed.\n> >\n> >     - `builtin/remote.c` also misreports the deleted branches for the\n> >       `prune` verb. The `rm` verb looks alright: if deletion of any\n> >       branch failed then it doesn't prune the remote's config in the end\n> >       and reports an error.\n> >\n> >     - `builtin/fetch.c` also misreports deleted branches with `--prune`.\n> >\n> > So most of these commands incorrectly handle the case where only a\n> > subset of branches has been deleted. This raises the question whether\n> > the interface provided by `refs_delete_refs()` is actually sensible if\n> > it's so easy to get wrong. It doesn't even report which branches could\n> > be removed and which couldn't. Furthermore, the question is whether new\n> > backends like the reftable backend which write all refs into a single\n> > slice would actually even be in a position to efficiently retain\n> > semantics of this function.\n> >\n> > I'm torn. There are valid usecases for eagerly deleting refs even if a\n> > subset of deletions failed, making this change a tough sell, but most of\n> > the callsites don't actually handle this correctly in the first place.\n\nThanks a lot for revoicing my concerns here. I also agree that overall\nthe changes are very much what I'd love to have as they simplify the\nimplementation and fix the issues at the same time.\n\n> At the time, the only solution I could see was to switch to\n> transaction-per-ref semantics, but this results in bad performance when\n> deleting tens of thousands of refs.\n> \n> One option might be to optimistically try to delete the refs in a single\n> transaction. If that fails for whatever reason and multiple ref deletions are\n> requested, we could fall back to a transaction-per-ref approach. That'd keep\n> the common case fast, and still provide best effort deletes.\n> \n> Thoughts?\n\nThe biggest downside I can think of with this approach is that it's now\nundeterministic whether we run the hooks once for all references, or\nonce for all references plus once for every single reference we're about\nto delete when there was e.g. a racy deletion. That makes it hard to use\nthe hook e.g. in setups where we vote on reference updates as it can be\nthat due to racy behaviour we now see different behaviour on different\nnodes.\n\nI'm still torn. Ideally, I'd just bite the bullet and say that\n`refs_delete_refs()` is atomic insofar that it will only ever delete all\nreferences or none, and not a best-effort implementation. But that is a\nchange I'm still scared to make given that it sounds like an easy way to\nrun into regressions.\n\nUnfortunately I still don't have an easy answer for how to properly fix\nthis, sorry :/\n\nPatrick\n\n> Cheers,\n> Michael Heemskerk\n> \n> On Fri, Jul 29, 2022 at 12:13 PM Jiang Xin <worldhello.net@gmail.com> wrote:\n> >\n> > From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n> >\n> > When delete references using \"git branch -d\" or \"git tag -d\", there will\n> > be duplicate call of \"reference-transaction committed\" for same refs.\n> > This is because \"refs_delete_refs()\" is called twice, once for\n> > files-backend and once for packed-backend, and we used to reinvented the\n> > wheel in \"files_delete_refs()\" and \"packed_delete_refs()\". By removing\n> > \"packed_delete_refs()\" and reimplement \"files_delete_refs()\", the\n> > \"reference-transaction\" hook will run only once for deleted branches and\n> > tags.\n> >\n> > The behavior of the following git commands and the last two testcases\n> > have been fixed in t1416:\n> >\n> >  * git branch -d <branch>\n> >  * git tag -d <tag>\n> >\n> > A testcase in t5510 is broken because we used to call the function\n> > \"packed_refs_lock()\", but it is not necessary if the deleted reference\n> > is not in the \"packed-refs\" file.\n> >\n> > Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n> > ---\n> >  refs/files-backend.c             | 21 ++++++-------\n> >  refs/packed-backend.c            | 51 +-------------------------------\n> >  t/t1416-ref-transaction-hooks.sh |  4 +--\n> >  t/t5510-fetch.sh                 | 17 +++++++++++\n> >  4 files changed, 29 insertions(+), 64 deletions(-)\n> >\n> > diff --git a/refs/files-backend.c b/refs/files-backend.c\n> > index 8baea66e58..21426efaae 100644\n> > --- a/refs/files-backend.c\n> > +++ b/refs/files-backend.c\n> > @@ -1268,31 +1268,27 @@ static int files_pack_refs(struct ref_store *ref_store, unsigned int flags)\n> >  static int files_delete_refs(struct ref_store *ref_store, const char *msg,\n> >                              struct string_list *refnames, unsigned int flags)\n> >  {\n> > -       struct files_ref_store *refs =\n> > -               files_downcast(ref_store, REF_STORE_WRITE, \"delete_refs\");\n> > +       struct ref_transaction *transaction;\n> >         struct strbuf err = STRBUF_INIT;\n> >         int i, result = 0;\n> >\n> >         if (!refnames->nr)\n> >                 return 0;\n> >\n> > -       if (packed_refs_lock(refs->packed_ref_store, 0, &err))\n> > -               goto error;\n> > -\n> > -       if (refs_delete_refs(refs->packed_ref_store, msg, refnames, flags)) {\n> > -               packed_refs_unlock(refs->packed_ref_store);\n> > +       transaction = ref_store_transaction_begin(ref_store, &err);\n> > +       if (!transaction)\n> >                 goto error;\n> > -       }\n> > -\n> > -       packed_refs_unlock(refs->packed_ref_store);\n> >\n> >         for (i = 0; i < refnames->nr; i++) {\n> >                 const char *refname = refnames->items[i].string;\n> > -\n> > -               if (refs_delete_ref(&refs->base, msg, refname, NULL, flags))\n> > +               if (ref_transaction_delete(transaction, refname, NULL,\n> > +                                          flags, msg, &err))\n> >                         result |= error(_(\"could not remove reference %s\"), refname);\n> >         }\n> > +       if (ref_transaction_commit(transaction, &err))\n> > +               goto error;\n> >\n> > +       ref_transaction_free(transaction);\n> >         strbuf_release(&err);\n> >         return result;\n> >\n> > @@ -1309,6 +1305,7 @@ static int files_delete_refs(struct ref_store *ref_store, const char *msg,\n> >         else\n> >                 error(_(\"could not delete references: %s\"), err.buf);\n> >\n> > +       ref_transaction_free(transaction);\n> >         strbuf_release(&err);\n> >         return -1;\n> >  }\n> > diff --git a/refs/packed-backend.c b/refs/packed-backend.c\n> > index 97b6837767..fdb7a0a52c 100644\n> > --- a/refs/packed-backend.c\n> > +++ b/refs/packed-backend.c\n> > @@ -1519,55 +1519,6 @@ static int packed_initial_transaction_commit(struct ref_store *ref_store,\n> >         return ref_transaction_commit(transaction, err);\n> >  }\n> >\n> > -static int packed_delete_refs(struct ref_store *ref_store, const char *msg,\n> > -                            struct string_list *refnames, unsigned int flags)\n> > -{\n> > -       struct packed_ref_store *refs =\n> > -               packed_downcast(ref_store, REF_STORE_WRITE, \"delete_refs\");\n> > -       struct strbuf err = STRBUF_INIT;\n> > -       struct ref_transaction *transaction;\n> > -       struct string_list_item *item;\n> > -       int ret;\n> > -\n> > -       (void)refs; /* We need the check above, but don't use the variable */\n> > -\n> > -       if (!refnames->nr)\n> > -               return 0;\n> > -\n> > -       /*\n> > -        * Since we don't check the references' old_oids, the\n> > -        * individual updates can't fail, so we can pack all of the\n> > -        * updates into a single transaction.\n> > -        */\n> > -\n> > -       transaction = ref_store_transaction_begin(ref_store, &err);\n> > -       if (!transaction)\n> > -               return -1;\n> > -\n> > -       for_each_string_list_item(item, refnames) {\n> > -               if (ref_transaction_delete(transaction, item->string, NULL,\n> > -                                          flags, msg, &err)) {\n> > -                       warning(_(\"could not delete reference %s: %s\"),\n> > -                               item->string, err.buf);\n> > -                       strbuf_reset(&err);\n> > -               }\n> > -       }\n> > -\n> > -       ret = ref_transaction_commit(transaction, &err);\n> > -\n> > -       if (ret) {\n> > -               if (refnames->nr == 1)\n> > -                       error(_(\"could not delete reference %s: %s\"),\n> > -                             refnames->items[0].string, err.buf);\n> > -               else\n> > -                       error(_(\"could not delete references: %s\"), err.buf);\n> > -       }\n> > -\n> > -       ref_transaction_free(transaction);\n> > -       strbuf_release(&err);\n> > -       return ret;\n> > -}\n> > -\n> >  static int packed_pack_refs(struct ref_store *ref_store, unsigned int flags)\n> >  {\n> >         /*\n> > @@ -1595,7 +1546,7 @@ struct ref_storage_be refs_be_packed = {\n> >\n> >         .pack_refs = packed_pack_refs,\n> >         .create_symref = NULL,\n> > -       .delete_refs = packed_delete_refs,\n> > +       .delete_refs = NULL,\n> >         .rename_ref = NULL,\n> >         .copy_ref = NULL,\n> >\n> > diff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\n> > index df75e5727c..f64166f9d7 100755\n> > --- a/t/t1416-ref-transaction-hooks.sh\n> > +++ b/t/t1416-ref-transaction-hooks.sh\n> > @@ -744,7 +744,7 @@ test_expect_success \"branch: rename branches\" '\n> >         test_cmp_heads_and_tags -C workdir expect\n> >  '\n> >\n> > -test_expect_failure \"branch: remove branches\" '\n> > +test_expect_success \"branch: remove branches\" '\n> >         test_when_finished \"rm -f $HOOK_OUTPUT\" &&\n> >\n> >         cat >expect <<-EOF &&\n> > @@ -873,7 +873,7 @@ test_expect_success \"tag: update refs to create loose refs\" '\n> >         test_cmp_heads_and_tags -C workdir expect\n> >  '\n> >\n> > -test_expect_failure \"tag: remove tags with mixed ref_stores\" '\n> > +test_expect_success \"tag: remove tags with mixed ref_stores\" '\n> >         test_when_finished \"rm -f $HOOK_OUTPUT\" &&\n> >\n> >         cat >expect <<-EOF &&\n> > diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\n> > index b45879a760..22de7ac9ec 100755\n> > --- a/t/t5510-fetch.sh\n> > +++ b/t/t5510-fetch.sh\n> > @@ -168,6 +168,8 @@ test_expect_success REFFILES 'fetch --prune fails to delete branches' '\n> >         cd \"$D\" &&\n> >         git clone . prune-fail &&\n> >         cd prune-fail &&\n> > +       git update-ref refs/remotes/origin/extrabranch main~ &&\n> > +       git pack-refs --all &&\n> >         git update-ref refs/remotes/origin/extrabranch main &&\n> >         : this will prevent --prune from locking packed-refs for deleting refs, but adding loose refs still succeeds  &&\n> >         >.git/packed-refs.new &&\n> > @@ -175,6 +177,21 @@ test_expect_success REFFILES 'fetch --prune fails to delete branches' '\n> >         test_must_fail git fetch --prune origin\n> >  '\n> >\n> > +test_expect_success REFFILES 'fetch --prune ok for loose refs not in locked packed-refs' '\n> > +       test_when_finished \"cd \\\"$D\\\"; rm -rf \\\"prune-ok-ref-not-packed\\\"\" &&\n> > +       cd \"$D\" &&\n> > +       git clone . prune-ok-ref-not-packed &&\n> > +       (\n> > +               cd prune-ok-ref-not-packed &&\n> > +               git update-ref refs/remotes/origin/extrabranch main &&\n> > +               : for loose refs not in packed-refs, we can delete them even the packed-refs is locked &&\n> > +               :>.git/packed-refs.new &&\n> > +\n> > +               git fetch --prune origin &&\n> > +               test_must_fail git rev-parse refs/remotes/origin/extrabranch --\n> > +       )\n> > +'\n> > +\n> >  test_expect_success 'fetch --atomic works with a single branch' '\n> >         test_when_finished \"rm -rf \\\"$D\\\"/atomic\" &&\n> >\n> > --\n> > 2.36.1.25.gc87d5ad63a.dirty\n> >\n"},{"id":"461490","messageId":"20220819032147.28841-1-worldhello.net@gmail.com","threadId":"58240","inReplyTo":"CANYiYbFw71bX827akAG87RSKOozPk313Hoe573O9dQ65_U6sLQ@mail.gmail.com","subject":"[PATCH v2 0/9] Fix issues of refx-txn hook for various git commands","fromName":"Jiang Xin","fromEmail":"worldhello.net@gmail.com","sentAt":"2022-08-19T03:21:38Z","receivedAt":"2022-08-19T03:22:02Z","isPatch":true,"sender":{"key":"worldhello.net@gmail.com","avatar":"https://avatars.githubusercontent.com/u/183860?v=4"},"body":"From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n\nBefore Patrick introduced the reference-transaction hook in git 2.28,\nWe had implemented some internal transaction hooks \"pre-txn\" and\n\"post-txn\" for our internal git infrastructure. The \"pre-txn\" hook is\nused to check our site-wide lockfile to disable write services for\none or multiple repositories. The \"post-txn\" hook is used to update\nthe timestamp and checksum of the repository. Recently I wanted to\nmigrate our internal implementation to use ref-txn hooks, but I ran\ninto the same issues reported by Michael and Bryan:\n\n * https://lore.kernel.org/git/CAGyf7-GYYA2DOnAVYW--=QEc2WjSHzUhp2OQyuyOr3EOtFDm6g@mail.gmail.com/\n * https://lore.kernel.org/git/pull.1228.git.1651676435634.gitgitgadget@gmail.com/\n\nThis patch series try to fix the issues I encountered.\n\nPatch 1/9 adds more testcases in t1416:\n\n * Some commands failed because the expected \"<old-oid>\" was\n   \"<zero-oid>\". Such as: \"git update-ref -d <ref>\".\n\n * Some commands failed because the \"reference-transaction committed\"\n   command was repeated multiple times for the same changes. Such as:\n   \"git update-ref -d <ref>\" and \"git tag -d <tag>\".\n\n * Some commands should not trigger the \"reference-transaction\" hook\n   because no real changes have occurred to the repository. Such as:\n   \"git pack-refs --all\" and \"git gc\".\n\n * Some commands did not execute the \"reference-transaction\" hook at\n   all. E.g.: \"git branch -c <src> <dest>\", \"git branch -m <old> <new>\".\n\nPatch 2/9 propagates real old-oid found in lockfile to the update\nentries for transaction, so we can get proper old-oid in \"prepared\" and\n\"committed\" stage for refs-txn hook.\n\nPatch 3/9 adds a new flag in transaction, and we can turn off running\nthe entire hook for git-pack-refs in patch 4/9. We can also turn off the\n\"committed\" and \"aborted\" stages of the ref-txn hook for\n\"packed-ref-store\", but we can still run \"prepared\" stage of the hook.\nSee patch 5/9.\n\nPatch 6/9 and 7/9 create an extended function\n\"ref_transaction_update_extended()\"\nto be used in patch 8/9 to reimplement branch copy and rename.\n\nPatch 9/9 reimplents \"files_delete_refs()\" to fix failed testcases for\n\"git branch -d\" and \"git tag -d\".\n\n## Commit log changed since v1:\n\n 1:  bd0ceab4a2 !  1:  2860af63ee t1416: more testcases for reference-transaction hook\n    @@ Commit message\n          * git tag <new-tag> <oid>                      # create new tag\n          * git update-ref --stdin                       # create new refs\n          * git update-ref <ref> <oid>                   # create new ref\n    +     * git update-ref <ref> <new-oid> <old-oid>     # update ref\n     \n    -    But 16 testcases failed.\n    +    But 17 testcases failed.\n     \n         Some commands failed because the expected \"<old-oid>\" became\n         \"<zero-oid>\". E.g.:\n    @@ Commit message\n          * git tag -d <tag>\n          * git update-ref --stdin                       # update/delete refs\n          * git update-ref -d <ref>\n    -     * git update-ref <ref> <new-oid> [<old-oid>]   # update ref\n    +     * git update-ref <ref> <new-oid>               # update ref\n     \n         Some commands failed because the \"reference-transaction committed\"\n         command was repeated multiple times for the same changes. E.g.:\n    @@ Commit message\n         We will fix the failed testcases in later commits.\n     \n         Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n    +    Helped-by: Eric Sunshine <sunshine@sunshineco.com>\n     \n 2:  915ff63007 !  2:  1dbd4251aa refs: update missing old-oid in transaction from lockfile\n    @@ Commit message\n         in any case, get proper old_oid from the lock file and propagate it to\n         the corresponding update entry of a transaction.\n     \n    -    The behavior of the following git commands and four testcases have been\n    +    The behavior of the following git commands and five testcases have been\n         fixed in t1416:\n     \n    -     * git branch [-f] <ref> <new-oid>              # update branch\n    +     * git branch [-f] <ref> <new-oid>      # update branch\n          * git cherry-pick <oid>\n          * git rebase\n          * git tag -d <tag>\n    -     * git update-ref --stdin                       # update refs\n    +     * git update-ref --stdin               # update refs\n          * git update-ref -d <ref>\n    -     * git update-ref <ref> <new-oid> [<old-oid>]   # update ref\n    +     * git update-ref <ref> <new-oid>       # update ref\n     \n         Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n\n 3:  cf7be214c6 !  3:  58658c782d refs: add new field in transaction for running transaction hook\n    @@ Commit message\n         and aborted. To update a reference, git may create two seperate\n         transactions, one for loose reference and one for packed ref-store. This\n         may cause duplicate running of the hook for same references. The new\n    -    field \"hook_flags\" in the transaction can turn off running of a specific\n    +    field \"hook_flags\" in the transaction can turn off running a specific\n         transaction. In some scenarios, we may only want to turn off certain\n         states of a transaction, such as \"committed\" and \"aborted\", but want to\n         turn on the \"prepared\" state of the hook to do some pre-checks, so the\n    @@ Commit message\n         By calling the \"ref_store_transaction_begin()\" function, all the flags\n         of the \"hook_flags\" field for the new initialized transaction will be\n         turned on. The new function \"ref_store_transaction_begin_extended()\"\n    -    will be used in later commits to custom the \"hook_flags\" field for a\n    -    new initialized transaction.\n    +    will be used in later commits with a specific \"hook_flags\" field to\n    +    initialize a new transaction.\n     \n         Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n     \n 4:  34f6a5803a !  4:  ec0c6a1aed refs: do not run transaction hook for git-pack-refs\n    @@ Metadata\n      ## Commit message ##\n         refs: do not run transaction hook for git-pack-refs\n     \n    -    git-pack-refs will call \"files_pack_refs()\" to pack loose references\n    -    into the \"packed-refs\" file, and there are no real changes to the\n    -    repository. Therefore, by initializing a transaction with an empty\n    -    \"hook_flags\" field, the \"reference-transaction\" hook will not run.\n    +    The git-pack-refs command will call \"files_pack_refs()\" to pack loose\n    +    references into the \"packed-refs\" file, and there are no real changes\n    +    to the repository. Therefore, by initializing a transaction with an\n    +    empty \"hook_flags\" field, the \"reference-transaction\" hook will not run.\n     \n         The \"prune_refs()\" and \"prune_ref()\" functions are called from\n         \"file_pack_refs()\", and they are used to prune loose refereces which\n\n 5:  649a5734c3 !  5:  d04425af29 refs: avoid duplicate running of the reference-transaction hook\n    @@ Commit message\n         If there are references to be deleted in a transaction, we should remove\n         each reference from both loose references and the \"packed-refs\" file.\n         The \"reference-transaction\" hook will run twice, once for the primary\n    -    ref-store (loose references), and another for the second ref-store (i.e.\n    +    ref-store (loose ref-store), and another for the second ref-store (i.e.\n         packed ref-store).\n     \n         To avoid duplicate running of the \"reference-trancaction\" hook, we pass\n         a special \"hook-flags\" parameter to initialize the second ref-store.\n         The \"REF_TRANSACTION_RUN_PREPARED_HOOK\" bit is preserved for the\n         transaction of the second ref-store because we may still want to call\n    -    command \"reference-trancaction prepared\" for some pre-checks, such as\n    -    terminate unwanted transaction for the \"packed-refs\" file.\n    +    command \"reference-trancaction prepared\" to do some checks. E.g.: We\n    +    can create a global lock file (such as \"site.lock\") to disable writing\n    +    permissions for one or multiple repositories.\n     \n         The behavior of the following git commands and five testcases have been\n         fixed in t1416:\n\n 6:  9c5ae8e592 !  6:  7443764f6f refs: add reflog_info to hold more fields for reflog entry\n    @@ Commit message\n         a new structure \"reflog_info\" to hold more custom fields for the new\n         reflog entry, and add two additional extended version functions.\n     \n    -    We will use this extension in a later commit to reimplement\n    -    \"files_copy_or_rename_ref()\" using \"refs_update_ref_extended()\" to\n    -    create new reference in a transaction and add proper reflog entry.\n    +    We will use this extension in a later commit to reimplement the function\n    +    \"files_copy_or_rename_ref()\" by calling \"refs_update_ref_extended()\" to\n    +    create a new reference in a transaction, while adding proper reflog\n    +    entry.\n     \n         Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n     \n 7:  602deefb6d !  7:  0347885acc refs: get error message via refs_update_ref_extended()\n    @@ Metadata\n      ## Commit message ##\n         refs: get error message via refs_update_ref_extended()\n     \n    -    The last parameter for \"refs_update_ref_extended()\" is an enum\n    -    \"action_on_err\", and we can not use this function to get the specific\n    -    error message. Extend this function again to get error message.\n    +    The last parameter of the function \"refs_update_ref()\" is an enum\n    +    \"action_on_err\", and we can not use this function to get the error\n    +    message. Extend this function to get error message.\n     \n         We will use the function \"refs_update_ref_extended()\" to reimplement\n         the function \"files_copy_or_rename_ref()\" in later commit.\n\n 8:  16f3c34db1 !  8:  3c41c0ac8d refs: reimplement files_copy_or_rename_ref() to run hook\n    @@ Metadata\n     Author: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n     \n      ## Commit message ##\n    -    refs: reimplement files_copy_or_rename_ref() to run hook\n    +    refs: reimplement files_copy_or_rename_ref() to run refs-txn hook\n     \n         When copying or renaming a branch, the \"reference-transaction\" hook is\n    -    not executed. This is because we used to reinvent the wheel in function\n    -    \"files_copy_or_rename_ref()\". By calling \"refs_update_ref_extended()\" to\n    -    reimplement \"files_copy_or_rename_ref()\", the \"reference-transaction\"\n    -    hook will run correctly.\n    +    not executed. This is because we called two low-level functions\n    +    \"lock_ref_oid_basic()\" and \"write_ref_to_lockfile()\", and reinvented\n    +    the wheel in \"commit_ref_update()\" to update the reference instead of\n    +    implementing \"files_copy_or_rename_ref()\" by calling \"refs_update_ref()\"\n    +    to update a reference in a transaction. The reason for this is that we\n    +    want to create a proper reflog for the newly copied reference.\n    +\n    +    Refactor \"files_copy_or_rename_ref()\" by calling the extended version\n    +    of \"refs_update_ref\", i.e. \"refs_update_ref_extended()\", so we can\n    +    create the target branch for copying or renaming a branch and generate\n    +    a correct reflog file at the same time.\n     \n         The behavior of the following git commands and two testcases have been\n         fixed in t1416:\n\n 9:  c206059145 !  9:  f201436741 refs: reimplement refs_delete_refs() and run hook once\n    @@ Metadata\n      ## Commit message ##\n         refs: reimplement refs_delete_refs() and run hook once\n     \n    -    When delete references using \"git branch -d\" or \"git tag -d\", there will\n    -    be duplicate call of \"reference-transaction committed\" for same refs.\n    -    This is because \"refs_delete_refs()\" is called twice, once for\n    -    files-backend and once for packed-backend, and we used to reinvented the\n    -    wheel in \"files_delete_refs()\" and \"packed_delete_refs()\". By removing\n    -    \"packed_delete_refs()\" and reimplement \"files_delete_refs()\", the\n    -    \"reference-transaction\" hook will run only once for deleted branches and\n    -    tags.\n    +    When using \"git branch -d\" or \"git tag -d\" to delete one or multiple\n    +    references, command \"reference-transaction committed\" will be called\n    +    repeatedly for the same references. This is because the function\n    +    \"refs_delete_refs()\" is called twice, once for loose ref-store and once\n    +    for packed ref-store.\n    +\n    +    The old story starts when running the function \"refs_delete_refs()\" on a\n    +    loose ref-store:\n    +\n    +     1. Try to remove the references from packed ref-store.\n    +\n    +        1.1. Lock the packed-ref-store by calling \"packed_refs_lock()\" in\n    +             \"files_delete_refs()\".\n    +\n    +        1.2. Call \"refs_delete_refs()\" on packed-ref-store, and then call\n    +             \"packed_delete_refs()\".\n    +\n    +        1.3. Create a transaction for packed-ref-store in function\n    +             \"packed_delete_refs()\" by calling the function\n    +             \"ref_store_transaction_begin()\".\n    +\n    +        2.2. Add update entries for all the references to be removed into\n    +             this transaction by calling \"ref_transaction_delete()\".\n    +\n    +        2.3. Call \"ref_transaction_commit()\" to commit the transaction.\n    +\n    +        2.4. Unlock the packed-ref-store.\n    +\n    +     2. Try to remove the references one by one by calling the function\n    +        \"refs_delete_ref()\".\n    +\n    +        2.1. Create a new transaction on loose-ref-store by calling\n    +             \"ref_store_transaction_begin()\".\n    +\n    +        2.2. Call \"ref_transaction_delete()\" to add a update entry\n    +             for the reference to be deleted into the transaction.\n    +\n    +        2.3. In \"ref_transaction_commit()\", it will call functions\n    +             \"files_transaction_prepare()\" and \"files_transaction_finish()\"\n    +             to commit the transaction.\n    +\n    +             2.3.1. Lock the loose reference.\n    +\n    +             2.3.2. Create a new packed-transaction, and add a new update\n    +                    entry to this packed-transaction. The previous step 1\n    +                    makes this operation unnecessary.\n    +\n    +             2.3.3. Lock the packed-ref-store and call fucntion\n    +                    \"is_packed_transaction_needed()\" to check whether it\n    +                    is necessary to commit the transaction, and then\n    +                    abort the transaction because the reference is already\n    +                    removed from the packed-ref-store in step 1.\n    +\n    +             2.3.4. Remove the reflog and the loose reference file for\n    +                    the reference to be deleted.\n    +\n    +             2.3.4. Unlock the loose reference.\n    +\n    +    From the above steps, we can see that \"refs_delete_refs()\" is not an\n    +    atomic operation, but a semi-atomic operation. The operation is atomic\n    +    if all references to be deleted are in the packed ref-store, but not\n    +    if some references are loose references because we delete the loose\n    +    references one by one by calling \"refs_delete_ref()\" .\n    +\n    +    Refactored function \"files_delete_refs()\" to delete references within a\n    +    transaction, so the \"reference-transaction\" hook will only run once for\n    +    deleted branches and tags.\n     \n         The behavior of the following git commands and the last two testcases\n         have been fixed in t1416:\n\n## Source code changed since v1:\n\n 2:  915ff63007 !  2:  1dbd4251aa refs: update missing old-oid in transaction from lockfile\n    \n      ## refs/files-backend.c ##\n     @@ refs/files-backend.c: static int lock_ref_for_update(struct files_ref_store *refs,\n    - \t\t\tgoto out;\n    - \t\t}\n    + \tupdate->backend_data = lock;\n      \n    + \tif (update->type & REF_ISSYMREF) {\n    ++\t\tconst char *ref;\n    ++\n     +\t\t/*\n    -+\t\t * Propagate old_oid from the lock to the update entry, so we can\n    -+\t\t * provide a real old-oid of to the \"reference-transaction\" hook.\n    ++\t\t * Read the referent even though we won't use it as part\n    ++\t\t * of the transaction, because we want to set a proper\n    ++\t\t * old_oid for this symref using the oid we got.\n     +\t\t */\n    -+\t\tif (!(update->flags & REF_HAVE_OLD)) {\n    -+\t\t\toidcpy(&update->old_oid, &lock->old_oid);\n    -+\t\t\tupdate->flags |= REF_HAVE_OLD;\n    -+\t\t}\n    ++\t\tref = refs_resolve_ref_unsafe(&refs->base,\n    ++\t\t\t\t\t      referent.buf, 0,\n    ++\t\t\t\t\t      &lock->old_oid, NULL);\n    ++\n    + \t\tif (update->flags & REF_NO_DEREF) {\n    + \t\t\t/*\n    + \t\t\t * We won't be reading the referent as part of\n    +-\t\t\t * the transaction, so we have to read it here\n    +-\t\t\t * to record and possibly check old_oid:\n    ++\t\t\t * the transaction, so we may need to check\n    ++\t\t\t * old_oid here:\n    + \t\t\t */\n    +-\t\t\tif (!refs_resolve_ref_unsafe(&refs->base,\n    +-\t\t\t\t\t\t     referent.buf, 0,\n    +-\t\t\t\t\t\t     &lock->old_oid, NULL)) {\n    ++\t\t\tif (!ref) {\n    + \t\t\t\tif (update->flags & REF_HAVE_OLD) {\n    + \t\t\t\t\tstrbuf_addf(err, \"cannot lock ref '%s': \"\n    + \t\t\t\t\t\t    \"error reading reference\",\n    +@@ refs/files-backend.c: static int lock_ref_for_update(struct files_ref_store *refs,\n    + \t\t}\n    + \t}\n    + \n    ++\t/*\n    ++\t * Propagate old_oid from the lock to the update entry, so we can\n    ++\t * provide a proper old-oid of to the \"reference-transaction\" hook.\n    ++\t */\n    ++\tif (!(update->flags & REF_HAVE_OLD) && !is_null_oid(&lock->old_oid)) {\n    ++\t\toidcpy(&update->old_oid, &lock->old_oid);\n    ++\t\tupdate->flags |= REF_HAVE_OLD;\n    ++\t}\n     +\n    - \t\t/*\n    - \t\t * If this update is happening indirectly because of a\n    - \t\t * symref update, record the old OID in the parent\n    + \tif ((update->flags & REF_HAVE_NEW) &&\n    + \t    !(update->flags & REF_DELETING) &&\n    + \t    !(update->flags & REF_LOG_ONLY)) {\n     \n 6:  9c5ae8e592 !  6:  7443764f6f refs: add reflog_info to hold more fields for reflog entry\n\n    @@ refs.c: struct ref_update *ref_transaction_add_update(\n      \t\toidcpy(&update->old_oid, old_oid);\n     -\tupdate->msg = normalize_reflog_message(msg);\n     +\tif (reflog_info) {\n    -+\t\tupdate->reflog_info = xmalloc(sizeof(*reflog_info));\n    ++\t\tupdate->reflog_info = xcalloc(1, sizeof(*reflog_info));\n     +\t\tupdate->reflog_info->msg = normalize_reflog_message(reflog_info->msg);\n     +\t\tif (reflog_info->old_oid)\n     +\t\t\tupdate->reflog_info->old_oid = oiddup(reflog_info->old_oid);\n\n## Testcases changed since v1 are not list here\n\n--\n\nJiang Xin (9):\n  t1416: more testcases for reference-transaction hook\n  refs: update missing old-oid in transaction from lockfile\n  refs: add new field in transaction for running transaction hook\n  refs: do not run transaction hook for git-pack-refs\n  refs: avoid duplicate running of the reference-transaction hook\n  refs: add reflog_info to hold more fields for reflog entry\n  refs: get error message via refs_update_ref_extended()\n  refs: reimplement files_copy_or_rename_ref() to run refs-txn hook\n  refs: reimplement refs_delete_refs() and run hook once\n\n refs.c                           |   99 ++-\n refs.h                           |   23 +\n refs/debug.c                     |    2 +-\n refs/files-backend.c             |  177 ++---\n refs/packed-backend.c            |   51 +-\n refs/refs-internal.h             |   25 +-\n t/t1416-ref-transaction-hooks.sh | 1115 +++++++++++++++++++++++++++++-\n t/t5510-fetch.sh                 |   16 +\n 8 files changed, 1318 insertions(+), 190 deletions(-)\n\n-- \n2.36.1.25.gc87d5ad63a.dirty\n\n"},{"id":"461491","messageId":"20220819032147.28841-3-worldhello.net@gmail.com","threadId":"58240","inReplyTo":"CANYiYbFw71bX827akAG87RSKOozPk313Hoe573O9dQ65_U6sLQ@mail.gmail.com","subject":"[PATCH v2 2/9] refs: update missing old-oid in transaction from lockfile","fromName":"Jiang Xin","fromEmail":"worldhello.net@gmail.com","sentAt":"2022-08-19T03:21:40Z","receivedAt":"2022-08-19T03:22:03Z","isPatch":true,"sender":{"key":"worldhello.net@gmail.com","avatar":"https://avatars.githubusercontent.com/u/183860?v=4"},"body":"From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n\nFor commands that update a reference without providing an old-oid, the\n\"reference-transaction\" hook will receive a zero-oid instead of the\ncorrect old-oid.\n\nIn order to provide the \"reference-transaction\" hook with a real old-oid\nin any case, get proper old_oid from the lock file and propagate it to\nthe corresponding update entry of a transaction.\n\nThe behavior of the following git commands and five testcases have been\nfixed in t1416:\n\n * git branch [-f] <ref> <new-oid>      # update branch\n * git cherry-pick <oid>\n * git rebase\n * git tag -d <tag>\n * git update-ref --stdin               # update refs\n * git update-ref -d <ref>\n * git update-ref <ref> <new-oid>       # update ref\n\nSigned-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n---\n refs/files-backend.c             |  28 +++-\n t/t1416-ref-transaction-hooks.sh | 231 ++++++-------------------------\n 2 files changed, 65 insertions(+), 194 deletions(-)\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 8db7882aac..7c1c25a25c 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2523,15 +2523,24 @@ static int lock_ref_for_update(struct files_ref_store *refs,\n \tupdate->backend_data = lock;\n \n \tif (update->type & REF_ISSYMREF) {\n+\t\tconst char *ref;\n+\n+\t\t/*\n+\t\t * Read the referent even though we won't use it as part\n+\t\t * of the transaction, because we want to set a proper\n+\t\t * old_oid for this symref using the oid we got.\n+\t\t */\n+\t\tref = refs_resolve_ref_unsafe(&refs->base,\n+\t\t\t\t\t      referent.buf, 0,\n+\t\t\t\t\t      &lock->old_oid, NULL);\n+\n \t\tif (update->flags & REF_NO_DEREF) {\n \t\t\t/*\n \t\t\t * We won't be reading the referent as part of\n-\t\t\t * the transaction, so we have to read it here\n-\t\t\t * to record and possibly check old_oid:\n+\t\t\t * the transaction, so we may need to check\n+\t\t\t * old_oid here:\n \t\t\t */\n-\t\t\tif (!refs_resolve_ref_unsafe(&refs->base,\n-\t\t\t\t\t\t     referent.buf, 0,\n-\t\t\t\t\t\t     &lock->old_oid, NULL)) {\n+\t\t\tif (!ref) {\n \t\t\t\tif (update->flags & REF_HAVE_OLD) {\n \t\t\t\t\tstrbuf_addf(err, \"cannot lock ref '%s': \"\n \t\t\t\t\t\t    \"error reading reference\",\n@@ -2578,6 +2587,15 @@ static int lock_ref_for_update(struct files_ref_store *refs,\n \t\t}\n \t}\n \n+\t/*\n+\t * Propagate old_oid from the lock to the update entry, so we can\n+\t * provide a proper old-oid of to the \"reference-transaction\" hook.\n+\t */\n+\tif (!(update->flags & REF_HAVE_OLD) && !is_null_oid(&lock->old_oid)) {\n+\t\toidcpy(&update->old_oid, &lock->old_oid);\n+\t\tupdate->flags |= REF_HAVE_OLD;\n+\t}\n+\n \tif ((update->flags & REF_HAVE_NEW) &&\n \t    !(update->flags & REF_DELETING) &&\n \t    !(update->flags & REF_LOG_ONLY)) {\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex 84509cb6a4..1ae601a73d 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -52,8 +52,8 @@ test_expect_success 'hook gets all queued updates in prepared state' '\n \t\tfi\n \tEOF\n \tcat >expect <<-EOF &&\n-\t\t$ZERO_OID $POST_OID HEAD\n-\t\t$ZERO_OID $POST_OID refs/heads/main\n+\t\t$PRE_OID $POST_OID HEAD\n+\t\t$PRE_OID $POST_OID refs/heads/main\n \tEOF\n \tgit update-ref HEAD POST <<-EOF &&\n \t\tupdate HEAD $ZERO_OID $POST_OID\n@@ -75,8 +75,8 @@ test_expect_success 'hook gets all queued updates in committed state' '\n \t\tfi\n \tEOF\n \tcat >expect <<-EOF &&\n-\t\t$ZERO_OID $POST_OID HEAD\n-\t\t$ZERO_OID $POST_OID refs/heads/main\n+\t\t$PRE_OID $POST_OID HEAD\n+\t\t$PRE_OID $POST_OID refs/heads/main\n \tEOF\n \tgit update-ref HEAD POST &&\n \ttest_cmp expect actual\n@@ -320,26 +320,7 @@ test_expect_success \"update-ref: create new refs\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-# Failed because the old-oids for the default branch and\n-# HEAD which points to the default branch were not the\n-# expected old-oids, but <ZERO-OID>.\n-#\n-# The differences are as follows:\n-#\n-#     @@ -5,8 +5,8 @@\n-#      <COMMIT-A> <COMMIT-B> refs/heads/topic1\n-#      <COMMIT-A> <COMMIT-B> HEAD\n-#      ## Call hook: reference-transaction  prepared ##\n-#     -<COMMIT-B> <COMMIT-A> refs/heads/topic1\n-#     -<COMMIT-B> <COMMIT-A> HEAD\n-#     +<ZERO-OID> <COMMIT-A> refs/heads/topic1\n-#     +<ZERO-OID> <COMMIT-A> HEAD\n-#      ## Call hook: reference-transaction committed ##\n-#     -<COMMIT-B> <COMMIT-A> refs/heads/topic1\n-#     -<COMMIT-B> <COMMIT-A> HEAD\n-#     +<ZERO-OID> <COMMIT-A> refs/heads/topic1\n-#     +<ZERO-OID> <COMMIT-A> HEAD\n-test_expect_failure \"update-ref: update default branch\" '\n+test_expect_success \"update-ref: update default branch\" '\n \ttest_when_finished \"git switch main; rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-\\EOF &&\n@@ -375,25 +356,7 @@ test_expect_failure \"update-ref: update default branch\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-# Failed because the old-oids for HEAD and the ref that the HEAD points\n-# to were not the expected old-oids, but <ZERO-OID>.\n-#\n-# The differences are as follows:\n-#\n-#     @@ -5,8 +5,8 @@\n-#      <COMMIT-A> <COMMIT-B> HEAD\n-#      <COMMIT-A> <COMMIT-B> refs/heads/topic1\n-#      ## Call hook: reference-transaction  prepared ##\n-#     -<COMMIT-B> <COMMIT-A> HEAD\n-#     -<COMMIT-B> <COMMIT-A> refs/heads/topic1\n-#     +<ZERO-OID> <COMMIT-A> HEAD\n-#     +<ZERO-OID> <COMMIT-A> refs/heads/topic1\n-#      ## Call hook: reference-transaction committed ##\n-#     -<COMMIT-B> <COMMIT-A> HEAD\n-#     -<COMMIT-B> <COMMIT-A> refs/heads/topic1\n-#     +<ZERO-OID> <COMMIT-A> HEAD\n-#     +<ZERO-OID> <COMMIT-A> refs/heads/topic1\n-test_expect_failure \"update-ref: update HEAD\" '\n+test_expect_success \"update-ref: update HEAD\" '\n \ttest_when_finished \"git switch main; rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-\\EOF &&\n@@ -438,32 +401,7 @@ test_expect_failure \"update-ref: prepare packed_ref_store using pack-refs\" '\n \ttest_path_is_missing $HOOK_OUTPUT\n '\n \n-# Failed because the old-oid was not the expected old-oid, but\n-# <ZERO-OID> for updating a reference using git-update-refs\n-# command without providing the old-oid parameter.\n-#\n-# The differences are as follows:\n-#\n-#     @@ -3,14 +3,14 @@\n-#      ## Call hook: reference-transaction committed ##\n-#      <COMMIT-A> <COMMIT-B> refs/heads/topic2\n-#      ## Call hook: reference-transaction  prepared ##\n-#     -<COMMIT-A> <COMMIT-C> refs/heads/topic3\n-#     +<ZERO-OID> <COMMIT-C> refs/heads/topic3\n-#      ## Call hook: reference-transaction committed ##\n-#     -<COMMIT-A> <COMMIT-C> refs/heads/topic3\n-#     +<ZERO-OID> <COMMIT-C> refs/heads/topic3\n-#      ## Call hook: reference-transaction  prepared ##\n-#      <ZERO-OID> <COMMIT-A> refs/heads/topic4\n-#      ## Call hook: reference-transaction committed ##\n-#      <ZERO-OID> <COMMIT-A> refs/heads/topic4\n-#      ## Call hook: reference-transaction  prepared ##\n-#     -<COMMIT-A> <COMMIT-C> refs/heads/topic4\n-#     +<ZERO-OID> <COMMIT-C> refs/heads/topic4\n-#      ## Call hook: reference-transaction committed ##\n-#     -<COMMIT-A> <COMMIT-C> refs/heads/topic4\n-#     +<ZERO-OID> <COMMIT-C> refs/heads/topic4\n-test_expect_failure \"update-ref: update refs already in packed_ref_store\" '\n+test_expect_success \"update-ref: update refs already in packed_ref_store\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-\\EOF &&\n@@ -510,9 +448,6 @@ test_expect_failure \"update-ref: update refs already in packed_ref_store\" '\n #  * The \"reference-transaction committed\" command was executed twice,\n #    once for packed ref-store, and once for loose ref-store.\n #\n-#  * The old-oid was not the expected old-oid, but <ZERO-OID> when\n-#    deleting a reference without providing the old-oid parameter.\n-#\n #  * Unexpected execution of the \"reference-transaction abort\" command.\n #\n # The differences are as follows:\n@@ -526,7 +461,7 @@ test_expect_failure \"update-ref: update refs already in packed_ref_store\" '\n #      <COMMIT-A> <ZERO-OID> refs/heads/topic1\n #      <COMMIT-A> <ZERO-OID> HEAD\n #      ## Call hook: reference-transaction  prepared ##\n-#     @@ -11,14 +13,20 @@\n+#     @@ -11,13 +13,19 @@\n #      ## Call hook: reference-transaction  prepared ##\n #      <COMMIT-B> <ZERO-OID> refs/heads/topic2\n #      ## Call hook: reference-transaction committed ##\n@@ -536,21 +471,16 @@ test_expect_failure \"update-ref: update refs already in packed_ref_store\" '\n #      ## Call hook: reference-transaction  prepared ##\n #      <ZERO-OID> <ZERO-OID> refs/heads/topic3\n #      ## Call hook: reference-transaction  prepared ##\n-#     -<COMMIT-C> <ZERO-OID> refs/heads/topic3\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n+#      <COMMIT-C> <ZERO-OID> refs/heads/topic3\n #      ## Call hook: reference-transaction committed ##\n-#     -<COMMIT-C> <ZERO-OID> refs/heads/topic3\n #     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n #     +## Call hook: reference-transaction committed ##\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n+#      <COMMIT-C> <ZERO-OID> refs/heads/topic3\n #     +## Call hook: reference-transaction   aborted ##\n #     +<ZERO-OID> <ZERO-OID> refs/heads/topic4\n #      ## Call hook: reference-transaction  prepared ##\n-#     -<COMMIT-C> <ZERO-OID> refs/heads/topic4\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic4\n+#      <COMMIT-C> <ZERO-OID> refs/heads/topic4\n #      ## Call hook: reference-transaction committed ##\n-#     -<COMMIT-C> <ZERO-OID> refs/heads/topic4\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic4\n test_expect_failure \"update-ref: remove refs with mixed ref_stores\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n@@ -638,24 +568,7 @@ test_expect_success \"update-ref --stdin: prepare packed_ref_store using pack-ref\n \tgit -C workdir pack-refs --all\n '\n \n-# Failed because the old-oid was not the expected old-oid, but\n-# <ZERO-OID> when running \"git update-ref --stdin\" to update a\n-# reference without providing an old-oid.\n-#\n-# The differences are as follows:\n-#\n-#     @@ -1,8 +1,8 @@\n-#      ## Call hook: reference-transaction  prepared ##\n-#      <COMMIT-A> <COMMIT-B> refs/heads/topic2\n-#     -<COMMIT-A> <COMMIT-C> refs/heads/topic3\n-#     +<ZERO-OID> <COMMIT-C> refs/heads/topic3\n-#      <ZERO-OID> <COMMIT-C> refs/heads/topic4\n-#      ## Call hook: reference-transaction committed ##\n-#      <COMMIT-A> <COMMIT-B> refs/heads/topic2\n-#     -<COMMIT-A> <COMMIT-C> refs/heads/topic3\n-#     +<ZERO-OID> <COMMIT-C> refs/heads/topic3\n-#      <ZERO-OID> <COMMIT-C> refs/heads/topic4\n-test_expect_failure \"update-ref --stdin: update refs\" '\n+test_expect_success \"update-ref --stdin: update refs\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-\\EOF &&\n@@ -699,39 +612,20 @@ test_expect_failure \"update-ref --stdin: update refs\" '\n #  * The \"reference-transaction committed\" command was executed twice,\n #    once for packed ref-store, and once for loose ref-store.\n #\n-#  * The old-oid was not the expected old-oid, but <ZERO-OID> when\n-#    deleting a ref without providing the old-oid parameter.\n-#\n # The differences are as follows:\n #\n-#     @@ -4,14 +4,19 @@\n-#      <ZERO-OID> <ZERO-OID> refs/heads/topic3\n-#      <ZERO-OID> <ZERO-OID> refs/heads/topic4\n-#      ## Call hook: reference-transaction  prepared ##\n-#     -<COMMIT-A> <ZERO-OID> refs/heads/topic1\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n-#      <COMMIT-B> <ZERO-OID> refs/heads/topic2\n-#     -<COMMIT-C> <ZERO-OID> refs/heads/topic3\n-#     -<COMMIT-C> <ZERO-OID> refs/heads/topic4\n-#     -<COMMIT-A> <ZERO-OID> HEAD\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic4\n-#     +<ZERO-OID> <ZERO-OID> HEAD\n+#     @@ -10,6 +10,11 @@\n+#      <COMMIT-C> <ZERO-OID> refs/heads/topic4\n+#      <COMMIT-A> <ZERO-OID> HEAD\n #      ## Call hook: reference-transaction committed ##\n-#     -<COMMIT-A> <ZERO-OID> refs/heads/topic1\n #     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n #     +<ZERO-OID> <ZERO-OID> refs/heads/topic2\n #     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n #     +<ZERO-OID> <ZERO-OID> refs/heads/topic4\n #     +## Call hook: reference-transaction committed ##\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n+#      <COMMIT-A> <ZERO-OID> refs/heads/topic1\n #      <COMMIT-B> <ZERO-OID> refs/heads/topic2\n-#     -<COMMIT-C> <ZERO-OID> refs/heads/topic3\n-#     -<COMMIT-C> <ZERO-OID> refs/heads/topic4\n-#     -<COMMIT-A> <ZERO-OID> HEAD\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic4\n-#     +<ZERO-OID> <ZERO-OID> HEAD\n+#      <COMMIT-C> <ZERO-OID> refs/heads/topic3\n test_expect_failure \"update-ref --stdin: delete refs\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n@@ -852,23 +746,7 @@ test_expect_failure \"branch: prepare packed_ref_store using gc\" '\n \ttest_path_is_missing $HOOK_OUTPUT\n '\n \n-# Failed because the old-oid was not the expected old-oid, but\n-# <ZERO-OID> when running git-branch to update a branch without\n-# providing an old-oid.\n-#\n-# The differences are as follows:\n-#\n-#     @@ -1,7 +1,7 @@\n-#      ## Call hook: reference-transaction  prepared ##\n-#     -<COMMIT-A> <COMMIT-B> refs/heads/topic2\n-#     +<ZERO-OID> <COMMIT-B> refs/heads/topic2\n-#      ## Call hook: reference-transaction committed ##\n-#     -<COMMIT-A> <COMMIT-B> refs/heads/topic2\n-#     +<ZERO-OID> <COMMIT-B> refs/heads/topic2\n-#      ## Call hook: reference-transaction  prepared ##\n-#      <ZERO-OID> <COMMIT-C> refs/heads/topic3\n-#      ## Call hook: reference-transaction committed ##\n-test_expect_failure \"branch: update branch without old-oid\" '\n+test_expect_success \"branch: update branch without old-oid\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-\\EOF &&\n@@ -1007,9 +885,6 @@ test_expect_failure \"branch: rename branches\" '\n \n # Mismatched hook output for \"git branch -d\":\n #\n-#  * The old-oid was not the expected old-oid, but <ZERO-OID> when\n-#    deleting a branch without providing the old-oid parameter.\n-#\n #  * The delete branches operation should be treated as one transaction,\n #    but was splitted into several transactions on loose references,\n #    and the \"reference-transaction committed\" command was executed\n@@ -1029,28 +904,25 @@ test_expect_failure \"branch: rename branches\" '\n #     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n #     +## Call hook: reference-transaction   aborted ##\n #     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n+#     +## Call hook: reference-transaction  prepared ##\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n+#     +## Call hook: reference-transaction committed ##\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n+#     +## Call hook: reference-transaction   aborted ##\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic2\n #      ## Call hook: reference-transaction  prepared ##\n #     -<COMMIT-A> <ZERO-OID> refs/heads/topic1\n-#     -<COMMIT-B> <ZERO-OID> refs/heads/topic2\n+#      <COMMIT-B> <ZERO-OID> refs/heads/topic2\n #     -<COMMIT-C> <ZERO-OID> refs/heads/topic3\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n #      ## Call hook: reference-transaction committed ##\n #     -<COMMIT-A> <ZERO-OID> refs/heads/topic1\n-#     -<COMMIT-B> <ZERO-OID> refs/heads/topic2\n-#     -<COMMIT-C> <ZERO-OID> refs/heads/topic3\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n-#     +## Call hook: reference-transaction   aborted ##\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic2\n-#     +## Call hook: reference-transaction  prepared ##\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic2\n-#     +## Call hook: reference-transaction committed ##\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic2\n+#      <COMMIT-B> <ZERO-OID> refs/heads/topic2\n #     +## Call hook: reference-transaction   aborted ##\n #     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n #     +## Call hook: reference-transaction  prepared ##\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n+#     +<COMMIT-C> <ZERO-OID> refs/heads/topic3\n #     +## Call hook: reference-transaction committed ##\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n+#      <COMMIT-C> <ZERO-OID> refs/heads/topic3\n test_expect_failure \"branch: remove branches\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n@@ -1184,9 +1056,6 @@ test_expect_success \"tag: update refs to create loose refs\" '\n \n # Mismatched hook output for \"git tag -d\":\n #\n-#  * The old-oid was not the expected old-oid, but <ZERO-OID> when\n-#    deleting a tag without providing the old-oid parameter.\n-#\n #  * The delete tags operation should be treated as one transaction,\n #    but was splitted into several transactions on loose references,\n #    and the \"reference-transaction committed\" command was executed\n@@ -1206,28 +1075,25 @@ test_expect_success \"tag: update refs to create loose refs\" '\n #     +<ZERO-OID> <ZERO-OID> refs/tags/v3\n #     +## Call hook: reference-transaction   aborted ##\n #     +<ZERO-OID> <ZERO-OID> refs/tags/v1\n+#     +## Call hook: reference-transaction  prepared ##\n+#     +<ZERO-OID> <ZERO-OID> refs/tags/v1\n+#     +## Call hook: reference-transaction committed ##\n+#     +<ZERO-OID> <ZERO-OID> refs/tags/v1\n+#     +## Call hook: reference-transaction   aborted ##\n+#     +<ZERO-OID> <ZERO-OID> refs/tags/v2\n #      ## Call hook: reference-transaction  prepared ##\n #     -<COMMIT-A> <ZERO-OID> refs/tags/v1\n-#     -<COMMIT-B> <ZERO-OID> refs/tags/v2\n+#      <COMMIT-B> <ZERO-OID> refs/tags/v2\n #     -<COMMIT-C> <ZERO-OID> refs/tags/v3\n-#     +<ZERO-OID> <ZERO-OID> refs/tags/v1\n #      ## Call hook: reference-transaction committed ##\n #     -<COMMIT-A> <ZERO-OID> refs/tags/v1\n-#     -<COMMIT-B> <ZERO-OID> refs/tags/v2\n-#     -<COMMIT-C> <ZERO-OID> refs/tags/v3\n-#     +<ZERO-OID> <ZERO-OID> refs/tags/v1\n-#     +## Call hook: reference-transaction   aborted ##\n-#     +<ZERO-OID> <ZERO-OID> refs/tags/v2\n-#     +## Call hook: reference-transaction  prepared ##\n-#     +<ZERO-OID> <ZERO-OID> refs/tags/v2\n-#     +## Call hook: reference-transaction committed ##\n-#     +<ZERO-OID> <ZERO-OID> refs/tags/v2\n+#      <COMMIT-B> <ZERO-OID> refs/tags/v2\n #     +## Call hook: reference-transaction   aborted ##\n #     +<ZERO-OID> <ZERO-OID> refs/tags/v3\n #     +## Call hook: reference-transaction  prepared ##\n-#     +<ZERO-OID> <ZERO-OID> refs/tags/v3\n+#     +<COMMIT-C> <ZERO-OID> refs/tags/v3\n #     +## Call hook: reference-transaction committed ##\n-#     +<ZERO-OID> <ZERO-OID> refs/tags/v3\n+#      <COMMIT-C> <ZERO-OID> refs/tags/v3\n test_expect_failure \"tag: remove tags with mixed ref_stores\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n@@ -1374,24 +1240,19 @@ test_expect_success \"worktree: topic2: merge\" '\n \n # Mismatched hook output for git-cherry-pick:\n #\n-#  * The old-oid was not the expected old-oid, but <ZERO-OID>.\n-#\n #  * Unexpected execution of the \"reference-transaction abort\" command.\n #\n # The differences are as follows:\n #\n-#     @@ -12,7 +12,9 @@\n+#     @@ -12,6 +12,8 @@\n #      ## Call hook: reference-transaction committed ##\n #      <COMMIT-A> <COMMIT-F> HEAD\n #      <COMMIT-A> <COMMIT-F> refs/heads/topic3\n #     +## Call hook: reference-transaction   aborted ##\n #     +<ZERO-OID> <ZERO-OID> CHERRY_PICK_HEAD\n #      ## Call hook: reference-transaction  prepared ##\n-#     -<COMMIT-C> <ZERO-OID> CHERRY_PICK_HEAD\n-#     +<ZERO-OID> <ZERO-OID> CHERRY_PICK_HEAD\n+#      <COMMIT-C> <ZERO-OID> CHERRY_PICK_HEAD\n #      ## Call hook: reference-transaction committed ##\n-#     -<COMMIT-C> <ZERO-OID> CHERRY_PICK_HEAD\n-#     +<ZERO-OID> <ZERO-OID> CHERRY_PICK_HEAD\n test_expect_failure \"worktree: topic3: cherry-pick\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n@@ -1438,8 +1299,6 @@ test_expect_failure \"worktree: topic3: cherry-pick\" '\n \n # Mismatched hook output for git-rebase:\n #\n-#  * The old-oid was not the expected old-oid, but <ZERO-OID>.\n-#\n #  * Unexpected execution of the \"reference-transaction abort\" command.\n #\n # The differences are as follows:\n@@ -1453,20 +1312,14 @@ test_expect_failure \"worktree: topic3: cherry-pick\" '\n #      ## Call hook: reference-transaction  prepared ##\n #      <ZERO-OID> <ZERO-OID> REBASE_HEAD\n #      ## Call hook: reference-transaction committed ##\n-#     @@ -18,10 +20,12 @@\n+#     @@ -18,6 +20,8 @@\n #      <COMMIT-C> <COMMIT-H> HEAD\n #      ## Call hook: reference-transaction committed ##\n #      <COMMIT-C> <COMMIT-H> HEAD\n #     +## Call hook: reference-transaction   aborted ##\n #     +<ZERO-OID> <ZERO-OID> CHERRY_PICK_HEAD\n #      ## Call hook: reference-transaction  prepared ##\n-#     -<COMMIT-G> <ZERO-OID> CHERRY_PICK_HEAD\n-#     +<ZERO-OID> <ZERO-OID> CHERRY_PICK_HEAD\n-#      ## Call hook: reference-transaction committed ##\n-#     -<COMMIT-G> <ZERO-OID> CHERRY_PICK_HEAD\n-#     +<ZERO-OID> <ZERO-OID> CHERRY_PICK_HEAD\n-#      ## Call hook: reference-transaction  prepared ##\n-#      <COMMIT-G> <COMMIT-H> refs/heads/topic4\n+#      <COMMIT-G> <ZERO-OID> CHERRY_PICK_HEAD\n #      ## Call hook: reference-transaction committed ##\n test_expect_failure \"worktree: topic4: rebase\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n-- \n2.36.1.25.gc87d5ad63a.dirty\n\n"},{"id":"461492","messageId":"20220819032147.28841-4-worldhello.net@gmail.com","threadId":"58240","inReplyTo":"CANYiYbFw71bX827akAG87RSKOozPk313Hoe573O9dQ65_U6sLQ@mail.gmail.com","subject":"[PATCH v2 3/9] refs: add new field in transaction for running transaction hook","fromName":"Jiang Xin","fromEmail":"worldhello.net@gmail.com","sentAt":"2022-08-19T03:21:41Z","receivedAt":"2022-08-19T03:22:06Z","isPatch":true,"sender":{"key":"worldhello.net@gmail.com","avatar":"https://avatars.githubusercontent.com/u/183860?v=4"},"body":"From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n\nAdd a new field \"hook_flags\" in transaction, and only run the\n\"reference-transaction\" hook if the specific flag is turned on.\n\nThe \"reference-transaction\" hook has three states: prepared, committed,\nand aborted. To update a reference, git may create two seperate\ntransactions, one for loose reference and one for packed ref-store. This\nmay cause duplicate running of the hook for same references. The new\nfield \"hook_flags\" in the transaction can turn off running a specific\ntransaction. In some scenarios, we may only want to turn off certain\nstates of a transaction, such as \"committed\" and \"aborted\", but want to\nturn on the \"prepared\" state of the hook to do some pre-checks, so the\n\"hook_flags\" field has three bits to control running of the three states\nof the hook.\n\nBy calling the \"ref_store_transaction_begin()\" function, all the flags\nof the \"hook_flags\" field for the new initialized transaction will be\nturned on. The new function \"ref_store_transaction_begin_extended()\"\nwill be used in later commits with a specific \"hook_flags\" field to\ninitialize a new transaction.\n\nSigned-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n---\n refs.c               | 25 +++++++++++++++++++++++--\n refs.h               |  3 +++\n refs/refs-internal.h |  8 ++++++++\n 3 files changed, 34 insertions(+), 2 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex 90bcb27168..48b69460e2 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -998,17 +998,27 @@ int read_ref_at(struct ref_store *refs, const char *refname,\n \treturn 1;\n }\n \n-struct ref_transaction *ref_store_transaction_begin(struct ref_store *refs,\n-\t\t\t\t\t\t    struct strbuf *err)\n+struct ref_transaction *ref_store_transaction_begin_extended(struct ref_store *refs,\n+\t\t\t\t\t\t\t     unsigned int hook_flags,\n+\t\t\t\t\t\t\t     struct strbuf *err)\n {\n \tstruct ref_transaction *tr;\n \tassert(err);\n \n \tCALLOC_ARRAY(tr, 1);\n \ttr->ref_store = refs;\n+\ttr->hook_flags = hook_flags;\n \treturn tr;\n }\n \n+struct ref_transaction *ref_store_transaction_begin(struct ref_store *refs,\n+\t\t\t\t\t\t    struct strbuf *err)\n+{\n+\treturn ref_store_transaction_begin_extended(refs,\n+\t\t\t\t\t\t    REF_TRANSACTION_RUN_ALL_HOOKS,\n+\t\t\t\t\t\t    err);\n+}\n+\n struct ref_transaction *ref_transaction_begin(struct strbuf *err)\n {\n \treturn ref_store_transaction_begin(get_main_ref_store(the_repository), err);\n@@ -2074,6 +2084,17 @@ static int run_transaction_hook(struct ref_transaction *transaction,\n \tconst char *hook;\n \tint ret = 0, i;\n \n+\tif (!strcmp(state, \"prepared\")) {\n+\t\tif (!(transaction->hook_flags & REF_TRANSACTION_RUN_PREPARED_HOOK))\n+\t\t\treturn 0;\n+\t} else if (!strcmp(state, \"committed\")) {\n+\t\tif (!(transaction->hook_flags & REF_TRANSACTION_RUN_COMMITTED_HOOK))\n+\t\t\treturn 0;\n+\t} else if (!strcmp(state, \"aborted\")) {\n+\t\tif (!(transaction->hook_flags & REF_TRANSACTION_RUN_ABORTED_HOOK))\n+\t\t\treturn 0;\n+\t}\n+\n \thook = find_hook(\"reference-transaction\");\n \tif (!hook)\n \t\treturn ret;\ndiff --git a/refs.h b/refs.h\nindex 47cb9edbaa..715127ab58 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -570,6 +570,9 @@ enum action_on_err {\n  * Begin a reference transaction.  The reference transaction must\n  * be freed by calling ref_transaction_free().\n  */\n+struct ref_transaction *ref_store_transaction_begin_extended(struct ref_store *refs,\n+\t\t\t\t\t\t\t     unsigned int hook_flags,\n+\t\t\t\t\t\t\t     struct strbuf *err);\n struct ref_transaction *ref_store_transaction_begin(struct ref_store *refs,\n \t\t\t\t\t\t    struct strbuf *err);\n struct ref_transaction *ref_transaction_begin(struct strbuf *err);\ndiff --git a/refs/refs-internal.h b/refs/refs-internal.h\nindex 69f93b0e2a..5220d1980d 100644\n--- a/refs/refs-internal.h\n+++ b/refs/refs-internal.h\n@@ -201,6 +201,13 @@ enum ref_transaction_state {\n \tREF_TRANSACTION_CLOSED   = 2\n };\n \n+#define REF_TRANSACTION_RUN_PREPARED_HOOK (1 << 0)\n+#define REF_TRANSACTION_RUN_COMMITTED_HOOK (1 << 1)\n+#define REF_TRANSACTION_RUN_ABORTED_HOOK (1 << 2)\n+#define REF_TRANSACTION_RUN_ALL_HOOKS         \\\n+\t(REF_TRANSACTION_RUN_PREPARED_HOOK  | \\\n+\t REF_TRANSACTION_RUN_COMMITTED_HOOK | \\\n+\t REF_TRANSACTION_RUN_ABORTED_HOOK)\n /*\n  * Data structure for holding a reference transaction, which can\n  * consist of checks and updates to multiple references, carried out\n@@ -212,6 +219,7 @@ struct ref_transaction {\n \tsize_t alloc;\n \tsize_t nr;\n \tenum ref_transaction_state state;\n+\tunsigned int hook_flags;\n \tvoid *backend_data;\n };\n \n-- \n2.36.1.25.gc87d5ad63a.dirty\n\n"},{"id":"461493","messageId":"20220819032147.28841-2-worldhello.net@gmail.com","threadId":"58240","inReplyTo":"CANYiYbFw71bX827akAG87RSKOozPk313Hoe573O9dQ65_U6sLQ@mail.gmail.com","subject":"[PATCH v2 1/9] t1416: more testcases for reference-transaction hook","fromName":"Jiang Xin","fromEmail":"worldhello.net@gmail.com","sentAt":"2022-08-19T03:21:39Z","receivedAt":"2022-08-19T03:22:10Z","isPatch":true,"sender":{"key":"worldhello.net@gmail.com","avatar":"https://avatars.githubusercontent.com/u/183860?v=4"},"body":"From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n\nAppend more testcases in t1416 for various git commands that may trigger\nthe \"reference-transaction\" hook.\n\nIn order to create a common \"reference-transaction\" hook, we define the\ncommon hook path using config variable \"core.hooksPath\", and create the\n\"reference-transaction\" hook in the path.\n\nSome commands trigger the \"reference-transaction\" hook properly and get\nthe expected output. E.g.:\n\n * git branch <new-branch> <oid>\t\t# create new branch\n * git clone\n * git commit [--amend]\n * git fetch\n * git merge\n * git push [--atomic]\n * git reset --hard\n * git tag [-f] <tag> <oid>\t\t\t# update tag\n * git tag <new-tag> <oid>\t\t\t# create new tag\n * git update-ref --stdin\t\t\t# create new refs\n * git update-ref <ref> <oid>\t\t\t# create new ref\n * git update-ref <ref> <new-oid> <old-oid>\t# update ref\n\nBut 17 testcases failed.\n\nSome commands failed because the expected \"<old-oid>\" became\n\"<zero-oid>\". E.g.:\n\n * git branch [-f] <ref> <new-oid>\t\t# update branch\n * git cherry-pick <oid>\n * git rebase\n * git tag -d <tag>\n * git update-ref --stdin\t\t\t# update/delete refs\n * git update-ref -d <ref>\n * git update-ref <ref> <new-oid>\t\t# update ref\n\nSome commands failed because the \"reference-transaction committed\"\ncommand was repeated multiple times for the same changes. E.g.:\n\n * git cherry-pick\n * git rebase\n * git revert\n * git tag -d <tag>\n * git update-ref -d <ref>\n * git update-ref --stdin\t\t\t# delete refs\n\nSome commands should not trigger the \"reference-transaction\" hook\nbecause no real changes have occurred to the repository. E.g.:\n\n * git gc\n * git pack-refs --all\n\nSome commands did not execute the \"reference-transaction\" hook at all.\nE.g.:\n\n * git branch -c <src> <dest>\t\t\t# copy branch\n * git branch -m <old> <new>\t\t\t# rename branch\n\nSome commands ran unexpected command \"reference-transaction aborted\".\nE.g.:\n\n * git branch -d <branch>\t\t\t# delete branch\n * git branch -m <old> <new>\t\t\t# rename branch\n * git cherr-pick <oid>\n * git rebase\n * git revert\n * git tag -d <tag>\t\t\t\t# delete tag\n * git update-ref -d <ref> \t\t\t# delete ref\n\nWe will fix the failed testcases in later commits.\n\nSigned-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\nHelped-by: Eric Sunshine <sunshine@sunshineco.com>\n---\n t/t1416-ref-transaction-hooks.sh | 1484 ++++++++++++++++++++++++++++++\n 1 file changed, 1484 insertions(+)\n\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex 27731722a5..84509cb6a4 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -133,4 +133,1488 @@ test_expect_success 'interleaving hook calls succeed' '\n \ttest_cmp expect target-repo.git/actual\n '\n \n+HOOK_OUTPUT=hook-output\n+\n+# Create commits in <repo> and assign each commit's oid to shell variables\n+# given in the arguments (A, B, and C). E.g.:\n+#\n+#     create_commits_in <repo> A B C\n+#\n+# NOTE: Never calling this function from a subshell since variable\n+# assignments will disappear when subshell exits.\n+create_commits_in () {\n+\tlocal repo=\"$1\" &&\n+\tshift &&\n+\twhile test $# -gt 0\n+\tdo\n+\t\tlocal name=$1 &&\n+\t\tshift &&\n+\t\ttest_commit -C \"$repo\" --no-tag \"$name\" &&\n+\t\tlocal rev=$(git -C \"$repo\" rev-parse HEAD) &&\n+\t\teval \"$name=$rev\" || return 1\n+\tdone\n+}\n+\n+get_abbrev_oid () {\n+\tlocal oid=$1 &&\n+\tlocal suffix=${oid#???????} &&\n+\toid=${oid%$suffix} &&\n+\tif test -n \"$oid\"\n+\tthen\n+\t\techo \"$oid\"\n+\telse\n+\t\techo \"undefined-oid\"\n+\tfi\n+}\n+\n+# Format the output of git-push, git-show-ref and other commands to make a\n+# user-friendly and stable text.  We can easily prepare the expect text\n+# without having to worry about future changes of the commit ID.\n+make_user_friendly_and_stable_output () {\n+\tsed \\\n+\t\t-e \"s/$(get_abbrev_oid $A)[0-9a-f]*/<COMMIT-A>/g\" \\\n+\t\t-e \"s/$(get_abbrev_oid $B)[0-9a-f]*/<COMMIT-B>/g\" \\\n+\t\t-e \"s/$(get_abbrev_oid $C)[0-9a-f]*/<COMMIT-C>/g\" \\\n+\t\t-e \"s/$(get_abbrev_oid $D)[0-9a-f]*/<COMMIT-D>/g\" \\\n+\t\t-e \"s/$(get_abbrev_oid $E)[0-9a-f]*/<COMMIT-E>/g\" \\\n+\t\t-e \"s/$(get_abbrev_oid $F)[0-9a-f]*/<COMMIT-F>/g\" \\\n+\t\t-e \"s/$(get_abbrev_oid $G)[0-9a-f]*/<COMMIT-G>/g\" \\\n+\t\t-e \"s/$(get_abbrev_oid $H)[0-9a-f]*/<COMMIT-H>/g\" \\\n+\t\t-e \"s/$(get_abbrev_oid $I)[0-9a-f]*/<COMMIT-I>/g\" \\\n+\t\t-e \"s/$ZERO_OID/<ZERO-OID>/g\"\n+}\n+\n+test_cmp_heads_and_tags () {\n+\tlocal indir= expect actual &&\n+\twhile test $# != 0\n+\tdo\n+\t\tcase \"$1\" in\n+\t\t-C)\n+\t\t\tindir=\"$2\" &&\n+\t\t\tshift\n+\t\t\t;;\n+\t\t*)\n+\t\t\tbreak\n+\t\t\t;;\n+\t\tesac &&\n+\t\tshift\n+\tdone &&\n+\texpect=${1:-expect} &&\n+\tactual=${2:-actual-heads-and-tags} &&\n+\tindir=${indir:+\"$indir\"/} &&\n+\ttest_path_is_file \"$expect\" &&\n+\ttest_when_finished \"rm -f \\\"$actual\\\"\" &&\n+\tgit ${indir:+ -C \"$indir\"} show-ref --heads --tags |\n+\t\tmake_user_friendly_and_stable_output >\"$actual\" &&\n+\ttest_cmp \"$expect\" \"$actual\"\n+}\n+\n+test_expect_success 'setup git config and hook' '\n+\tgit config --global core.hooksPath \"$HOME/test-hooks\" &&\n+\tgit config --global core.abbrev 7 &&\n+\tmkdir \"test-hooks\" &&\n+\twrite_script \"test-hooks/reference-transaction\" <<-EOF\n+\t\texec >>\"$HOME/$HOOK_OUTPUT\"\n+\t\tprintf \"## Call hook: reference-transaction %9s ##\\n\" \"\\$@\"\n+\t\twhile read -r line\n+\t\tdo\n+\t\t    echo \"\\$line\"\n+\t\tdone\n+\tEOF\n+'\n+\n+test_expect_success \"setup base repository\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> HEAD\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/main\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> HEAD\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/main\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <COMMIT-B> HEAD\n+\t\t<COMMIT-A> <COMMIT-B> refs/heads/main\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <COMMIT-B> HEAD\n+\t\t<COMMIT-A> <COMMIT-B> refs/heads/main\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-B> <COMMIT-C> HEAD\n+\t\t<COMMIT-B> <COMMIT-C> refs/heads/main\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-B> <COMMIT-C> HEAD\n+\t\t<COMMIT-B> <COMMIT-C> refs/heads/main\n+\tEOF\n+\n+\tgit init base &&\n+\tcreate_commits_in base A B C &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\tEOF\n+\ttest_cmp_heads_and_tags -C base expect\n+'\n+\n+test_expect_success \"update-ref: setup workdir using git-clone\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/remotes/origin/main\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/remotes/origin/main\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> HEAD\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/main\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> HEAD\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/main\n+\tEOF\n+\n+\tgit clone base workdir &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_success \"update-ref: create new refs\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic1\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic1\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic2\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic2\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic3\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic3\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit update-ref refs/heads/topic1 $A &&\n+\t\tgit update-ref refs/heads/topic2 $A &&\n+\t\tgit update-ref refs/heads/topic3 $A\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-A> refs/heads/topic1\n+\t\t<COMMIT-A> refs/heads/topic2\n+\t\t<COMMIT-A> refs/heads/topic3\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+# Failed because the old-oids for the default branch and\n+# HEAD which points to the default branch were not the\n+# expected old-oids, but <ZERO-OID>.\n+#\n+# The differences are as follows:\n+#\n+#     @@ -5,8 +5,8 @@\n+#      <COMMIT-A> <COMMIT-B> refs/heads/topic1\n+#      <COMMIT-A> <COMMIT-B> HEAD\n+#      ## Call hook: reference-transaction  prepared ##\n+#     -<COMMIT-B> <COMMIT-A> refs/heads/topic1\n+#     -<COMMIT-B> <COMMIT-A> HEAD\n+#     +<ZERO-OID> <COMMIT-A> refs/heads/topic1\n+#     +<ZERO-OID> <COMMIT-A> HEAD\n+#      ## Call hook: reference-transaction committed ##\n+#     -<COMMIT-B> <COMMIT-A> refs/heads/topic1\n+#     -<COMMIT-B> <COMMIT-A> HEAD\n+#     +<ZERO-OID> <COMMIT-A> refs/heads/topic1\n+#     +<ZERO-OID> <COMMIT-A> HEAD\n+test_expect_failure \"update-ref: update default branch\" '\n+\ttest_when_finished \"git switch main; rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <COMMIT-B> refs/heads/topic1\n+\t\t<COMMIT-A> <COMMIT-B> HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <COMMIT-B> refs/heads/topic1\n+\t\t<COMMIT-A> <COMMIT-B> HEAD\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-B> <COMMIT-A> refs/heads/topic1\n+\t\t<COMMIT-B> <COMMIT-A> HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-B> <COMMIT-A> refs/heads/topic1\n+\t\t<COMMIT-B> <COMMIT-A> HEAD\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit switch topic1 &&\n+\t\tgit update-ref refs/heads/topic1 $B $A &&\n+\t\tgit update-ref refs/heads/topic1 $A\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-A> refs/heads/topic1\n+\t\t<COMMIT-A> refs/heads/topic2\n+\t\t<COMMIT-A> refs/heads/topic3\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+# Failed because the old-oids for HEAD and the ref that the HEAD points\n+# to were not the expected old-oids, but <ZERO-OID>.\n+#\n+# The differences are as follows:\n+#\n+#     @@ -5,8 +5,8 @@\n+#      <COMMIT-A> <COMMIT-B> HEAD\n+#      <COMMIT-A> <COMMIT-B> refs/heads/topic1\n+#      ## Call hook: reference-transaction  prepared ##\n+#     -<COMMIT-B> <COMMIT-A> HEAD\n+#     -<COMMIT-B> <COMMIT-A> refs/heads/topic1\n+#     +<ZERO-OID> <COMMIT-A> HEAD\n+#     +<ZERO-OID> <COMMIT-A> refs/heads/topic1\n+#      ## Call hook: reference-transaction committed ##\n+#     -<COMMIT-B> <COMMIT-A> HEAD\n+#     -<COMMIT-B> <COMMIT-A> refs/heads/topic1\n+#     +<ZERO-OID> <COMMIT-A> HEAD\n+#     +<ZERO-OID> <COMMIT-A> refs/heads/topic1\n+test_expect_failure \"update-ref: update HEAD\" '\n+\ttest_when_finished \"git switch main; rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <COMMIT-B> HEAD\n+\t\t<COMMIT-A> <COMMIT-B> refs/heads/topic1\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <COMMIT-B> HEAD\n+\t\t<COMMIT-A> <COMMIT-B> refs/heads/topic1\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-B> <COMMIT-A> HEAD\n+\t\t<COMMIT-B> <COMMIT-A> refs/heads/topic1\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-B> <COMMIT-A> HEAD\n+\t\t<COMMIT-B> <COMMIT-A> refs/heads/topic1\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit switch topic1 &&\n+\t\tgit update-ref HEAD $B $A &&\n+\t\tgit update-ref HEAD $A\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-A> refs/heads/topic1\n+\t\t<COMMIT-A> refs/heads/topic2\n+\t\t<COMMIT-A> refs/heads/topic3\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+# Failed because the reference-transaction hook was executed even\n+# though no refs were changed by running git-pack-refs.\n+test_expect_failure \"update-ref: prepare packed_ref_store using pack-refs\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\tgit -C workdir pack-refs --all &&\n+\ttest_path_is_file workdir/.git/packed-refs &&\n+\ttest_path_is_missing $HOOK_OUTPUT\n+'\n+\n+# Failed because the old-oid was not the expected old-oid, but\n+# <ZERO-OID> for updating a reference using git-update-refs\n+# command without providing the old-oid parameter.\n+#\n+# The differences are as follows:\n+#\n+#     @@ -3,14 +3,14 @@\n+#      ## Call hook: reference-transaction committed ##\n+#      <COMMIT-A> <COMMIT-B> refs/heads/topic2\n+#      ## Call hook: reference-transaction  prepared ##\n+#     -<COMMIT-A> <COMMIT-C> refs/heads/topic3\n+#     +<ZERO-OID> <COMMIT-C> refs/heads/topic3\n+#      ## Call hook: reference-transaction committed ##\n+#     -<COMMIT-A> <COMMIT-C> refs/heads/topic3\n+#     +<ZERO-OID> <COMMIT-C> refs/heads/topic3\n+#      ## Call hook: reference-transaction  prepared ##\n+#      <ZERO-OID> <COMMIT-A> refs/heads/topic4\n+#      ## Call hook: reference-transaction committed ##\n+#      <ZERO-OID> <COMMIT-A> refs/heads/topic4\n+#      ## Call hook: reference-transaction  prepared ##\n+#     -<COMMIT-A> <COMMIT-C> refs/heads/topic4\n+#     +<ZERO-OID> <COMMIT-C> refs/heads/topic4\n+#      ## Call hook: reference-transaction committed ##\n+#     -<COMMIT-A> <COMMIT-C> refs/heads/topic4\n+#     +<ZERO-OID> <COMMIT-C> refs/heads/topic4\n+test_expect_failure \"update-ref: update refs already in packed_ref_store\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <COMMIT-B> refs/heads/topic2\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <COMMIT-B> refs/heads/topic2\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <COMMIT-C> refs/heads/topic3\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <COMMIT-C> refs/heads/topic3\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic4\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic4\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <COMMIT-C> refs/heads/topic4\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <COMMIT-C> refs/heads/topic4\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit update-ref refs/heads/topic2 $B $A &&\n+\t\tgit update-ref refs/heads/topic3 $C &&\n+\t\tgit update-ref refs/heads/topic4 $A &&\n+\t\tgit update-ref refs/heads/topic4 $C\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-A> refs/heads/topic1\n+\t\t<COMMIT-B> refs/heads/topic2\n+\t\t<COMMIT-C> refs/heads/topic3\n+\t\t<COMMIT-C> refs/heads/topic4\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+# Mismatched hook output when deleting refs using \"git update-refs -d\":\n+#\n+#  * The \"reference-transaction committed\" command was executed twice,\n+#    once for packed ref-store, and once for loose ref-store.\n+#\n+#  * The old-oid was not the expected old-oid, but <ZERO-OID> when\n+#    deleting a reference without providing the old-oid parameter.\n+#\n+#  * Unexpected execution of the \"reference-transaction abort\" command.\n+#\n+# The differences are as follows:\n+#\n+#     @@ -4,6 +4,8 @@\n+#      <COMMIT-A> <ZERO-OID> refs/heads/topic1\n+#      <COMMIT-A> <ZERO-OID> HEAD\n+#      ## Call hook: reference-transaction committed ##\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n+#     +## Call hook: reference-transaction committed ##\n+#      <COMMIT-A> <ZERO-OID> refs/heads/topic1\n+#      <COMMIT-A> <ZERO-OID> HEAD\n+#      ## Call hook: reference-transaction  prepared ##\n+#     @@ -11,14 +13,20 @@\n+#      ## Call hook: reference-transaction  prepared ##\n+#      <COMMIT-B> <ZERO-OID> refs/heads/topic2\n+#      ## Call hook: reference-transaction committed ##\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic2\n+#     +## Call hook: reference-transaction committed ##\n+#      <COMMIT-B> <ZERO-OID> refs/heads/topic2\n+#      ## Call hook: reference-transaction  prepared ##\n+#      <ZERO-OID> <ZERO-OID> refs/heads/topic3\n+#      ## Call hook: reference-transaction  prepared ##\n+#     -<COMMIT-C> <ZERO-OID> refs/heads/topic3\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n+#      ## Call hook: reference-transaction committed ##\n+#     -<COMMIT-C> <ZERO-OID> refs/heads/topic3\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n+#     +## Call hook: reference-transaction committed ##\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n+#     +## Call hook: reference-transaction   aborted ##\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic4\n+#      ## Call hook: reference-transaction  prepared ##\n+#     -<COMMIT-C> <ZERO-OID> refs/heads/topic4\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic4\n+#      ## Call hook: reference-transaction committed ##\n+#     -<COMMIT-C> <ZERO-OID> refs/heads/topic4\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic4\n+test_expect_failure \"update-ref: remove refs with mixed ref_stores\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <ZERO-OID> refs/heads/topic1\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <ZERO-OID> refs/heads/topic1\n+\t\t<COMMIT-A> <ZERO-OID> HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <ZERO-OID> refs/heads/topic1\n+\t\t<COMMIT-A> <ZERO-OID> HEAD\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <ZERO-OID> refs/heads/topic2\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-B> <ZERO-OID> refs/heads/topic2\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-B> <ZERO-OID> refs/heads/topic2\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <ZERO-OID> refs/heads/topic3\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic3\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic3\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic4\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic4\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit update-ref -d refs/heads/topic1 $A &&\n+\t\tgit update-ref -d refs/heads/topic2 $B &&\n+\t\tgit update-ref -d refs/heads/topic3 &&\n+\t\tgit update-ref -d refs/heads/topic4\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_success \"update-ref --stdin: create new refs\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic1\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic2\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic3\n+\t\t<ZERO-OID> <COMMIT-A> HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic1\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic2\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic3\n+\t\t<ZERO-OID> <COMMIT-A> HEAD\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit update-ref --stdin <<-EOF\n+\t\t\tcreate refs/heads/topic1 $A\n+\t\t\tcreate refs/heads/topic2 $A\n+\t\t\tcreate refs/heads/topic3 $A\n+\t\tEOF\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-A> refs/heads/topic1\n+\t\t<COMMIT-A> refs/heads/topic2\n+\t\t<COMMIT-A> refs/heads/topic3\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_success \"update-ref --stdin: prepare packed_ref_store using pack-refs\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\tgit -C workdir pack-refs --all\n+'\n+\n+# Failed because the old-oid was not the expected old-oid, but\n+# <ZERO-OID> when running \"git update-ref --stdin\" to update a\n+# reference without providing an old-oid.\n+#\n+# The differences are as follows:\n+#\n+#     @@ -1,8 +1,8 @@\n+#      ## Call hook: reference-transaction  prepared ##\n+#      <COMMIT-A> <COMMIT-B> refs/heads/topic2\n+#     -<COMMIT-A> <COMMIT-C> refs/heads/topic3\n+#     +<ZERO-OID> <COMMIT-C> refs/heads/topic3\n+#      <ZERO-OID> <COMMIT-C> refs/heads/topic4\n+#      ## Call hook: reference-transaction committed ##\n+#      <COMMIT-A> <COMMIT-B> refs/heads/topic2\n+#     -<COMMIT-A> <COMMIT-C> refs/heads/topic3\n+#     +<ZERO-OID> <COMMIT-C> refs/heads/topic3\n+#      <ZERO-OID> <COMMIT-C> refs/heads/topic4\n+test_expect_failure \"update-ref --stdin: update refs\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <COMMIT-B> refs/heads/topic2\n+\t\t<COMMIT-A> <COMMIT-C> refs/heads/topic3\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic4\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <COMMIT-B> refs/heads/topic2\n+\t\t<COMMIT-A> <COMMIT-C> refs/heads/topic3\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic4\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit update-ref --stdin <<-EOF\n+\t\t\tstart\n+\t\t\tupdate refs/heads/topic2 $B $A\n+\t\t\tupdate refs/heads/topic3 $C\n+\t\t\tcreate refs/heads/topic4 $C\n+\t\t\tprepare\n+\t\t\tcommit\n+\t\tEOF\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-A> refs/heads/topic1\n+\t\t<COMMIT-B> refs/heads/topic2\n+\t\t<COMMIT-C> refs/heads/topic3\n+\t\t<COMMIT-C> refs/heads/topic4\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+# Mismatched hook output when deleting refs using \"git update-refs\n+# --stdin\":\n+#\n+#  * The \"reference-transaction committed\" command was executed twice,\n+#    once for packed ref-store, and once for loose ref-store.\n+#\n+#  * The old-oid was not the expected old-oid, but <ZERO-OID> when\n+#    deleting a ref without providing the old-oid parameter.\n+#\n+# The differences are as follows:\n+#\n+#     @@ -4,14 +4,19 @@\n+#      <ZERO-OID> <ZERO-OID> refs/heads/topic3\n+#      <ZERO-OID> <ZERO-OID> refs/heads/topic4\n+#      ## Call hook: reference-transaction  prepared ##\n+#     -<COMMIT-A> <ZERO-OID> refs/heads/topic1\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n+#      <COMMIT-B> <ZERO-OID> refs/heads/topic2\n+#     -<COMMIT-C> <ZERO-OID> refs/heads/topic3\n+#     -<COMMIT-C> <ZERO-OID> refs/heads/topic4\n+#     -<COMMIT-A> <ZERO-OID> HEAD\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic4\n+#     +<ZERO-OID> <ZERO-OID> HEAD\n+#      ## Call hook: reference-transaction committed ##\n+#     -<COMMIT-A> <ZERO-OID> refs/heads/topic1\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic2\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic4\n+#     +## Call hook: reference-transaction committed ##\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n+#      <COMMIT-B> <ZERO-OID> refs/heads/topic2\n+#     -<COMMIT-C> <ZERO-OID> refs/heads/topic3\n+#     -<COMMIT-C> <ZERO-OID> refs/heads/topic4\n+#     -<COMMIT-A> <ZERO-OID> HEAD\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic4\n+#     +<ZERO-OID> <ZERO-OID> HEAD\n+test_expect_failure \"update-ref --stdin: delete refs\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <ZERO-OID> refs/heads/topic1\n+\t\t<ZERO-OID> <ZERO-OID> refs/heads/topic2\n+\t\t<ZERO-OID> <ZERO-OID> refs/heads/topic3\n+\t\t<ZERO-OID> <ZERO-OID> refs/heads/topic4\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <ZERO-OID> refs/heads/topic1\n+\t\t<COMMIT-B> <ZERO-OID> refs/heads/topic2\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic3\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic4\n+\t\t<COMMIT-A> <ZERO-OID> HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <ZERO-OID> refs/heads/topic1\n+\t\t<COMMIT-B> <ZERO-OID> refs/heads/topic2\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic3\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic4\n+\t\t<COMMIT-A> <ZERO-OID> HEAD\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit update-ref --stdin <<-EOF\n+\t\t\tstart\n+\t\t\tdelete refs/heads/topic1\n+\t\t\tdelete refs/heads/topic2 $B\n+\t\t\tdelete refs/heads/topic3\n+\t\t\tdelete refs/heads/topic4\n+\t\t\tprepare\n+\t\t\tcommit\n+\t\tEOF\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_success \"branch: setup workdir using git-fetch\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/remotes/origin/main\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/remotes/origin/main\n+\tEOF\n+\n+\trm -rf workdir &&\n+\tgit init workdir &&\n+\tgit -C workdir remote add origin ../base &&\n+\tgit -C workdir fetch origin &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/main\n+\t\t<ZERO-OID> <COMMIT-C> HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/main\n+\t\t<ZERO-OID> <COMMIT-C> HEAD\n+\tEOF\n+\n+\trm $HOOK_OUTPUT &&\n+\tgit -C workdir switch -c main origin/main &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_success \"branch: create new branches\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic1\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic1\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic2\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic2\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit branch topic1 $A &&\n+\t\tgit branch topic2 $A\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-A> refs/heads/topic1\n+\t\t<COMMIT-A> refs/heads/topic2\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+# Failed because the reference-transaction hook was executed even\n+# though no refs were changed by running git-gc.\n+test_expect_failure \"branch: prepare packed_ref_store using gc\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\tgit -C workdir gc &&\n+\ttest_path_is_file workdir/.git/packed-refs &&\n+\ttest_path_is_missing $HOOK_OUTPUT\n+'\n+\n+# Failed because the old-oid was not the expected old-oid, but\n+# <ZERO-OID> when running git-branch to update a branch without\n+# providing an old-oid.\n+#\n+# The differences are as follows:\n+#\n+#     @@ -1,7 +1,7 @@\n+#      ## Call hook: reference-transaction  prepared ##\n+#     -<COMMIT-A> <COMMIT-B> refs/heads/topic2\n+#     +<ZERO-OID> <COMMIT-B> refs/heads/topic2\n+#      ## Call hook: reference-transaction committed ##\n+#     -<COMMIT-A> <COMMIT-B> refs/heads/topic2\n+#     +<ZERO-OID> <COMMIT-B> refs/heads/topic2\n+#      ## Call hook: reference-transaction  prepared ##\n+#      <ZERO-OID> <COMMIT-C> refs/heads/topic3\n+#      ## Call hook: reference-transaction committed ##\n+test_expect_failure \"branch: update branch without old-oid\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <COMMIT-B> refs/heads/topic2\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <COMMIT-B> refs/heads/topic2\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic3\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic3\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit branch -f topic2 $B &&\n+\t\tgit branch topic3 $C\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-A> refs/heads/topic1\n+\t\t<COMMIT-B> refs/heads/topic2\n+\t\t<COMMIT-C> refs/heads/topic3\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+# Failed because the reference-transaction hook was not executed at all\n+# when copying a branch using \"git branch -c\".\n+test_expect_failure \"branch: copy branches\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-B> refs/heads/topic4\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-B> refs/heads/topic4\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic5\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic5\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit branch -c topic2 topic4 &&\n+\t\tgit branch -c topic3 topic5\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-A> refs/heads/topic1\n+\t\t<COMMIT-B> refs/heads/topic2\n+\t\t<COMMIT-C> refs/heads/topic3\n+\t\t<COMMIT-B> refs/heads/topic4\n+\t\t<COMMIT-C> refs/heads/topic5\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+# Mismatched hook output for \"git branch -m\":\n+#\n+#  * The \"reference-transaction committed\" command was not executed\n+#    for the target branch.\n+#\n+#  * Unexpected execution of the \"reference-transaction abort\" command.\n+#\n+# The differences are as follows:\n+#\n+#     @@ -1,16 +1,12 @@\n+#     +## Call hook: reference-transaction   aborted ##\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic4\n+#      ## Call hook: reference-transaction  prepared ##\n+#      <COMMIT-B> <ZERO-OID> refs/heads/topic4\n+#      ## Call hook: reference-transaction committed ##\n+#      <COMMIT-B> <ZERO-OID> refs/heads/topic4\n+#     -## Call hook: reference-transaction  prepared ##\n+#     -<ZERO-OID> <COMMIT-B> refs/heads/topic6\n+#     -## Call hook: reference-transaction committed ##\n+#     -<ZERO-OID> <COMMIT-B> refs/heads/topic6\n+#     +## Call hook: reference-transaction   aborted ##\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic5\n+#      ## Call hook: reference-transaction  prepared ##\n+#      <COMMIT-C> <ZERO-OID> refs/heads/topic5\n+#      ## Call hook: reference-transaction committed ##\n+#      <COMMIT-C> <ZERO-OID> refs/heads/topic5\n+#     -## Call hook: reference-transaction  prepared ##\n+#     -<ZERO-OID> <COMMIT-C> refs/heads/topic7\n+#     -## Call hook: reference-transaction committed ##\n+#     -<ZERO-OID> <COMMIT-C> refs/heads/topic7\n+test_expect_failure \"branch: rename branches\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-B> <ZERO-OID> refs/heads/topic4\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-B> <ZERO-OID> refs/heads/topic4\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-B> refs/heads/topic6\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-B> refs/heads/topic6\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic5\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic5\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic7\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic7\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit branch -m topic4 topic6 &&\n+\t\tgit branch -m topic5 topic7\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-A> refs/heads/topic1\n+\t\t<COMMIT-B> refs/heads/topic2\n+\t\t<COMMIT-C> refs/heads/topic3\n+\t\t<COMMIT-B> refs/heads/topic6\n+\t\t<COMMIT-C> refs/heads/topic7\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+# Mismatched hook output for \"git branch -d\":\n+#\n+#  * The old-oid was not the expected old-oid, but <ZERO-OID> when\n+#    deleting a branch without providing the old-oid parameter.\n+#\n+#  * The delete branches operation should be treated as one transaction,\n+#    but was splitted into several transactions on loose references,\n+#    and the \"reference-transaction committed\" command was executed\n+#    redundantly on the packed-ref-store.\n+#\n+#  * Unexpected execution of the \"reference-transaction abort\" command.\n+#\n+# The differences are as follows:\n+#\n+#     @@ -2,11 +2,25 @@\n+#      <ZERO-OID> <ZERO-OID> refs/heads/topic1\n+#      <ZERO-OID> <ZERO-OID> refs/heads/topic2\n+#      <ZERO-OID> <ZERO-OID> refs/heads/topic3\n+#     +## Call hook: reference-transaction committed ##\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic2\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n+#     +## Call hook: reference-transaction   aborted ##\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n+#      ## Call hook: reference-transaction  prepared ##\n+#     -<COMMIT-A> <ZERO-OID> refs/heads/topic1\n+#     -<COMMIT-B> <ZERO-OID> refs/heads/topic2\n+#     -<COMMIT-C> <ZERO-OID> refs/heads/topic3\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n+#      ## Call hook: reference-transaction committed ##\n+#     -<COMMIT-A> <ZERO-OID> refs/heads/topic1\n+#     -<COMMIT-B> <ZERO-OID> refs/heads/topic2\n+#     -<COMMIT-C> <ZERO-OID> refs/heads/topic3\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n+#     +## Call hook: reference-transaction   aborted ##\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic2\n+#     +## Call hook: reference-transaction  prepared ##\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic2\n+#     +## Call hook: reference-transaction committed ##\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic2\n+#     +## Call hook: reference-transaction   aborted ##\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n+#     +## Call hook: reference-transaction  prepared ##\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n+#     +## Call hook: reference-transaction committed ##\n+#     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n+test_expect_failure \"branch: remove branches\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <ZERO-OID> refs/heads/topic1\n+\t\t<ZERO-OID> <ZERO-OID> refs/heads/topic2\n+\t\t<ZERO-OID> <ZERO-OID> refs/heads/topic3\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <ZERO-OID> refs/heads/topic1\n+\t\t<COMMIT-B> <ZERO-OID> refs/heads/topic2\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic3\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <ZERO-OID> refs/heads/topic1\n+\t\t<COMMIT-B> <ZERO-OID> refs/heads/topic2\n+\t\t<COMMIT-C> <ZERO-OID> refs/heads/topic3\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit branch -d topic1 topic2 topic3\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-B> refs/heads/topic6\n+\t\t<COMMIT-C> refs/heads/topic7\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_success \"tag: setup workdir using git-push\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/main\n+\t\t<ZERO-OID> <COMMIT-C> HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/main\n+\t\t<ZERO-OID> <COMMIT-C> HEAD\n+\tEOF\n+\n+\trm -rf workdir &&\n+\tgit init workdir &&\n+\tgit -C workdir config receive.denyCurrentBranch ignore &&\n+\tgit -C base push ../workdir \"+refs/heads/*:refs/heads/*\" &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect &&\n+\n+\tgit -C workdir restore --staged -- . &&\n+\tgit -C workdir restore -- .\n+'\n+\n+test_expect_success \"tag: create new tags\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/tags/v1\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/tags/v1\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/tags/v2\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/tags/v2\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit tag v1 $A &&\n+\t\tgit tag v2 $A\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-A> refs/tags/v1\n+\t\t<COMMIT-A> refs/tags/v2\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+# Failed because the reference-transaction hook was executed even\n+# though no refs were changed by running git-pack-refs.\n+test_expect_failure \"tag: prepare packed_ref_store using pack-refs\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\tgit -C workdir pack-refs --all &&\n+\ttest_path_is_file workdir/.git/packed-refs &&\n+\ttest_path_is_missing $HOOK_OUTPUT\n+'\n+\n+test_expect_success \"tag: update refs to create loose refs\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <COMMIT-B> refs/tags/v2\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <COMMIT-B> refs/tags/v2\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/tags/v3\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/tags/v3\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit tag -f v2 $B &&\n+\t\tgit tag v3 $C\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-A> refs/tags/v1\n+\t\t<COMMIT-B> refs/tags/v2\n+\t\t<COMMIT-C> refs/tags/v3\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+# Mismatched hook output for \"git tag -d\":\n+#\n+#  * The old-oid was not the expected old-oid, but <ZERO-OID> when\n+#    deleting a tag without providing the old-oid parameter.\n+#\n+#  * The delete tags operation should be treated as one transaction,\n+#    but was splitted into several transactions on loose references,\n+#    and the \"reference-transaction committed\" command was executed\n+#    redundantly on the packed-ref-store.\n+#\n+#  * Unexpected execution of the \"reference-transaction abort\" command.\n+#\n+# The differences are as follows:\n+#\n+#     @@ -2,11 +2,25 @@\n+#      <ZERO-OID> <ZERO-OID> refs/tags/v1\n+#      <ZERO-OID> <ZERO-OID> refs/tags/v2\n+#      <ZERO-OID> <ZERO-OID> refs/tags/v3\n+#     +## Call hook: reference-transaction committed ##\n+#     +<ZERO-OID> <ZERO-OID> refs/tags/v1\n+#     +<ZERO-OID> <ZERO-OID> refs/tags/v2\n+#     +<ZERO-OID> <ZERO-OID> refs/tags/v3\n+#     +## Call hook: reference-transaction   aborted ##\n+#     +<ZERO-OID> <ZERO-OID> refs/tags/v1\n+#      ## Call hook: reference-transaction  prepared ##\n+#     -<COMMIT-A> <ZERO-OID> refs/tags/v1\n+#     -<COMMIT-B> <ZERO-OID> refs/tags/v2\n+#     -<COMMIT-C> <ZERO-OID> refs/tags/v3\n+#     +<ZERO-OID> <ZERO-OID> refs/tags/v1\n+#      ## Call hook: reference-transaction committed ##\n+#     -<COMMIT-A> <ZERO-OID> refs/tags/v1\n+#     -<COMMIT-B> <ZERO-OID> refs/tags/v2\n+#     -<COMMIT-C> <ZERO-OID> refs/tags/v3\n+#     +<ZERO-OID> <ZERO-OID> refs/tags/v1\n+#     +## Call hook: reference-transaction   aborted ##\n+#     +<ZERO-OID> <ZERO-OID> refs/tags/v2\n+#     +## Call hook: reference-transaction  prepared ##\n+#     +<ZERO-OID> <ZERO-OID> refs/tags/v2\n+#     +## Call hook: reference-transaction committed ##\n+#     +<ZERO-OID> <ZERO-OID> refs/tags/v2\n+#     +## Call hook: reference-transaction   aborted ##\n+#     +<ZERO-OID> <ZERO-OID> refs/tags/v3\n+#     +## Call hook: reference-transaction  prepared ##\n+#     +<ZERO-OID> <ZERO-OID> refs/tags/v3\n+#     +## Call hook: reference-transaction committed ##\n+#     +<ZERO-OID> <ZERO-OID> refs/tags/v3\n+test_expect_failure \"tag: remove tags with mixed ref_stores\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <ZERO-OID> refs/tags/v1\n+\t\t<ZERO-OID> <ZERO-OID> refs/tags/v2\n+\t\t<ZERO-OID> <ZERO-OID> refs/tags/v3\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <ZERO-OID> refs/tags/v1\n+\t\t<COMMIT-B> <ZERO-OID> refs/tags/v2\n+\t\t<COMMIT-C> <ZERO-OID> refs/tags/v3\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <ZERO-OID> refs/tags/v1\n+\t\t<COMMIT-B> <ZERO-OID> refs/tags/v2\n+\t\t<COMMIT-C> <ZERO-OID> refs/tags/v3\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit tag -d v1 v2 v3\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_success \"worktree: setup workdir using push --atomic\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/main\n+\t\t<ZERO-OID> <COMMIT-C> HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/main\n+\t\t<ZERO-OID> <COMMIT-C> HEAD\n+\tEOF\n+\n+\trm -rf workdir &&\n+\tgit init --bare repo.git &&\n+\tgit -C base push --atomic --mirror ../repo.git &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\trm $HOOK_OUTPUT &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/remotes/origin/main\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/remotes/origin/main\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> HEAD\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/main\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> HEAD\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/main\n+\tEOF\n+\n+\tgit clone --no-local repo.git workdir &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_success \"worktree: topic1: commit --amend\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic1\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic1\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-C> <COMMIT-D> HEAD\n+\t\t<COMMIT-C> <COMMIT-D> refs/heads/topic1\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-C> <COMMIT-D> HEAD\n+\t\t<COMMIT-C> <COMMIT-D> refs/heads/topic1\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit checkout -b topic1 &&\n+\t\tgit commit --amend -m \"C (amend)\"\n+\t) &&\n+\tD=$(git -C workdir rev-parse HEAD) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-D> refs/heads/topic1\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_success \"worktree: topic2: merge\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic2\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic2\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> ORIG_HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> ORIG_HEAD\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <COMMIT-E> HEAD\n+\t\t<COMMIT-A> <COMMIT-E> refs/heads/topic2\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <COMMIT-E> HEAD\n+\t\t<COMMIT-A> <COMMIT-E> refs/heads/topic2\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit checkout -b topic2 $A &&\n+\t\tgit merge --no-ff main &&\n+\t\ttest_path_is_file B.t &&\n+\t\ttest_path_is_file C.t\n+\t) &&\n+\tE=$(git -C workdir rev-parse HEAD) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-D> refs/heads/topic1\n+\t\t<COMMIT-E> refs/heads/topic2\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+# Mismatched hook output for git-cherry-pick:\n+#\n+#  * The old-oid was not the expected old-oid, but <ZERO-OID>.\n+#\n+#  * Unexpected execution of the \"reference-transaction abort\" command.\n+#\n+# The differences are as follows:\n+#\n+#     @@ -12,7 +12,9 @@\n+#      ## Call hook: reference-transaction committed ##\n+#      <COMMIT-A> <COMMIT-F> HEAD\n+#      <COMMIT-A> <COMMIT-F> refs/heads/topic3\n+#     +## Call hook: reference-transaction   aborted ##\n+#     +<ZERO-OID> <ZERO-OID> CHERRY_PICK_HEAD\n+#      ## Call hook: reference-transaction  prepared ##\n+#     -<COMMIT-C> <ZERO-OID> CHERRY_PICK_HEAD\n+#     +<ZERO-OID> <ZERO-OID> CHERRY_PICK_HEAD\n+#      ## Call hook: reference-transaction committed ##\n+#     -<COMMIT-C> <ZERO-OID> CHERRY_PICK_HEAD\n+#     +<ZERO-OID> <ZERO-OID> CHERRY_PICK_HEAD\n+test_expect_failure \"worktree: topic3: cherry-pick\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic3\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-A> refs/heads/topic3\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> CHERRY_PICK_HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> CHERRY_PICK_HEAD\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <COMMIT-F> HEAD\n+\t\t<COMMIT-A> <COMMIT-F> refs/heads/topic3\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <COMMIT-F> HEAD\n+\t\t<COMMIT-A> <COMMIT-F> refs/heads/topic3\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-C> <ZERO-OID> CHERRY_PICK_HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-C> <ZERO-OID> CHERRY_PICK_HEAD\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit checkout -b topic3 $A &&\n+\t\tgit cherry-pick $C &&\n+\t\ttest_path_is_file C.t &&\n+\t\ttest_path_is_missing B.t\n+\t) &&\n+\tF=$(git -C workdir rev-parse HEAD) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-D> refs/heads/topic1\n+\t\t<COMMIT-E> refs/heads/topic2\n+\t\t<COMMIT-F> refs/heads/topic3\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+# Mismatched hook output for git-rebase:\n+#\n+#  * The old-oid was not the expected old-oid, but <ZERO-OID>.\n+#\n+#  * Unexpected execution of the \"reference-transaction abort\" command.\n+#\n+# The differences are as follows:\n+#\n+#     @@ -6,6 +6,8 @@\n+#      <COMMIT-G> <COMMIT-C> HEAD\n+#      ## Call hook: reference-transaction committed ##\n+#      <COMMIT-G> <COMMIT-C> HEAD\n+#     +## Call hook: reference-transaction   aborted ##\n+#     +<ZERO-OID> <ZERO-OID> REBASE_HEAD\n+#      ## Call hook: reference-transaction  prepared ##\n+#      <ZERO-OID> <ZERO-OID> REBASE_HEAD\n+#      ## Call hook: reference-transaction committed ##\n+#     @@ -18,10 +20,12 @@\n+#      <COMMIT-C> <COMMIT-H> HEAD\n+#      ## Call hook: reference-transaction committed ##\n+#      <COMMIT-C> <COMMIT-H> HEAD\n+#     +## Call hook: reference-transaction   aborted ##\n+#     +<ZERO-OID> <ZERO-OID> CHERRY_PICK_HEAD\n+#      ## Call hook: reference-transaction  prepared ##\n+#     -<COMMIT-G> <ZERO-OID> CHERRY_PICK_HEAD\n+#     +<ZERO-OID> <ZERO-OID> CHERRY_PICK_HEAD\n+#      ## Call hook: reference-transaction committed ##\n+#     -<COMMIT-G> <ZERO-OID> CHERRY_PICK_HEAD\n+#     +<ZERO-OID> <ZERO-OID> CHERRY_PICK_HEAD\n+#      ## Call hook: reference-transaction  prepared ##\n+#      <COMMIT-G> <COMMIT-H> refs/heads/topic4\n+#      ## Call hook: reference-transaction committed ##\n+test_expect_failure \"worktree: topic4: rebase\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-A> <COMMIT-G> ORIG_HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-A> <COMMIT-G> ORIG_HEAD\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-G> <COMMIT-C> HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-G> <COMMIT-C> HEAD\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <ZERO-OID> REBASE_HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <ZERO-OID> REBASE_HEAD\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-G> CHERRY_PICK_HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-G> CHERRY_PICK_HEAD\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-C> <COMMIT-H> HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-C> <COMMIT-H> HEAD\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-G> <ZERO-OID> CHERRY_PICK_HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-G> <ZERO-OID> CHERRY_PICK_HEAD\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-G> <COMMIT-H> refs/heads/topic4\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-G> <COMMIT-H> refs/heads/topic4\n+\tEOF\n+\n+\tgit -C workdir checkout -b topic4 $A &&\n+\tcreate_commits_in workdir G &&\n+\trm $HOOK_OUTPUT &&\n+\tgit -C workdir rebase main &&\n+\tH=$(git -C workdir rev-parse HEAD) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-D> refs/heads/topic1\n+\t\t<COMMIT-E> refs/heads/topic2\n+\t\t<COMMIT-F> refs/heads/topic3\n+\t\t<COMMIT-H> refs/heads/topic4\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+# Mismatched hook output for git-revert:\n+#\n+#  * Unexpected execution of the \"reference-transaction abort\" command.\n+#\n+# The differences are as follows:\n+#\n+#     @@ -8,6 +8,8 @@\n+#      ## Call hook: reference-transaction committed ##\n+#      <COMMIT-C> <COMMIT-I> HEAD\n+#      <COMMIT-C> <COMMIT-I> refs/heads/topic5\n+#     +## Call hook: reference-transaction   aborted ##\n+#     +<ZERO-OID> <ZERO-OID> CHERRY_PICK_HEAD\n+#      ## Call hook: reference-transaction  prepared ##\n+#      <ZERO-OID> <ZERO-OID> CHERRY_PICK_HEAD\n+#      ## Call hook: reference-transaction committed ##\n+test_expect_failure \"worktree: topic5: revert\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic5\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic5\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-C> <COMMIT-I> HEAD\n+\t\t<COMMIT-C> <COMMIT-I> refs/heads/topic5\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-C> <COMMIT-I> HEAD\n+\t\t<COMMIT-C> <COMMIT-I> refs/heads/topic5\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <ZERO-OID> CHERRY_PICK_HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <ZERO-OID> CHERRY_PICK_HEAD\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit checkout -b topic5 $C &&\n+\t\tgit revert HEAD &&\n+\t\ttest_path_is_file B.t &&\n+\t\ttest_path_is_missing C.t\n+\t) &&\n+\tI=$(git -C workdir rev-parse HEAD) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-D> refs/heads/topic1\n+\t\t<COMMIT-E> refs/heads/topic2\n+\t\t<COMMIT-F> refs/heads/topic3\n+\t\t<COMMIT-H> refs/heads/topic4\n+\t\t<COMMIT-I> refs/heads/topic5\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n+test_expect_success \"worktree: topic6: reset\" '\n+\ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic6\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<ZERO-OID> <COMMIT-C> refs/heads/topic6\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-G> <COMMIT-C> ORIG_HEAD\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-G> <COMMIT-C> ORIG_HEAD\n+\t\t## Call hook: reference-transaction  prepared ##\n+\t\t<COMMIT-C> <COMMIT-B> HEAD\n+\t\t<COMMIT-C> <COMMIT-B> refs/heads/topic6\n+\t\t## Call hook: reference-transaction committed ##\n+\t\t<COMMIT-C> <COMMIT-B> HEAD\n+\t\t<COMMIT-C> <COMMIT-B> refs/heads/topic6\n+\tEOF\n+\n+\t(\n+\t\tcd workdir &&\n+\t\tgit checkout -b topic6 $C &&\n+\t\tgit reset --hard $B\n+\t) &&\n+\tmake_user_friendly_and_stable_output <$HOOK_OUTPUT >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tcat >expect <<-\\EOF &&\n+\t\t<COMMIT-C> refs/heads/main\n+\t\t<COMMIT-D> refs/heads/topic1\n+\t\t<COMMIT-E> refs/heads/topic2\n+\t\t<COMMIT-F> refs/heads/topic3\n+\t\t<COMMIT-H> refs/heads/topic4\n+\t\t<COMMIT-I> refs/heads/topic5\n+\t\t<COMMIT-B> refs/heads/topic6\n+\tEOF\n+\ttest_cmp_heads_and_tags -C workdir expect\n+'\n+\n test_done\n-- \n2.36.1.25.gc87d5ad63a.dirty\n\n"},{"id":"461494","messageId":"20220819032147.28841-5-worldhello.net@gmail.com","threadId":"58240","inReplyTo":"CANYiYbFw71bX827akAG87RSKOozPk313Hoe573O9dQ65_U6sLQ@mail.gmail.com","subject":"[PATCH v2 4/9] refs: do not run transaction hook for git-pack-refs","fromName":"Jiang Xin","fromEmail":"worldhello.net@gmail.com","sentAt":"2022-08-19T03:21:42Z","receivedAt":"2022-08-19T03:22:13Z","isPatch":true,"sender":{"key":"worldhello.net@gmail.com","avatar":"https://avatars.githubusercontent.com/u/183860?v=4"},"body":"From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n\nThe git-pack-refs command will call \"files_pack_refs()\" to pack loose\nreferences into the \"packed-refs\" file, and there are no real changes\nto the repository. Therefore, by initializing a transaction with an\nempty \"hook_flags\" field, the \"reference-transaction\" hook will not run.\n\nThe \"prune_refs()\" and \"prune_ref()\" functions are called from\n\"file_pack_refs()\", and they are used to prune loose refereces which\nhave already been packed into \"packed-refs\". The transaction instance\nin \"prune_ref()\" should also be initialized with an empty \"hook_flags\"\nfield.\n\nThe behavior of the following git commands and three testcases have been\nfixed in t1416:\n\n * git gc\n * git pack-refs\n\nSigned-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n---\n refs/files-backend.c             | 10 ++++++++--\n t/t1416-ref-transaction-hooks.sh | 12 +++---------\n 2 files changed, 11 insertions(+), 11 deletions(-)\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 7c1c25a25c..f0947c9dca 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -1136,7 +1136,8 @@ static void prune_ref(struct files_ref_store *refs, struct ref_to_prune *r)\n \tif (check_refname_format(r->name, 0))\n \t\treturn;\n \n-\ttransaction = ref_store_transaction_begin(&refs->base, &err);\n+\t/* Called by \"files_pack_refs()\" and should not run any hooks. */\n+\ttransaction = ref_store_transaction_begin_extended(&refs->base, 0, &err);\n \tif (!transaction)\n \t\tgoto cleanup;\n \tref_transaction_add_update(\n@@ -1207,7 +1208,12 @@ static int files_pack_refs(struct ref_store *ref_store, unsigned int flags)\n \tstruct strbuf err = STRBUF_INIT;\n \tstruct ref_transaction *transaction;\n \n-\ttransaction = ref_store_transaction_begin(refs->packed_ref_store, &err);\n+\t/*\n+\t * No real changes have occurred to the repository and no hooks\n+\t * should be run for this transaction.\n+\t */\n+\ttransaction = ref_store_transaction_begin_extended(refs->packed_ref_store,\n+\t\t\t\t\t\t\t   0, &err);\n \tif (!transaction)\n \t\treturn -1;\n \ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex 1ae601a73d..b7ffc9415b 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -392,9 +392,7 @@ test_expect_success \"update-ref: update HEAD\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-# Failed because the reference-transaction hook was executed even\n-# though no refs were changed by running git-pack-refs.\n-test_expect_failure \"update-ref: prepare packed_ref_store using pack-refs\" '\n+test_expect_success \"update-ref: prepare packed_ref_store using pack-refs\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \tgit -C workdir pack-refs --all &&\n \ttest_path_is_file workdir/.git/packed-refs &&\n@@ -737,9 +735,7 @@ test_expect_success \"branch: create new branches\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-# Failed because the reference-transaction hook was executed even\n-# though no refs were changed by running git-gc.\n-test_expect_failure \"branch: prepare packed_ref_store using gc\" '\n+test_expect_success \"branch: prepare packed_ref_store using gc\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \tgit -C workdir gc &&\n \ttest_path_is_file workdir/.git/packed-refs &&\n@@ -1014,9 +1010,7 @@ test_expect_success \"tag: create new tags\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-# Failed because the reference-transaction hook was executed even\n-# though no refs were changed by running git-pack-refs.\n-test_expect_failure \"tag: prepare packed_ref_store using pack-refs\" '\n+test_expect_success \"tag: prepare packed_ref_store using pack-refs\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \tgit -C workdir pack-refs --all &&\n \ttest_path_is_file workdir/.git/packed-refs &&\n-- \n2.36.1.25.gc87d5ad63a.dirty\n\n"},{"id":"461495","messageId":"20220819032147.28841-6-worldhello.net@gmail.com","threadId":"58240","inReplyTo":"CANYiYbFw71bX827akAG87RSKOozPk313Hoe573O9dQ65_U6sLQ@mail.gmail.com","subject":"[PATCH v2 5/9] refs: avoid duplicate running of the reference-transaction hook","fromName":"Jiang Xin","fromEmail":"worldhello.net@gmail.com","sentAt":"2022-08-19T03:21:43Z","receivedAt":"2022-08-19T03:22:16Z","isPatch":true,"sender":{"key":"worldhello.net@gmail.com","avatar":"https://avatars.githubusercontent.com/u/183860?v=4"},"body":"From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n\nIf there are references to be deleted in a transaction, we should remove\neach reference from both loose references and the \"packed-refs\" file.\nThe \"reference-transaction\" hook will run twice, once for the primary\nref-store (loose ref-store), and another for the second ref-store (i.e.\npacked ref-store).\n\nTo avoid duplicate running of the \"reference-trancaction\" hook, we pass\na special \"hook-flags\" parameter to initialize the second ref-store.\nThe \"REF_TRANSACTION_RUN_PREPARED_HOOK\" bit is preserved for the\ntransaction of the second ref-store because we may still want to call\ncommand \"reference-trancaction prepared\" to do some checks. E.g.: We\ncan create a global lock file (such as \"site.lock\") to disable writing\npermissions for one or multiple repositories.\n\nThe behavior of the following git commands and five testcases have been\nfixed in t1416:\n\n * git cherry-pick\n * git rebase\n * git revert\n * git update-ref -d <ref>\n * git update-ref --stdin\t# delete refs\n\nSigned-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n---\n refs/files-backend.c             |   7 +-\n t/t1416-ref-transaction-hooks.sh | 156 ++-----------------------------\n 2 files changed, 15 insertions(+), 148 deletions(-)\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex f0947c9dca..9ec4c23a16 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2797,8 +2797,11 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n \t\t\t * packed-refs if it exists there.\n \t\t\t */\n \t\t\tif (!packed_transaction) {\n-\t\t\t\tpacked_transaction = ref_store_transaction_begin(\n-\t\t\t\t\t\trefs->packed_ref_store, err);\n+\t\t\t\tpacked_transaction = ref_store_transaction_begin_extended(\n+\t\t\t\t\t\trefs->packed_ref_store,\n+\t\t\t\t\t\ttransaction->hook_flags &\n+\t\t\t\t\t\t\tREF_TRANSACTION_RUN_PREPARED_HOOK,\n+\t\t\t\t\t\terr);\n \t\t\t\tif (!packed_transaction) {\n \t\t\t\t\tret = TRANSACTION_GENERIC_ERROR;\n \t\t\t\t\tgoto cleanup;\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex b7ffc9415b..6ba7ba746c 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -441,45 +441,7 @@ test_expect_success \"update-ref: update refs already in packed_ref_store\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-# Mismatched hook output when deleting refs using \"git update-refs -d\":\n-#\n-#  * The \"reference-transaction committed\" command was executed twice,\n-#    once for packed ref-store, and once for loose ref-store.\n-#\n-#  * Unexpected execution of the \"reference-transaction abort\" command.\n-#\n-# The differences are as follows:\n-#\n-#     @@ -4,6 +4,8 @@\n-#      <COMMIT-A> <ZERO-OID> refs/heads/topic1\n-#      <COMMIT-A> <ZERO-OID> HEAD\n-#      ## Call hook: reference-transaction committed ##\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n-#     +## Call hook: reference-transaction committed ##\n-#      <COMMIT-A> <ZERO-OID> refs/heads/topic1\n-#      <COMMIT-A> <ZERO-OID> HEAD\n-#      ## Call hook: reference-transaction  prepared ##\n-#     @@ -11,13 +13,19 @@\n-#      ## Call hook: reference-transaction  prepared ##\n-#      <COMMIT-B> <ZERO-OID> refs/heads/topic2\n-#      ## Call hook: reference-transaction committed ##\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic2\n-#     +## Call hook: reference-transaction committed ##\n-#      <COMMIT-B> <ZERO-OID> refs/heads/topic2\n-#      ## Call hook: reference-transaction  prepared ##\n-#      <ZERO-OID> <ZERO-OID> refs/heads/topic3\n-#      ## Call hook: reference-transaction  prepared ##\n-#      <COMMIT-C> <ZERO-OID> refs/heads/topic3\n-#      ## Call hook: reference-transaction committed ##\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n-#     +## Call hook: reference-transaction committed ##\n-#      <COMMIT-C> <ZERO-OID> refs/heads/topic3\n-#     +## Call hook: reference-transaction   aborted ##\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic4\n-#      ## Call hook: reference-transaction  prepared ##\n-#      <COMMIT-C> <ZERO-OID> refs/heads/topic4\n-#      ## Call hook: reference-transaction committed ##\n-test_expect_failure \"update-ref: remove refs with mixed ref_stores\" '\n+test_expect_success \"update-ref: remove refs with mixed ref_stores\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-\\EOF &&\n@@ -604,27 +566,7 @@ test_expect_success \"update-ref --stdin: update refs\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-# Mismatched hook output when deleting refs using \"git update-refs\n-# --stdin\":\n-#\n-#  * The \"reference-transaction committed\" command was executed twice,\n-#    once for packed ref-store, and once for loose ref-store.\n-#\n-# The differences are as follows:\n-#\n-#     @@ -10,6 +10,11 @@\n-#      <COMMIT-C> <ZERO-OID> refs/heads/topic4\n-#      <COMMIT-A> <ZERO-OID> HEAD\n-#      ## Call hook: reference-transaction committed ##\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic2\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic4\n-#     +## Call hook: reference-transaction committed ##\n-#      <COMMIT-A> <ZERO-OID> refs/heads/topic1\n-#      <COMMIT-B> <ZERO-OID> refs/heads/topic2\n-#      <COMMIT-C> <ZERO-OID> refs/heads/topic3\n-test_expect_failure \"update-ref --stdin: delete refs\" '\n+test_expect_success \"update-ref --stdin: delete refs\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-\\EOF &&\n@@ -813,24 +755,16 @@ test_expect_failure \"branch: copy branches\" '\n #  * The \"reference-transaction committed\" command was not executed\n #    for the target branch.\n #\n-#  * Unexpected execution of the \"reference-transaction abort\" command.\n-#\n # The differences are as follows:\n #\n-#     @@ -1,16 +1,12 @@\n-#     +## Call hook: reference-transaction   aborted ##\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic4\n-#      ## Call hook: reference-transaction  prepared ##\n-#      <COMMIT-B> <ZERO-OID> refs/heads/topic4\n+#     @@ -3,14 +3,6 @@\n #      ## Call hook: reference-transaction committed ##\n #      <COMMIT-B> <ZERO-OID> refs/heads/topic4\n-#     -## Call hook: reference-transaction  prepared ##\n+#      ## Call hook: reference-transaction  prepared ##\n #     -<ZERO-OID> <COMMIT-B> refs/heads/topic6\n #     -## Call hook: reference-transaction committed ##\n #     -<ZERO-OID> <COMMIT-B> refs/heads/topic6\n-#     +## Call hook: reference-transaction   aborted ##\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic5\n-#      ## Call hook: reference-transaction  prepared ##\n+#     -## Call hook: reference-transaction  prepared ##\n #      <COMMIT-C> <ZERO-OID> refs/heads/topic5\n #      ## Call hook: reference-transaction committed ##\n #      <COMMIT-C> <ZERO-OID> refs/heads/topic5\n@@ -886,11 +820,9 @@ test_expect_failure \"branch: rename branches\" '\n #    and the \"reference-transaction committed\" command was executed\n #    redundantly on the packed-ref-store.\n #\n-#  * Unexpected execution of the \"reference-transaction abort\" command.\n-#\n # The differences are as follows:\n #\n-#     @@ -2,11 +2,25 @@\n+#     @@ -2,11 +2,19 @@\n #      <ZERO-OID> <ZERO-OID> refs/heads/topic1\n #      <ZERO-OID> <ZERO-OID> refs/heads/topic2\n #      <ZERO-OID> <ZERO-OID> refs/heads/topic3\n@@ -898,14 +830,10 @@ test_expect_failure \"branch: rename branches\" '\n #     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n #     +<ZERO-OID> <ZERO-OID> refs/heads/topic2\n #     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n-#     +## Call hook: reference-transaction   aborted ##\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n #     +## Call hook: reference-transaction  prepared ##\n #     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n #     +## Call hook: reference-transaction committed ##\n #     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n-#     +## Call hook: reference-transaction   aborted ##\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic2\n #      ## Call hook: reference-transaction  prepared ##\n #     -<COMMIT-A> <ZERO-OID> refs/heads/topic1\n #      <COMMIT-B> <ZERO-OID> refs/heads/topic2\n@@ -913,8 +841,6 @@ test_expect_failure \"branch: rename branches\" '\n #      ## Call hook: reference-transaction committed ##\n #     -<COMMIT-A> <ZERO-OID> refs/heads/topic1\n #      <COMMIT-B> <ZERO-OID> refs/heads/topic2\n-#     +## Call hook: reference-transaction   aborted ##\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n #     +## Call hook: reference-transaction  prepared ##\n #     +<COMMIT-C> <ZERO-OID> refs/heads/topic3\n #     +## Call hook: reference-transaction committed ##\n@@ -1055,11 +981,9 @@ test_expect_success \"tag: update refs to create loose refs\" '\n #    and the \"reference-transaction committed\" command was executed\n #    redundantly on the packed-ref-store.\n #\n-#  * Unexpected execution of the \"reference-transaction abort\" command.\n-#\n # The differences are as follows:\n #\n-#     @@ -2,11 +2,25 @@\n+#     @@ -2,11 +2,19 @@\n #      <ZERO-OID> <ZERO-OID> refs/tags/v1\n #      <ZERO-OID> <ZERO-OID> refs/tags/v2\n #      <ZERO-OID> <ZERO-OID> refs/tags/v3\n@@ -1067,14 +991,10 @@ test_expect_success \"tag: update refs to create loose refs\" '\n #     +<ZERO-OID> <ZERO-OID> refs/tags/v1\n #     +<ZERO-OID> <ZERO-OID> refs/tags/v2\n #     +<ZERO-OID> <ZERO-OID> refs/tags/v3\n-#     +## Call hook: reference-transaction   aborted ##\n-#     +<ZERO-OID> <ZERO-OID> refs/tags/v1\n #     +## Call hook: reference-transaction  prepared ##\n #     +<ZERO-OID> <ZERO-OID> refs/tags/v1\n #     +## Call hook: reference-transaction committed ##\n #     +<ZERO-OID> <ZERO-OID> refs/tags/v1\n-#     +## Call hook: reference-transaction   aborted ##\n-#     +<ZERO-OID> <ZERO-OID> refs/tags/v2\n #      ## Call hook: reference-transaction  prepared ##\n #     -<COMMIT-A> <ZERO-OID> refs/tags/v1\n #      <COMMIT-B> <ZERO-OID> refs/tags/v2\n@@ -1082,8 +1002,6 @@ test_expect_success \"tag: update refs to create loose refs\" '\n #      ## Call hook: reference-transaction committed ##\n #     -<COMMIT-A> <ZERO-OID> refs/tags/v1\n #      <COMMIT-B> <ZERO-OID> refs/tags/v2\n-#     +## Call hook: reference-transaction   aborted ##\n-#     +<ZERO-OID> <ZERO-OID> refs/tags/v3\n #     +## Call hook: reference-transaction  prepared ##\n #     +<COMMIT-C> <ZERO-OID> refs/tags/v3\n #     +## Call hook: reference-transaction committed ##\n@@ -1232,22 +1150,7 @@ test_expect_success \"worktree: topic2: merge\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-# Mismatched hook output for git-cherry-pick:\n-#\n-#  * Unexpected execution of the \"reference-transaction abort\" command.\n-#\n-# The differences are as follows:\n-#\n-#     @@ -12,6 +12,8 @@\n-#      ## Call hook: reference-transaction committed ##\n-#      <COMMIT-A> <COMMIT-F> HEAD\n-#      <COMMIT-A> <COMMIT-F> refs/heads/topic3\n-#     +## Call hook: reference-transaction   aborted ##\n-#     +<ZERO-OID> <ZERO-OID> CHERRY_PICK_HEAD\n-#      ## Call hook: reference-transaction  prepared ##\n-#      <COMMIT-C> <ZERO-OID> CHERRY_PICK_HEAD\n-#      ## Call hook: reference-transaction committed ##\n-test_expect_failure \"worktree: topic3: cherry-pick\" '\n+test_expect_success \"worktree: topic3: cherry-pick\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-\\EOF &&\n@@ -1291,31 +1194,7 @@ test_expect_failure \"worktree: topic3: cherry-pick\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-# Mismatched hook output for git-rebase:\n-#\n-#  * Unexpected execution of the \"reference-transaction abort\" command.\n-#\n-# The differences are as follows:\n-#\n-#     @@ -6,6 +6,8 @@\n-#      <COMMIT-G> <COMMIT-C> HEAD\n-#      ## Call hook: reference-transaction committed ##\n-#      <COMMIT-G> <COMMIT-C> HEAD\n-#     +## Call hook: reference-transaction   aborted ##\n-#     +<ZERO-OID> <ZERO-OID> REBASE_HEAD\n-#      ## Call hook: reference-transaction  prepared ##\n-#      <ZERO-OID> <ZERO-OID> REBASE_HEAD\n-#      ## Call hook: reference-transaction committed ##\n-#     @@ -18,6 +20,8 @@\n-#      <COMMIT-C> <COMMIT-H> HEAD\n-#      ## Call hook: reference-transaction committed ##\n-#      <COMMIT-C> <COMMIT-H> HEAD\n-#     +## Call hook: reference-transaction   aborted ##\n-#     +<ZERO-OID> <ZERO-OID> CHERRY_PICK_HEAD\n-#      ## Call hook: reference-transaction  prepared ##\n-#      <COMMIT-G> <ZERO-OID> CHERRY_PICK_HEAD\n-#      ## Call hook: reference-transaction committed ##\n-test_expect_failure \"worktree: topic4: rebase\" '\n+test_expect_success \"worktree: topic4: rebase\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-\\EOF &&\n@@ -1367,22 +1246,7 @@ test_expect_failure \"worktree: topic4: rebase\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-# Mismatched hook output for git-revert:\n-#\n-#  * Unexpected execution of the \"reference-transaction abort\" command.\n-#\n-# The differences are as follows:\n-#\n-#     @@ -8,6 +8,8 @@\n-#      ## Call hook: reference-transaction committed ##\n-#      <COMMIT-C> <COMMIT-I> HEAD\n-#      <COMMIT-C> <COMMIT-I> refs/heads/topic5\n-#     +## Call hook: reference-transaction   aborted ##\n-#     +<ZERO-OID> <ZERO-OID> CHERRY_PICK_HEAD\n-#      ## Call hook: reference-transaction  prepared ##\n-#      <ZERO-OID> <ZERO-OID> CHERRY_PICK_HEAD\n-#      ## Call hook: reference-transaction committed ##\n-test_expect_failure \"worktree: topic5: revert\" '\n+test_expect_success \"worktree: topic5: revert\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-\\EOF &&\n-- \n2.36.1.25.gc87d5ad63a.dirty\n\n"},{"id":"461496","messageId":"20220819032147.28841-7-worldhello.net@gmail.com","threadId":"58240","inReplyTo":"CANYiYbFw71bX827akAG87RSKOozPk313Hoe573O9dQ65_U6sLQ@mail.gmail.com","subject":"[PATCH v2 6/9] refs: add reflog_info to hold more fields for reflog entry","fromName":"Jiang Xin","fromEmail":"worldhello.net@gmail.com","sentAt":"2022-08-19T03:21:44Z","receivedAt":"2022-08-19T03:22:19Z","isPatch":true,"sender":{"key":"worldhello.net@gmail.com","avatar":"https://avatars.githubusercontent.com/u/183860?v=4"},"body":"From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n\nThe parameter \"msg\" of the functions \"ref_transaction_add_update()\" and\n\"refs_update_ref()\" is used as a comment for creating a new reflog\nentry. For some cases, like copying or renaming a branch, we may need\nmore custom fields for the new reflog entry, such as old-oid which is\ndifferent from the oid we get from the lock file. Therefore, we create\na new structure \"reflog_info\" to hold more custom fields for the new\nreflog entry, and add two additional extended version functions.\n\nWe will use this extension in a later commit to reimplement the function\n\"files_copy_or_rename_ref()\" by calling \"refs_update_ref_extended()\" to\ncreate a new reference in a transaction, while adding proper reflog\nentry.\n\nSigned-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n---\n refs.c               | 54 +++++++++++++++++++++++++++++++++++++++-----\n refs.h               | 20 ++++++++++++++++\n refs/debug.c         |  2 +-\n refs/files-backend.c | 14 ++++++++----\n refs/refs-internal.h | 17 ++++++++++++--\n 5 files changed, 94 insertions(+), 13 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex 48b69460e2..15130f09b9 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1045,7 +1045,11 @@ void ref_transaction_free(struct ref_transaction *transaction)\n \t}\n \n \tfor (i = 0; i < transaction->nr; i++) {\n-\t\tfree(transaction->updates[i]->msg);\n+\t\tif (transaction->updates[i]->reflog_info) {\n+\t\t\tfree(transaction->updates[i]->reflog_info->msg);\n+\t\t\tfree(transaction->updates[i]->reflog_info->old_oid);\n+\t\t\tfree(transaction->updates[i]->reflog_info);\n+\t\t}\n \t\tfree(transaction->updates[i]);\n \t}\n \tfree(transaction->updates);\n@@ -1057,7 +1061,7 @@ struct ref_update *ref_transaction_add_update(\n \t\tconst char *refname, unsigned int flags,\n \t\tconst struct object_id *new_oid,\n \t\tconst struct object_id *old_oid,\n-\t\tconst char *msg)\n+\t\tconst struct reflog_info *reflog_info)\n {\n \tstruct ref_update *update;\n \n@@ -1074,7 +1078,12 @@ struct ref_update *ref_transaction_add_update(\n \t\toidcpy(&update->new_oid, new_oid);\n \tif (flags & REF_HAVE_OLD)\n \t\toidcpy(&update->old_oid, old_oid);\n-\tupdate->msg = normalize_reflog_message(msg);\n+\tif (reflog_info) {\n+\t\tupdate->reflog_info = xcalloc(1, sizeof(*reflog_info));\n+\t\tupdate->reflog_info->msg = normalize_reflog_message(reflog_info->msg);\n+\t\tif (reflog_info->old_oid)\n+\t\t\tupdate->reflog_info->old_oid = oiddup(reflog_info->old_oid);\n+\t}\n \treturn update;\n }\n \n@@ -1084,6 +1093,23 @@ int ref_transaction_update(struct ref_transaction *transaction,\n \t\t\t   const struct object_id *old_oid,\n \t\t\t   unsigned int flags, const char *msg,\n \t\t\t   struct strbuf *err)\n+{\n+\tstruct reflog_info reflog_info;\n+\n+\treflog_info.msg = (char *)msg;\n+\treflog_info.old_oid = NULL;\n+\treturn ref_transaction_update_extended(transaction,\n+\t\t\t\t\t       refname, new_oid, old_oid,\n+\t\t\t\t\t       flags, &reflog_info, err);\n+}\n+\n+int ref_transaction_update_extended(struct ref_transaction *transaction,\n+\t\t\t\t    const char *refname,\n+\t\t\t\t    const struct object_id *new_oid,\n+\t\t\t\t    const struct object_id *old_oid,\n+\t\t\t\t    unsigned int flags,\n+\t\t\t\t    const struct reflog_info *reflog_info,\n+\t\t\t\t    struct strbuf *err)\n {\n \tassert(err);\n \n@@ -1109,7 +1135,7 @@ int ref_transaction_update(struct ref_transaction *transaction,\n \tflags |= (new_oid ? REF_HAVE_NEW : 0) | (old_oid ? REF_HAVE_OLD : 0);\n \n \tref_transaction_add_update(transaction, refname, flags,\n-\t\t\t\t   new_oid, old_oid, msg);\n+\t\t\t\t   new_oid, old_oid, reflog_info);\n \treturn 0;\n }\n \n@@ -1157,6 +1183,22 @@ int refs_update_ref(struct ref_store *refs, const char *msg,\n \t\t    const char *refname, const struct object_id *new_oid,\n \t\t    const struct object_id *old_oid, unsigned int flags,\n \t\t    enum action_on_err onerr)\n+{\n+\tstruct reflog_info reflog_info;\n+\n+\treflog_info.msg = (char *)msg;\n+\treflog_info.old_oid = NULL;\n+\treturn refs_update_ref_extended(refs, refname, new_oid, old_oid,\n+\t\t\t\t\tflags, &reflog_info, onerr);\n+}\n+\n+int refs_update_ref_extended(struct ref_store *refs,\n+\t\t\t     const char *refname,\n+\t\t\t     const struct object_id *new_oid,\n+\t\t\t     const struct object_id *old_oid,\n+\t\t\t     unsigned int flags,\n+\t\t\t     const struct reflog_info *reflog_info,\n+\t\t\t     enum action_on_err onerr)\n {\n \tstruct ref_transaction *t = NULL;\n \tstruct strbuf err = STRBUF_INIT;\n@@ -1164,8 +1206,8 @@ int refs_update_ref(struct ref_store *refs, const char *msg,\n \n \tt = ref_store_transaction_begin(refs, &err);\n \tif (!t ||\n-\t    ref_transaction_update(t, refname, new_oid, old_oid, flags, msg,\n-\t\t\t\t   &err) ||\n+\t    ref_transaction_update_extended(t, refname, new_oid, old_oid,\n+\t\t\t\t\t    flags, reflog_info, &err) ||\n \t    ref_transaction_commit(t, &err)) {\n \t\tret = 1;\n \t\tref_transaction_free(t);\ndiff --git a/refs.h b/refs.h\nindex 715127ab58..0f21ba259f 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -10,6 +10,7 @@ struct strbuf;\n struct string_list;\n struct string_list_item;\n struct worktree;\n+struct reflog_info;\n \n /*\n  * Resolve a reference, recursively following symbolic refererences.\n@@ -677,6 +678,18 @@ int ref_transaction_update(struct ref_transaction *transaction,\n \t\t\t   const struct object_id *old_oid,\n \t\t\t   unsigned int flags, const char *msg,\n \t\t\t   struct strbuf *err);\n+/*\n+ * Extended version of ref_transaction_update() that allows us to\n+ * provide more fields (in reflog_info) to custom reflog, such\n+ * as msg and old_oid.\n+ */\n+int ref_transaction_update_extended(struct ref_transaction *transaction,\n+\t\t\t\t    const char *refname,\n+\t\t\t\t    const struct object_id *new_oid,\n+\t\t\t\t    const struct object_id *old_oid,\n+\t\t\t\t    unsigned int flags,\n+\t\t\t\t    const struct reflog_info *reflog_info,\n+\t\t\t\t    struct strbuf *err);\n \n /*\n  * Add a reference creation to transaction. new_oid is the value that\n@@ -806,6 +819,13 @@ void ref_transaction_free(struct ref_transaction *transaction);\n int refs_update_ref(struct ref_store *refs, const char *msg, const char *refname,\n \t\t    const struct object_id *new_oid, const struct object_id *old_oid,\n \t\t    unsigned int flags, enum action_on_err onerr);\n+int refs_update_ref_extended(struct ref_store *refs,\n+\t\t\t     const char *refname,\n+\t\t\t     const struct object_id *new_oid,\n+\t\t\t     const struct object_id *old_oid,\n+\t\t\t     unsigned int flags,\n+\t\t\t     const struct reflog_info *reflog_info,\n+\t\t\t     enum action_on_err onerr);\n int update_ref(const char *msg, const char *refname,\n \t       const struct object_id *new_oid, const struct object_id *old_oid,\n \t       unsigned int flags, enum action_on_err onerr);\ndiff --git a/refs/debug.c b/refs/debug.c\nindex eed8bc94b0..1e60507249 100644\n--- a/refs/debug.c\n+++ b/refs/debug.c\n@@ -79,7 +79,7 @@ static void print_transaction(struct ref_transaction *transaction)\n \tfor (i = 0; i < transaction->nr; i++) {\n \t\tstruct ref_update *u = transaction->updates[i];\n \t\tprint_update(i, u->refname, &u->old_oid, &u->new_oid, u->flags,\n-\t\t\t     u->type, u->msg);\n+\t\t\t     u->type, u->reflog_info? u->reflog_info->msg : NULL);\n \t}\n \ttrace_printf_key(&trace_refs, \"}\\n\");\n }\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 9ec4c23a16..336384daa0 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2340,7 +2340,7 @@ static int split_head_update(struct ref_update *update,\n \t\t\ttransaction, \"HEAD\",\n \t\t\tupdate->flags | REF_LOG_ONLY | REF_NO_DEREF,\n \t\t\t&update->new_oid, &update->old_oid,\n-\t\t\tupdate->msg);\n+\t\t\tupdate->reflog_info);\n \n \t/*\n \t * Add \"HEAD\". This insertion is O(N) in the transaction\n@@ -2403,7 +2403,7 @@ static int split_symref_update(struct ref_update *update,\n \tnew_update = ref_transaction_add_update(\n \t\t\ttransaction, referent, new_flags,\n \t\t\t&update->new_oid, &update->old_oid,\n-\t\t\tupdate->msg);\n+\t\t\tupdate->reflog_info);\n \n \tnew_update->parent_update = update;\n \n@@ -2902,9 +2902,15 @@ static int files_transaction_finish(struct ref_store *ref_store,\n \t\t    update->flags & REF_LOG_ONLY) {\n \t\t\tif (files_log_ref_write(refs,\n \t\t\t\t\t\tlock->ref_name,\n-\t\t\t\t\t\t&lock->old_oid,\n+\t\t\t\t\t\tupdate->reflog_info &&\n+\t\t\t\t\t\tupdate->reflog_info->old_oid ?\n+\t\t\t\t\t\t\tupdate->reflog_info->old_oid :\n+\t\t\t\t\t\t\t&lock->old_oid,\n \t\t\t\t\t\t&update->new_oid,\n-\t\t\t\t\t\tupdate->msg, update->flags,\n+\t\t\t\t\t\tupdate->reflog_info ?\n+\t\t\t\t\t\t\tupdate->reflog_info->msg :\n+\t\t\t\t\t\t\tNULL,\n+\t\t\t\t\t\tupdate->flags,\n \t\t\t\t\t\terr)) {\n \t\t\t\tchar *old_msg = strbuf_detach(err, NULL);\n \ndiff --git a/refs/refs-internal.h b/refs/refs-internal.h\nindex 5220d1980d..782cf5fa78 100644\n--- a/refs/refs-internal.h\n+++ b/refs/refs-internal.h\n@@ -104,6 +104,19 @@ enum peel_status {\n  */\n enum peel_status peel_object(const struct object_id *name, struct object_id *oid);\n \n+/*\n+ * When using refs_update_ref() to copy or rename a branch, the old-oid\n+ * for the new created branch is null_oid, but the old_oid for the new\n+ * appended log entry for the reflog file which is copied from the\n+ * original reflog should be the same as the new_oid for the target\n+ * branch. Use \"reflog_info\" to hold log message and old_oid for the\n+ * new reflog entry.\n+ */\n+struct reflog_info {\n+\tstruct object_id *old_oid;\n+\tchar *msg;\n+};\n+\n /**\n  * Information needed for a single ref update. Set new_oid to the new\n  * value or to null_oid to delete the ref. To check the old value\n@@ -133,7 +146,7 @@ struct ref_update {\n \n \tvoid *backend_data;\n \tunsigned int type;\n-\tchar *msg;\n+\tstruct reflog_info *reflog_info;\n \n \t/*\n \t * If this ref_update was split off of a symref update via\n@@ -174,7 +187,7 @@ struct ref_update *ref_transaction_add_update(\n \t\tconst char *refname, unsigned int flags,\n \t\tconst struct object_id *new_oid,\n \t\tconst struct object_id *old_oid,\n-\t\tconst char *msg);\n+\t\tconst struct reflog_info *reflog_info);\n \n /*\n  * Transaction states.\n-- \n2.36.1.25.gc87d5ad63a.dirty\n\n"},{"id":"461497","messageId":"20220819032147.28841-10-worldhello.net@gmail.com","threadId":"58240","inReplyTo":"CANYiYbFw71bX827akAG87RSKOozPk313Hoe573O9dQ65_U6sLQ@mail.gmail.com","subject":"[PATCH v2 9/9] refs: reimplement refs_delete_refs() and run hook once","fromName":"Jiang Xin","fromEmail":"worldhello.net@gmail.com","sentAt":"2022-08-19T03:21:47Z","receivedAt":"2022-08-19T03:22:21Z","isPatch":true,"sender":{"key":"worldhello.net@gmail.com","avatar":"https://avatars.githubusercontent.com/u/183860?v=4"},"body":"From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n\nWhen using \"git branch -d\" or \"git tag -d\" to delete one or multiple\nreferences, command \"reference-transaction committed\" will be called\nrepeatedly for the same references. This is because the function\n\"refs_delete_refs()\" is called twice, once for loose ref-store and once\nfor packed ref-store.\n\nThe old story starts when running the function \"refs_delete_refs()\" on a\nloose ref-store:\n\n 1. Try to remove the references from packed ref-store.\n\n    1.1. Lock the packed-ref-store by calling \"packed_refs_lock()\" in\n         \"files_delete_refs()\".\n\n    1.2. Call \"refs_delete_refs()\" on packed-ref-store, and then call\n         \"packed_delete_refs()\".\n\n    1.3. Create a transaction for packed-ref-store in function\n         \"packed_delete_refs()\" by calling the function\n         \"ref_store_transaction_begin()\".\n\n    2.2. Add update entries for all the references to be removed into\n         this transaction by calling \"ref_transaction_delete()\".\n\n    2.3. Call \"ref_transaction_commit()\" to commit the transaction.\n\n    2.4. Unlock the packed-ref-store.\n\n 2. Try to remove the references one by one by calling the function\n    \"refs_delete_ref()\".\n\n    2.1. Create a new transaction on loose-ref-store by calling\n         \"ref_store_transaction_begin()\".\n\n    2.2. Call \"ref_transaction_delete()\" to add a update entry\n         for the reference to be deleted into the transaction.\n\n    2.3. In \"ref_transaction_commit()\", it will call functions\n         \"files_transaction_prepare()\" and \"files_transaction_finish()\"\n         to commit the transaction.\n\n         2.3.1. Lock the loose reference.\n\n         2.3.2. Create a new packed-transaction, and add a new update\n                entry to this packed-transaction. The previous step 1\n                makes this operation unnecessary.\n\n         2.3.3. Lock the packed-ref-store and call fucntion\n                \"is_packed_transaction_needed()\" to check whether it\n                is necessary to commit the transaction, and then\n                abort the transaction because the reference is already\n                removed from the packed-ref-store in step 1.\n\n         2.3.4. Remove the reflog and the loose reference file for\n                the reference to be deleted.\n\n         2.3.4. Unlock the loose reference.\n\nFrom the above steps, we can see that \"refs_delete_refs()\" is not an\natomic operation, but a semi-atomic operation. The operation is atomic\nif all references to be deleted are in the packed ref-store, but not\nif some references are loose references because we delete the loose\nreferences one by one by calling \"refs_delete_ref()\" .\n\nRefactored function \"files_delete_refs()\" to delete references within a\ntransaction, so the \"reference-transaction\" hook will only run once for\ndeleted branches and tags.\n\nThe behavior of the following git commands and the last two testcases\nhave been fixed in t1416:\n\n * git branch -d <branch>\n * git tag -d <tag>\n\nA testcase in t5510 is broken because we used to call the function\n\"packed_refs_lock()\", but it is not necessary if the deleted reference\nis not in the \"packed-refs\" file.\n\nSigned-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n---\n refs/files-backend.c             | 21 +++++-----\n refs/packed-backend.c            | 51 +-----------------------\n t/t1416-ref-transaction-hooks.sh | 68 +-------------------------------\n t/t5510-fetch.sh                 | 16 ++++++++\n 4 files changed, 28 insertions(+), 128 deletions(-)\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex e029f5a885..8f3deddc71 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -1268,31 +1268,27 @@ static int files_pack_refs(struct ref_store *ref_store, unsigned int flags)\n static int files_delete_refs(struct ref_store *ref_store, const char *msg,\n \t\t\t     struct string_list *refnames, unsigned int flags)\n {\n-\tstruct files_ref_store *refs =\n-\t\tfiles_downcast(ref_store, REF_STORE_WRITE, \"delete_refs\");\n+\tstruct ref_transaction *transaction;\n \tstruct strbuf err = STRBUF_INIT;\n \tint i, result = 0;\n \n \tif (!refnames->nr)\n \t\treturn 0;\n \n-\tif (packed_refs_lock(refs->packed_ref_store, 0, &err))\n-\t\tgoto error;\n-\n-\tif (refs_delete_refs(refs->packed_ref_store, msg, refnames, flags)) {\n-\t\tpacked_refs_unlock(refs->packed_ref_store);\n+\ttransaction = ref_store_transaction_begin(ref_store, &err);\n+\tif (!transaction)\n \t\tgoto error;\n-\t}\n-\n-\tpacked_refs_unlock(refs->packed_ref_store);\n \n \tfor (i = 0; i < refnames->nr; i++) {\n \t\tconst char *refname = refnames->items[i].string;\n-\n-\t\tif (refs_delete_ref(&refs->base, msg, refname, NULL, flags))\n+\t\tif (ref_transaction_delete(transaction, refname, NULL,\n+\t\t\t\t\t   flags, msg, &err))\n \t\t\tresult |= error(_(\"could not remove reference %s\"), refname);\n \t}\n+\tif (ref_transaction_commit(transaction, &err))\n+\t\tgoto error;\n \n+\tref_transaction_free(transaction);\n \tstrbuf_release(&err);\n \treturn result;\n \n@@ -1309,6 +1305,7 @@ static int files_delete_refs(struct ref_store *ref_store, const char *msg,\n \telse\n \t\terror(_(\"could not delete references: %s\"), err.buf);\n \n+\tref_transaction_free(transaction);\n \tstrbuf_release(&err);\n \treturn -1;\n }\ndiff --git a/refs/packed-backend.c b/refs/packed-backend.c\nindex 97b6837767..fdb7a0a52c 100644\n--- a/refs/packed-backend.c\n+++ b/refs/packed-backend.c\n@@ -1519,55 +1519,6 @@ static int packed_initial_transaction_commit(struct ref_store *ref_store,\n \treturn ref_transaction_commit(transaction, err);\n }\n \n-static int packed_delete_refs(struct ref_store *ref_store, const char *msg,\n-\t\t\t     struct string_list *refnames, unsigned int flags)\n-{\n-\tstruct packed_ref_store *refs =\n-\t\tpacked_downcast(ref_store, REF_STORE_WRITE, \"delete_refs\");\n-\tstruct strbuf err = STRBUF_INIT;\n-\tstruct ref_transaction *transaction;\n-\tstruct string_list_item *item;\n-\tint ret;\n-\n-\t(void)refs; /* We need the check above, but don't use the variable */\n-\n-\tif (!refnames->nr)\n-\t\treturn 0;\n-\n-\t/*\n-\t * Since we don't check the references' old_oids, the\n-\t * individual updates can't fail, so we can pack all of the\n-\t * updates into a single transaction.\n-\t */\n-\n-\ttransaction = ref_store_transaction_begin(ref_store, &err);\n-\tif (!transaction)\n-\t\treturn -1;\n-\n-\tfor_each_string_list_item(item, refnames) {\n-\t\tif (ref_transaction_delete(transaction, item->string, NULL,\n-\t\t\t\t\t   flags, msg, &err)) {\n-\t\t\twarning(_(\"could not delete reference %s: %s\"),\n-\t\t\t\titem->string, err.buf);\n-\t\t\tstrbuf_reset(&err);\n-\t\t}\n-\t}\n-\n-\tret = ref_transaction_commit(transaction, &err);\n-\n-\tif (ret) {\n-\t\tif (refnames->nr == 1)\n-\t\t\terror(_(\"could not delete reference %s: %s\"),\n-\t\t\t      refnames->items[0].string, err.buf);\n-\t\telse\n-\t\t\terror(_(\"could not delete references: %s\"), err.buf);\n-\t}\n-\n-\tref_transaction_free(transaction);\n-\tstrbuf_release(&err);\n-\treturn ret;\n-}\n-\n static int packed_pack_refs(struct ref_store *ref_store, unsigned int flags)\n {\n \t/*\n@@ -1595,7 +1546,7 @@ struct ref_storage_be refs_be_packed = {\n \n \t.pack_refs = packed_pack_refs,\n \t.create_symref = NULL,\n-\t.delete_refs = packed_delete_refs,\n+\t.delete_refs = NULL,\n \t.rename_ref = NULL,\n \t.copy_ref = NULL,\n \ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex 77996017d7..3d39e1634a 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -789,39 +789,7 @@ test_expect_success \"branch: rename branches\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-# Mismatched hook output for \"git branch -d\":\n-#\n-#  * The delete branches operation should be treated as one transaction,\n-#    but was splitted into several transactions on loose references,\n-#    and the \"reference-transaction committed\" command was executed\n-#    redundantly on the packed-ref-store.\n-#\n-# The differences are as follows:\n-#\n-#     @@ -2,11 +2,19 @@\n-#      <ZERO-OID> <ZERO-OID> refs/heads/topic1\n-#      <ZERO-OID> <ZERO-OID> refs/heads/topic2\n-#      <ZERO-OID> <ZERO-OID> refs/heads/topic3\n-#     +## Call hook: reference-transaction committed ##\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic2\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic3\n-#     +## Call hook: reference-transaction  prepared ##\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n-#     +## Call hook: reference-transaction committed ##\n-#     +<ZERO-OID> <ZERO-OID> refs/heads/topic1\n-#      ## Call hook: reference-transaction  prepared ##\n-#     -<COMMIT-A> <ZERO-OID> refs/heads/topic1\n-#      <COMMIT-B> <ZERO-OID> refs/heads/topic2\n-#     -<COMMIT-C> <ZERO-OID> refs/heads/topic3\n-#      ## Call hook: reference-transaction committed ##\n-#     -<COMMIT-A> <ZERO-OID> refs/heads/topic1\n-#      <COMMIT-B> <ZERO-OID> refs/heads/topic2\n-#     +## Call hook: reference-transaction  prepared ##\n-#     +<COMMIT-C> <ZERO-OID> refs/heads/topic3\n-#     +## Call hook: reference-transaction committed ##\n-#      <COMMIT-C> <ZERO-OID> refs/heads/topic3\n-test_expect_failure \"branch: remove branches\" '\n+test_expect_success \"branch: remove branches\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-\\EOF &&\n@@ -950,39 +918,7 @@ test_expect_success \"tag: update refs to create loose refs\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-# Mismatched hook output for \"git tag -d\":\n-#\n-#  * The delete tags operation should be treated as one transaction,\n-#    but was splitted into several transactions on loose references,\n-#    and the \"reference-transaction committed\" command was executed\n-#    redundantly on the packed-ref-store.\n-#\n-# The differences are as follows:\n-#\n-#     @@ -2,11 +2,19 @@\n-#      <ZERO-OID> <ZERO-OID> refs/tags/v1\n-#      <ZERO-OID> <ZERO-OID> refs/tags/v2\n-#      <ZERO-OID> <ZERO-OID> refs/tags/v3\n-#     +## Call hook: reference-transaction committed ##\n-#     +<ZERO-OID> <ZERO-OID> refs/tags/v1\n-#     +<ZERO-OID> <ZERO-OID> refs/tags/v2\n-#     +<ZERO-OID> <ZERO-OID> refs/tags/v3\n-#     +## Call hook: reference-transaction  prepared ##\n-#     +<ZERO-OID> <ZERO-OID> refs/tags/v1\n-#     +## Call hook: reference-transaction committed ##\n-#     +<ZERO-OID> <ZERO-OID> refs/tags/v1\n-#      ## Call hook: reference-transaction  prepared ##\n-#     -<COMMIT-A> <ZERO-OID> refs/tags/v1\n-#      <COMMIT-B> <ZERO-OID> refs/tags/v2\n-#     -<COMMIT-C> <ZERO-OID> refs/tags/v3\n-#      ## Call hook: reference-transaction committed ##\n-#     -<COMMIT-A> <ZERO-OID> refs/tags/v1\n-#      <COMMIT-B> <ZERO-OID> refs/tags/v2\n-#     +## Call hook: reference-transaction  prepared ##\n-#     +<COMMIT-C> <ZERO-OID> refs/tags/v3\n-#     +## Call hook: reference-transaction committed ##\n-#      <COMMIT-C> <ZERO-OID> refs/tags/v3\n-test_expect_failure \"tag: remove tags with mixed ref_stores\" '\n+test_expect_success \"tag: remove tags with mixed ref_stores\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-\\EOF &&\ndiff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\nindex b45879a760..dfdab09600 100755\n--- a/t/t5510-fetch.sh\n+++ b/t/t5510-fetch.sh\n@@ -168,6 +168,8 @@ test_expect_success REFFILES 'fetch --prune fails to delete branches' '\n \tcd \"$D\" &&\n \tgit clone . prune-fail &&\n \tcd prune-fail &&\n+\tgit update-ref refs/remotes/origin/extrabranch main~ &&\n+\tgit pack-refs --all &&\n \tgit update-ref refs/remotes/origin/extrabranch main &&\n \t: this will prevent --prune from locking packed-refs for deleting refs, but adding loose refs still succeeds  &&\n \t>.git/packed-refs.new &&\n@@ -175,6 +177,20 @@ test_expect_success REFFILES 'fetch --prune fails to delete branches' '\n \ttest_must_fail git fetch --prune origin\n '\n \n+test_expect_success REFFILES 'fetch --prune ok for loose refs not in locked packed-refs' '\n+\tcd \"$D\" &&\n+\tgit clone . prune-ok-ref-not-packed &&\n+\t(\n+\t\tcd prune-ok-ref-not-packed &&\n+\t\tgit update-ref refs/remotes/origin/extrabranch main &&\n+\t\t: for loose refs not in packed-refs, we can delete them even the packed-refs is locked &&\n+\t\t:>.git/packed-refs.new &&\n+\n+\t\tgit fetch --prune origin &&\n+\t\ttest_must_fail git rev-parse refs/remotes/origin/extrabranch --\n+\t)\n+'\n+\n test_expect_success 'fetch --atomic works with a single branch' '\n \ttest_when_finished \"rm -rf \\\"$D\\\"/atomic\" &&\n \n-- \n2.36.1.25.gc87d5ad63a.dirty\n\n"},{"id":"461498","messageId":"20220819032147.28841-8-worldhello.net@gmail.com","threadId":"58240","inReplyTo":"CANYiYbFw71bX827akAG87RSKOozPk313Hoe573O9dQ65_U6sLQ@mail.gmail.com","subject":"[PATCH v2 7/9] refs: get error message via refs_update_ref_extended()","fromName":"Jiang Xin","fromEmail":"worldhello.net@gmail.com","sentAt":"2022-08-19T03:21:45Z","receivedAt":"2022-08-19T03:22:26Z","isPatch":true,"sender":{"key":"worldhello.net@gmail.com","avatar":"https://avatars.githubusercontent.com/u/183860?v=4"},"body":"From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n\nThe last parameter of the function \"refs_update_ref()\" is an enum\n\"action_on_err\", and we can not use this function to get the error\nmessage. Extend this function to get error message.\n\nWe will use the function \"refs_update_ref_extended()\" to reimplement\nthe function \"files_copy_or_rename_ref()\" in later commit.\n\nSigned-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n---\n refs.c | 50 +++++++++++++++++++++++++-------------------------\n refs.h |  2 +-\n 2 files changed, 26 insertions(+), 26 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex 15130f09b9..a528473a46 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1185,11 +1185,29 @@ int refs_update_ref(struct ref_store *refs, const char *msg,\n \t\t    enum action_on_err onerr)\n {\n \tstruct reflog_info reflog_info;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tint ret;\n \n \treflog_info.msg = (char *)msg;\n \treflog_info.old_oid = NULL;\n-\treturn refs_update_ref_extended(refs, refname, new_oid, old_oid,\n-\t\t\t\t\tflags, &reflog_info, onerr);\n+\tret = refs_update_ref_extended(refs, refname, new_oid, old_oid,\n+\t\t\t\t       flags, &reflog_info, &err);\n+\tif (ret) {\n+\t\tconst char *str = _(\"update_ref failed for ref '%s': %s\");\n+\n+\t\tswitch (onerr) {\n+\t\tcase UPDATE_REFS_MSG_ON_ERR:\n+\t\t\terror(str, refname, err.buf);\n+\t\t\tbreak;\n+\t\tcase UPDATE_REFS_DIE_ON_ERR:\n+\t\t\tdie(str, refname, err.buf);\n+\t\t\tbreak;\n+\t\tcase UPDATE_REFS_QUIET_ON_ERR:\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\tstrbuf_release(&err);\n+\treturn ret;\n }\n \n int refs_update_ref_extended(struct ref_store *refs,\n@@ -1198,37 +1216,19 @@ int refs_update_ref_extended(struct ref_store *refs,\n \t\t\t     const struct object_id *old_oid,\n \t\t\t     unsigned int flags,\n \t\t\t     const struct reflog_info *reflog_info,\n-\t\t\t     enum action_on_err onerr)\n+\t\t\t     struct strbuf *err)\n {\n \tstruct ref_transaction *t = NULL;\n-\tstruct strbuf err = STRBUF_INIT;\n-\tint ret = 0;\n \n-\tt = ref_store_transaction_begin(refs, &err);\n+\tt = ref_store_transaction_begin(refs, err);\n \tif (!t ||\n \t    ref_transaction_update_extended(t, refname, new_oid, old_oid,\n-\t\t\t\t\t    flags, reflog_info, &err) ||\n-\t    ref_transaction_commit(t, &err)) {\n-\t\tret = 1;\n+\t\t\t\t\t    flags, reflog_info, err) ||\n+\t    ref_transaction_commit(t, err)) {\n \t\tref_transaction_free(t);\n-\t}\n-\tif (ret) {\n-\t\tconst char *str = _(\"update_ref failed for ref '%s': %s\");\n-\n-\t\tswitch (onerr) {\n-\t\tcase UPDATE_REFS_MSG_ON_ERR:\n-\t\t\terror(str, refname, err.buf);\n-\t\t\tbreak;\n-\t\tcase UPDATE_REFS_DIE_ON_ERR:\n-\t\t\tdie(str, refname, err.buf);\n-\t\t\tbreak;\n-\t\tcase UPDATE_REFS_QUIET_ON_ERR:\n-\t\t\tbreak;\n-\t\t}\n-\t\tstrbuf_release(&err);\n \t\treturn 1;\n \t}\n-\tstrbuf_release(&err);\n+\n \tif (t)\n \t\tref_transaction_free(t);\n \treturn 0;\ndiff --git a/refs.h b/refs.h\nindex 0f21ba259f..85832c4863 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -825,7 +825,7 @@ int refs_update_ref_extended(struct ref_store *refs,\n \t\t\t     const struct object_id *old_oid,\n \t\t\t     unsigned int flags,\n \t\t\t     const struct reflog_info *reflog_info,\n-\t\t\t     enum action_on_err onerr);\n+\t\t\t     struct strbuf *err);\n int update_ref(const char *msg, const char *refname,\n \t       const struct object_id *new_oid, const struct object_id *old_oid,\n \t       unsigned int flags, enum action_on_err onerr);\n-- \n2.36.1.25.gc87d5ad63a.dirty\n\n"},{"id":"461499","messageId":"20220819032147.28841-9-worldhello.net@gmail.com","threadId":"58240","inReplyTo":"CANYiYbFw71bX827akAG87RSKOozPk313Hoe573O9dQ65_U6sLQ@mail.gmail.com","subject":"[PATCH v2 8/9] refs: reimplement files_copy_or_rename_ref() to run refs-txn hook","fromName":"Jiang Xin","fromEmail":"worldhello.net@gmail.com","sentAt":"2022-08-19T03:21:46Z","receivedAt":"2022-08-19T03:22:29Z","isPatch":true,"sender":{"key":"worldhello.net@gmail.com","avatar":"https://avatars.githubusercontent.com/u/183860?v=4"},"body":"From: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n\nWhen copying or renaming a branch, the \"reference-transaction\" hook is\nnot executed. This is because we called two low-level functions\n\"lock_ref_oid_basic()\" and \"write_ref_to_lockfile()\", and reinvented\nthe wheel in \"commit_ref_update()\" to update the reference instead of\nimplementing \"files_copy_or_rename_ref()\" by calling \"refs_update_ref()\"\nto update a reference in a transaction. The reason for this is that we\nwant to create a proper reflog for the newly copied reference.\n\nRefactor \"files_copy_or_rename_ref()\" by calling the extended version\nof \"refs_update_ref\", i.e. \"refs_update_ref_extended()\", so we can\ncreate the target branch for copying or renaming a branch and generate\na correct reflog file at the same time.\n\nThe behavior of the following git commands and two testcases have been\nfixed in t1416:\n\n * git branch -c <src> <dest>\t# copy branch\n * git branch -m <old> <new>\t# rename branch\n\nSigned-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>\n---\n refs/files-backend.c             | 97 +++-----------------------------\n t/t1416-ref-transaction-hooks.sh | 28 +--------\n 2 files changed, 9 insertions(+), 116 deletions(-)\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 336384daa0..e029f5a885 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -1376,10 +1376,6 @@ static int rename_tmp_log(struct files_ref_store *refs, const char *newrefname)\n static int write_ref_to_lockfile(struct ref_lock *lock,\n \t\t\t\t const struct object_id *oid,\n \t\t\t\t int skip_oid_verification, struct strbuf *err);\n-static int commit_ref_update(struct files_ref_store *refs,\n-\t\t\t     struct ref_lock *lock,\n-\t\t\t     const struct object_id *oid, const char *logmsg,\n-\t\t\t     struct strbuf *err);\n \n /*\n  * Emit a better error message than lockfile.c's\n@@ -1418,13 +1414,13 @@ static int files_copy_or_rename_ref(struct ref_store *ref_store,\n \t\tfiles_downcast(ref_store, REF_STORE_WRITE, \"rename_ref\");\n \tstruct object_id orig_oid;\n \tint flag = 0, logmoved = 0;\n-\tstruct ref_lock *lock;\n \tstruct stat loginfo;\n \tstruct strbuf sb_oldref = STRBUF_INIT;\n \tstruct strbuf sb_newref = STRBUF_INIT;\n \tstruct strbuf tmp_renamed_log = STRBUF_INIT;\n \tint log, ret;\n \tstruct strbuf err = STRBUF_INIT;\n+\tstruct reflog_info reflog_info;\n \n \tfiles_reflog_path(refs, &sb_oldref, oldrefname);\n \tfiles_reflog_path(refs, &sb_newref, newrefname);\n@@ -1510,8 +1506,10 @@ static int files_copy_or_rename_ref(struct ref_store *ref_store,\n \n \tlogmoved = log;\n \n-\tlock = lock_ref_oid_basic(refs, newrefname, &err);\n-\tif (!lock) {\n+\treflog_info.msg = (char *)logmsg;\n+\treflog_info.old_oid = &orig_oid;\n+\tif (refs_update_ref_extended(ref_store, newrefname, &orig_oid, NULL,\n+\t\t\t\t     REF_NO_DEREF, &reflog_info, &err)) {\n \t\tif (copy)\n \t\t\terror(\"unable to copy '%s' to '%s': %s\", oldrefname, newrefname, err.buf);\n \t\telse\n@@ -1519,36 +1517,20 @@ static int files_copy_or_rename_ref(struct ref_store *ref_store,\n \t\tstrbuf_release(&err);\n \t\tgoto rollback;\n \t}\n-\toidcpy(&lock->old_oid, &orig_oid);\n-\n-\tif (write_ref_to_lockfile(lock, &orig_oid, 0, &err) ||\n-\t    commit_ref_update(refs, lock, &orig_oid, logmsg, &err)) {\n-\t\terror(\"unable to write current sha1 into %s: %s\", newrefname, err.buf);\n-\t\tstrbuf_release(&err);\n-\t\tgoto rollback;\n-\t}\n \n \tret = 0;\n \tgoto out;\n \n  rollback:\n-\tlock = lock_ref_oid_basic(refs, oldrefname, &err);\n-\tif (!lock) {\n-\t\terror(\"unable to lock %s for rollback: %s\", oldrefname, err.buf);\n-\t\tstrbuf_release(&err);\n-\t\tgoto rollbacklog;\n-\t}\n-\n \tflag = log_all_ref_updates;\n \tlog_all_ref_updates = LOG_REFS_NONE;\n-\tif (write_ref_to_lockfile(lock, &orig_oid, 0, &err) ||\n-\t    commit_ref_update(refs, lock, &orig_oid, NULL, &err)) {\n+\tif (refs_update_ref_extended(ref_store, oldrefname, &orig_oid, NULL,\n+\t\t\t\t     REF_NO_DEREF, NULL, &err)) {\n \t\terror(\"unable to write current sha1 into %s: %s\", oldrefname, err.buf);\n \t\tstrbuf_release(&err);\n \t}\n \tlog_all_ref_updates = flag;\n \n- rollbacklog:\n \tif (logmoved && rename(sb_newref.buf, sb_oldref.buf))\n \t\terror(\"unable to restore logfile %s from %s: %s\",\n \t\t\toldrefname, newrefname, strerror(errno));\n@@ -1815,71 +1797,6 @@ static int write_ref_to_lockfile(struct ref_lock *lock,\n \treturn 0;\n }\n \n-/*\n- * Commit a change to a loose reference that has already been written\n- * to the loose reference lockfile. Also update the reflogs if\n- * necessary, using the specified lockmsg (which can be NULL).\n- */\n-static int commit_ref_update(struct files_ref_store *refs,\n-\t\t\t     struct ref_lock *lock,\n-\t\t\t     const struct object_id *oid, const char *logmsg,\n-\t\t\t     struct strbuf *err)\n-{\n-\tfiles_assert_main_repository(refs, \"commit_ref_update\");\n-\n-\tclear_loose_ref_cache(refs);\n-\tif (files_log_ref_write(refs, lock->ref_name,\n-\t\t\t\t&lock->old_oid, oid,\n-\t\t\t\tlogmsg, 0, err)) {\n-\t\tchar *old_msg = strbuf_detach(err, NULL);\n-\t\tstrbuf_addf(err, \"cannot update the ref '%s': %s\",\n-\t\t\t    lock->ref_name, old_msg);\n-\t\tfree(old_msg);\n-\t\tunlock_ref(lock);\n-\t\treturn -1;\n-\t}\n-\n-\tif (strcmp(lock->ref_name, \"HEAD\") != 0) {\n-\t\t/*\n-\t\t * Special hack: If a branch is updated directly and HEAD\n-\t\t * points to it (may happen on the remote side of a push\n-\t\t * for example) then logically the HEAD reflog should be\n-\t\t * updated too.\n-\t\t * A generic solution implies reverse symref information,\n-\t\t * but finding all symrefs pointing to the given branch\n-\t\t * would be rather costly for this rare event (the direct\n-\t\t * update of a branch) to be worth it.  So let's cheat and\n-\t\t * check with HEAD only which should cover 99% of all usage\n-\t\t * scenarios (even 100% of the default ones).\n-\t\t */\n-\t\tint head_flag;\n-\t\tconst char *head_ref;\n-\n-\t\thead_ref = refs_resolve_ref_unsafe(&refs->base, \"HEAD\",\n-\t\t\t\t\t\t   RESOLVE_REF_READING,\n-\t\t\t\t\t\t   NULL, &head_flag);\n-\t\tif (head_ref && (head_flag & REF_ISSYMREF) &&\n-\t\t    !strcmp(head_ref, lock->ref_name)) {\n-\t\t\tstruct strbuf log_err = STRBUF_INIT;\n-\t\t\tif (files_log_ref_write(refs, \"HEAD\",\n-\t\t\t\t\t\t&lock->old_oid, oid,\n-\t\t\t\t\t\tlogmsg, 0, &log_err)) {\n-\t\t\t\terror(\"%s\", log_err.buf);\n-\t\t\t\tstrbuf_release(&log_err);\n-\t\t\t}\n-\t\t}\n-\t}\n-\n-\tif (commit_ref(lock)) {\n-\t\tstrbuf_addf(err, \"couldn't set '%s'\", lock->ref_name);\n-\t\tunlock_ref(lock);\n-\t\treturn -1;\n-\t}\n-\n-\tunlock_ref(lock);\n-\treturn 0;\n-}\n-\n static int create_ref_symlink(struct ref_lock *lock, const char *target)\n {\n \tint ret = -1;\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex 6ba7ba746c..77996017d7 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -715,9 +715,7 @@ test_expect_success \"branch: update branch without old-oid\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-# Failed because the reference-transaction hook was not executed at all\n-# when copying a branch using \"git branch -c\".\n-test_expect_failure \"branch: copy branches\" '\n+test_expect_success \"branch: copy branches\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-\\EOF &&\n@@ -750,29 +748,7 @@ test_expect_failure \"branch: copy branches\" '\n \ttest_cmp_heads_and_tags -C workdir expect\n '\n \n-# Mismatched hook output for \"git branch -m\":\n-#\n-#  * The \"reference-transaction committed\" command was not executed\n-#    for the target branch.\n-#\n-# The differences are as follows:\n-#\n-#     @@ -3,14 +3,6 @@\n-#      ## Call hook: reference-transaction committed ##\n-#      <COMMIT-B> <ZERO-OID> refs/heads/topic4\n-#      ## Call hook: reference-transaction  prepared ##\n-#     -<ZERO-OID> <COMMIT-B> refs/heads/topic6\n-#     -## Call hook: reference-transaction committed ##\n-#     -<ZERO-OID> <COMMIT-B> refs/heads/topic6\n-#     -## Call hook: reference-transaction  prepared ##\n-#      <COMMIT-C> <ZERO-OID> refs/heads/topic5\n-#      ## Call hook: reference-transaction committed ##\n-#      <COMMIT-C> <ZERO-OID> refs/heads/topic5\n-#     -## Call hook: reference-transaction  prepared ##\n-#     -<ZERO-OID> <COMMIT-C> refs/heads/topic7\n-#     -## Call hook: reference-transaction committed ##\n-#     -<ZERO-OID> <COMMIT-C> refs/heads/topic7\n-test_expect_failure \"branch: rename branches\" '\n+test_expect_success \"branch: rename branches\" '\n \ttest_when_finished \"rm -f $HOOK_OUTPUT\" &&\n \n \tcat >expect <<-\\EOF &&\n-- \n2.36.1.25.gc87d5ad63a.dirty\n\n"}]}