{"thread":{"id":"63064","subject":"[PATCH v3 1/8] refs/files: remove redundant check in split_symref_update()","startedAt":"2025-03-05T17:39:16Z","lastAt":"2025-06-03T23:08:26Z","messageCount":59,"participants":["Karthik Nayak","Junio C Hamano","Jeff King","Patrick Steinhardt","Taylor Blau","Calvin Wan","Mike Hommey"],"isPatch":true,"patchVersion":3,"patchTotal":8},"messages":[{"id":"513606","messageId":"20250305-245-partially-atomic-ref-updates-v3-1-0c64e3052354@gmail.com","threadId":"63064","inReplyTo":"20250305-245-partially-atomic-ref-updates-v3-0-0c64e3052354@gmail.com","subject":"[PATCH v3 1/8] refs/files: remove redundant check in split_symref_update()","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-03-05T17:38:56Z","receivedAt":"2025-03-05T17:39:16Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"In `split_symref_update()`, there were two checks for duplicate\nrefnames:\n\n  - At the start, `string_list_has_string()` ensures the refname is not\n    already in `affected_refnames`, preventing duplicates from being\n    added.\n\n  - After adding the refname, another check verifies whether the newly\n    inserted item has a `util` value.\n\nThe second check is unnecessary because the first one guarantees that\n`string_list_insert()` will never encounter a preexisting entry.\n\nSince `item->util` is only used in this context, remove the assignment and\nsimplify the surrounding code.\n\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n refs/files-backend.c | 20 +++-----------------\n 1 file changed, 3 insertions(+), 17 deletions(-)\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 4e1c50fead..6c7df30738 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2382,7 +2382,6 @@ static int split_head_update(struct ref_update *update,\n \t\t\t     struct string_list *affected_refnames,\n \t\t\t     struct strbuf *err)\n {\n-\tstruct string_list_item *item;\n \tstruct ref_update *new_update;\n \n \tif ((update->flags & REF_LOG_ONLY) ||\n@@ -2421,8 +2420,7 @@ static int split_head_update(struct ref_update *update,\n \t */\n \tif (strcmp(new_update->refname, \"HEAD\"))\n \t\tBUG(\"%s unexpectedly not 'HEAD'\", new_update->refname);\n-\titem = string_list_insert(affected_refnames, new_update->refname);\n-\titem->util = new_update;\n+\tstring_list_insert(affected_refnames, new_update->refname);\n \n \treturn 0;\n }\n@@ -2441,7 +2439,6 @@ static int split_symref_update(struct ref_update *update,\n \t\t\t       struct string_list *affected_refnames,\n \t\t\t       struct strbuf *err)\n {\n-\tstruct string_list_item *item;\n \tstruct ref_update *new_update;\n \tunsigned int new_flags;\n \n@@ -2496,11 +2493,7 @@ static int split_symref_update(struct ref_update *update,\n \t * be valid as long as affected_refnames is in use, and NOT\n \t * referent, which might soon be freed by our caller.\n \t */\n-\titem = string_list_insert(affected_refnames, new_update->refname);\n-\tif (item->util)\n-\t\tBUG(\"%s unexpectedly found in affected_refnames\",\n-\t\t    new_update->refname);\n-\titem->util = new_update;\n+\tstring_list_insert(affected_refnames, new_update->refname);\n \n \treturn 0;\n }\n@@ -2834,7 +2827,6 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n \t */\n \tfor (i = 0; i < transaction->nr; i++) {\n \t\tstruct ref_update *update = transaction->updates[i];\n-\t\tstruct string_list_item *item;\n \n \t\tif ((update->flags & REF_IS_PRUNING) &&\n \t\t    !(update->flags & REF_NO_DEREF))\n@@ -2843,13 +2835,7 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n \t\tif (update->flags & REF_LOG_ONLY)\n \t\t\tcontinue;\n \n-\t\titem = string_list_append(&affected_refnames, update->refname);\n-\t\t/*\n-\t\t * We store a pointer to update in item->util, but at\n-\t\t * the moment we never use the value of this field\n-\t\t * except to check whether it is non-NULL.\n-\t\t */\n-\t\titem->util = update;\n+\t\tstring_list_append(&affected_refnames, update->refname);\n \t}\n \tstring_list_sort(&affected_refnames);\n \tif (ref_update_reject_duplicates(&affected_refnames, err)) {\n\n-- \n2.48.1\n\n"},{"id":"513609","messageId":"20250305-245-partially-atomic-ref-updates-v3-0-0c64e3052354@gmail.com","threadId":"63064","inReplyTo":"20250207-245-partially-atomic-ref-updates-v1-0-e6a3690ff23a@gmail.com","subject":"[PATCH v3 0/8] refs: introduce support for partial reference transactions","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-03-05T17:38:55Z","receivedAt":"2025-03-05T17:39:16Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":" Documentation/git-update-ref.adoc |  17 +-\n builtin/fetch.c                   |   2 +-\n builtin/update-ref.c              |  67 ++++-\n refs.c                            | 162 ++++++++++--\n refs.h                            |  76 ++++--\n refs/files-backend.c              | 314 +++++++++++-------------\n refs/packed-backend.c             |  69 +++---\n refs/refs-internal.h              |  51 +++-\n refs/reftable-backend.c           | 502 +++++++++++++++++++-------------------\n t/t1400-update-ref.sh             | 233 ++++++++++++++++++\n 10 files changed, 971 insertions(+), 522 deletions(-)\n\nKarthik Nayak (8):\n      refs/files: remove redundant check in split_symref_update()\n      refs: move duplicate refname update check to generic layer\n      refs/files: remove duplicate duplicates check\n      refs/reftable: extract code from the transaction preparation\n      refs: introduce enum-based transaction error types\n      refs: implement partial reference transaction support\n      refs: support partial update rejections during F/D checks\n      update-ref: add --allow-partial flag for stdin mode\n\nGit's reference updates are traditionally all or nothing - when updating\nmultiple references in a transaction, either all updates succeed or none\ndo. While this behavior is generally desirable, it can be limiting in\ncertain scenarios, particularly with the reftable backend where batching\nmultiple reference updates is more efficient than performing them\nsequentially.\n\nThis series introduces support for partial reference transactions,\nallowing individual reference updates to fail while letting others\nproceed. This capability is exposed through git-update-ref's\n`--allow-partial` flag, which can be used in `--stdin` mode to batch\nupdates and handle failures gracefully.\n\nThe changes are structured to carefully build up this functionality:\n\nFirst, we clean up and consolidate the reference update checking logic.\nThis includes removing duplicate checks in the files backend and moving\nrefname tracking to the generic layer, which simplifies the codebase and\nprepares it for the new feature.\n\nWe then restructure the reftable backend's transaction preparation code,\nextracting the update validation logic into a dedicated function. This\nnot only improves code organization but sets the stage for implementing\npartial transaction support.\n\nTo ensure we only skip errors which are user-oriented, we introduce\ntyped errors for transactions with 'enum ref_transaction_error'. We\nextend the existing errors to include other scenarios and use this new\nerrors throughout the refs code.\n\nWith this groundwork in place, we implement the core partial transaction\nsupport in the refs subsystem. This adds the necessary infrastructure to\ntrack and report rejected updates while allowing transactions to proceed.\nAll reference backends are modified to support this behavior when enabled.\n\nFinally, we expose this functionality to users through\ngit-update-ref(1)'s `--allow-partial` flag, complete with test coverage\nand documentation. The flag is specifically limited to `--stdin` mode\nwhere batching multiple updates is most relevant.\n\nThis enhancement improves Git's flexibility in handling reference\nupdates while maintaining the safety of atomic transactions by default.\nIt's particularly valuable for tools and workflows that need to handle\nreference update failures gracefully without abandoning the entire batch\nof updates.\n\nThis series is based on top of b838bf1938 (Merge branch 'master' of\nhttps://github.com/j6t/gitk, 2025-02-20) with Patrick's series 'refs:\nbatch refname availability checks' [1] merged in.\n\n[1]: https://lore.kernel.org/all/20250217-pks-update-ref-optimization-v1-0-a2b6d87a24af@pks.im/\n\n---\nChanges in v3:\n- Changed 'transaction_error' to 'ref_transaction_error' along with the\n  error names. Removed 'TRANSACTION_OK' since it can potentially be\n  missed instead of simply 'return 0'.\n- Rename 'ref_transaction_set_rejected' to\n  'ref_transaction_maybe_set_rejected' and move logic around error\n  checks to within this function.\n- Add a new struct 'ref_transaction_rejections' to track the rejections\n  within a transaction. This allows us to only iterate over rejected\n  updates.\n- Add a new commit to also support partial transactions within the\n  batched F/D checks.\n- Remove NUL delimited outputs in 'git-update-ref(1)'.\n- Remove translations for plumbing outputs.\n- Other small cleanups in the commit message and code.\n\nChanges in v2:\n- Introduce and use structured errors. This consolidates the errors\n  and their handling between the ref backends.\n- In the previous version, we skipped over all failures. This include\n  system failures such as low memory or IO problems. Let's instead, only\n  skip user-oriented failures, such as invalid old OID and so on.\n- Change the rejection function name to `ref_transaction_set_rejected()`.\n- Modify the commit messages and documentation to be a little more\n  verbose.\n- Link to v1: https://lore.kernel.org/r/20250207-245-partially-atomic-ref-updates-v1-0-e6a3690ff23a@gmail.com\n\nRange-diff versus v2:\n\n1:  a7a5f8c752 = 1:  1bd0878fd7 refs/files: remove redundant check in split_symref_update()\n2:  61ebc1e133 = 2:  92181469bf refs: move duplicate refname update check to generic layer\n3:  f54f3d7722 = 3:  6fb0b6b03d refs/files: remove duplicate duplicates check\n4:  463e043cd2 = 4:  07788f97e9 refs/reftable: extract code from the transaction preparation\n5:  baa94ddfb6 ! 5:  2f872b650f refs: introduce enum-based transaction error types\n    @@ Commit message\n         refs: introduce enum-based transaction error types\n     \n         Replace preprocessor-defined transaction errors with a strongly-typed\n    -    enum `transaction_error`. This change:\n    +    enum `ref_transaction_error`. This change:\n     \n           - Improves type safety and function signature clarity.\n           - Makes error handling more explicit and discoverable.\n    @@ Commit message\n     \n         Signed-off-by: Karthik Nayak <karthik.188@gmail.com>\n     \n    + ## builtin/fetch.c ##\n    +@@ builtin/fetch.c: static int s_update_ref(const char *action,\n    + \t\tswitch (ref_transaction_commit(our_transaction, &err)) {\n    + \t\tcase 0:\n    + \t\t\tbreak;\n    +-\t\tcase TRANSACTION_NAME_CONFLICT:\n    ++\t\tcase REF_TRANSACTION_ERROR_NAME_CONFLICT:\n    + \t\t\tret = STORE_REF_ERROR_DF_CONFLICT;\n    + \t\t\tgoto out;\n    + \t\tdefault:\n    +\n      ## refs.c ##\n    +@@ refs.c: int refs_update_symref_extended(struct ref_store *refs, const char *ref,\n    + \t\t\t\t\t   REF_NO_DEREF, logmsg, &err))\n    + \t\t\tgoto error_return;\n    + \t\tprepret = ref_transaction_prepare(transaction, &err);\n    +-\t\tif (prepret && prepret != TRANSACTION_CREATE_EXISTS)\n    ++\t\tif (prepret && prepret != REF_TRANSACTION_ERROR_CREATE_EXISTS)\n    + \t\t\tgoto error_return;\n    + \t} else {\n    + \t\tif (ref_transaction_update(transaction, ref, NULL, NULL,\n    +@@ refs.c: int refs_update_symref_extended(struct ref_store *refs, const char *ref,\n    + \t\t}\n    + \t}\n    + \n    +-\tif (prepret == TRANSACTION_CREATE_EXISTS)\n    ++\tif (prepret == REF_TRANSACTION_ERROR_CREATE_EXISTS)\n    + \t\tgoto cleanup;\n    + \n    + \tif (ref_transaction_commit(transaction, &err))\n    +@@ refs.c: int ref_transaction_prepare(struct ref_transaction *transaction,\n    + \n    + \tstring_list_sort(&transaction->refnames);\n    + \tif (ref_update_reject_duplicates(&transaction->refnames, err))\n    +-\t\treturn TRANSACTION_GENERIC_ERROR;\n    ++\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n    + \n    + \tret = refs->be->transaction_prepare(refs, transaction, err);\n    + \tif (ret)\n     @@ refs.c: int ref_transaction_commit(struct ref_transaction *transaction,\n      \treturn ret;\n      }\n    @@ refs.c: int ref_transaction_commit(struct ref_transaction *transaction,\n     -\t\t\t\t   const struct string_list *skip,\n     -\t\t\t\t   unsigned int initial_transaction,\n     -\t\t\t\t   struct strbuf *err)\n    -+enum transaction_error refs_verify_refnames_available(struct ref_store *refs,\n    -+\t\t\t\t\t\t      const struct string_list *refnames,\n    -+\t\t\t\t\t\t      const struct string_list *extras,\n    -+\t\t\t\t\t\t      const struct string_list *skip,\n    -+\t\t\t\t\t\t      unsigned int initial_transaction,\n    -+\t\t\t\t\t\t      struct strbuf *err)\n    ++enum ref_transaction_error refs_verify_refnames_available(struct ref_store *refs,\n    ++\t\t\t\t\t  const struct string_list *refnames,\n    ++\t\t\t\t\t  const struct string_list *extras,\n    ++\t\t\t\t\t  const struct string_list *skip,\n    ++\t\t\t\t\t  unsigned int initial_transaction,\n    ++\t\t\t\t\t  struct strbuf *err)\n      {\n      \tstruct strbuf dirname = STRBUF_INIT;\n      \tstruct strbuf referent = STRBUF_INIT;\n      \tstruct ref_iterator *iter = NULL;\n      \tstruct strset dirnames;\n     -\tint ret = -1;\n    -+\tint ret = TRANSACTION_NAME_CONFLICT;\n    ++\tint ret = REF_TRANSACTION_ERROR_NAME_CONFLICT;\n      \n      \t/*\n      \t * For the sake of comments in this function, suppose that\n    @@ refs.c: int refs_verify_refnames_available(struct ref_store *refs,\n     -\t\t\t\t  const struct string_list *skip,\n     -\t\t\t\t  unsigned int initial_transaction,\n     -\t\t\t\t  struct strbuf *err)\n    -+enum transaction_error refs_verify_refname_available(struct ref_store *refs,\n    -+\t\t\t\t\t\t     const char *refname,\n    -+\t\t\t\t\t\t     const struct string_list *extras,\n    -+\t\t\t\t\t\t     const struct string_list *skip,\n    -+\t\t\t\t\t\t     unsigned int initial_transaction,\n    -+\t\t\t\t\t\t     struct strbuf *err)\n    ++enum ref_transaction_error refs_verify_refname_available(\n    ++\tstruct ref_store *refs,\n    ++\tconst char *refname,\n    ++\tconst struct string_list *extras,\n    ++\tconst struct string_list *skip,\n    ++\tunsigned int initial_transaction,\n    ++\tstruct strbuf *err)\n      {\n      \tstruct string_list_item item = { .string = (char *) refname };\n      \tstruct string_list refnames = {\n    @@ refs.c: int ref_update_has_null_new_value(struct ref_update *update)\n      \n     -int ref_update_check_old_target(const char *referent, struct ref_update *update,\n     -\t\t\t\tstruct strbuf *err)\n    -+enum transaction_error ref_update_check_old_target(const char *referent,\n    -+\t\t\t\t\t      struct ref_update *update,\n    -+\t\t\t\t\t      struct strbuf *err)\n    ++enum ref_transaction_error ref_update_check_old_target(const char *referent,\n    ++\t\t\t\t\t\t       struct ref_update *update,\n    ++\t\t\t\t\t\t       struct strbuf *err)\n      {\n      \tif (!update->old_target)\n      \t\tBUG(\"called without old_target set\");\n    - \n    +@@ refs.c: int ref_update_check_old_target(const char *referent, struct ref_update *update,\n      \tif (!strcmp(referent, update->old_target))\n    --\t\treturn 0;\n    -+\t\treturn TRANSACTION_OK;\n    + \t\treturn 0;\n      \n     -\tif (!strcmp(referent, \"\"))\n     +\tif (!strcmp(referent, \"\")) {\n    @@ refs.c: int ref_update_has_null_new_value(struct ref_update *update)\n     -\telse\n     -\t\tstrbuf_addf(err, \"verifying symref target: '%s': \"\n     -\t\t\t    \"is at %s but expected %s\",\n    -+\t\treturn TRANSACTION_NONEXISTENT_REF;\n    ++\t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n     +\t}\n     +\n     +\tstrbuf_addf(err, \"verifying symref target: '%s': is at %s but expected %s\",\n      \t\t\t    ref_update_original_update_refname(update),\n      \t\t\t    referent, update->old_target);\n     -\treturn -1;\n    -+\treturn TRANSACTION_INCORRECT_OLD_VALUE;\n    ++\treturn REF_TRANSACTION_ERROR_INCORRECT_OLD_VALUE;\n      }\n      \n      struct migration_data {\n    @@ refs.h: struct worktree;\n      const char *ref_storage_format_to_name(enum ref_storage_format ref_storage_format);\n      \n     +/*\n    -+ * enum transaction_error represents the following return codes:\n    -+ * TRANSACTION_OK: success code.\n    -+ * TRANSACTION_GENERIC_ERROR error_code: default error code.\n    -+ * TRANSACTION_NAME_CONFLICT error_code: ref name conflict like A vs A/B.\n    -+ * TRANSACTION_CREATE_EXISTS error_code: ref to be created already exists.\n    -+ * TRANSACTION_NONEXISTENT_REF error_code: ref expected but doesn't exist.\n    -+ * TRANSACTION_INCORRECT_OLD_VALUE error_code: provided old_oid or old_target of\n    ++ * enum ref_transaction_error represents the following return codes:\n    ++ * REF_TRANSACTION_ERROR_GENERIC error_code: default error code.\n    ++ * REF_TRANSACTION_ERROR_NAME_CONFLICT error_code: ref name conflict like A vs A/B.\n    ++ * REF_TRANSACTION_ERROR_CREATE_EXISTS error_code: ref to be created already exists.\n    ++ * REF_TRANSACTION_ERROR_NONEXISTENT_REF error_code: ref expected but doesn't exist.\n    ++ * REF_TRANSACTION_ERROR_INCORRECT_OLD_VALUE error_code: provided old_oid or old_target of\n     + * reference doesn't match actual.\n    -+ * TRANSACTION_INVALID_NEW_VALUE error_code: provided new_oid or new_target is\n    ++ * REF_TRANSACTION_ERROR_INVALID_NEW_VALUE error_code: provided new_oid or new_target is\n     + * invalid.\n    -+ * TRANSACTION_EXPECTED_SYMREF error_code: expected ref to be symref, but is a\n    ++ * REF_TRANSACTION_ERROR_EXPECTED_SYMREF error_code: expected ref to be symref, but is a\n     + * regular ref.\n     + */\n    -+enum transaction_error {\n    -+\tTRANSACTION_OK = 0,\n    -+\tTRANSACTION_GENERIC_ERROR = -1,\n    -+\tTRANSACTION_NAME_CONFLICT = -2,\n    -+\tTRANSACTION_CREATE_EXISTS = -3,\n    -+\tTRANSACTION_NONEXISTENT_REF = -4,\n    -+\tTRANSACTION_INCORRECT_OLD_VALUE = -5,\n    -+\tTRANSACTION_INVALID_NEW_VALUE = -6,\n    -+\tTRANSACTION_EXPECTED_SYMREF = -7,\n    ++enum ref_transaction_error {\n    ++\tREF_TRANSACTION_ERROR_GENERIC = -1,\n    ++\tREF_TRANSACTION_ERROR_NAME_CONFLICT = -2,\n    ++\tREF_TRANSACTION_ERROR_CREATE_EXISTS = -3,\n    ++\tREF_TRANSACTION_ERROR_NONEXISTENT_REF = -4,\n    ++\tREF_TRANSACTION_ERROR_INCORRECT_OLD_VALUE = -5,\n    ++\tREF_TRANSACTION_ERROR_INVALID_NEW_VALUE = -6,\n    ++\tREF_TRANSACTION_ERROR_EXPECTED_SYMREF = -7,\n     +};\n     +\n      /*\n    @@ refs.h: int refs_read_symbolic_ref(struct ref_store *ref_store, const char *refn\n     -\t\t\t\t  const struct string_list *skip,\n     -\t\t\t\t  unsigned int initial_transaction,\n     -\t\t\t\t  struct strbuf *err);\n    -+enum transaction_error refs_verify_refname_available(struct ref_store *refs,\n    -+\t\t\t\t\t\t     const char *refname,\n    -+\t\t\t\t\t\t     const struct string_list *extras,\n    -+\t\t\t\t\t\t     const struct string_list *skip,\n    -+\t\t\t\t\t\t     unsigned int initial_transaction,\n    -+\t\t\t\t\t\t     struct strbuf *err);\n    ++enum ref_transaction_error refs_verify_refname_available(struct ref_store *refs,\n    ++\t\t\t\t\t\t const char *refname,\n    ++\t\t\t\t\t\t const struct string_list *extras,\n    ++\t\t\t\t\t\t const struct string_list *skip,\n    ++\t\t\t\t\t\t unsigned int initial_transaction,\n    ++\t\t\t\t\t\t struct strbuf *err);\n      \n      /*\n       * Same as `refs_verify_refname_available()`, but checking for a list of\n    @@ refs.h: int refs_read_symbolic_ref(struct ref_store *ref_store, const char *refn\n     -\t\t\t\t   const struct string_list *skip,\n     -\t\t\t\t   unsigned int initial_transaction,\n     -\t\t\t\t   struct strbuf *err);\n    -+enum transaction_error refs_verify_refnames_available(struct ref_store *refs,\n    -+\t\t\t\t\t\t      const struct string_list *refnames,\n    -+\t\t\t\t\t\t      const struct string_list *extras,\n    -+\t\t\t\t\t\t      const struct string_list *skip,\n    -+\t\t\t\t\t\t      unsigned int initial_transaction,\n    -+\t\t\t\t\t\t      struct strbuf *err);\n    ++enum ref_transaction_error refs_verify_refnames_available(struct ref_store *refs,\n    ++\t\t\t\t\t  const struct string_list *refnames,\n    ++\t\t\t\t\t  const struct string_list *extras,\n    ++\t\t\t\t\t  const struct string_list *skip,\n    ++\t\t\t\t\t  unsigned int initial_transaction,\n    ++\t\t\t\t\t  struct strbuf *err);\n      \n      int refs_ref_exists(struct ref_store *refs, const char *refname);\n      \n    @@ refs.h: int ref_transaction_verify(struct ref_transaction *transaction,\n       * any needed locks, check preconditions, etc.; basically, do as much\n     \n      ## refs/files-backend.c ##\n    +@@ refs/files-backend.c: static void unlock_ref(struct ref_lock *lock)\n    +  * broken, lock the reference anyway but clear old_oid.\n    +  *\n    +  * Return 0 on success. On failure, write an error message to err and\n    +- * return TRANSACTION_NAME_CONFLICT or TRANSACTION_GENERIC_ERROR.\n    ++ * return REF_TRANSACTION_ERROR_NAME_CONFLICT or REF_TRANSACTION_ERROR_GENERIC.\n    +  *\n    +  * Implementation note: This function is basically\n    +  *\n     @@ refs/files-backend.c: static void unlock_ref(struct ref_lock *lock)\n       *   avoided, namely if we were successfully able to read the ref\n       * - Generate informative error messages in the case of failure\n    @@ refs/files-backend.c: static void unlock_ref(struct ref_lock *lock)\n     -\t\t\tstruct strbuf *referent,\n     -\t\t\tunsigned int *type,\n     -\t\t\tstruct strbuf *err)\n    -+static enum transaction_error lock_raw_ref(struct files_ref_store *refs,\n    -+\t\t\t\t\t   const char *refname, int mustexist,\n    -+\t\t\t\t\t   struct string_list *refnames_to_check,\n    -+\t\t\t\t\t   const struct string_list *extras,\n    -+\t\t\t\t\t   struct ref_lock **lock_p,\n    -+\t\t\t\t\t   struct strbuf *referent,\n    -+\t\t\t\t\t   unsigned int *type,\n    -+\t\t\t\t\t   struct strbuf *err)\n    - {\n    -+\tenum transaction_error ret = TRANSACTION_GENERIC_ERROR;\n    +-{\n    ++static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs,\n    ++\t\t\t\t\t       const char *refname,\n    ++\t\t\t\t\t       int mustexist,\n    ++\t\t\t\t\t       struct string_list *refnames_to_check,\n    ++\t\t\t\t\t       const struct string_list *extras,\n    ++\t\t\t\t\t       struct ref_lock **lock_p,\n    ++\t\t\t\t\t       struct strbuf *referent,\n    ++\t\t\t\t\t       unsigned int *type,\n    ++\t\t\t\t\t       struct strbuf *err)\n    ++{\n    ++\tenum ref_transaction_error ret = REF_TRANSACTION_ERROR_GENERIC;\n      \tstruct ref_lock *lock;\n      \tstruct strbuf ref_file = STRBUF_INIT;\n      \tint attempts_remaining = 3;\n    @@ refs/files-backend.c: static int lock_raw_ref(struct files_ref_store *refs,\n      \t\t\t\tstrbuf_reset(err);\n      \t\t\t\tstrbuf_addf(err, \"unable to resolve reference '%s'\",\n      \t\t\t\t\t    refname);\n    -+\t\t\t\tret = TRANSACTION_NONEXISTENT_REF;\n    ++\t\t\t\tret = REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n      \t\t\t} else {\n      \t\t\t\t/*\n      \t\t\t\t * The error message set by\n    + \t\t\t\t * refs_verify_refname_available() is\n    + \t\t\t\t * OK.\n    + \t\t\t\t */\n    +-\t\t\t\tret = TRANSACTION_NAME_CONFLICT;\n    ++\t\t\t\tret = REF_TRANSACTION_ERROR_NAME_CONFLICT;\n    + \t\t\t}\n    + \t\t} else {\n    + \t\t\t/*\n     @@ refs/files-backend.c: static int lock_raw_ref(struct files_ref_store *refs,\n      \t\t\t\t/* Garden variety missing reference. */\n      \t\t\t\tstrbuf_addf(err, \"unable to resolve reference '%s'\",\n      \t\t\t\t\t    refname);\n    -+\t\t\t\tret = TRANSACTION_NONEXISTENT_REF;\n    ++\t\t\t\tret = REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n      \t\t\t\tgoto error_return;\n      \t\t\t} else {\n      \t\t\t\t/*\n    @@ refs/files-backend.c: static int lock_raw_ref(struct files_ref_store *refs,\n      \t\t\t\t/* Garden variety missing reference. */\n      \t\t\t\tstrbuf_addf(err, \"unable to resolve reference '%s'\",\n      \t\t\t\t\t    refname);\n    -+\t\t\t\tret = TRANSACTION_NONEXISTENT_REF;\n    ++\t\t\t\tret = REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n      \t\t\t\tgoto error_return;\n      \t\t\t} else if (remove_dir_recursively(&ref_file,\n      \t\t\t\t\t\t\t  REMOVE_DIR_EMPTY_ONLY)) {\n     @@ refs/files-backend.c: static int lock_raw_ref(struct files_ref_store *refs,\n    - \t\tstring_list_insert(refnames_to_check, refname);\n    - \t}\n    - \n    --\tret = 0;\n    -+\tret = TRANSACTION_OK;\n    - \tgoto out;\n    - \n    - error_return:\n    + \t\t\t\t\t * The error message set by\n    + \t\t\t\t\t * verify_refname_available() is OK.\n    + \t\t\t\t\t */\n    +-\t\t\t\t\tret = TRANSACTION_NAME_CONFLICT;\n    ++\t\t\t\t\tret = REF_TRANSACTION_ERROR_NAME_CONFLICT;\n    + \t\t\t\t\tgoto error_return;\n    + \t\t\t\t} else {\n    + \t\t\t\t\t/*\n     @@ refs/files-backend.c: static int rename_tmp_log(struct files_ref_store *refs, const char *newrefname)\n      \treturn ret;\n      }\n    @@ refs/files-backend.c: static int rename_tmp_log(struct files_ref_store *refs, co\n     -\t\t\t\t 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 enum transaction_error write_ref_to_lockfile(struct files_ref_store *refs,\n    -+\t\t\t\t\t\t    struct ref_lock *lock,\n    -+\t\t\t\t\t\t    const struct object_id *oid,\n    -+\t\t\t\t\t\t    int skip_oid_verification, struct strbuf *err);\n    ++static enum ref_transaction_error write_ref_to_lockfile(struct files_ref_store *refs,\n    ++\t\t\t\t\t\t\tstruct ref_lock *lock,\n    ++\t\t\t\t\t\t\tconst struct object_id *oid,\n    ++\t\t\t\t\t\t\tint skip_oid_verification,\n    ++\t\t\t\t\t\t\tstruct 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    @@ refs/files-backend.c: static int files_log_ref_write(struct files_ref_store *ref\n     -\t\t\t\t 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 enum transaction_error write_ref_to_lockfile(struct files_ref_store *refs,\n    -+\t\t\t\t\t\t    struct ref_lock *lock,\n    -+\t\t\t\t\t\t    const struct object_id *oid,\n    -+\t\t\t\t\t\t    int skip_oid_verification,\n    -+\t\t\t\t\t\t    struct strbuf *err)\n    ++static enum ref_transaction_error write_ref_to_lockfile(struct files_ref_store *refs,\n    ++\t\t\t\t\t\t\tstruct ref_lock *lock,\n    ++\t\t\t\t\t\t\tconst struct object_id *oid,\n    ++\t\t\t\t\t\t\tint skip_oid_verification,\n    ++\t\t\t\t\t\t\tstruct strbuf *err)\n      {\n      \tstatic char term = '\\n';\n      \tstruct object *o;\n    @@ refs/files-backend.c: static int write_ref_to_lockfile(struct files_ref_store *r\n      \t\t\t\tlock->ref_name, oid_to_hex(oid));\n      \t\t\tunlock_ref(lock);\n     -\t\t\treturn -1;\n    -+\t\t\treturn TRANSACTION_INVALID_NEW_VALUE;\n    ++\t\t\treturn REF_TRANSACTION_ERROR_INVALID_NEW_VALUE;\n      \t\t}\n      \t\tif (o->type != OBJ_COMMIT && is_branch(lock->ref_name)) {\n      \t\t\tstrbuf_addf(\n    @@ refs/files-backend.c: static int write_ref_to_lockfile(struct files_ref_store *r\n      \t\t\t\toid_to_hex(oid), lock->ref_name);\n      \t\t\tunlock_ref(lock);\n     -\t\t\treturn -1;\n    -+\t\t\treturn TRANSACTION_INVALID_NEW_VALUE;\n    ++\t\t\treturn REF_TRANSACTION_ERROR_INVALID_NEW_VALUE;\n      \t\t}\n      \t}\n      \tfd = get_lock_file_fd(&lock->lk);\n    @@ refs/files-backend.c: static int write_ref_to_lockfile(struct files_ref_store *r\n      \t\t\t    \"couldn't write '%s'\", get_lock_file_path(&lock->lk));\n      \t\tunlock_ref(lock);\n     -\t\treturn -1;\n    -+\t\treturn TRANSACTION_GENERIC_ERROR;\n    ++\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n      \t}\n    --\treturn 0;\n    -+\treturn TRANSACTION_OK;\n    + \treturn 0;\n      }\n    - \n    - /*\n     @@ refs/files-backend.c: static struct ref_iterator *files_reflog_iterator_begin(struct ref_store *ref_st\n       * If update is a direct update of head_ref (the reference pointed to\n       * by HEAD), then add an extra REF_LOG_ONLY update for HEAD.\n    @@ refs/files-backend.c: static struct ref_iterator *files_reflog_iterator_begin(st\n     -static int split_head_update(struct ref_update *update,\n     -\t\t\t     struct ref_transaction *transaction,\n     -\t\t\t     const char *head_ref, struct strbuf *err)\n    -+static enum transaction_error split_head_update(struct ref_update *update,\n    -+\t\t\t\t\t\tstruct ref_transaction *transaction,\n    -+\t\t\t\t\t\tconst char *head_ref,\n    -+\t\t\t\t\t\tstruct strbuf *err)\n    ++static enum ref_transaction_error split_head_update(struct ref_update *update,\n    ++\t\t\t\t\t\t    struct ref_transaction *transaction,\n    ++\t\t\t\t\t\t    const char *head_ref,\n    ++\t\t\t\t\t\t    struct strbuf *err)\n      {\n      \tstruct ref_update *new_update;\n      \n     @@ refs/files-backend.c: static int split_head_update(struct ref_update *update,\n    - \t    (update->flags & REF_SKIP_CREATE_REFLOG) ||\n    - \t    (update->flags & REF_IS_PRUNING) ||\n    - \t    (update->flags & REF_UPDATE_VIA_HEAD))\n    --\t\treturn 0;\n    -+\t\treturn TRANSACTION_OK;\n    - \n    - \tif (strcmp(update->refname, head_ref))\n    --\t\treturn 0;\n    -+\t\treturn TRANSACTION_OK;\n    - \n    - \t/*\n    - \t * First make sure that HEAD is not already in the\n    -@@ refs/files-backend.c: static int split_head_update(struct ref_update *update,\n    - \tif (strcmp(new_update->refname, \"HEAD\"))\n    - \t\tBUG(\"%s unexpectedly not 'HEAD'\", new_update->refname);\n    - \n    --\treturn 0;\n    -+\treturn TRANSACTION_OK;\n    - }\n    + \t\t\t    \"multiple updates for 'HEAD' (including one \"\n    + \t\t\t    \"via its referent '%s') are not allowed\",\n    + \t\t\t    update->refname);\n    +-\t\treturn TRANSACTION_NAME_CONFLICT;\n    ++\t\treturn REF_TRANSACTION_ERROR_NAME_CONFLICT;\n    + \t}\n      \n    - /*\n    + \tnew_update = ref_transaction_add_update(\n     @@ refs/files-backend.c: static int split_head_update(struct ref_update *update,\n       * Note that the new update will itself be subject to splitting when\n       * the iteration gets to it.\n    @@ refs/files-backend.c: static int split_head_update(struct ref_update *update,\n     -\t\t\t       const char *referent,\n     -\t\t\t       struct ref_transaction *transaction,\n     -\t\t\t       struct strbuf *err)\n    -+static enum transaction_error split_symref_update(struct ref_update *update,\n    -+\t\t\t\t\t\t  const char *referent,\n    -+\t\t\t\t\t\t  struct ref_transaction *transaction,\n    -+\t\t\t\t\t\t  struct strbuf *err)\n    ++static enum ref_transaction_error split_symref_update(struct ref_update *update,\n    ++\t\t\t\t\t\t      const char *referent,\n    ++\t\t\t\t\t\t      struct ref_transaction *transaction,\n    ++\t\t\t\t\t\t      struct strbuf *err)\n      {\n      \tstruct ref_update *new_update;\n      \tunsigned int new_flags;\n     @@ refs/files-backend.c: static int split_symref_update(struct ref_update *update,\n    - \tupdate->flags |= REF_LOG_ONLY | REF_NO_DEREF;\n    - \tupdate->flags &= ~REF_HAVE_OLD;\n    - \n    --\treturn 0;\n    -+\treturn TRANSACTION_OK;\n    - }\n    + \t\t\t    \"multiple updates for '%s' (including one \"\n    + \t\t\t    \"via symref '%s') are not allowed\",\n    + \t\t\t    referent, update->refname);\n    +-\t\treturn TRANSACTION_NAME_CONFLICT;\n    ++\t\treturn REF_TRANSACTION_ERROR_NAME_CONFLICT;\n    + \t}\n      \n    - /*\n    + \tnew_flags = update->flags;\n     @@ refs/files-backend.c: static int split_symref_update(struct ref_update *update,\n       * everything is OK, return 0; otherwise, write an error message to\n       * err and return -1.\n       */\n     -static int check_old_oid(struct ref_update *update, struct object_id *oid,\n     -\t\t\t struct strbuf *err)\n    -+static enum transaction_error check_old_oid(struct ref_update *update,\n    -+\t\t\t\t\t    struct object_id *oid,\n    -+\t\t\t\t\t    struct strbuf *err)\n    ++static enum ref_transaction_error check_old_oid(struct ref_update *update,\n    ++\t\t\t\t\t\tstruct object_id *oid,\n    ++\t\t\t\t\t\tstruct strbuf *err)\n      {\n     -\tint ret = TRANSACTION_GENERIC_ERROR;\n     -\n      \tif (!(update->flags & REF_HAVE_OLD) ||\n      \t\t   oideq(oid, &update->old_oid))\n    --\t\treturn 0;\n    -+\t\treturn TRANSACTION_OK;\n    - \n    - \tif (is_null_oid(&update->old_oid)) {\n    + \t\treturn 0;\n    +@@ refs/files-backend.c: static int check_old_oid(struct ref_update *update, struct object_id *oid,\n      \t\tstrbuf_addf(err, \"cannot lock ref '%s': \"\n      \t\t\t    \"reference already exists\",\n      \t\t\t    ref_update_original_update_refname(update));\n     -\t\tret = TRANSACTION_CREATE_EXISTS;\n     -\t}\n     -\telse if (is_null_oid(oid))\n    -+\t\treturn TRANSACTION_CREATE_EXISTS;\n    ++\t\treturn REF_TRANSACTION_ERROR_CREATE_EXISTS;\n     +\t} else if (is_null_oid(oid)) {\n      \t\tstrbuf_addf(err, \"cannot lock ref '%s': \"\n      \t\t\t    \"reference is missing but expected %s\",\n    @@ refs/files-backend.c: static int split_symref_update(struct ref_update *update,\n     -\t\t\t    ref_update_original_update_refname(update),\n     -\t\t\t    oid_to_hex(oid),\n     -\t\t\t    oid_to_hex(&update->old_oid));\n    -+\t\treturn TRANSACTION_NONEXISTENT_REF;\n    ++\t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n     +\t}\n      \n     -\treturn ret;\n    @@ refs/files-backend.c: static int split_symref_update(struct ref_update *update,\n     +\t\t    ref_update_original_update_refname(update), oid_to_hex(oid),\n     +\t\t    oid_to_hex(&update->old_oid));\n     +\n    -+\treturn TRANSACTION_INCORRECT_OLD_VALUE;\n    ++\treturn REF_TRANSACTION_ERROR_INCORRECT_OLD_VALUE;\n      }\n      \n      struct files_transaction_backend_data {\n    @@ refs/files-backend.c: struct files_transaction_backend_data {\n     -\t\t\t       const char *head_ref,\n     -\t\t\t       struct string_list *refnames_to_check,\n     -\t\t\t       struct strbuf *err)\n    -+static enum transaction_error lock_ref_for_update(struct files_ref_store *refs,\n    -+\t\t\t\t\t\t  struct ref_update *update,\n    -+\t\t\t\t\t\t  struct ref_transaction *transaction,\n    -+\t\t\t\t\t\t  const char *head_ref,\n    -+\t\t\t\t\t\t  struct string_list *refnames_to_check,\n    -+\t\t\t\t\t\t  struct strbuf *err)\n    ++static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *refs,\n    ++\t\t\t\t\t\t      struct ref_update *update,\n    ++\t\t\t\t\t\t      struct ref_transaction *transaction,\n    ++\t\t\t\t\t\t      const char *head_ref,\n    ++\t\t\t\t\t\t      struct string_list *refnames_to_check,\n    ++\t\t\t\t\t\t      struct strbuf *err)\n      {\n      \tstruct strbuf referent = STRBUF_INIT;\n      \tint mustexist = ref_update_expects_existing_old_ref(update);\n      \tstruct files_transaction_backend_data *backend_data;\n     -\tint ret = 0;\n    -+\tenum transaction_error ret = TRANSACTION_OK;\n    ++\tenum ref_transaction_error ret = 0;\n      \tstruct ref_lock *lock;\n      \n      \tfiles_assert_main_repository(refs, \"lock_ref_for_update\");\n     @@ refs/files-backend.c: static int lock_ref_for_update(struct files_ref_store *refs,\n    + \t\t\t\t\tstrbuf_addf(err, \"cannot lock ref '%s': \"\n    + \t\t\t\t\t\t    \"error reading reference\",\n    + \t\t\t\t\t\t    ref_update_original_update_refname(update));\n    +-\t\t\t\t\tret = TRANSACTION_GENERIC_ERROR;\n    ++\t\t\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n    + \t\t\t\t\tgoto out;\n      \t\t\t\t}\n      \t\t\t}\n      \n    @@ refs/files-backend.c: static int lock_ref_for_update(struct files_ref_store *ref\n     -\t\t\t\tif  (ret) {\n     -\t\t\t\t\tgoto out;\n     -\t\t\t\t}\n    -+\t\t\tif (ret) {\n    +-\t\t\t}\n    ++\t\t\tif (ret)\n     +\t\t\t\tgoto out;\n    - \t\t\t}\n      \t\t} else {\n      \t\t\t/*\n    + \t\t\t * Create a new update for the reference this\n     @@ refs/files-backend.c: static int lock_ref_for_update(struct files_ref_store *refs,\n      \t\t\t\t\t   \"but is a regular ref\"),\n      \t\t\t\t    ref_update_original_update_refname(update),\n      \t\t\t\t    update->old_target);\n     -\t\t\tret = TRANSACTION_GENERIC_ERROR;\n    -+\t\t\tret = TRANSACTION_EXPECTED_SYMREF;\n    ++\t\t\tret = REF_TRANSACTION_ERROR_EXPECTED_SYMREF;\n      \t\t\tgoto out;\n      \t\t} else {\n      \t\t\tret = check_old_oid(update, &lock->old_oid, err);\n    +@@ refs/files-backend.c: static int lock_ref_for_update(struct files_ref_store *refs,\n    + \n    + \tif (update->new_target && !(update->flags & REF_LOG_ONLY)) {\n    + \t\tif (create_symref_lock(lock, update->new_target, err)) {\n    +-\t\t\tret = TRANSACTION_GENERIC_ERROR;\n    ++\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n    + \t\t\tgoto out;\n    + \t\t}\n    + \n    + \t\tif (close_ref_gently(lock)) {\n    + \t\t\tstrbuf_addf(err, \"couldn't close '%s.lock'\",\n    + \t\t\t\t    update->refname);\n    +-\t\t\tret = TRANSACTION_GENERIC_ERROR;\n    ++\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n    + \t\t\tgoto out;\n    + \t\t}\n    + \n     @@ refs/files-backend.c: static int lock_ref_for_update(struct files_ref_store *refs,\n      \t\t\t * The reference already has the desired\n      \t\t\t * value, so we don't need to write it.\n    @@ refs/files-backend.c: static int lock_ref_for_update(struct files_ref_store *ref\n      \t\t}\n      \t}\n      \tif (!(update->flags & REF_NEEDS_COMMIT)) {\n    +@@ refs/files-backend.c: static int lock_ref_for_update(struct files_ref_store *refs,\n    + \t\tif (close_ref_gently(lock)) {\n    + \t\t\tstrbuf_addf(err, \"couldn't close '%s.lock'\",\n    + \t\t\t\t    update->refname);\n    +-\t\t\tret = TRANSACTION_GENERIC_ERROR;\n    ++\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n    + \t\t\tgoto out;\n    + \t\t}\n    + \t}\n    +@@ refs/files-backend.c: static int files_transaction_prepare(struct ref_store *ref_store,\n    + \t\t\t\t\t\trefs->packed_ref_store,\n    + \t\t\t\t\t\ttransaction->flags, err);\n    + \t\t\t\tif (!packed_transaction) {\n    +-\t\t\t\t\tret = TRANSACTION_GENERIC_ERROR;\n    ++\t\t\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n    + \t\t\t\t\tgoto cleanup;\n    + \t\t\t\t}\n    + \n    +@@ refs/files-backend.c: static int files_transaction_prepare(struct ref_store *ref_store,\n    + \t */\n    + \tif (refs_verify_refnames_available(refs->packed_ref_store, &refnames_to_check,\n    + \t\t\t\t\t   &transaction->refnames, NULL, 0, err)) {\n    +-\t\tret = TRANSACTION_NAME_CONFLICT;\n    ++\t\tret = REF_TRANSACTION_ERROR_NAME_CONFLICT;\n    + \t\tgoto cleanup;\n    + \t}\n    + \n    + \tif (packed_transaction) {\n    + \t\tif (packed_refs_lock(refs->packed_ref_store, 0, err)) {\n    +-\t\t\tret = TRANSACTION_GENERIC_ERROR;\n    ++\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n    + \t\t\tgoto cleanup;\n    + \t\t}\n    + \t\tbackend_data->packed_refs_locked = 1;\n    +@@ refs/files-backend.c: static int files_transaction_prepare(struct ref_store *ref_store,\n    + \t\t\t */\n    + \t\t\tbackend_data->packed_transaction = NULL;\n    + \t\t\tif (ref_transaction_abort(packed_transaction, err)) {\n    +-\t\t\t\tret = TRANSACTION_GENERIC_ERROR;\n    ++\t\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n    + \t\t\t\tgoto cleanup;\n    + \t\t\t}\n    + \t\t}\n    +@@ refs/files-backend.c: static int files_transaction_finish_initial(struct files_ref_store *refs,\n    + \tpacked_transaction = ref_store_transaction_begin(refs->packed_ref_store,\n    + \t\t\t\t\t\t\t transaction->flags, err);\n    + \tif (!packed_transaction) {\n    +-\t\tret = TRANSACTION_GENERIC_ERROR;\n    ++\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n    + \t\tgoto cleanup;\n    + \t}\n    + \n    +@@ refs/files-backend.c: static int files_transaction_finish_initial(struct files_ref_store *refs,\n    + \t\t\tif (!loose_transaction) {\n    + \t\t\t\tloose_transaction = ref_store_transaction_begin(&refs->base, 0, err);\n    + \t\t\t\tif (!loose_transaction) {\n    +-\t\t\t\t\tret = TRANSACTION_GENERIC_ERROR;\n    ++\t\t\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n    + \t\t\t\t\tgoto cleanup;\n    + \t\t\t\t}\n    + \t\t\t}\n    +@@ refs/files-backend.c: static int files_transaction_finish_initial(struct files_ref_store *refs,\n    + \t}\n    + \n    + \tif (packed_refs_lock(refs->packed_ref_store, 0, err)) {\n    +-\t\tret = TRANSACTION_GENERIC_ERROR;\n    ++\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n    + \t\tgoto cleanup;\n    + \t}\n    + \n    + \tif (refs_verify_refnames_available(&refs->base, &refnames_to_check,\n    + \t\t\t\t\t   &affected_refnames, NULL, 1, err)) {\n    + \t\tpacked_refs_unlock(refs->packed_ref_store);\n    +-\t\tret = TRANSACTION_NAME_CONFLICT;\n    ++\t\tret = REF_TRANSACTION_ERROR_NAME_CONFLICT;\n    + \t\tgoto cleanup;\n    + \t}\n    + \n    + \tif (ref_transaction_commit(packed_transaction, err)) {\n    +-\t\tret = TRANSACTION_GENERIC_ERROR;\n    ++\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n    + \t\tgoto cleanup;\n    + \t}\n    + \tpacked_refs_unlock(refs->packed_ref_store);\n    +@@ refs/files-backend.c: static int files_transaction_finish_initial(struct files_ref_store *refs,\n    + \tif (loose_transaction) {\n    + \t\tif (ref_transaction_prepare(loose_transaction, err) ||\n    + \t\t    ref_transaction_commit(loose_transaction, err)) {\n    +-\t\t\tret = TRANSACTION_GENERIC_ERROR;\n    ++\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n    + \t\t\tgoto cleanup;\n    + \t\t}\n    + \t}\n    +@@ refs/files-backend.c: static int files_transaction_finish(struct ref_store *ref_store,\n    + \t\tif (update->flags & REF_NEEDS_COMMIT ||\n    + \t\t    update->flags & REF_LOG_ONLY) {\n    + \t\t\tif (parse_and_write_reflog(refs, update, lock, err)) {\n    +-\t\t\t\tret = TRANSACTION_GENERIC_ERROR;\n    ++\t\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n    + \t\t\t\tgoto cleanup;\n    + \t\t\t}\n    + \t\t}\n    +@@ refs/files-backend.c: static int files_transaction_finish(struct ref_store *ref_store,\n    + \t\t\t\tstrbuf_addf(err, \"couldn't set '%s'\", lock->ref_name);\n    + \t\t\t\tunlock_ref(lock);\n    + \t\t\t\tupdate->backend_data = NULL;\n    +-\t\t\t\tret = TRANSACTION_GENERIC_ERROR;\n    ++\t\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n    + \t\t\t\tgoto cleanup;\n    + \t\t\t}\n    + \t\t}\n    +@@ refs/files-backend.c: static int files_transaction_finish(struct ref_store *ref_store,\n    + \t\t\t\tstrbuf_reset(&sb);\n    + \t\t\t\tfiles_ref_path(refs, &sb, lock->ref_name);\n    + \t\t\t\tif (unlink_or_msg(sb.buf, err)) {\n    +-\t\t\t\t\tret = TRANSACTION_GENERIC_ERROR;\n    ++\t\t\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n    + \t\t\t\t\tgoto cleanup;\n    + \t\t\t\t}\n    + \t\t\t}\n     \n      ## refs/packed-backend.c ##\n     @@ refs/packed-backend.c: static int packed_ref_store_remove_on_disk(struct ref_store *ref_store,\n    @@ refs/packed-backend.c: static int packed_ref_store_remove_on_disk(struct ref_sto\n     -static int write_with_updates(struct packed_ref_store *refs,\n     -\t\t\t      struct string_list *updates,\n     -\t\t\t      struct strbuf *err)\n    -+static enum transaction_error write_with_updates(struct packed_ref_store *refs,\n    -+\t\t\t\t\t\t struct string_list *updates,\n    -+\t\t\t\t\t\t struct strbuf *err)\n    ++static enum ref_transaction_error write_with_updates(struct packed_ref_store *refs,\n    ++\t\t\t\t\t\t     struct string_list *updates,\n    ++\t\t\t\t\t\t     struct strbuf *err)\n      {\n    -+\tenum transaction_error ret = TRANSACTION_GENERIC_ERROR;\n    ++\tenum ref_transaction_error ret = REF_TRANSACTION_ERROR_GENERIC;\n      \tstruct ref_iterator *iter = NULL;\n      \tsize_t i;\n      \tint ok;\n    @@ refs/packed-backend.c: static int write_with_updates(struct packed_ref_store *re\n      \t\t\t    sb.buf, strerror(errno));\n      \t\tstrbuf_release(&sb);\n     -\t\treturn -1;\n    -+\t\treturn TRANSACTION_GENERIC_ERROR;\n    ++\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n      \t}\n      \tstrbuf_release(&sb);\n      \n    @@ refs/packed-backend.c: static int write_with_updates(struct packed_ref_store *re\n      \t\t\t\t\tstrbuf_addf(err, \"cannot update ref '%s': \"\n      \t\t\t\t\t\t    \"reference already exists\",\n      \t\t\t\t\t\t    update->refname);\n    -+\t\t\t\t\tret = TRANSACTION_CREATE_EXISTS;\n    ++\t\t\t\t\tret = REF_TRANSACTION_ERROR_CREATE_EXISTS;\n      \t\t\t\t\tgoto error;\n      \t\t\t\t} else if (!oideq(&update->old_oid, iter->oid)) {\n      \t\t\t\t\tstrbuf_addf(err, \"cannot update ref '%s': \"\n    @@ refs/packed-backend.c: static int write_with_updates(struct packed_ref_store *re\n      \t\t\t\t\t\t    update->refname,\n      \t\t\t\t\t\t    oid_to_hex(iter->oid),\n      \t\t\t\t\t\t    oid_to_hex(&update->old_oid));\n    -+\t\t\t\t\tret = TRANSACTION_INCORRECT_OLD_VALUE;\n    ++\t\t\t\t\tret = REF_TRANSACTION_ERROR_INCORRECT_OLD_VALUE;\n      \t\t\t\t\tgoto error;\n      \t\t\t\t}\n      \t\t\t}\n    @@ refs/packed-backend.c: static int write_with_updates(struct packed_ref_store *re\n      \t\t\t\t\t    \"reference is missing but expected %s\",\n      \t\t\t\t\t    update->refname,\n      \t\t\t\t\t    oid_to_hex(&update->old_oid));\n    -+\t\t\t\treturn TRANSACTION_NONEXISTENT_REF;\n    ++\t\t\t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n      \t\t\t\tgoto error;\n      \t\t\t}\n      \t\t}\n    @@ refs/packed-backend.c: static int write_with_updates(struct packed_ref_store *re\n      \t\tstrbuf_release(&sb);\n      \t\tdelete_tempfile(&refs->tempfile);\n     -\t\treturn -1;\n    -+\t\treturn TRANSACTION_GENERIC_ERROR;\n    ++\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n      \t}\n      \n    --\treturn 0;\n    -+\treturn TRANSACTION_OK;\n    - \n    - write_error:\n    - \tstrbuf_addf(err, \"error writing to %s: %s\",\n    + \treturn 0;\n     @@ refs/packed-backend.c: static int write_with_updates(struct packed_ref_store *refs,\n      error:\n      \tref_iterator_free(iter);\n    @@ refs/packed-backend.c: static int write_with_updates(struct packed_ref_store *re\n      }\n      \n      int is_packed_transaction_needed(struct ref_store *ref_store,\n    +@@ refs/packed-backend.c: static int packed_transaction_prepare(struct ref_store *ref_store,\n    + \t\t\tREF_STORE_READ | REF_STORE_WRITE | REF_STORE_ODB,\n    + \t\t\t\"ref_transaction_prepare\");\n    + \tstruct packed_transaction_backend_data *data;\n    +-\tint ret = TRANSACTION_GENERIC_ERROR;\n    ++\tenum ref_transaction_error ret = REF_TRANSACTION_ERROR_GENERIC;\n    + \n    + \t/*\n    + \t * Note that we *don't* skip transactions with zero updates,\n     @@ refs/packed-backend.c: static int packed_transaction_prepare(struct ref_store *ref_store,\n      \t\tdata->own_lock = 1;\n      \t}\n    @@ refs/packed-backend.c: static int packed_transaction_prepare(struct ref_store *r\n      \t\tgoto failure;\n      \n      \ttransaction->state = REF_TRANSACTION_PREPARED;\n    +@@ refs/packed-backend.c: static int packed_transaction_finish(struct ref_store *ref_store,\n    + \t\t\tref_store,\n    + \t\t\tREF_STORE_READ | REF_STORE_WRITE | REF_STORE_ODB,\n    + \t\t\t\"ref_transaction_finish\");\n    +-\tint ret = TRANSACTION_GENERIC_ERROR;\n    ++\tint ret = REF_TRANSACTION_ERROR_GENERIC;\n    + \tchar *packed_refs_path;\n    + \n    + \tclear_snapshot(refs);\n     \n      ## refs/refs-internal.h ##\n     @@ refs/refs-internal.h: int ref_update_has_null_new_value(struct ref_update *update);\n    @@ refs/refs-internal.h: int ref_update_has_null_new_value(struct ref_update *updat\n       */\n     -int ref_update_check_old_target(const char *referent, struct ref_update *update,\n     -\t\t\t\tstruct strbuf *err);\n    -+enum transaction_error ref_update_check_old_target(const char *referent,\n    -+\t\t\t\t\t      struct ref_update *update,\n    -+\t\t\t\t\t      struct strbuf *err);\n    ++enum ref_transaction_error ref_update_check_old_target(const char *referent,\n    ++\t\t\t\t\t\t       struct ref_update *update,\n    ++\t\t\t\t\t\t       struct strbuf *err);\n      \n      /*\n       * Check if the ref must exist, this means that the old_oid or\n    @@ refs/reftable-backend.c: static int queue_transaction_update(struct reftable_ref\n     -\t\t\t\t struct strbuf *head_referent,\n     -\t\t\t\t struct strbuf *referent,\n     -\t\t\t\t struct strbuf *err)\n    -+static enum transaction_error prepare_single_update(struct reftable_ref_store *refs,\n    -+\t\t\t\t\t\t    struct reftable_transaction_data *tx_data,\n    -+\t\t\t\t\t\t    struct ref_transaction *transaction,\n    -+\t\t\t\t\t\t    struct reftable_backend *be,\n    -+\t\t\t\t\t\t    struct ref_update *u,\n    -+\t\t\t\t\t\t    struct string_list *refnames_to_check,\n    -+\t\t\t\t\t\t    unsigned int head_type,\n    -+\t\t\t\t\t\t    struct strbuf *head_referent,\n    -+\t\t\t\t\t\t    struct strbuf *referent,\n    -+\t\t\t\t\t\t    struct strbuf *err)\n    ++static enum ref_transaction_error prepare_single_update(struct reftable_ref_store *refs,\n    ++\t\t\t\t\t\t\tstruct reftable_transaction_data *tx_data,\n    ++\t\t\t\t\t\t\tstruct ref_transaction *transaction,\n    ++\t\t\t\t\t\t\tstruct reftable_backend *be,\n    ++\t\t\t\t\t\t\tstruct ref_update *u,\n    ++\t\t\t\t\t\t\tstruct string_list *refnames_to_check,\n    ++\t\t\t\t\t\t\tunsigned int head_type,\n    ++\t\t\t\t\t\t\tstruct strbuf *head_referent,\n    ++\t\t\t\t\t\t\tstruct strbuf *referent,\n    ++\t\t\t\t\t\t\tstruct strbuf *err)\n      {\n    -+\tenum transaction_error ret = TRANSACTION_OK;\n    ++\tenum ref_transaction_error ret = 0;\n      \tstruct object_id current_oid = {0};\n      \tconst char *rewritten_ref;\n     -\tint ret = 0;\n    @@ refs/reftable-backend.c: static int prepare_single_update(struct reftable_ref_st\n      \tret = backend_for(&be, refs, u->refname, &rewritten_ref, 0);\n      \tif (ret)\n     -\t\treturn ret;\n    -+\t\treturn TRANSACTION_GENERIC_ERROR;\n    ++\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n      \n      \t/* Verify that the new object ID is valid. */\n      \tif ((u->flags & REF_HAVE_NEW) && !is_null_oid(&u->new_oid) &&\n    @@ refs/reftable-backend.c: static int prepare_single_update(struct reftable_ref_st\n      \t\t\t\t    _(\"trying to write ref '%s' with nonexistent object %s\"),\n      \t\t\t\t    u->refname, oid_to_hex(&u->new_oid));\n     -\t\t\treturn -1;\n    -+\t\t\treturn TRANSACTION_INVALID_NEW_VALUE;\n    ++\t\t\treturn REF_TRANSACTION_ERROR_INVALID_NEW_VALUE;\n      \t\t}\n      \n      \t\tif (o->type != OBJ_COMMIT && is_branch(u->refname)) {\n      \t\t\tstrbuf_addf(err, _(\"trying to write non-commit object %s to branch '%s'\"),\n      \t\t\t\t    oid_to_hex(&u->new_oid), u->refname);\n     -\t\t\treturn -1;\n    -+\t\t\treturn TRANSACTION_INVALID_NEW_VALUE;\n    ++\t\t\treturn REF_TRANSACTION_ERROR_INVALID_NEW_VALUE;\n      \t\t}\n      \t}\n      \n    +@@ refs/reftable-backend.c: static int prepare_single_update(struct reftable_ref_store *refs,\n    + \t\t\t\t    _(\"multiple updates for 'HEAD' (including one \"\n    + \t\t\t\t      \"via its referent '%s') are not allowed\"),\n    + \t\t\t\t    u->refname);\n    +-\t\t\treturn TRANSACTION_NAME_CONFLICT;\n    ++\t\t\treturn REF_TRANSACTION_ERROR_NAME_CONFLICT;\n    + \t\t}\n    + \n    + \t\tref_transaction_add_update(\n     @@ refs/reftable-backend.c: static int prepare_single_update(struct reftable_ref_store *refs,\n      \tret = reftable_backend_read_ref(be, rewritten_ref,\n      \t\t\t\t\t&current_oid, referent, &u->type);\n      \tif (ret < 0)\n     -\t\treturn ret;\n    -+\t\treturn TRANSACTION_GENERIC_ERROR;\n    ++\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n      \tif (ret > 0 && !ref_update_expects_existing_old_ref(u)) {\n      \t\t/*\n      \t\t * The reference does not exist, and we either have no\n    @@ refs/reftable-backend.c: static int prepare_single_update(struct reftable_ref_st\n      \t\t\t\t\t\t       &current_oid, err);\n      \t\t\tif (ret)\n     -\t\t\t\treturn ret;\n    -+\t\t\t\treturn TRANSACTION_GENERIC_ERROR;\n    ++\t\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n      \t\t}\n      \n      \t\treturn 0;\n    @@ refs/reftable-backend.c: static int prepare_single_update(struct reftable_ref_st\n      \t\t\t\t   \"unable to resolve reference '%s'\"),\n      \t\t\t    ref_update_original_update_refname(u), u->refname);\n     -\t\treturn -1;\n    -+\t\treturn TRANSACTION_NONEXISTENT_REF;\n    ++\t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n      \t}\n      \n      \tif (u->type & REF_ISSYMREF) {\n    @@ refs/reftable-backend.c: static int prepare_single_update(struct reftable_ref_st\n      \t\t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n      \t\t\t\t\t\t   \"error reading reference\"), u->refname);\n     -\t\t\t\treturn -1;\n    -+\t\t\t\treturn TRANSACTION_GENERIC_ERROR;\n    ++\t\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n      \t\t\t}\n      \t\t} else {\n      \t\t\tstruct ref_update *new_update;\n    +@@ refs/reftable-backend.c: static int prepare_single_update(struct reftable_ref_store *refs,\n    + \t\t\t\t\t    _(\"multiple updates for '%s' (including one \"\n    + \t\t\t\t\t      \"via symref '%s') are not allowed\"),\n    + \t\t\t\t\t    referent->buf, u->refname);\n    +-\t\t\t\treturn TRANSACTION_NAME_CONFLICT;\n    ++\t\t\t\treturn REF_TRANSACTION_ERROR_NAME_CONFLICT;\n    + \t\t\t}\n    + \n    + \t\t\t/*\n     @@ refs/reftable-backend.c: static int prepare_single_update(struct reftable_ref_store *refs,\n      \t\t\t\t\t   \"but is a regular ref\"),\n      \t\t\t\t    ref_update_original_update_refname(u),\n      \t\t\t\t    u->old_target);\n     -\t\t\treturn -1;\n    -+\t\t\treturn TRANSACTION_EXPECTED_SYMREF;\n    ++\t\t\treturn REF_TRANSACTION_ERROR_EXPECTED_SYMREF;\n      \t\t}\n      \n     -\t\tif (ref_update_check_old_target(referent->buf, u, err)) {\n     -\t\t\treturn -1;\n    +-\t\t}\n     +\t\tret = ref_update_check_old_target(referent->buf, u, err);\n    -+\t\tif (ret) {\n    ++\t\tif (ret)\n     +\t\t\treturn ret;\n    - \t\t}\n      \t} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {\n      \t\tif (is_null_oid(&u->old_oid)) {\n    -@@ refs/reftable-backend.c: static int prepare_single_update(struct reftable_ref_store *refs,\n    + \t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n    + \t\t\t\t\t   \"reference already exists\"),\n      \t\t\t\t    ref_update_original_update_refname(u));\n    - \t\t\treturn TRANSACTION_CREATE_EXISTS;\n    - \t\t}\n    +-\t\t\treturn TRANSACTION_CREATE_EXISTS;\n    +-\t\t}\n     -\t\telse if (is_null_oid(&current_oid))\n    -+\t\telse if (is_null_oid(&current_oid)) {\n    ++\t\t\treturn REF_TRANSACTION_ERROR_CREATE_EXISTS;\n    ++\t\t} else if (is_null_oid(&current_oid)) {\n      \t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n      \t\t\t\t\t   \"reference is missing but expected %s\"),\n      \t\t\t\t    ref_update_original_update_refname(u),\n      \t\t\t\t    oid_to_hex(&u->old_oid));\n     -\t\telse\n    -+\t\t\treturn TRANSACTION_NONEXISTENT_REF;\n    -+\n    ++\t\t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n     +\t\t} else {\n      \t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n      \t\t\t\t\t   \"is at %s but expected %s\"),\n    @@ refs/reftable-backend.c: static int prepare_single_update(struct reftable_ref_st\n      \t\t\t\t    oid_to_hex(&current_oid),\n      \t\t\t\t    oid_to_hex(&u->old_oid));\n     -\t\treturn TRANSACTION_NAME_CONFLICT;\n    -+\t\t\treturn TRANSACTION_INCORRECT_OLD_VALUE;\n    ++\t\t\treturn REF_TRANSACTION_ERROR_INCORRECT_OLD_VALUE;\n     +\t\t}\n      \t}\n      \n    @@ refs/reftable-backend.c: static int prepare_single_update(struct reftable_ref_st\n     -\t\treturn queue_transaction_update(refs, tx_data, u,\n     -\t\t\t\t\t       &current_oid, err);\n     +\t\tif (queue_transaction_update(refs, tx_data, u, &current_oid, err))\n    -+\t\t\treturn TRANSACTION_GENERIC_ERROR;\n    ++\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n      \n    --\treturn 0;\n    -+\treturn TRANSACTION_OK;\n    + \treturn 0;\n      }\n    - \n    - static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n     @@ refs/reftable-backend.c: static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n      \ttransaction->state = REF_TRANSACTION_PREPARED;\n      \n6:  49a0e65427 ! 6:  73f8970cb9 refs: implement partial reference transaction support\n    @@ Commit message\n         'REF_TRANSACTION_ALLOW_PARTIAL'. When enabled, this flag allows\n         individual reference updates that would typically cause the entire\n         transaction to fail due to non-system-related errors to be marked as\n    -    rejected while permitting other updates to proceed. Non-system-related\n    -    errors include issues caused by user-provided input values, whereas\n    -    system-related errors, such as I/O failures or memory issues, continue\n    -    to result in a full transaction failure. This approach enhances\n    -    flexibility while preserving transactional integrity where necessary.\n    +    rejected while permitting other updates to proceed. System errors\n    +    referred by 'REF_TRANSACTION_ERROR_GENERIC' continue to result in the\n    +    entire transaction failing. This approach enhances flexibility while\n    +    preserving transactional integrity where necessary.\n     \n         The implementation introduces several key components:\n     \n           - Add 'rejection_err' field to struct `ref_update` to track failed\n             updates with failure reason.\n     \n    +      - Add a new struct `ref_transaction_rejections` and a field within\n    +        `ref_transaction` to this struct to allow quick iteration over\n    +        rejected updates.\n    +\n           - Modify reference backends (files, packed, reftable) to handle\n             partial transactions by using `ref_transaction_set_rejected()`\n             instead of failing the entire transaction when\n    @@ Commit message\n             examine which updates were rejected and why.\n     \n         This foundational change enables partial transaction support throughout\n    -    the reference subsystem. The next commit will expose this capability to\n    -    users by adding a `--allow-partial` flag to 'git-update-ref(1)',\n    +    the reference subsystem. A following commit will expose this capability\n    +    to users by adding a `--allow-partial` flag to 'git-update-ref(1)',\n         providing both a user-facing feature and a testable implementation.\n     \n         Signed-off-by: Karthik Nayak <karthik.188@gmail.com>\n     \n      ## refs.c ##\n    +@@ refs.c: struct ref_transaction *ref_store_transaction_begin(struct ref_store *refs,\n    + \ttr->ref_store = refs;\n    + \ttr->flags = flags;\n    + \tstring_list_init_dup(&tr->refnames);\n    ++\n    ++\tif (flags & REF_TRANSACTION_ALLOW_PARTIAL)\n    ++\t\tCALLOC_ARRAY(tr->rejections, 1);\n    ++\n    + \treturn tr;\n    + }\n    + \n     @@ refs.c: void ref_transaction_free(struct ref_transaction *transaction)\n    + \t\tfree((char *)transaction->updates[i]->old_target);\n    + \t\tfree(transaction->updates[i]);\n    + \t}\n    ++\n    ++\tif (transaction->rejections)\n    ++\t\tfree(transaction->rejections->update_indices);\n    ++\tfree(transaction->rejections);\n    ++\n    + \tstring_list_clear(&transaction->refnames, 0);\n    + \tfree(transaction->updates);\n      \tfree(transaction);\n      }\n      \n    -+void ref_transaction_set_rejected(struct ref_transaction *transaction,\n    -+\t\t\t\t  size_t update_idx,\n    -+\t\t\t\t  enum transaction_error err)\n    ++int ref_transaction_maybe_set_rejected(struct ref_transaction *transaction,\n    ++\t\t\t\t       size_t update_idx,\n    ++\t\t\t\t       enum ref_transaction_error err)\n     +{\n     +\tif (update_idx >= transaction->nr)\n     +\t\tBUG(\"trying to set rejection on invalid update index\");\n    ++\n    ++\tif (!(transaction->flags & REF_TRANSACTION_ALLOW_PARTIAL))\n    ++\t\treturn 0;\n    ++\n    ++\tif (!transaction->rejections)\n    ++\t\tBUG(\"transaction not inititalized with partial support\");\n    ++\n    ++\t/*\n    ++\t * Don't accept generic errors, since these errors are not user\n    ++\t * input related.\n    ++\t */\n    ++\tif (err == REF_TRANSACTION_ERROR_GENERIC)\n    ++\t\treturn 0;\n    ++\n     +\ttransaction->updates[update_idx]->rejection_err = err;\n    ++\tALLOC_GROW(transaction->rejections->update_indices,\n    ++\t\t   transaction->rejections->nr + 1,\n    ++\t\t   transaction->rejections->alloc);\n    ++\ttransaction->rejections->update_indices[transaction->rejections->nr++] = update_idx;\n    ++\n    ++\treturn 1;\n     +}\n     +\n      struct ref_update *ref_transaction_add_update(\n    @@ refs.c: struct ref_update *ref_transaction_add_update(\n      \ttransaction->updates[transaction->nr++] = update;\n      \n      \tupdate->flags = flags;\n    -+\tupdate->rejection_err = TRANSACTION_OK;\n    ++\tupdate->rejection_err = 0;\n      \n      \tupdate->new_target = xstrdup_or_null(new_target);\n      \tupdate->old_target = xstrdup_or_null(old_target);\n    @@ refs.c: void ref_transaction_for_each_queued_update(struct ref_transaction *tran\n     +\t\t\t\t\t      ref_transaction_for_each_rejected_update_fn cb,\n     +\t\t\t\t\t      void *cb_data)\n     +{\n    -+\tif (!(transaction->flags & REF_TRANSACTION_ALLOW_PARTIAL))\n    ++\tif (!transaction->rejections)\n     +\t\treturn;\n     +\n    -+\tfor (size_t i = 0; i < transaction->nr; i++) {\n    -+\t\tstruct ref_update *update = transaction->updates[i];\n    ++\tfor (size_t i = 0; i < transaction->rejections->nr; i++) {\n    ++\t\tsize_t update_index = transaction->rejections->update_indices[i];\n    ++\t\tstruct ref_update *update = transaction->updates[update_index];\n     +\n     +\t\tif (!update->rejection_err)\n     +\t\t\tcontinue;\n    @@ refs.h: void ref_transaction_for_each_queued_update(struct ref_transaction *tran\n     +\t\t\t\t\t\t\t const struct object_id *new_oid,\n     +\t\t\t\t\t\t\t const char *old_target,\n     +\t\t\t\t\t\t\t const char *new_target,\n    -+\t\t\t\t\t\t\t enum transaction_error err,\n    ++\t\t\t\t\t\t\t enum ref_transaction_error err,\n     +\t\t\t\t\t\t\t void *cb_data);\n     +void ref_transaction_for_each_rejected_update(struct ref_transaction *transaction,\n     +\t\t\t\t\t      ref_transaction_for_each_rejected_update_fn cb,\n    @@ refs/files-backend.c: static int files_transaction_prepare(struct ref_store *ref\n      \t\t\t\t\t  err);\n     -\t\tif (ret)\n     +\t\tif (ret) {\n    -+\t\t\tif (transaction->flags & REF_TRANSACTION_ALLOW_PARTIAL &&\n    -+\t\t\t    ret != TRANSACTION_GENERIC_ERROR) {\n    -+\t\t\t\tref_transaction_set_rejected(transaction, i, ret);\n    -+\n    ++\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n     +\t\t\t\tstrbuf_setlen(err, 0);\n    -+\t\t\t\tret = TRANSACTION_OK;\n    ++\t\t\t\tret = 0;\n     +\n     +\t\t\t\tcontinue;\n     +\t\t\t}\n    @@ refs/files-backend.c: static int files_transaction_prepare(struct ref_store *ref\n      \n      \t\tif (update->flags & REF_DELETING &&\n      \t\t    !(update->flags & REF_LOG_ONLY) &&\n    +@@ refs/files-backend.c: static int files_transaction_finish(struct ref_store *ref_store,\n    + \t\tstruct ref_update *update = transaction->updates[i];\n    + \t\tstruct ref_lock *lock = update->backend_data;\n    + \n    ++\t\tif (update->rejection_err)\n    ++\t\t\tcontinue;\n    ++\n    + \t\tif (update->flags & REF_NEEDS_COMMIT ||\n    + \t\t    update->flags & REF_LOG_ONLY) {\n    + \t\t\tif (parse_and_write_reflog(refs, update, lock, err)) {\n     \n      ## refs/packed-backend.c ##\n     @@ refs/packed-backend.c: static int packed_ref_store_remove_on_disk(struct ref_store *ref_store,\n       * remain locked when it is done.\n       */\n    - static enum transaction_error write_with_updates(struct packed_ref_store *refs,\n    --\t\t\t\t\t\t struct string_list *updates,\n    -+\t\t\t\t\t\t struct ref_transaction *transaction,\n    - \t\t\t\t\t\t struct strbuf *err)\n    + static enum ref_transaction_error write_with_updates(struct packed_ref_store *refs,\n    +-\t\t\t\t\t\t     struct string_list *updates,\n    ++\t\t\t\t\t\t     struct ref_transaction *transaction,\n    + \t\t\t\t\t\t     struct strbuf *err)\n      {\n    - \tenum transaction_error ret = TRANSACTION_GENERIC_ERROR;\n    + \tenum ref_transaction_error ret = REF_TRANSACTION_ERROR_GENERIC;\n     +\tstruct string_list *updates = &transaction->refnames;\n      \tstruct ref_iterator *iter = NULL;\n      \tsize_t i;\n      \tint ok;\n    -@@ refs/packed-backend.c: static enum transaction_error write_with_updates(struct packed_ref_store *refs,\n    +@@ refs/packed-backend.c: static enum ref_transaction_error write_with_updates(struct packed_ref_store *re\n      \t\t\t\t\t\t    \"reference already exists\",\n      \t\t\t\t\t\t    update->refname);\n    - \t\t\t\t\tret = TRANSACTION_CREATE_EXISTS;\n    + \t\t\t\t\tret = REF_TRANSACTION_ERROR_CREATE_EXISTS;\n     +\n    -+\t\t\t\t\tif (transaction->flags & REF_TRANSACTION_ALLOW_PARTIAL) {\n    -+\t\t\t\t\t\tref_transaction_set_rejected(transaction, i, ret);\n    ++\t\t\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n     +\t\t\t\t\t\tstrbuf_setlen(err, 0);\n     +\t\t\t\t\t\tret = 0;\n     +\t\t\t\t\t\tcontinue;\n    @@ refs/packed-backend.c: static enum transaction_error write_with_updates(struct p\n      \t\t\t\t\tgoto error;\n      \t\t\t\t} else if (!oideq(&update->old_oid, iter->oid)) {\n      \t\t\t\t\tstrbuf_addf(err, \"cannot update ref '%s': \"\n    -@@ refs/packed-backend.c: static enum transaction_error write_with_updates(struct packed_ref_store *refs,\n    +@@ refs/packed-backend.c: static enum ref_transaction_error write_with_updates(struct packed_ref_store *re\n      \t\t\t\t\t\t    oid_to_hex(iter->oid),\n      \t\t\t\t\t\t    oid_to_hex(&update->old_oid));\n    - \t\t\t\t\tret = TRANSACTION_INCORRECT_OLD_VALUE;\n    + \t\t\t\t\tret = REF_TRANSACTION_ERROR_INCORRECT_OLD_VALUE;\n     +\n    -+\t\t\t\t\tif (transaction->flags & REF_TRANSACTION_ALLOW_PARTIAL) {\n    -+\t\t\t\t\t\tref_transaction_set_rejected(transaction, i, ret);\n    ++\t\t\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n     +\t\t\t\t\t\tstrbuf_setlen(err, 0);\n     +\t\t\t\t\t\tret = 0;\n     +\t\t\t\t\t\tcontinue;\n    @@ refs/packed-backend.c: static enum transaction_error write_with_updates(struct p\n      \t\t\t\t\tgoto error;\n      \t\t\t\t}\n      \t\t\t}\n    -@@ refs/packed-backend.c: static enum transaction_error write_with_updates(struct packed_ref_store *refs,\n    +@@ refs/packed-backend.c: static enum ref_transaction_error write_with_updates(struct packed_ref_store *re\n      \t\t\t\t\t    update->refname,\n      \t\t\t\t\t    oid_to_hex(&update->old_oid));\n    - \t\t\t\treturn TRANSACTION_NONEXISTENT_REF;\n    + \t\t\t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n     +\n    -+\t\t\t\tif (transaction->flags & REF_TRANSACTION_ALLOW_PARTIAL) {\n    -+\t\t\t\t\tref_transaction_set_rejected(transaction, i, ret);\n    ++\t\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n     +\t\t\t\t\tstrbuf_setlen(err, 0);\n     +\t\t\t\t\tret = 0;\n     +\t\t\t\t\tcontinue;\n    @@ refs/packed-backend.c: static enum transaction_error write_with_updates(struct p\n      \t\t\t\tgoto error;\n      \t\t\t}\n      \t\t}\n    -@@ refs/packed-backend.c: static enum transaction_error write_with_updates(struct packed_ref_store *refs,\n    +@@ refs/packed-backend.c: static enum ref_transaction_error write_with_updates(struct packed_ref_store *re\n      write_error:\n      \tstrbuf_addf(err, \"error writing to %s: %s\",\n      \t\t    get_tempfile_path(refs->tempfile), strerror(errno));\n    -+\tret = TRANSACTION_GENERIC_ERROR;\n    ++\tret = REF_TRANSACTION_ERROR_GENERIC;\n      \n      error:\n      \tref_iterator_free(iter);\n    @@ refs/refs-internal.h: struct ref_update {\n     +\t/*\n     +\t * Used in partial transactions to mark if a given update was rejected.\n     +\t */\n    -+\tenum transaction_error rejection_err;\n    ++\tenum ref_transaction_error rejection_err;\n     +\n      \t/*\n      \t * If this ref_update was split off of a symref update via\n    @@ refs/refs-internal.h: int refs_read_raw_ref(struct ref_store *ref_store, const c\n      \t\t      unsigned int *type, int *failure_errno);\n      \n     +/*\n    -+ * Mark a given update as rejected with a given reason. To be used in conjuction\n    -+ * with the `REF_TRANSACTION_ALLOW_PARTIAL` flag to allow partial transactions.\n    ++ * Mark a given update as rejected with a given reason.\n     + */\n    -+void ref_transaction_set_rejected(struct ref_transaction *transaction,\n    -+\t\t\t\t  size_t update_idx,\n    -+\t\t\t\t  enum transaction_error err);\n    ++int ref_transaction_maybe_set_rejected(struct ref_transaction *transaction,\n    ++\t\t\t\t       size_t update_idx,\n    ++\t\t\t\t       enum ref_transaction_error err);\n     +\n      /*\n       * Add a ref_update with the specified properties to transaction, and\n       * return a pointer to the new object. This function does not verify\n    +@@ refs/refs-internal.h: enum ref_transaction_state {\n    + \tREF_TRANSACTION_CLOSED   = 2\n    + };\n    + \n    ++/*\n    ++ * Data structure to hold indices of updates which were rejected, when\n    ++ * partial transactions where enabled. While the updates themselves hold\n    ++ * the rejection error, this structure allows a transaction to iterate\n    ++ * only over the rejected updates.\n    ++ */\n    ++struct ref_transaction_rejections {\n    ++\tsize_t *update_indices;\n    ++\tsize_t alloc;\n    ++\tsize_t nr;\n    ++};\n    ++\n    + /*\n    +  * Data structure for holding a reference transaction, which can\n    +  * consist of checks and updates to multiple references, carried out\n    +@@ refs/refs-internal.h: struct ref_transaction {\n    + \tsize_t alloc;\n    + \tsize_t nr;\n    + \tenum ref_transaction_state state;\n    ++\tstruct ref_transaction_rejections *rejections;\n    + \tvoid *backend_data;\n    + \tunsigned int flags;\n    + \tuint64_t max_index;\n     \n      ## refs/reftable-backend.c ##\n     @@ refs/reftable-backend.c: static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n    @@ refs/reftable-backend.c: static int reftable_be_transaction_prepare(struct ref_s\n      \t\t\t\t\t    &head_referent, &referent, err);\n     -\t\tif (ret)\n     +\t\tif (ret) {\n    -+\t\t\tif (transaction->flags & REF_TRANSACTION_ALLOW_PARTIAL &&\n    -+\t\t\t    ret != TRANSACTION_GENERIC_ERROR) {\n    -+\t\t\t\tref_transaction_set_rejected(transaction, i, ret);\n    -+\n    ++\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n     +\t\t\t\tstrbuf_setlen(err, 0);\n    -+\t\t\t\tret = TRANSACTION_OK;\n    ++\t\t\t\tret = 0;\n     +\n     +\t\t\t\tcontinue;\n     +\t\t\t}\n    @@ refs/reftable-backend.c: static int reftable_be_transaction_prepare(struct ref_s\n      \t}\n      \n      \tstring_list_sort(&refnames_to_check);\n    +@@ refs/reftable-backend.c: static int write_transaction_table(struct reftable_writer *writer, void *cb_data\n    + \t\tstruct reftable_transaction_update *tx_update = &arg->updates[i];\n    + \t\tstruct ref_update *u = tx_update->update;\n    + \n    ++\t\tif (u->rejection_err)\n    ++\t\t\tcontinue;\n    ++\n    + \t\t/*\n    + \t\t * Write a reflog entry when updating a ref to point to\n    + \t\t * something new in either of the following cases:\n-:  ---------- > 7:  f0284388ce refs: support partial update rejections during F/D checks\n7:  0dc37f87a7 ! 8:  f0e7c44eb7 update-ref: add --allow-partial flag for stdin mode\n    @@ Commit message\n     \n           rejected SP (<old-oid> | <old-target>) SP (<new-oid> | <new-target>) SP <rejection-reason> LF\n     \n    -    or with `-z`:\n    -\n    -      rejected NUL (<old-oid> | <old-target>) NUL (<new-oid> | <new-target>) NUL <rejection-reason> NUL\n    -\n         Update the documentation to reflect this change and also tests to cover\n         different scenarios where an update could be rejected.\n     \n    @@ Documentation/git-update-ref.adoc: performs all modifications together.  Specify\n      Quote fields containing whitespace as if they were strings in C source\n      code; i.e., surrounded by double-quotes and with backslash escapes.\n      Use 40 \"0\" characters or the empty string to specify a zero value.  To\n    -@@ Documentation/git-update-ref.adoc: quoting:\n    - In this format, use 40 \"0\" to specify a zero value, and use the empty\n    - string to specify a missing value.\n    - \n    -+With `-z`, `--allow-partial` will print rejections in the following form:\n    -+\n    -+\trejected NUL (<old-oid> | <old-target>) NUL (<new-oid> | <new-target>) NUL <rejection-reason> NUL\n    -+\n    - In either format, values can be specified in any form that Git\n    - recognizes as an object name.  Commands in any other format or a\n    - repeated <ref> produce an error.  Command meanings are:\n     \n      ## builtin/update-ref.c ##\n     @@\n    @@ builtin/update-ref.c: static void parse_cmd_abort(struct ref_transaction *transa\n     +\t\t\t\tconst struct object_id *new_oid,\n     +\t\t\t\tconst char *old_target,\n     +\t\t\t\tconst char *new_target,\n    -+\t\t\t\tenum transaction_error err,\n    ++\t\t\t\tenum ref_transaction_error err,\n     +\t\t\t\tvoid *cb_data UNUSED)\n     +{\n     +\tstruct strbuf sb = STRBUF_INIT;\n    -+\tchar space = ' ';\n     +\tconst char *reason = \"\";\n     +\n     +\tswitch (err) {\n    -+\tcase TRANSACTION_NAME_CONFLICT:\n    -+\t\treason = _(\"refname conflict\");\n    ++\tcase REF_TRANSACTION_ERROR_NAME_CONFLICT:\n    ++\t\treason = \"refname conflict\";\n     +\t\tbreak;\n    -+\tcase TRANSACTION_CREATE_EXISTS:\n    -+\t\treason = _(\"reference already exists\");\n    ++\tcase REF_TRANSACTION_ERROR_CREATE_EXISTS:\n    ++\t\treason = \"reference already exists\";\n     +\t\tbreak;\n    -+\tcase TRANSACTION_NONEXISTENT_REF:\n    -+\t\treason = _(\"reference does not exist\");\n    ++\tcase REF_TRANSACTION_ERROR_NONEXISTENT_REF:\n    ++\t\treason = \"reference does not exist\";\n     +\t\tbreak;\n    -+\tcase TRANSACTION_INCORRECT_OLD_VALUE:\n    -+\t\treason = _(\"incorrect old value provided\");\n    ++\tcase REF_TRANSACTION_ERROR_INCORRECT_OLD_VALUE:\n    ++\t\treason = \"incorrect old value provided\";\n     +\t\tbreak;\n    -+\tcase TRANSACTION_INVALID_NEW_VALUE:\n    -+\t\treason = _(\"invalid new value provided\");\n    ++\tcase REF_TRANSACTION_ERROR_INVALID_NEW_VALUE:\n    ++\t\treason = \"invalid new value provided\";\n     +\t\tbreak;\n    -+\tcase TRANSACTION_EXPECTED_SYMREF:\n    -+\t\treason = _(\"expected symref but found regular ref\");\n    ++\tcase REF_TRANSACTION_ERROR_EXPECTED_SYMREF:\n    ++\t\treason = \"expected symref but found regular ref\";\n     +\t\tbreak;\n     +\tdefault:\n    -+\t\treason = _(\"unkown failure\");\n    ++\t\treason = \"unkown failure\";\n     +\t}\n     +\n    -+\tif (!line_termination)\n    -+\t\tspace = line_termination;\n    -+\n    -+\tstrbuf_addf(&sb, \"rejected%c%s%c%s%c%c%s%c%s%c\", space,\n    -+\t\t    refname, space, new_oid ? oid_to_hex(new_oid) : new_target,\n    -+\t\t    space, space, old_oid ? oid_to_hex(old_oid) : old_target,\n    -+\t\t    space, reason, line_termination);\n    ++\tstrbuf_addf(&sb, \"rejected %s %s %s %s\\n\", refname,\n    ++\t\t    new_oid ? oid_to_hex(new_oid) : new_target,\n    ++\t\t    old_oid ? oid_to_hex(old_oid) : old_target,\n    ++\t\t    reason);\n     +\n     +\tfwrite(sb.buf, sb.len, 1, stdout);\n     +\tstrbuf_release(&sb);\n    -+\tfflush(stdout);\n     +}\n     +\n      static void parse_cmd_commit(struct ref_transaction *transaction,\n    @@ builtin/update-ref.c: static void update_refs_stdin(void)\n      \t\tbreak;\n      \tcase UPDATE_REFS_STARTED:\n     @@ builtin/update-ref.c: int cmd_update_ref(int argc,\n    - \tconst char *refname, *oldval;\n      \tstruct object_id oid, oldoid;\n      \tint delete = 0, no_deref = 0, read_stdin = 0, end_null = 0;\n    --\tint create_reflog = 0;\n    -+\tint create_reflog = 0, allow_partial = 0;\n    + \tint create_reflog = 0;\n     +\tunsigned int flags = 0;\n     +\n      \tstruct option options[] = {\n    @@ builtin/update-ref.c: int cmd_update_ref(int argc,\n     +\t\tupdate_refs_stdin(flags);\n      \t\treturn 0;\n     -\t}\n    -+\t} else if (allow_partial)\n    ++\t} else if (flags & REF_TRANSACTION_ALLOW_PARTIAL)\n     +\t\tdie(\"--allow-partial can only be used with --stdin\");\n      \n      \tif (end_null)\n    @@ t/t1400-update-ref.sh: do\n     +\t\t)\n     +\t'\n     +\n    -+\t# F/D conflicts on the files backend are resolved on an individual\n    -+\t# update level since refs are stored as files. On the reftable backend\n    -+\t# this check is batched to optimize for performance, so failures cannot\n    -+\t# be isolated to a single update.\n    -+\ttest_expect_success REFFILES \"stdin $type allow-partial refname conflict\" '\n    ++\ttest_expect_success \"stdin $type allow-partial refname conflict\" '\n     +\t\tgit init repo &&\n     +\t\ttest_when_finished \"rm -fr repo\" &&\n     +\t\t(\n    @@ t/t1400-update-ref.sh: do\n     +\t\t\ttest_cmp expect actual &&\n     +\t\t\ttest_grep -q \"refname conflict\" stdout\n     +\t\t)\n    ++\t'\n    ++\n    ++\ttest_expect_success \"stdin $type allow-partial refname conflict new ref\" '\n    ++\t\tgit init repo &&\n    ++\t\ttest_when_finished \"rm -fr repo\" &&\n    ++\t\t(\n    ++\t\t\tcd repo &&\n    ++\t\t\ttest_commit one &&\n    ++\t\t\told_head=$(git rev-parse HEAD) &&\n    ++\t\t\ttest_commit two &&\n    ++\t\t\thead=$(git rev-parse HEAD) &&\n    ++\t\t\tgit update-ref refs/heads/ref/foo $head &&\n    ++\n    ++\t\t\tformat_command $type \"update refs/heads/foo\" \"$old_head\" \"\" >stdin &&\n    ++\t\t\tformat_command $type \"update refs/heads/ref\" \"$old_head\" \"\" >>stdin &&\n    ++\t\t\tgit update-ref $type --stdin --allow-partial <stdin >stdout &&\n    ++\t\t\techo $old_head >expect &&\n    ++\t\t\tgit rev-parse refs/heads/foo >actual &&\n    ++\t\t\ttest_cmp expect actual &&\n    ++\t\t\ttest_grep -q \"refname conflict\" stdout\n    ++\t\t)\n     +\t'\n      done\n      \n\n\nbase-commit: f032e4cb6777d229cc1e662e142e99ef71741eb4\nchange-id: 20241206-245-partially-atomic-ref-updates-9fe8b080345c\n\nThanks\n- Karthik\n\n"},{"id":"513607","messageId":"20250305-245-partially-atomic-ref-updates-v3-3-0c64e3052354@gmail.com","threadId":"63064","inReplyTo":"20250305-245-partially-atomic-ref-updates-v3-0-0c64e3052354@gmail.com","subject":"[PATCH v3 3/8] refs/files: remove duplicate duplicates check","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-03-05T17:38:58Z","receivedAt":"2025-03-05T17:39:18Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Within the files reference backend's transaction's 'finish' phase, a\nverification step is currently performed wherein the refnames list is\nsorted and examined for multiple updates targeting the same refname.\n\nIt has been observed that this verification is redundant, as an\nidentical check is already executed during the transaction's 'prepare'\nstage. Since the refnames list remains unmodified following the\n'prepare' stage, this secondary verification can be safely eliminated.\n\nThe duplicate check has been removed accordingly, and the\n`ref_update_reject_duplicates()` function has been marked as static, as\nits usage is now confined to 'refs.c'.\n\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n refs.c               | 9 +++++++--\n refs/files-backend.c | 6 ------\n refs/refs-internal.h | 8 --------\n 3 files changed, 7 insertions(+), 16 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex ab69746947..69f385f344 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2303,8 +2303,13 @@ int refs_update_symref_extended(struct ref_store *refs, const char *ref,\n \treturn ret;\n }\n \n-int ref_update_reject_duplicates(struct string_list *refnames,\n-\t\t\t\t struct strbuf *err)\n+/*\n+ * Write an error to `err` and return a nonzero value iff the same\n+ * refname appears multiple times in `refnames`. `refnames` must be\n+ * sorted on entry to this function.\n+ */\n+static int ref_update_reject_duplicates(struct string_list *refnames,\n+\t\t\t\t\tstruct strbuf *err)\n {\n \tsize_t i, n = refnames->nr;\n \ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 85ed85ad87..7c6a0b3478 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -3016,12 +3016,6 @@ static int files_transaction_finish_initial(struct files_ref_store *refs,\n \tif (transaction->state != REF_TRANSACTION_PREPARED)\n \t\tBUG(\"commit called for transaction that is not prepared\");\n \n-\tstring_list_sort(&transaction->refnames);\n-\tif (ref_update_reject_duplicates(&transaction->refnames, err)) {\n-\t\tret = TRANSACTION_GENERIC_ERROR;\n-\t\tgoto cleanup;\n-\t}\n-\n \t/*\n \t * It's really undefined to call this function in an active\n \t * repository or when there are existing references: we are\ndiff --git a/refs/refs-internal.h b/refs/refs-internal.h\nindex 92db793026..6d3770d0cc 100644\n--- a/refs/refs-internal.h\n+++ b/refs/refs-internal.h\n@@ -142,14 +142,6 @@ int refs_read_raw_ref(struct ref_store *ref_store, const char *refname,\n \t\t      struct object_id *oid, struct strbuf *referent,\n \t\t      unsigned int *type, int *failure_errno);\n \n-/*\n- * Write an error to `err` and return a nonzero value iff the same\n- * refname appears multiple times in `refnames`. `refnames` must be\n- * sorted on entry to this function.\n- */\n-int ref_update_reject_duplicates(struct string_list *refnames,\n-\t\t\t\t struct strbuf *err);\n-\n /*\n  * Add a ref_update with the specified properties to transaction, and\n  * return a pointer to the new object. This function does not verify\n\n-- \n2.48.1\n\n"},{"id":"513608","messageId":"20250305-245-partially-atomic-ref-updates-v3-2-0c64e3052354@gmail.com","threadId":"63064","inReplyTo":"20250305-245-partially-atomic-ref-updates-v3-0-0c64e3052354@gmail.com","subject":"[PATCH v3 2/8] refs: move duplicate refname update check to generic layer","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-03-05T17:38:57Z","receivedAt":"2025-03-05T17:39:18Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Move the tracking of refnames in `affected_refnames` from individual\nbackends into the generic layer in 'refs.c'. This centralizes the\nduplicate refname detection that was previously handled separately by\neach backend.\n\nMake some changes to accommodate this move:\n\n  - Add a `string_list` field `refnames` to `ref_transaction` to contain\n    all the references in a transaction. This field is updated whenever\n    a new update is added via `ref_transaction_add_update`, so manual\n    additions in reference backends are dropped.\n\n  - Modify the backends to use this field internally as needed. The\n    backends need to check if an update for refname already exists when\n    splitting symrefs or adding an update for 'HEAD'.\n\n  - In the reftable backend, within `reftable_be_transaction_prepare()`,\n    move the `string_list_has_string()` check above\n    `ref_transaction_add_update()`. Since `ref_transaction_add_update()`\n    automatically adds the refname to `transaction->refnames`,\n    performing the check after will always return true, so we perform\n    the check before adding the update.\n\nThis helps reduce duplication of functionality between the backends and\nmakes it easier to make changes in a more centralized manner.\n\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n refs.c                  | 17 +++++++++++++\n refs/files-backend.c    | 67 +++++++++++--------------------------------------\n refs/packed-backend.c   | 25 +-----------------\n refs/refs-internal.h    |  2 ++\n refs/reftable-backend.c | 54 +++++++++++++--------------------------\n 5 files changed, 51 insertions(+), 114 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex 54fd5ce21e..ab69746947 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1175,6 +1175,7 @@ struct ref_transaction *ref_store_transaction_begin(struct ref_store *refs,\n \tCALLOC_ARRAY(tr, 1);\n \ttr->ref_store = refs;\n \ttr->flags = flags;\n+\tstring_list_init_dup(&tr->refnames);\n \treturn tr;\n }\n \n@@ -1205,6 +1206,7 @@ void ref_transaction_free(struct ref_transaction *transaction)\n \t\tfree((char *)transaction->updates[i]->old_target);\n \t\tfree(transaction->updates[i]);\n \t}\n+\tstring_list_clear(&transaction->refnames, 0);\n \tfree(transaction->updates);\n \tfree(transaction);\n }\n@@ -1218,6 +1220,7 @@ struct ref_update *ref_transaction_add_update(\n \t\tconst char *committer_info,\n \t\tconst char *msg)\n {\n+\tstruct string_list_item *item;\n \tstruct ref_update *update;\n \n \tif (transaction->state != REF_TRANSACTION_OPEN)\n@@ -1245,6 +1248,16 @@ struct ref_update *ref_transaction_add_update(\n \t\tupdate->msg = normalize_reflog_message(msg);\n \t}\n \n+\t/*\n+\t * This list is generally used by the backends to avoid duplicates.\n+\t * But we do support multiple log updates for a given refname within\n+\t * a single transaction.\n+\t */\n+\tif (!(update->flags & REF_LOG_ONLY)) {\n+\t\titem = string_list_append(&transaction->refnames, refname);\n+\t\titem->util = update;\n+\t}\n+\n \treturn update;\n }\n \n@@ -2405,6 +2418,10 @@ int ref_transaction_prepare(struct ref_transaction *transaction,\n \t\treturn -1;\n \t}\n \n+\tstring_list_sort(&transaction->refnames);\n+\tif (ref_update_reject_duplicates(&transaction->refnames, err))\n+\t\treturn TRANSACTION_GENERIC_ERROR;\n+\n \tret = refs->be->transaction_prepare(refs, transaction, err);\n \tif (ret)\n \t\treturn ret;\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 6c7df30738..85ed85ad87 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2378,9 +2378,7 @@ static struct ref_iterator *files_reflog_iterator_begin(struct ref_store *ref_st\n  */\n static int split_head_update(struct ref_update *update,\n \t\t\t     struct ref_transaction *transaction,\n-\t\t\t     const char *head_ref,\n-\t\t\t     struct string_list *affected_refnames,\n-\t\t\t     struct strbuf *err)\n+\t\t\t     const char *head_ref, struct strbuf *err)\n {\n \tstruct ref_update *new_update;\n \n@@ -2398,7 +2396,7 @@ static int split_head_update(struct ref_update *update,\n \t * transaction. This check is O(lg N) in the transaction\n \t * size, but it happens at most once per transaction.\n \t */\n-\tif (string_list_has_string(affected_refnames, \"HEAD\")) {\n+\tif (string_list_has_string(&transaction->refnames, \"HEAD\")) {\n \t\t/* An entry already existed */\n \t\tstrbuf_addf(err,\n \t\t\t    \"multiple updates for 'HEAD' (including one \"\n@@ -2420,7 +2418,6 @@ static int split_head_update(struct ref_update *update,\n \t */\n \tif (strcmp(new_update->refname, \"HEAD\"))\n \t\tBUG(\"%s unexpectedly not 'HEAD'\", new_update->refname);\n-\tstring_list_insert(affected_refnames, new_update->refname);\n \n \treturn 0;\n }\n@@ -2436,7 +2433,6 @@ static int split_head_update(struct ref_update *update,\n static int split_symref_update(struct ref_update *update,\n \t\t\t       const char *referent,\n \t\t\t       struct ref_transaction *transaction,\n-\t\t\t       struct string_list *affected_refnames,\n \t\t\t       struct strbuf *err)\n {\n \tstruct ref_update *new_update;\n@@ -2448,7 +2444,7 @@ static int split_symref_update(struct ref_update *update,\n \t * size, but it happens at most once per symref in a\n \t * transaction.\n \t */\n-\tif (string_list_has_string(affected_refnames, referent)) {\n+\tif (string_list_has_string(&transaction->refnames, referent)) {\n \t\t/* An entry already exists */\n \t\tstrbuf_addf(err,\n \t\t\t    \"multiple updates for '%s' (including one \"\n@@ -2486,15 +2482,6 @@ static int split_symref_update(struct ref_update *update,\n \tupdate->flags |= REF_LOG_ONLY | REF_NO_DEREF;\n \tupdate->flags &= ~REF_HAVE_OLD;\n \n-\t/*\n-\t * Add the referent. This insertion is O(N) in the transaction\n-\t * size, but it happens at most once per symref in a\n-\t * transaction. Make sure to add new_update->refname, which will\n-\t * be valid as long as affected_refnames is in use, and NOT\n-\t * referent, which might soon be freed by our caller.\n-\t */\n-\tstring_list_insert(affected_refnames, new_update->refname);\n-\n \treturn 0;\n }\n \n@@ -2558,7 +2545,6 @@ static int lock_ref_for_update(struct files_ref_store *refs,\n \t\t\t       struct ref_transaction *transaction,\n \t\t\t       const char *head_ref,\n \t\t\t       struct string_list *refnames_to_check,\n-\t\t\t       struct string_list *affected_refnames,\n \t\t\t       struct strbuf *err)\n {\n \tstruct strbuf referent = STRBUF_INIT;\n@@ -2575,8 +2561,7 @@ static int lock_ref_for_update(struct files_ref_store *refs,\n \t\tupdate->flags |= REF_DELETING;\n \n \tif (head_ref) {\n-\t\tret = split_head_update(update, transaction, head_ref,\n-\t\t\t\t\taffected_refnames, err);\n+\t\tret = split_head_update(update, transaction, head_ref, err);\n \t\tif (ret)\n \t\t\tgoto out;\n \t}\n@@ -2586,9 +2571,8 @@ static int lock_ref_for_update(struct files_ref_store *refs,\n \t\tlock->count++;\n \t} else {\n \t\tret = lock_raw_ref(refs, update->refname, mustexist,\n-\t\t\t\t   refnames_to_check, affected_refnames,\n-\t\t\t\t   &lock, &referent,\n-\t\t\t\t   &update->type, err);\n+\t\t\t\t   refnames_to_check, &transaction->refnames,\n+\t\t\t\t   &lock, &referent, &update->type, err);\n \t\tif (ret) {\n \t\t\tchar *reason;\n \n@@ -2642,9 +2626,8 @@ static int lock_ref_for_update(struct files_ref_store *refs,\n \t\t\t * of processing the split-off update, so we\n \t\t\t * don't have to do it here.\n \t\t\t */\n-\t\t\tret = split_symref_update(update,\n-\t\t\t\t\t\t  referent.buf, transaction,\n-\t\t\t\t\t\t  affected_refnames, err);\n+\t\t\tret = split_symref_update(update, referent.buf,\n+\t\t\t\t\t\t  transaction, err);\n \t\t\tif (ret)\n \t\t\t\tgoto out;\n \t\t}\n@@ -2799,7 +2782,6 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n \t\t\t       \"ref_transaction_prepare\");\n \tsize_t i;\n \tint ret = 0;\n-\tstruct string_list affected_refnames = STRING_LIST_INIT_NODUP;\n \tstruct string_list refnames_to_check = STRING_LIST_INIT_NODUP;\n \tchar *head_ref = NULL;\n \tint head_type;\n@@ -2818,12 +2800,7 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n \ttransaction->backend_data = backend_data;\n \n \t/*\n-\t * Fail if a refname appears more than once in the\n-\t * transaction. (If we end up splitting up any updates using\n-\t * split_symref_update() or split_head_update(), those\n-\t * functions will check that the new updates don't have the\n-\t * same refname as any existing ones.) Also fail if any of the\n-\t * updates use REF_IS_PRUNING without REF_NO_DEREF.\n+\t * Fail if any of the updates use REF_IS_PRUNING without REF_NO_DEREF.\n \t */\n \tfor (i = 0; i < transaction->nr; i++) {\n \t\tstruct ref_update *update = transaction->updates[i];\n@@ -2831,16 +2808,6 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n \t\tif ((update->flags & REF_IS_PRUNING) &&\n \t\t    !(update->flags & REF_NO_DEREF))\n \t\t\tBUG(\"REF_IS_PRUNING set without REF_NO_DEREF\");\n-\n-\t\tif (update->flags & REF_LOG_ONLY)\n-\t\t\tcontinue;\n-\n-\t\tstring_list_append(&affected_refnames, update->refname);\n-\t}\n-\tstring_list_sort(&affected_refnames);\n-\tif (ref_update_reject_duplicates(&affected_refnames, err)) {\n-\t\tret = TRANSACTION_GENERIC_ERROR;\n-\t\tgoto cleanup;\n \t}\n \n \t/*\n@@ -2882,7 +2849,7 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n \n \t\tret = lock_ref_for_update(refs, update, transaction,\n \t\t\t\t\t  head_ref, &refnames_to_check,\n-\t\t\t\t\t  &affected_refnames, err);\n+\t\t\t\t\t  err);\n \t\tif (ret)\n \t\t\tgoto cleanup;\n \n@@ -2929,7 +2896,7 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n \t * So instead, we accept the race for now.\n \t */\n \tif (refs_verify_refnames_available(refs->packed_ref_store, &refnames_to_check,\n-\t\t\t\t\t   &affected_refnames, NULL, 0, err)) {\n+\t\t\t\t\t   &transaction->refnames, NULL, 0, err)) {\n \t\tret = TRANSACTION_NAME_CONFLICT;\n \t\tgoto cleanup;\n \t}\n@@ -2975,7 +2942,6 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n \n cleanup:\n \tfree(head_ref);\n-\tstring_list_clear(&affected_refnames, 0);\n \tstring_list_clear(&refnames_to_check, 0);\n \n \tif (ret)\n@@ -3050,13 +3016,8 @@ static int files_transaction_finish_initial(struct files_ref_store *refs,\n \tif (transaction->state != REF_TRANSACTION_PREPARED)\n \t\tBUG(\"commit called for transaction that is not prepared\");\n \n-\t/* Fail if a refname appears more than once in the transaction: */\n-\tfor (i = 0; i < transaction->nr; i++)\n-\t\tif (!(transaction->updates[i]->flags & REF_LOG_ONLY))\n-\t\t\tstring_list_append(&affected_refnames,\n-\t\t\t\t\t   transaction->updates[i]->refname);\n-\tstring_list_sort(&affected_refnames);\n-\tif (ref_update_reject_duplicates(&affected_refnames, err)) {\n+\tstring_list_sort(&transaction->refnames);\n+\tif (ref_update_reject_duplicates(&transaction->refnames, err)) {\n \t\tret = TRANSACTION_GENERIC_ERROR;\n \t\tgoto cleanup;\n \t}\n@@ -3074,7 +3035,7 @@ static int files_transaction_finish_initial(struct files_ref_store *refs,\n \t * that we are creating already exists.\n \t */\n \tif (refs_for_each_rawref(&refs->base, ref_present,\n-\t\t\t\t &affected_refnames))\n+\t\t\t\t &transaction->refnames))\n \t\tBUG(\"initial ref transaction called with existing refs\");\n \n \tpacked_transaction = ref_store_transaction_begin(refs->packed_ref_store,\ndiff --git a/refs/packed-backend.c b/refs/packed-backend.c\nindex f4c82ba2c7..19220d2e99 100644\n--- a/refs/packed-backend.c\n+++ b/refs/packed-backend.c\n@@ -1622,8 +1622,6 @@ int is_packed_transaction_needed(struct ref_store *ref_store,\n struct packed_transaction_backend_data {\n \t/* True iff the transaction owns the packed-refs lock. */\n \tint own_lock;\n-\n-\tstruct string_list updates;\n };\n \n static void packed_transaction_cleanup(struct packed_ref_store *refs,\n@@ -1632,8 +1630,6 @@ static void packed_transaction_cleanup(struct packed_ref_store *refs,\n \tstruct packed_transaction_backend_data *data = transaction->backend_data;\n \n \tif (data) {\n-\t\tstring_list_clear(&data->updates, 0);\n-\n \t\tif (is_tempfile_active(refs->tempfile))\n \t\t\tdelete_tempfile(&refs->tempfile);\n \n@@ -1658,7 +1654,6 @@ static int packed_transaction_prepare(struct ref_store *ref_store,\n \t\t\tREF_STORE_READ | REF_STORE_WRITE | REF_STORE_ODB,\n \t\t\t\"ref_transaction_prepare\");\n \tstruct packed_transaction_backend_data *data;\n-\tsize_t i;\n \tint ret = TRANSACTION_GENERIC_ERROR;\n \n \t/*\n@@ -1671,34 +1666,16 @@ static int packed_transaction_prepare(struct ref_store *ref_store,\n \t */\n \n \tCALLOC_ARRAY(data, 1);\n-\tstring_list_init_nodup(&data->updates);\n \n \ttransaction->backend_data = data;\n \n-\t/*\n-\t * Stick the updates in a string list by refname so that we\n-\t * can sort them:\n-\t */\n-\tfor (i = 0; i < transaction->nr; i++) {\n-\t\tstruct ref_update *update = transaction->updates[i];\n-\t\tstruct string_list_item *item =\n-\t\t\tstring_list_append(&data->updates, update->refname);\n-\n-\t\t/* Store a pointer to update in item->util: */\n-\t\titem->util = update;\n-\t}\n-\tstring_list_sort(&data->updates);\n-\n-\tif (ref_update_reject_duplicates(&data->updates, err))\n-\t\tgoto failure;\n-\n \tif (!is_lock_file_locked(&refs->lock)) {\n \t\tif (packed_refs_lock(ref_store, 0, err))\n \t\t\tgoto failure;\n \t\tdata->own_lock = 1;\n \t}\n \n-\tif (write_with_updates(refs, &data->updates, err))\n+\tif (write_with_updates(refs, &transaction->refnames, err))\n \t\tgoto failure;\n \n \ttransaction->state = REF_TRANSACTION_PREPARED;\ndiff --git a/refs/refs-internal.h b/refs/refs-internal.h\nindex e5862757a7..92db793026 100644\n--- a/refs/refs-internal.h\n+++ b/refs/refs-internal.h\n@@ -3,6 +3,7 @@\n \n #include \"refs.h\"\n #include \"iterator.h\"\n+#include \"string-list.h\"\n \n struct fsck_options;\n struct ref_transaction;\n@@ -198,6 +199,7 @@ enum ref_transaction_state {\n struct ref_transaction {\n \tstruct ref_store *ref_store;\n \tstruct ref_update **updates;\n+\tstruct string_list refnames;\n \tsize_t alloc;\n \tsize_t nr;\n \tenum ref_transaction_state state;\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 441b8c69c1..f616d9aabe 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -1076,7 +1076,6 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n \tstruct reftable_ref_store *refs =\n \t\treftable_be_downcast(ref_store, REF_STORE_WRITE|REF_STORE_MAIN, \"ref_transaction_prepare\");\n \tstruct strbuf referent = STRBUF_INIT, head_referent = STRBUF_INIT;\n-\tstruct string_list affected_refnames = STRING_LIST_INIT_NODUP;\n \tstruct string_list refnames_to_check = STRING_LIST_INIT_NODUP;\n \tstruct reftable_transaction_data *tx_data = NULL;\n \tstruct reftable_backend *be;\n@@ -1101,10 +1100,6 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n \t\t\t\t\t\t transaction->updates[i], err);\n \t\tif (ret)\n \t\t\tgoto done;\n-\n-\t\tif (!(transaction->updates[i]->flags & REF_LOG_ONLY))\n-\t\t\tstring_list_append(&affected_refnames,\n-\t\t\t\t\t   transaction->updates[i]->refname);\n \t}\n \n \t/*\n@@ -1116,17 +1111,6 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n \t\ttx_data->args[i].updates_alloc = tx_data->args[i].updates_expected;\n \t}\n \n-\t/*\n-\t * Fail if a refname appears more than once in the transaction.\n-\t * This code is taken from the files backend and is a good candidate to\n-\t * be moved into the generic layer.\n-\t */\n-\tstring_list_sort(&affected_refnames);\n-\tif (ref_update_reject_duplicates(&affected_refnames, err)) {\n-\t\tret = TRANSACTION_GENERIC_ERROR;\n-\t\tgoto done;\n-\t}\n-\n \t/*\n \t * TODO: it's dubious whether we should reload the stack that \"HEAD\"\n \t * belongs to or not. In theory, it may happen that we only modify\n@@ -1194,14 +1178,12 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n \t\t    !(u->flags & REF_LOG_ONLY) &&\n \t\t    !(u->flags & REF_UPDATE_VIA_HEAD) &&\n \t\t    !strcmp(rewritten_ref, head_referent.buf)) {\n-\t\t\tstruct ref_update *new_update;\n-\n \t\t\t/*\n \t\t\t * First make sure that HEAD is not already in the\n \t\t\t * transaction. This check is O(lg N) in the transaction\n \t\t\t * size, but it happens at most once per transaction.\n \t\t\t */\n-\t\t\tif (string_list_has_string(&affected_refnames, \"HEAD\")) {\n+\t\t\tif (string_list_has_string(&transaction->refnames, \"HEAD\")) {\n \t\t\t\t/* An entry already existed */\n \t\t\t\tstrbuf_addf(err,\n \t\t\t\t\t    _(\"multiple updates for 'HEAD' (including one \"\n@@ -1211,12 +1193,11 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n \t\t\t\tgoto done;\n \t\t\t}\n \n-\t\t\tnew_update = ref_transaction_add_update(\n-\t\t\t\t\ttransaction, \"HEAD\",\n-\t\t\t\t\tu->flags | REF_LOG_ONLY | REF_NO_DEREF,\n-\t\t\t\t\t&u->new_oid, &u->old_oid, NULL, NULL, NULL,\n-\t\t\t\t\tu->msg);\n-\t\t\tstring_list_insert(&affected_refnames, new_update->refname);\n+\t\t\tref_transaction_add_update(\n+\t\t\t\ttransaction, \"HEAD\",\n+\t\t\t\tu->flags | REF_LOG_ONLY | REF_NO_DEREF,\n+\t\t\t\t&u->new_oid, &u->old_oid, NULL, NULL, NULL,\n+\t\t\t\tu->msg);\n \t\t}\n \n \t\tret = reftable_backend_read_ref(be, rewritten_ref,\n@@ -1281,6 +1262,15 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n \t\t\t\tif (!strcmp(rewritten_ref, \"HEAD\"))\n \t\t\t\t\tnew_flags |= REF_UPDATE_VIA_HEAD;\n \n+\t\t\t\tif (string_list_has_string(&transaction->refnames, referent.buf)) {\n+\t\t\t\t\tstrbuf_addf(err,\n+\t\t\t\t\t\t    _(\"multiple updates for '%s' (including one \"\n+\t\t\t\t\t\t    \"via symref '%s') are not allowed\"),\n+\t\t\t\t\t\t    referent.buf, u->refname);\n+\t\t\t\t\tret = TRANSACTION_NAME_CONFLICT;\n+\t\t\t\t\tgoto done;\n+\t\t\t\t}\n+\n \t\t\t\t/*\n \t\t\t\t * If we are updating a symref (eg. HEAD), we should also\n \t\t\t\t * update the branch that the symref points to.\n@@ -1305,16 +1295,6 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n \t\t\t\t */\n \t\t\t\tu->flags |= REF_LOG_ONLY | REF_NO_DEREF;\n \t\t\t\tu->flags &= ~REF_HAVE_OLD;\n-\n-\t\t\t\tif (string_list_has_string(&affected_refnames, new_update->refname)) {\n-\t\t\t\t\tstrbuf_addf(err,\n-\t\t\t\t\t\t    _(\"multiple updates for '%s' (including one \"\n-\t\t\t\t\t\t    \"via symref '%s') are not allowed\"),\n-\t\t\t\t\t\t    referent.buf, u->refname);\n-\t\t\t\t\tret = TRANSACTION_NAME_CONFLICT;\n-\t\t\t\t\tgoto done;\n-\t\t\t\t}\n-\t\t\t\tstring_list_insert(&affected_refnames, new_update->refname);\n \t\t\t}\n \t\t}\n \n@@ -1384,7 +1364,8 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n \t}\n \n \tstring_list_sort(&refnames_to_check);\n-\tret = refs_verify_refnames_available(ref_store, &refnames_to_check, &affected_refnames, NULL,\n+\tret = refs_verify_refnames_available(ref_store, &refnames_to_check,\n+\t\t\t\t\t     &transaction->refnames, NULL,\n \t\t\t\t\t     transaction->flags & REF_TRANSACTION_FLAG_INITIAL,\n \t\t\t\t\t     err);\n \tif (ret < 0)\n@@ -1402,7 +1383,6 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n \t\t\tstrbuf_addf(err, _(\"reftable: transaction prepare: %s\"),\n \t\t\t\t    reftable_error_str(ret));\n \t}\n-\tstring_list_clear(&affected_refnames, 0);\n \tstrbuf_release(&referent);\n \tstrbuf_release(&head_referent);\n \tstring_list_clear(&refnames_to_check, 0);\n\n-- \n2.48.1\n\n"},{"id":"513610","messageId":"20250305-245-partially-atomic-ref-updates-v3-4-0c64e3052354@gmail.com","threadId":"63064","inReplyTo":"20250305-245-partially-atomic-ref-updates-v3-0-0c64e3052354@gmail.com","subject":"[PATCH v3 4/8] refs/reftable: extract code from the transaction preparation","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-03-05T17:38:59Z","receivedAt":"2025-03-05T17:39:19Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Extract the core logic for preparing individual reference updates from\n`reftable_be_transaction_prepare()` into `prepare_single_update()`. This\ndedicated function now handles all validation and preparation steps for\neach reference update in the transaction, including object ID\nverification, HEAD reference handling, and symref processing.\n\nThe refactoring consolidates all reference update validation into a\nsingle logical block, which improves code maintainability and\nreadability. More importantly, this restructuring lays the groundwork\nfor implementing partial transaction support in the reftable backend,\nwhich will be introduced in the following commit.\n\nNo functional changes are included in this commit - it is purely a code\nreorganization to support future enhancements.\n\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n refs/reftable-backend.c | 463 +++++++++++++++++++++++++-----------------------\n 1 file changed, 237 insertions(+), 226 deletions(-)\n\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex f616d9aabe..2c1e2995de 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -1069,6 +1069,239 @@ static int queue_transaction_update(struct reftable_ref_store *refs,\n \treturn 0;\n }\n \n+static int prepare_single_update(struct reftable_ref_store *refs,\n+\t\t\t\t struct reftable_transaction_data *tx_data,\n+\t\t\t\t struct ref_transaction *transaction,\n+\t\t\t\t struct reftable_backend *be,\n+\t\t\t\t struct ref_update *u,\n+\t\t\t\t struct string_list *refnames_to_check,\n+\t\t\t\t unsigned int head_type,\n+\t\t\t\t struct strbuf *head_referent,\n+\t\t\t\t struct strbuf *referent,\n+\t\t\t\t struct strbuf *err)\n+{\n+\tstruct object_id current_oid = {0};\n+\tconst char *rewritten_ref;\n+\tint ret = 0;\n+\n+\t/*\n+\t * There is no need to reload the respective backends here as\n+\t * we have already reloaded them when preparing the transaction\n+\t * update. And given that the stacks have been locked there\n+\t * shouldn't have been any concurrent modifications of the\n+\t * stack.\n+\t */\n+\tret = backend_for(&be, refs, u->refname, &rewritten_ref, 0);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\t/* Verify that the new object ID is valid. */\n+\tif ((u->flags & REF_HAVE_NEW) && !is_null_oid(&u->new_oid) &&\n+\t    !(u->flags & REF_SKIP_OID_VERIFICATION) &&\n+\t    !(u->flags & REF_LOG_ONLY)) {\n+\t\tstruct object *o = parse_object(refs->base.repo, &u->new_oid);\n+\t\tif (!o) {\n+\t\t\tstrbuf_addf(err,\n+\t\t\t\t    _(\"trying to write ref '%s' with nonexistent object %s\"),\n+\t\t\t\t    u->refname, oid_to_hex(&u->new_oid));\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tif (o->type != OBJ_COMMIT && is_branch(u->refname)) {\n+\t\t\tstrbuf_addf(err, _(\"trying to write non-commit object %s to branch '%s'\"),\n+\t\t\t\t    oid_to_hex(&u->new_oid), u->refname);\n+\t\t\treturn -1;\n+\t\t}\n+\t}\n+\n+\t/*\n+\t * When we update the reference that HEAD points to we enqueue\n+\t * a second log-only update for HEAD so that its reflog is\n+\t * updated accordingly.\n+\t */\n+\tif (head_type == REF_ISSYMREF &&\n+\t    !(u->flags & REF_LOG_ONLY) &&\n+\t    !(u->flags & REF_UPDATE_VIA_HEAD) &&\n+\t    !strcmp(rewritten_ref, head_referent->buf)) {\n+\t\t/*\n+\t\t * First make sure that HEAD is not already in the\n+\t\t * transaction. This check is O(lg N) in the transaction\n+\t\t * size, but it happens at most once per transaction.\n+\t\t */\n+\t\tif (string_list_has_string(&transaction->refnames, \"HEAD\")) {\n+\t\t\t/* An entry already existed */\n+\t\t\tstrbuf_addf(err,\n+\t\t\t\t    _(\"multiple updates for 'HEAD' (including one \"\n+\t\t\t\t      \"via its referent '%s') are not allowed\"),\n+\t\t\t\t    u->refname);\n+\t\t\treturn TRANSACTION_NAME_CONFLICT;\n+\t\t}\n+\n+\t\tref_transaction_add_update(\n+\t\t\ttransaction, \"HEAD\",\n+\t\t\tu->flags | REF_LOG_ONLY | REF_NO_DEREF,\n+\t\t\t&u->new_oid, &u->old_oid, NULL, NULL, NULL,\n+\t\t\tu->msg);\n+\t}\n+\n+\tret = reftable_backend_read_ref(be, rewritten_ref,\n+\t\t\t\t\t&current_oid, referent, &u->type);\n+\tif (ret < 0)\n+\t\treturn ret;\n+\tif (ret > 0 && !ref_update_expects_existing_old_ref(u)) {\n+\t\t/*\n+\t\t * The reference does not exist, and we either have no\n+\t\t * old object ID or expect the reference to not exist.\n+\t\t * We can thus skip below safety checks as well as the\n+\t\t * symref splitting. But we do want to verify that\n+\t\t * there is no conflicting reference here so that we\n+\t\t * can output a proper error message instead of failing\n+\t\t * at a later point.\n+\t\t */\n+\t\tstring_list_append(refnames_to_check, u->refname);\n+\n+\t\t/*\n+\t\t * There is no need to write the reference deletion\n+\t\t * when the reference in question doesn't exist.\n+\t\t */\n+\t\tif ((u->flags & REF_HAVE_NEW) && !ref_update_has_null_new_value(u)) {\n+\t\t\tret = queue_transaction_update(refs, tx_data, u,\n+\t\t\t\t\t\t       &current_oid, err);\n+\t\t\tif (ret)\n+\t\t\t\treturn ret;\n+\t\t}\n+\n+\t\treturn 0;\n+\t}\n+\tif (ret > 0) {\n+\t\t/* The reference does not exist, but we expected it to. */\n+\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n+\n+\n+\t\t\t\t   \"unable to resolve reference '%s'\"),\n+\t\t\t    ref_update_original_update_refname(u), u->refname);\n+\t\treturn -1;\n+\t}\n+\n+\tif (u->type & REF_ISSYMREF) {\n+\t\t/*\n+\t\t * The reftable stack is locked at this point already,\n+\t\t * so it is safe to call `refs_resolve_ref_unsafe()`\n+\t\t * here without causing races.\n+\t\t */\n+\t\tconst char *resolved = refs_resolve_ref_unsafe(&refs->base, u->refname, 0,\n+\t\t\t\t\t\t\t       &current_oid, NULL);\n+\n+\t\tif (u->flags & REF_NO_DEREF) {\n+\t\t\tif (u->flags & REF_HAVE_OLD && !resolved) {\n+\t\t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n+\t\t\t\t\t\t   \"error reading reference\"), u->refname);\n+\t\t\t\treturn -1;\n+\t\t\t}\n+\t\t} else {\n+\t\t\tstruct ref_update *new_update;\n+\t\t\tint new_flags;\n+\n+\t\t\tnew_flags = u->flags;\n+\t\t\tif (!strcmp(rewritten_ref, \"HEAD\"))\n+\t\t\t\tnew_flags |= REF_UPDATE_VIA_HEAD;\n+\n+\t\t\tif (string_list_has_string(&transaction->refnames, referent->buf)) {\n+\t\t\t\tstrbuf_addf(err,\n+\t\t\t\t\t    _(\"multiple updates for '%s' (including one \"\n+\t\t\t\t\t      \"via symref '%s') are not allowed\"),\n+\t\t\t\t\t    referent->buf, u->refname);\n+\t\t\t\treturn TRANSACTION_NAME_CONFLICT;\n+\t\t\t}\n+\n+\t\t\t/*\n+\t\t\t * If we are updating a symref (eg. HEAD), we should also\n+\t\t\t * update the branch that the symref points to.\n+\t\t\t *\n+\t\t\t * This is generic functionality, and would be better\n+\t\t\t * done in refs.c, but the current implementation is\n+\t\t\t * intertwined with the locking in files-backend.c.\n+\t\t\t */\n+\t\t\tnew_update = ref_transaction_add_update(\n+\t\t\t\ttransaction, referent->buf, new_flags,\n+\t\t\t\tu->new_target ? NULL : &u->new_oid,\n+\t\t\t\tu->old_target ? NULL : &u->old_oid,\n+\t\t\t\tu->new_target, u->old_target,\n+\t\t\t\tu->committer_info, u->msg);\n+\n+\t\t\tnew_update->parent_update = u;\n+\n+\t\t\t/*\n+\t\t\t * Change the symbolic ref update to log only. Also, it\n+\t\t\t * doesn't need to check its old OID value, as that will be\n+\t\t\t * done when new_update is processed.\n+\t\t\t */\n+\t\t\tu->flags |= REF_LOG_ONLY | REF_NO_DEREF;\n+\t\t\tu->flags &= ~REF_HAVE_OLD;\n+\t\t}\n+\t}\n+\n+\t/*\n+\t * Verify that the old object matches our expectations. Note\n+\t * that the error messages here do not make a lot of sense in\n+\t * the context of the reftable backend as we never lock\n+\t * individual refs. But the error messages match what the files\n+\t * backend returns, which keeps our tests happy.\n+\t */\n+\tif (u->old_target) {\n+\t\tif (!(u->type & REF_ISSYMREF)) {\n+\t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n+\t\t\t\t\t   \"expected symref with target '%s': \"\n+\t\t\t\t\t   \"but is a regular ref\"),\n+\t\t\t\t    ref_update_original_update_refname(u),\n+\t\t\t\t    u->old_target);\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tif (ref_update_check_old_target(referent->buf, u, err)) {\n+\t\t\treturn -1;\n+\t\t}\n+\t} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {\n+\t\tif (is_null_oid(&u->old_oid)) {\n+\t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n+\t\t\t\t\t   \"reference already exists\"),\n+\t\t\t\t    ref_update_original_update_refname(u));\n+\t\t\treturn TRANSACTION_CREATE_EXISTS;\n+\t\t}\n+\t\telse if (is_null_oid(&current_oid))\n+\t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n+\t\t\t\t\t   \"reference is missing but expected %s\"),\n+\t\t\t\t    ref_update_original_update_refname(u),\n+\t\t\t\t    oid_to_hex(&u->old_oid));\n+\t\telse\n+\t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n+\t\t\t\t\t   \"is at %s but expected %s\"),\n+\t\t\t\t    ref_update_original_update_refname(u),\n+\t\t\t\t    oid_to_hex(&current_oid),\n+\t\t\t\t    oid_to_hex(&u->old_oid));\n+\t\treturn TRANSACTION_NAME_CONFLICT;\n+\t}\n+\n+\t/*\n+\t * If all of the following conditions are true:\n+\t *\n+\t *   - We're not about to write a symref.\n+\t *   - We're not about to write a log-only entry.\n+\t *   - Old and new object ID are different.\n+\t *\n+\t * Then we're essentially doing a no-op update that can be\n+\t * skipped. This is not only for the sake of efficiency, but\n+\t * also skips writing unneeded reflog entries.\n+\t */\n+\tif ((u->type & REF_ISSYMREF) ||\n+\t    (u->flags & REF_LOG_ONLY) ||\n+\t    (u->flags & REF_HAVE_NEW && !oideq(&current_oid, &u->new_oid)))\n+\t\treturn queue_transaction_update(refs, tx_data, u,\n+\t\t\t\t\t       &current_oid, err);\n+\n+\treturn 0;\n+}\n+\n static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n \t\t\t\t\t   struct ref_transaction *transaction,\n \t\t\t\t\t   struct strbuf *err)\n@@ -1133,234 +1366,12 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n \tret = 0;\n \n \tfor (i = 0; i < transaction->nr; i++) {\n-\t\tstruct ref_update *u = transaction->updates[i];\n-\t\tstruct object_id current_oid = {0};\n-\t\tconst char *rewritten_ref;\n-\n-\t\t/*\n-\t\t * There is no need to reload the respective backends here as\n-\t\t * we have already reloaded them when preparing the transaction\n-\t\t * update. And given that the stacks have been locked there\n-\t\t * shouldn't have been any concurrent modifications of the\n-\t\t * stack.\n-\t\t */\n-\t\tret = backend_for(&be, refs, u->refname, &rewritten_ref, 0);\n+\t\tret = prepare_single_update(refs, tx_data, transaction, be,\n+\t\t\t\t\t    transaction->updates[i],\n+\t\t\t\t\t    &refnames_to_check, head_type,\n+\t\t\t\t\t    &head_referent, &referent, err);\n \t\tif (ret)\n \t\t\tgoto done;\n-\n-\t\t/* Verify that the new object ID is valid. */\n-\t\tif ((u->flags & REF_HAVE_NEW) && !is_null_oid(&u->new_oid) &&\n-\t\t    !(u->flags & REF_SKIP_OID_VERIFICATION) &&\n-\t\t    !(u->flags & REF_LOG_ONLY)) {\n-\t\t\tstruct object *o = parse_object(refs->base.repo, &u->new_oid);\n-\t\t\tif (!o) {\n-\t\t\t\tstrbuf_addf(err,\n-\t\t\t\t\t    _(\"trying to write ref '%s' with nonexistent object %s\"),\n-\t\t\t\t\t    u->refname, oid_to_hex(&u->new_oid));\n-\t\t\t\tret = -1;\n-\t\t\t\tgoto done;\n-\t\t\t}\n-\n-\t\t\tif (o->type != OBJ_COMMIT && is_branch(u->refname)) {\n-\t\t\t\tstrbuf_addf(err, _(\"trying to write non-commit object %s to branch '%s'\"),\n-\t\t\t\t\t    oid_to_hex(&u->new_oid), u->refname);\n-\t\t\t\tret = -1;\n-\t\t\t\tgoto done;\n-\t\t\t}\n-\t\t}\n-\n-\t\t/*\n-\t\t * When we update the reference that HEAD points to we enqueue\n-\t\t * a second log-only update for HEAD so that its reflog is\n-\t\t * updated accordingly.\n-\t\t */\n-\t\tif (head_type == REF_ISSYMREF &&\n-\t\t    !(u->flags & REF_LOG_ONLY) &&\n-\t\t    !(u->flags & REF_UPDATE_VIA_HEAD) &&\n-\t\t    !strcmp(rewritten_ref, head_referent.buf)) {\n-\t\t\t/*\n-\t\t\t * First make sure that HEAD is not already in the\n-\t\t\t * transaction. This check is O(lg N) in the transaction\n-\t\t\t * size, but it happens at most once per transaction.\n-\t\t\t */\n-\t\t\tif (string_list_has_string(&transaction->refnames, \"HEAD\")) {\n-\t\t\t\t/* An entry already existed */\n-\t\t\t\tstrbuf_addf(err,\n-\t\t\t\t\t    _(\"multiple updates for 'HEAD' (including one \"\n-\t\t\t\t\t    \"via its referent '%s') are not allowed\"),\n-\t\t\t\t\t    u->refname);\n-\t\t\t\tret = TRANSACTION_NAME_CONFLICT;\n-\t\t\t\tgoto done;\n-\t\t\t}\n-\n-\t\t\tref_transaction_add_update(\n-\t\t\t\ttransaction, \"HEAD\",\n-\t\t\t\tu->flags | REF_LOG_ONLY | REF_NO_DEREF,\n-\t\t\t\t&u->new_oid, &u->old_oid, NULL, NULL, NULL,\n-\t\t\t\tu->msg);\n-\t\t}\n-\n-\t\tret = reftable_backend_read_ref(be, rewritten_ref,\n-\t\t\t\t\t\t&current_oid, &referent, &u->type);\n-\t\tif (ret < 0)\n-\t\t\tgoto done;\n-\t\tif (ret > 0 && !ref_update_expects_existing_old_ref(u)) {\n-\t\t\t/*\n-\t\t\t * The reference does not exist, and we either have no\n-\t\t\t * old object ID or expect the reference to not exist.\n-\t\t\t * We can thus skip below safety checks as well as the\n-\t\t\t * symref splitting. But we do want to verify that\n-\t\t\t * there is no conflicting reference here so that we\n-\t\t\t * can output a proper error message instead of failing\n-\t\t\t * at a later point.\n-\t\t\t */\n-\t\t\tstring_list_append(&refnames_to_check, u->refname);\n-\n-\t\t\t/*\n-\t\t\t * There is no need to write the reference deletion\n-\t\t\t * when the reference in question doesn't exist.\n-\t\t\t */\n-\t\t\t if ((u->flags & REF_HAVE_NEW) && !ref_update_has_null_new_value(u)) {\n-\t\t\t\t ret = queue_transaction_update(refs, tx_data, u,\n-\t\t\t\t\t\t\t\t&current_oid, err);\n-\t\t\t\t if (ret)\n-\t\t\t\t\t goto done;\n-\t\t\t }\n-\n-\t\t\tcontinue;\n-\t\t}\n-\t\tif (ret > 0) {\n-\t\t\t/* The reference does not exist, but we expected it to. */\n-\t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n-\t\t\t\t    \"unable to resolve reference '%s'\"),\n-\t\t\t\t    ref_update_original_update_refname(u), u->refname);\n-\t\t\tret = -1;\n-\t\t\tgoto done;\n-\t\t}\n-\n-\t\tif (u->type & REF_ISSYMREF) {\n-\t\t\t/*\n-\t\t\t * The reftable stack is locked at this point already,\n-\t\t\t * so it is safe to call `refs_resolve_ref_unsafe()`\n-\t\t\t * here without causing races.\n-\t\t\t */\n-\t\t\tconst char *resolved = refs_resolve_ref_unsafe(&refs->base, u->refname, 0,\n-\t\t\t\t\t\t\t\t       &current_oid, NULL);\n-\n-\t\t\tif (u->flags & REF_NO_DEREF) {\n-\t\t\t\tif (u->flags & REF_HAVE_OLD && !resolved) {\n-\t\t\t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n-\t\t\t\t\t\t    \"error reading reference\"), u->refname);\n-\t\t\t\t\tret = -1;\n-\t\t\t\t\tgoto done;\n-\t\t\t\t}\n-\t\t\t} else {\n-\t\t\t\tstruct ref_update *new_update;\n-\t\t\t\tint new_flags;\n-\n-\t\t\t\tnew_flags = u->flags;\n-\t\t\t\tif (!strcmp(rewritten_ref, \"HEAD\"))\n-\t\t\t\t\tnew_flags |= REF_UPDATE_VIA_HEAD;\n-\n-\t\t\t\tif (string_list_has_string(&transaction->refnames, referent.buf)) {\n-\t\t\t\t\tstrbuf_addf(err,\n-\t\t\t\t\t\t    _(\"multiple updates for '%s' (including one \"\n-\t\t\t\t\t\t    \"via symref '%s') are not allowed\"),\n-\t\t\t\t\t\t    referent.buf, u->refname);\n-\t\t\t\t\tret = TRANSACTION_NAME_CONFLICT;\n-\t\t\t\t\tgoto done;\n-\t\t\t\t}\n-\n-\t\t\t\t/*\n-\t\t\t\t * If we are updating a symref (eg. HEAD), we should also\n-\t\t\t\t * update the branch that the symref points to.\n-\t\t\t\t *\n-\t\t\t\t * This is generic functionality, and would be better\n-\t\t\t\t * done in refs.c, but the current implementation is\n-\t\t\t\t * intertwined with the locking in files-backend.c.\n-\t\t\t\t */\n-\t\t\t\tnew_update = ref_transaction_add_update(\n-\t\t\t\t\ttransaction, referent.buf, new_flags,\n-\t\t\t\t\tu->new_target ? NULL : &u->new_oid,\n-\t\t\t\t\tu->old_target ? NULL : &u->old_oid,\n-\t\t\t\t\tu->new_target, u->old_target,\n-\t\t\t\t\tu->committer_info, u->msg);\n-\n-\t\t\t\tnew_update->parent_update = u;\n-\n-\t\t\t\t/*\n-\t\t\t\t * Change the symbolic ref update to log only. Also, it\n-\t\t\t\t * doesn't need to check its old OID value, as that will be\n-\t\t\t\t * done when new_update is processed.\n-\t\t\t\t */\n-\t\t\t\tu->flags |= REF_LOG_ONLY | REF_NO_DEREF;\n-\t\t\t\tu->flags &= ~REF_HAVE_OLD;\n-\t\t\t}\n-\t\t}\n-\n-\t\t/*\n-\t\t * Verify that the old object matches our expectations. Note\n-\t\t * that the error messages here do not make a lot of sense in\n-\t\t * the context of the reftable backend as we never lock\n-\t\t * individual refs. But the error messages match what the files\n-\t\t * backend returns, which keeps our tests happy.\n-\t\t */\n-\t\tif (u->old_target) {\n-\t\t\tif (!(u->type & REF_ISSYMREF)) {\n-\t\t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n-\t\t\t\t\t   \"expected symref with target '%s': \"\n-\t\t\t\t\t   \"but is a regular ref\"),\n-\t\t\t\t\t    ref_update_original_update_refname(u),\n-\t\t\t\t\t    u->old_target);\n-\t\t\t\tret = -1;\n-\t\t\t\tgoto done;\n-\t\t\t}\n-\n-\t\t\tif (ref_update_check_old_target(referent.buf, u, err)) {\n-\t\t\t\tret = -1;\n-\t\t\t\tgoto done;\n-\t\t\t}\n-\t\t} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {\n-\t\t\tret = TRANSACTION_NAME_CONFLICT;\n-\t\t\tif (is_null_oid(&u->old_oid)) {\n-\t\t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n-\t\t\t\t\t\t   \"reference already exists\"),\n-\t\t\t\t\t    ref_update_original_update_refname(u));\n-\t\t\t\tret = TRANSACTION_CREATE_EXISTS;\n-\t\t\t}\n-\t\t\telse if (is_null_oid(&current_oid))\n-\t\t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n-\t\t\t\t\t\t   \"reference is missing but expected %s\"),\n-\t\t\t\t\t    ref_update_original_update_refname(u),\n-\t\t\t\t\t    oid_to_hex(&u->old_oid));\n-\t\t\telse\n-\t\t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n-\t\t\t\t\t\t   \"is at %s but expected %s\"),\n-\t\t\t\t\t    ref_update_original_update_refname(u),\n-\t\t\t\t\t    oid_to_hex(&current_oid),\n-\t\t\t\t\t    oid_to_hex(&u->old_oid));\n-\t\t\tgoto done;\n-\t\t}\n-\n-\t\t/*\n-\t\t * If all of the following conditions are true:\n-\t\t *\n-\t\t *   - We're not about to write a symref.\n-\t\t *   - We're not about to write a log-only entry.\n-\t\t *   - Old and new object ID are different.\n-\t\t *\n-\t\t * Then we're essentially doing a no-op update that can be\n-\t\t * skipped. This is not only for the sake of efficiency, but\n-\t\t * also skips writing unneeded reflog entries.\n-\t\t */\n-\t\tif ((u->type & REF_ISSYMREF) ||\n-\t\t    (u->flags & REF_LOG_ONLY) ||\n-\t\t    (u->flags & REF_HAVE_NEW && !oideq(&current_oid, &u->new_oid))) {\n-\t\t\tret = queue_transaction_update(refs, tx_data, u,\n-\t\t\t\t\t\t       &current_oid, err);\n-\t\t\tif (ret)\n-\t\t\t\tgoto done;\n-\t\t}\n \t}\n \n \tstring_list_sort(&refnames_to_check);\n\n-- \n2.48.1\n\n"},{"id":"513612","messageId":"20250305-245-partially-atomic-ref-updates-v3-5-0c64e3052354@gmail.com","threadId":"63064","inReplyTo":"20250305-245-partially-atomic-ref-updates-v3-0-0c64e3052354@gmail.com","subject":"[PATCH v3 5/8] refs: introduce enum-based transaction error types","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-03-05T17:39:00Z","receivedAt":"2025-03-05T17:39:20Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Replace preprocessor-defined transaction errors with a strongly-typed\nenum `ref_transaction_error`. This change:\n\n  - Improves type safety and function signature clarity.\n  - Makes error handling more explicit and discoverable.\n  - Maintains existing error cases, while adding new error cases for\n    common scenarios.\n\nThis refactoring paves the way for more comprehensive error handling\nwhich we will utilize in the upcoming commits to add partial transaction\nsupport.\n\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n builtin/fetch.c         |   2 +-\n refs.c                  |  49 ++++++------\n refs.h                  |  54 ++++++++-----\n refs/files-backend.c    | 202 ++++++++++++++++++++++++------------------------\n refs/packed-backend.c   |  23 +++---\n refs/refs-internal.h    |   5 +-\n refs/reftable-backend.c |  64 +++++++--------\n 7 files changed, 213 insertions(+), 186 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 1c740d5aac..52c913d28a 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -687,7 +687,7 @@ static int s_update_ref(const char *action,\n \t\tswitch (ref_transaction_commit(our_transaction, &err)) {\n \t\tcase 0:\n \t\t\tbreak;\n-\t\tcase TRANSACTION_NAME_CONFLICT:\n+\t\tcase REF_TRANSACTION_ERROR_NAME_CONFLICT:\n \t\t\tret = STORE_REF_ERROR_DF_CONFLICT;\n \t\t\tgoto out;\n \t\tdefault:\ndiff --git a/refs.c b/refs.c\nindex 69f385f344..63b8050ce2 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2271,7 +2271,7 @@ int refs_update_symref_extended(struct ref_store *refs, const char *ref,\n \t\t\t\t\t   REF_NO_DEREF, logmsg, &err))\n \t\t\tgoto error_return;\n \t\tprepret = ref_transaction_prepare(transaction, &err);\n-\t\tif (prepret && prepret != TRANSACTION_CREATE_EXISTS)\n+\t\tif (prepret && prepret != REF_TRANSACTION_ERROR_CREATE_EXISTS)\n \t\t\tgoto error_return;\n \t} else {\n \t\tif (ref_transaction_update(transaction, ref, NULL, NULL,\n@@ -2289,7 +2289,7 @@ int refs_update_symref_extended(struct ref_store *refs, const char *ref,\n \t\t}\n \t}\n \n-\tif (prepret == TRANSACTION_CREATE_EXISTS)\n+\tif (prepret == REF_TRANSACTION_ERROR_CREATE_EXISTS)\n \t\tgoto cleanup;\n \n \tif (ref_transaction_commit(transaction, &err))\n@@ -2425,7 +2425,7 @@ int ref_transaction_prepare(struct ref_transaction *transaction,\n \n \tstring_list_sort(&transaction->refnames);\n \tif (ref_update_reject_duplicates(&transaction->refnames, err))\n-\t\treturn TRANSACTION_GENERIC_ERROR;\n+\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n \n \tret = refs->be->transaction_prepare(refs, transaction, err);\n \tif (ret)\n@@ -2497,18 +2497,18 @@ int ref_transaction_commit(struct ref_transaction *transaction,\n \treturn ret;\n }\n \n-int refs_verify_refnames_available(struct ref_store *refs,\n-\t\t\t\t   const struct string_list *refnames,\n-\t\t\t\t   const struct string_list *extras,\n-\t\t\t\t   const struct string_list *skip,\n-\t\t\t\t   unsigned int initial_transaction,\n-\t\t\t\t   struct strbuf *err)\n+enum ref_transaction_error refs_verify_refnames_available(struct ref_store *refs,\n+\t\t\t\t\t  const struct string_list *refnames,\n+\t\t\t\t\t  const struct string_list *extras,\n+\t\t\t\t\t  const struct string_list *skip,\n+\t\t\t\t\t  unsigned int initial_transaction,\n+\t\t\t\t\t  struct strbuf *err)\n {\n \tstruct strbuf dirname = STRBUF_INIT;\n \tstruct strbuf referent = STRBUF_INIT;\n \tstruct ref_iterator *iter = NULL;\n \tstruct strset dirnames;\n-\tint ret = -1;\n+\tint ret = REF_TRANSACTION_ERROR_NAME_CONFLICT;\n \n \t/*\n \t * For the sake of comments in this function, suppose that\n@@ -2624,12 +2624,13 @@ int refs_verify_refnames_available(struct ref_store *refs,\n \treturn ret;\n }\n \n-int refs_verify_refname_available(struct ref_store *refs,\n-\t\t\t\t  const char *refname,\n-\t\t\t\t  const struct string_list *extras,\n-\t\t\t\t  const struct string_list *skip,\n-\t\t\t\t  unsigned int initial_transaction,\n-\t\t\t\t  struct strbuf *err)\n+enum ref_transaction_error refs_verify_refname_available(\n+\tstruct ref_store *refs,\n+\tconst char *refname,\n+\tconst struct string_list *extras,\n+\tconst struct string_list *skip,\n+\tunsigned int initial_transaction,\n+\tstruct strbuf *err)\n {\n \tstruct string_list_item item = { .string = (char *) refname };\n \tstruct string_list refnames = {\n@@ -2817,8 +2818,9 @@ int ref_update_has_null_new_value(struct ref_update *update)\n \treturn !update->new_target && is_null_oid(&update->new_oid);\n }\n \n-int ref_update_check_old_target(const char *referent, struct ref_update *update,\n-\t\t\t\tstruct strbuf *err)\n+enum ref_transaction_error ref_update_check_old_target(const char *referent,\n+\t\t\t\t\t\t       struct ref_update *update,\n+\t\t\t\t\t\t       struct strbuf *err)\n {\n \tif (!update->old_target)\n \t\tBUG(\"called without old_target set\");\n@@ -2826,17 +2828,18 @@ int ref_update_check_old_target(const char *referent, struct ref_update *update,\n \tif (!strcmp(referent, update->old_target))\n \t\treturn 0;\n \n-\tif (!strcmp(referent, \"\"))\n+\tif (!strcmp(referent, \"\")) {\n \t\tstrbuf_addf(err, \"verifying symref target: '%s': \"\n \t\t\t    \"reference is missing but expected %s\",\n \t\t\t    ref_update_original_update_refname(update),\n \t\t\t    update->old_target);\n-\telse\n-\t\tstrbuf_addf(err, \"verifying symref target: '%s': \"\n-\t\t\t    \"is at %s but expected %s\",\n+\t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n+\t}\n+\n+\tstrbuf_addf(err, \"verifying symref target: '%s': is at %s but expected %s\",\n \t\t\t    ref_update_original_update_refname(update),\n \t\t\t    referent, update->old_target);\n-\treturn -1;\n+\treturn REF_TRANSACTION_ERROR_INCORRECT_OLD_VALUE;\n }\n \n struct migration_data {\ndiff --git a/refs.h b/refs.h\nindex b14ba1f9ff..1b9213f9ce 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -16,6 +16,29 @@ struct worktree;\n enum ref_storage_format ref_storage_format_by_name(const char *name);\n const char *ref_storage_format_to_name(enum ref_storage_format ref_storage_format);\n \n+/*\n+ * enum ref_transaction_error represents the following return codes:\n+ * REF_TRANSACTION_ERROR_GENERIC error_code: default error code.\n+ * REF_TRANSACTION_ERROR_NAME_CONFLICT error_code: ref name conflict like A vs A/B.\n+ * REF_TRANSACTION_ERROR_CREATE_EXISTS error_code: ref to be created already exists.\n+ * REF_TRANSACTION_ERROR_NONEXISTENT_REF error_code: ref expected but doesn't exist.\n+ * REF_TRANSACTION_ERROR_INCORRECT_OLD_VALUE error_code: provided old_oid or old_target of\n+ * reference doesn't match actual.\n+ * REF_TRANSACTION_ERROR_INVALID_NEW_VALUE error_code: provided new_oid or new_target is\n+ * invalid.\n+ * REF_TRANSACTION_ERROR_EXPECTED_SYMREF error_code: expected ref to be symref, but is a\n+ * regular ref.\n+ */\n+enum ref_transaction_error {\n+\tREF_TRANSACTION_ERROR_GENERIC = -1,\n+\tREF_TRANSACTION_ERROR_NAME_CONFLICT = -2,\n+\tREF_TRANSACTION_ERROR_CREATE_EXISTS = -3,\n+\tREF_TRANSACTION_ERROR_NONEXISTENT_REF = -4,\n+\tREF_TRANSACTION_ERROR_INCORRECT_OLD_VALUE = -5,\n+\tREF_TRANSACTION_ERROR_INVALID_NEW_VALUE = -6,\n+\tREF_TRANSACTION_ERROR_EXPECTED_SYMREF = -7,\n+};\n+\n /*\n  * Resolve a reference, recursively following symbolic references.\n  *\n@@ -117,24 +140,24 @@ int refs_read_symbolic_ref(struct ref_store *ref_store, const char *refname,\n  *\n  * extras and skip must be sorted.\n  */\n-int refs_verify_refname_available(struct ref_store *refs,\n-\t\t\t\t  const char *refname,\n-\t\t\t\t  const struct string_list *extras,\n-\t\t\t\t  const struct string_list *skip,\n-\t\t\t\t  unsigned int initial_transaction,\n-\t\t\t\t  struct strbuf *err);\n+enum ref_transaction_error refs_verify_refname_available(struct ref_store *refs,\n+\t\t\t\t\t\t const char *refname,\n+\t\t\t\t\t\t const struct string_list *extras,\n+\t\t\t\t\t\t const struct string_list *skip,\n+\t\t\t\t\t\t unsigned int initial_transaction,\n+\t\t\t\t\t\t struct strbuf *err);\n \n /*\n  * Same as `refs_verify_refname_available()`, but checking for a list of\n  * refnames instead of only a single item. This is more efficient in the case\n  * where one needs to check multiple refnames.\n  */\n-int refs_verify_refnames_available(struct ref_store *refs,\n-\t\t\t\t   const struct string_list *refnames,\n-\t\t\t\t   const struct string_list *extras,\n-\t\t\t\t   const struct string_list *skip,\n-\t\t\t\t   unsigned int initial_transaction,\n-\t\t\t\t   struct strbuf *err);\n+enum ref_transaction_error refs_verify_refnames_available(struct ref_store *refs,\n+\t\t\t\t\t  const struct string_list *refnames,\n+\t\t\t\t\t  const struct string_list *extras,\n+\t\t\t\t\t  const struct string_list *skip,\n+\t\t\t\t\t  unsigned int initial_transaction,\n+\t\t\t\t\t  struct strbuf *err);\n \n int refs_ref_exists(struct ref_store *refs, const char *refname);\n \n@@ -830,13 +853,6 @@ int ref_transaction_verify(struct ref_transaction *transaction,\n \t\t\t   unsigned int flags,\n \t\t\t   struct strbuf *err);\n \n-/* Naming conflict (for example, the ref names A and A/B conflict). */\n-#define TRANSACTION_NAME_CONFLICT -1\n-/* When only creation was requested, but the ref already exists. */\n-#define TRANSACTION_CREATE_EXISTS -2\n-/* All other errors. */\n-#define TRANSACTION_GENERIC_ERROR -3\n-\n /*\n  * Perform the preparatory stages of committing `transaction`. Acquire\n  * any needed locks, check preconditions, etc.; basically, do as much\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 7c6a0b3478..1e1663f44b 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -663,7 +663,7 @@ static void unlock_ref(struct ref_lock *lock)\n  * broken, lock the reference anyway but clear old_oid.\n  *\n  * Return 0 on success. On failure, write an error message to err and\n- * return TRANSACTION_NAME_CONFLICT or TRANSACTION_GENERIC_ERROR.\n+ * return REF_TRANSACTION_ERROR_NAME_CONFLICT or REF_TRANSACTION_ERROR_GENERIC.\n  *\n  * Implementation note: This function is basically\n  *\n@@ -676,19 +676,20 @@ static void unlock_ref(struct ref_lock *lock)\n  *   avoided, namely if we were successfully able to read the ref\n  * - Generate informative error messages in the case of failure\n  */\n-static int lock_raw_ref(struct files_ref_store *refs,\n-\t\t\tconst char *refname, int mustexist,\n-\t\t\tstruct string_list *refnames_to_check,\n-\t\t\tconst struct string_list *extras,\n-\t\t\tstruct ref_lock **lock_p,\n-\t\t\tstruct strbuf *referent,\n-\t\t\tunsigned int *type,\n-\t\t\tstruct strbuf *err)\n-{\n+static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs,\n+\t\t\t\t\t       const char *refname,\n+\t\t\t\t\t       int mustexist,\n+\t\t\t\t\t       struct string_list *refnames_to_check,\n+\t\t\t\t\t       const struct string_list *extras,\n+\t\t\t\t\t       struct ref_lock **lock_p,\n+\t\t\t\t\t       struct strbuf *referent,\n+\t\t\t\t\t       unsigned int *type,\n+\t\t\t\t\t       struct strbuf *err)\n+{\n+\tenum ref_transaction_error ret = REF_TRANSACTION_ERROR_GENERIC;\n \tstruct ref_lock *lock;\n \tstruct strbuf ref_file = STRBUF_INIT;\n \tint attempts_remaining = 3;\n-\tint ret = TRANSACTION_GENERIC_ERROR;\n \tint failure_errno;\n \n \tassert(err);\n@@ -728,13 +729,14 @@ static int lock_raw_ref(struct files_ref_store *refs,\n \t\t\t\tstrbuf_reset(err);\n \t\t\t\tstrbuf_addf(err, \"unable to resolve reference '%s'\",\n \t\t\t\t\t    refname);\n+\t\t\t\tret = REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n \t\t\t} else {\n \t\t\t\t/*\n \t\t\t\t * The error message set by\n \t\t\t\t * refs_verify_refname_available() is\n \t\t\t\t * OK.\n \t\t\t\t */\n-\t\t\t\tret = TRANSACTION_NAME_CONFLICT;\n+\t\t\t\tret = REF_TRANSACTION_ERROR_NAME_CONFLICT;\n \t\t\t}\n \t\t} else {\n \t\t\t/*\n@@ -788,6 +790,7 @@ static int lock_raw_ref(struct files_ref_store *refs,\n \t\t\t\t/* Garden variety missing reference. */\n \t\t\t\tstrbuf_addf(err, \"unable to resolve reference '%s'\",\n \t\t\t\t\t    refname);\n+\t\t\t\tret = REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n \t\t\t\tgoto error_return;\n \t\t\t} else {\n \t\t\t\t/*\n@@ -820,6 +823,7 @@ static int lock_raw_ref(struct files_ref_store *refs,\n \t\t\t\t/* Garden variety missing reference. */\n \t\t\t\tstrbuf_addf(err, \"unable to resolve reference '%s'\",\n \t\t\t\t\t    refname);\n+\t\t\t\tret = REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n \t\t\t\tgoto error_return;\n \t\t\t} else if (remove_dir_recursively(&ref_file,\n \t\t\t\t\t\t\t  REMOVE_DIR_EMPTY_ONLY)) {\n@@ -830,7 +834,7 @@ static int lock_raw_ref(struct files_ref_store *refs,\n \t\t\t\t\t * The error message set by\n \t\t\t\t\t * verify_refname_available() is OK.\n \t\t\t\t\t */\n-\t\t\t\t\tret = TRANSACTION_NAME_CONFLICT;\n+\t\t\t\t\tret = REF_TRANSACTION_ERROR_NAME_CONFLICT;\n \t\t\t\t\tgoto error_return;\n \t\t\t\t} else {\n \t\t\t\t\t/*\n@@ -1517,10 +1521,11 @@ static int rename_tmp_log(struct files_ref_store *refs, const char *newrefname)\n \treturn ret;\n }\n \n-static int write_ref_to_lockfile(struct files_ref_store *refs,\n-\t\t\t\t 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 enum ref_transaction_error write_ref_to_lockfile(struct files_ref_store *refs,\n+\t\t\t\t\t\t\tstruct ref_lock *lock,\n+\t\t\t\t\t\t\tconst struct object_id *oid,\n+\t\t\t\t\t\t\tint skip_oid_verification,\n+\t\t\t\t\t\t\tstruct 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@@ -1926,10 +1931,11 @@ static int files_log_ref_write(struct files_ref_store *refs,\n  * Write oid into the open lockfile, then close the lockfile. On\n  * errors, rollback the lockfile, fill in *err and return -1.\n  */\n-static int write_ref_to_lockfile(struct files_ref_store *refs,\n-\t\t\t\t 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 enum ref_transaction_error write_ref_to_lockfile(struct files_ref_store *refs,\n+\t\t\t\t\t\t\tstruct ref_lock *lock,\n+\t\t\t\t\t\t\tconst struct object_id *oid,\n+\t\t\t\t\t\t\tint skip_oid_verification,\n+\t\t\t\t\t\t\tstruct strbuf *err)\n {\n \tstatic char term = '\\n';\n \tstruct object *o;\n@@ -1943,7 +1949,7 @@ static int write_ref_to_lockfile(struct files_ref_store *refs,\n \t\t\t\t\"trying to write ref '%s' with nonexistent object %s\",\n \t\t\t\tlock->ref_name, oid_to_hex(oid));\n \t\t\tunlock_ref(lock);\n-\t\t\treturn -1;\n+\t\t\treturn REF_TRANSACTION_ERROR_INVALID_NEW_VALUE;\n \t\t}\n \t\tif (o->type != OBJ_COMMIT && is_branch(lock->ref_name)) {\n \t\t\tstrbuf_addf(\n@@ -1951,7 +1957,7 @@ static int write_ref_to_lockfile(struct files_ref_store *refs,\n \t\t\t\t\"trying to write non-commit object %s to branch '%s'\",\n \t\t\t\toid_to_hex(oid), lock->ref_name);\n \t\t\tunlock_ref(lock);\n-\t\t\treturn -1;\n+\t\t\treturn REF_TRANSACTION_ERROR_INVALID_NEW_VALUE;\n \t\t}\n \t}\n \tfd = get_lock_file_fd(&lock->lk);\n@@ -1962,7 +1968,7 @@ static int write_ref_to_lockfile(struct files_ref_store *refs,\n \t\tstrbuf_addf(err,\n \t\t\t    \"couldn't write '%s'\", get_lock_file_path(&lock->lk));\n \t\tunlock_ref(lock);\n-\t\treturn -1;\n+\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n \t}\n \treturn 0;\n }\n@@ -2376,9 +2382,10 @@ static struct ref_iterator *files_reflog_iterator_begin(struct ref_store *ref_st\n  * If update is a direct update of head_ref (the reference pointed to\n  * by HEAD), then add an extra REF_LOG_ONLY update for HEAD.\n  */\n-static int split_head_update(struct ref_update *update,\n-\t\t\t     struct ref_transaction *transaction,\n-\t\t\t     const char *head_ref, struct strbuf *err)\n+static enum ref_transaction_error split_head_update(struct ref_update *update,\n+\t\t\t\t\t\t    struct ref_transaction *transaction,\n+\t\t\t\t\t\t    const char *head_ref,\n+\t\t\t\t\t\t    struct strbuf *err)\n {\n \tstruct ref_update *new_update;\n \n@@ -2402,7 +2409,7 @@ static int split_head_update(struct ref_update *update,\n \t\t\t    \"multiple updates for 'HEAD' (including one \"\n \t\t\t    \"via its referent '%s') are not allowed\",\n \t\t\t    update->refname);\n-\t\treturn TRANSACTION_NAME_CONFLICT;\n+\t\treturn REF_TRANSACTION_ERROR_NAME_CONFLICT;\n \t}\n \n \tnew_update = ref_transaction_add_update(\n@@ -2430,10 +2437,10 @@ static int split_head_update(struct ref_update *update,\n  * Note that the new update will itself be subject to splitting when\n  * the iteration gets to it.\n  */\n-static int split_symref_update(struct ref_update *update,\n-\t\t\t       const char *referent,\n-\t\t\t       struct ref_transaction *transaction,\n-\t\t\t       struct strbuf *err)\n+static enum ref_transaction_error split_symref_update(struct ref_update *update,\n+\t\t\t\t\t\t      const char *referent,\n+\t\t\t\t\t\t      struct ref_transaction *transaction,\n+\t\t\t\t\t\t      struct strbuf *err)\n {\n \tstruct ref_update *new_update;\n \tunsigned int new_flags;\n@@ -2450,7 +2457,7 @@ static int split_symref_update(struct ref_update *update,\n \t\t\t    \"multiple updates for '%s' (including one \"\n \t\t\t    \"via symref '%s') are not allowed\",\n \t\t\t    referent, update->refname);\n-\t\treturn TRANSACTION_NAME_CONFLICT;\n+\t\treturn REF_TRANSACTION_ERROR_NAME_CONFLICT;\n \t}\n \n \tnew_flags = update->flags;\n@@ -2491,11 +2498,10 @@ static int split_symref_update(struct ref_update *update,\n  * everything is OK, return 0; otherwise, write an error message to\n  * err and return -1.\n  */\n-static int check_old_oid(struct ref_update *update, struct object_id *oid,\n-\t\t\t struct strbuf *err)\n+static enum ref_transaction_error check_old_oid(struct ref_update *update,\n+\t\t\t\t\t\tstruct object_id *oid,\n+\t\t\t\t\t\tstruct strbuf *err)\n {\n-\tint ret = TRANSACTION_GENERIC_ERROR;\n-\n \tif (!(update->flags & REF_HAVE_OLD) ||\n \t\t   oideq(oid, &update->old_oid))\n \t\treturn 0;\n@@ -2504,21 +2510,20 @@ static int check_old_oid(struct ref_update *update, struct object_id *oid,\n \t\tstrbuf_addf(err, \"cannot lock ref '%s': \"\n \t\t\t    \"reference already exists\",\n \t\t\t    ref_update_original_update_refname(update));\n-\t\tret = TRANSACTION_CREATE_EXISTS;\n-\t}\n-\telse if (is_null_oid(oid))\n+\t\treturn REF_TRANSACTION_ERROR_CREATE_EXISTS;\n+\t} else if (is_null_oid(oid)) {\n \t\tstrbuf_addf(err, \"cannot lock ref '%s': \"\n \t\t\t    \"reference is missing but expected %s\",\n \t\t\t    ref_update_original_update_refname(update),\n \t\t\t    oid_to_hex(&update->old_oid));\n-\telse\n-\t\tstrbuf_addf(err, \"cannot lock ref '%s': \"\n-\t\t\t    \"is at %s but expected %s\",\n-\t\t\t    ref_update_original_update_refname(update),\n-\t\t\t    oid_to_hex(oid),\n-\t\t\t    oid_to_hex(&update->old_oid));\n+\t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n+\t}\n \n-\treturn ret;\n+\tstrbuf_addf(err, \"cannot lock ref '%s': is at %s but expected %s\",\n+\t\t    ref_update_original_update_refname(update), oid_to_hex(oid),\n+\t\t    oid_to_hex(&update->old_oid));\n+\n+\treturn REF_TRANSACTION_ERROR_INCORRECT_OLD_VALUE;\n }\n \n struct files_transaction_backend_data {\n@@ -2540,17 +2545,17 @@ struct files_transaction_backend_data {\n  * - If it is an update of head_ref, add a corresponding REF_LOG_ONLY\n  *   update of HEAD.\n  */\n-static int lock_ref_for_update(struct files_ref_store *refs,\n-\t\t\t       struct ref_update *update,\n-\t\t\t       struct ref_transaction *transaction,\n-\t\t\t       const char *head_ref,\n-\t\t\t       struct string_list *refnames_to_check,\n-\t\t\t       struct strbuf *err)\n+static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *refs,\n+\t\t\t\t\t\t      struct ref_update *update,\n+\t\t\t\t\t\t      struct ref_transaction *transaction,\n+\t\t\t\t\t\t      const char *head_ref,\n+\t\t\t\t\t\t      struct string_list *refnames_to_check,\n+\t\t\t\t\t\t      struct strbuf *err)\n {\n \tstruct strbuf referent = STRBUF_INIT;\n \tint mustexist = ref_update_expects_existing_old_ref(update);\n \tstruct files_transaction_backend_data *backend_data;\n-\tint ret = 0;\n+\tenum ref_transaction_error ret = 0;\n \tstruct ref_lock *lock;\n \n \tfiles_assert_main_repository(refs, \"lock_ref_for_update\");\n@@ -2602,22 +2607,17 @@ static int lock_ref_for_update(struct files_ref_store *refs,\n \t\t\t\t\tstrbuf_addf(err, \"cannot lock ref '%s': \"\n \t\t\t\t\t\t    \"error reading reference\",\n \t\t\t\t\t\t    ref_update_original_update_refname(update));\n-\t\t\t\t\tret = TRANSACTION_GENERIC_ERROR;\n+\t\t\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n \t\t\t\t\tgoto out;\n \t\t\t\t}\n \t\t\t}\n \n-\t\t\tif (update->old_target) {\n-\t\t\t\tif (ref_update_check_old_target(referent.buf, update, err)) {\n-\t\t\t\t\tret = TRANSACTION_GENERIC_ERROR;\n-\t\t\t\t\tgoto out;\n-\t\t\t\t}\n-\t\t\t} else {\n+\t\t\tif (update->old_target)\n+\t\t\t\tret = ref_update_check_old_target(referent.buf, update, err);\n+\t\t\telse\n \t\t\t\tret = check_old_oid(update, &lock->old_oid, err);\n-\t\t\t\tif  (ret) {\n-\t\t\t\t\tgoto out;\n-\t\t\t\t}\n-\t\t\t}\n+\t\t\tif (ret)\n+\t\t\t\tgoto out;\n \t\t} else {\n \t\t\t/*\n \t\t\t * Create a new update for the reference this\n@@ -2644,7 +2644,7 @@ static int lock_ref_for_update(struct files_ref_store *refs,\n \t\t\t\t\t   \"but is a regular ref\"),\n \t\t\t\t    ref_update_original_update_refname(update),\n \t\t\t\t    update->old_target);\n-\t\t\tret = TRANSACTION_GENERIC_ERROR;\n+\t\t\tret = REF_TRANSACTION_ERROR_EXPECTED_SYMREF;\n \t\t\tgoto out;\n \t\t} else {\n \t\t\tret = check_old_oid(update, &lock->old_oid, err);\n@@ -2668,14 +2668,14 @@ static int lock_ref_for_update(struct files_ref_store *refs,\n \n \tif (update->new_target && !(update->flags & REF_LOG_ONLY)) {\n \t\tif (create_symref_lock(lock, update->new_target, err)) {\n-\t\t\tret = TRANSACTION_GENERIC_ERROR;\n+\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n \t\t\tgoto out;\n \t\t}\n \n \t\tif (close_ref_gently(lock)) {\n \t\t\tstrbuf_addf(err, \"couldn't close '%s.lock'\",\n \t\t\t\t    update->refname);\n-\t\t\tret = TRANSACTION_GENERIC_ERROR;\n+\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n \t\t\tgoto out;\n \t\t}\n \n@@ -2693,25 +2693,27 @@ static int lock_ref_for_update(struct files_ref_store *refs,\n \t\t\t * The reference already has the desired\n \t\t\t * value, so we don't need to write it.\n \t\t\t */\n-\t\t} else if (write_ref_to_lockfile(\n-\t\t\t\t   refs, lock, &update->new_oid,\n-\t\t\t\t   update->flags & REF_SKIP_OID_VERIFICATION,\n-\t\t\t\t   err)) {\n-\t\t\tchar *write_err = strbuf_detach(err, NULL);\n-\n-\t\t\t/*\n-\t\t\t * The lock was freed upon failure of\n-\t\t\t * write_ref_to_lockfile():\n-\t\t\t */\n-\t\t\tupdate->backend_data = NULL;\n-\t\t\tstrbuf_addf(err,\n-\t\t\t\t    \"cannot update ref '%s': %s\",\n-\t\t\t\t    update->refname, write_err);\n-\t\t\tfree(write_err);\n-\t\t\tret = TRANSACTION_GENERIC_ERROR;\n-\t\t\tgoto out;\n \t\t} else {\n-\t\t\tupdate->flags |= REF_NEEDS_COMMIT;\n+\t\t\tret = write_ref_to_lockfile(\n+\t\t\t\trefs, lock, &update->new_oid,\n+\t\t\t\tupdate->flags & REF_SKIP_OID_VERIFICATION,\n+\t\t\t\terr);\n+\t\t\tif (ret) {\n+\t\t\t\tchar *write_err = strbuf_detach(err, NULL);\n+\n+\t\t\t\t/*\n+\t\t\t\t * The lock was freed upon failure of\n+\t\t\t\t * write_ref_to_lockfile():\n+\t\t\t\t */\n+\t\t\t\tupdate->backend_data = NULL;\n+\t\t\t\tstrbuf_addf(err,\n+\t\t\t\t\t    \"cannot update ref '%s': %s\",\n+\t\t\t\t\t    update->refname, write_err);\n+\t\t\t\tfree(write_err);\n+\t\t\t\tgoto out;\n+\t\t\t} else {\n+\t\t\t\tupdate->flags |= REF_NEEDS_COMMIT;\n+\t\t\t}\n \t\t}\n \t}\n \tif (!(update->flags & REF_NEEDS_COMMIT)) {\n@@ -2723,7 +2725,7 @@ static int lock_ref_for_update(struct files_ref_store *refs,\n \t\tif (close_ref_gently(lock)) {\n \t\t\tstrbuf_addf(err, \"couldn't close '%s.lock'\",\n \t\t\t\t    update->refname);\n-\t\t\tret = TRANSACTION_GENERIC_ERROR;\n+\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n \t\t\tgoto out;\n \t\t}\n \t}\n@@ -2865,7 +2867,7 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n \t\t\t\t\t\trefs->packed_ref_store,\n \t\t\t\t\t\ttransaction->flags, err);\n \t\t\t\tif (!packed_transaction) {\n-\t\t\t\t\tret = TRANSACTION_GENERIC_ERROR;\n+\t\t\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n \t\t\t\t\tgoto cleanup;\n \t\t\t\t}\n \n@@ -2897,13 +2899,13 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n \t */\n \tif (refs_verify_refnames_available(refs->packed_ref_store, &refnames_to_check,\n \t\t\t\t\t   &transaction->refnames, NULL, 0, err)) {\n-\t\tret = TRANSACTION_NAME_CONFLICT;\n+\t\tret = REF_TRANSACTION_ERROR_NAME_CONFLICT;\n \t\tgoto cleanup;\n \t}\n \n \tif (packed_transaction) {\n \t\tif (packed_refs_lock(refs->packed_ref_store, 0, err)) {\n-\t\t\tret = TRANSACTION_GENERIC_ERROR;\n+\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n \t\t\tgoto cleanup;\n \t\t}\n \t\tbackend_data->packed_refs_locked = 1;\n@@ -2934,7 +2936,7 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n \t\t\t */\n \t\t\tbackend_data->packed_transaction = NULL;\n \t\t\tif (ref_transaction_abort(packed_transaction, err)) {\n-\t\t\t\tret = TRANSACTION_GENERIC_ERROR;\n+\t\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n \t\t\t\tgoto cleanup;\n \t\t\t}\n \t\t}\n@@ -3035,7 +3037,7 @@ static int files_transaction_finish_initial(struct files_ref_store *refs,\n \tpacked_transaction = ref_store_transaction_begin(refs->packed_ref_store,\n \t\t\t\t\t\t\t transaction->flags, err);\n \tif (!packed_transaction) {\n-\t\tret = TRANSACTION_GENERIC_ERROR;\n+\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n \t\tgoto cleanup;\n \t}\n \n@@ -3058,7 +3060,7 @@ static int files_transaction_finish_initial(struct files_ref_store *refs,\n \t\t\tif (!loose_transaction) {\n \t\t\t\tloose_transaction = ref_store_transaction_begin(&refs->base, 0, err);\n \t\t\t\tif (!loose_transaction) {\n-\t\t\t\t\tret = TRANSACTION_GENERIC_ERROR;\n+\t\t\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n \t\t\t\t\tgoto cleanup;\n \t\t\t\t}\n \t\t\t}\n@@ -3083,19 +3085,19 @@ static int files_transaction_finish_initial(struct files_ref_store *refs,\n \t}\n \n \tif (packed_refs_lock(refs->packed_ref_store, 0, err)) {\n-\t\tret = TRANSACTION_GENERIC_ERROR;\n+\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n \t\tgoto cleanup;\n \t}\n \n \tif (refs_verify_refnames_available(&refs->base, &refnames_to_check,\n \t\t\t\t\t   &affected_refnames, NULL, 1, err)) {\n \t\tpacked_refs_unlock(refs->packed_ref_store);\n-\t\tret = TRANSACTION_NAME_CONFLICT;\n+\t\tret = REF_TRANSACTION_ERROR_NAME_CONFLICT;\n \t\tgoto cleanup;\n \t}\n \n \tif (ref_transaction_commit(packed_transaction, err)) {\n-\t\tret = TRANSACTION_GENERIC_ERROR;\n+\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n \t\tgoto cleanup;\n \t}\n \tpacked_refs_unlock(refs->packed_ref_store);\n@@ -3103,7 +3105,7 @@ static int files_transaction_finish_initial(struct files_ref_store *refs,\n \tif (loose_transaction) {\n \t\tif (ref_transaction_prepare(loose_transaction, err) ||\n \t\t    ref_transaction_commit(loose_transaction, err)) {\n-\t\t\tret = TRANSACTION_GENERIC_ERROR;\n+\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n \t\t\tgoto cleanup;\n \t\t}\n \t}\n@@ -3152,7 +3154,7 @@ static int files_transaction_finish(struct ref_store *ref_store,\n \t\tif (update->flags & REF_NEEDS_COMMIT ||\n \t\t    update->flags & REF_LOG_ONLY) {\n \t\t\tif (parse_and_write_reflog(refs, update, lock, err)) {\n-\t\t\t\tret = TRANSACTION_GENERIC_ERROR;\n+\t\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n \t\t\t\tgoto cleanup;\n \t\t\t}\n \t\t}\n@@ -3171,7 +3173,7 @@ static int files_transaction_finish(struct ref_store *ref_store,\n \t\t\t\tstrbuf_addf(err, \"couldn't set '%s'\", lock->ref_name);\n \t\t\t\tunlock_ref(lock);\n \t\t\t\tupdate->backend_data = NULL;\n-\t\t\t\tret = TRANSACTION_GENERIC_ERROR;\n+\t\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n \t\t\t\tgoto cleanup;\n \t\t\t}\n \t\t}\n@@ -3227,7 +3229,7 @@ static int files_transaction_finish(struct ref_store *ref_store,\n \t\t\t\tstrbuf_reset(&sb);\n \t\t\t\tfiles_ref_path(refs, &sb, lock->ref_name);\n \t\t\t\tif (unlink_or_msg(sb.buf, err)) {\n-\t\t\t\t\tret = TRANSACTION_GENERIC_ERROR;\n+\t\t\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n \t\t\t\t\tgoto cleanup;\n \t\t\t\t}\n \t\t\t}\ndiff --git a/refs/packed-backend.c b/refs/packed-backend.c\nindex 19220d2e99..5458952624 100644\n--- a/refs/packed-backend.c\n+++ b/refs/packed-backend.c\n@@ -1326,10 +1326,11 @@ static int packed_ref_store_remove_on_disk(struct ref_store *ref_store,\n  * The packfile must be locked before calling this function and will\n  * remain locked when it is done.\n  */\n-static int write_with_updates(struct packed_ref_store *refs,\n-\t\t\t      struct string_list *updates,\n-\t\t\t      struct strbuf *err)\n+static enum ref_transaction_error write_with_updates(struct packed_ref_store *refs,\n+\t\t\t\t\t\t     struct string_list *updates,\n+\t\t\t\t\t\t     struct strbuf *err)\n {\n+\tenum ref_transaction_error ret = REF_TRANSACTION_ERROR_GENERIC;\n \tstruct ref_iterator *iter = NULL;\n \tsize_t i;\n \tint ok;\n@@ -1353,7 +1354,7 @@ static int write_with_updates(struct packed_ref_store *refs,\n \t\tstrbuf_addf(err, \"unable to create file %s: %s\",\n \t\t\t    sb.buf, strerror(errno));\n \t\tstrbuf_release(&sb);\n-\t\treturn -1;\n+\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n \t}\n \tstrbuf_release(&sb);\n \n@@ -1409,6 +1410,7 @@ static int write_with_updates(struct packed_ref_store *refs,\n \t\t\t\t\tstrbuf_addf(err, \"cannot update ref '%s': \"\n \t\t\t\t\t\t    \"reference already exists\",\n \t\t\t\t\t\t    update->refname);\n+\t\t\t\t\tret = REF_TRANSACTION_ERROR_CREATE_EXISTS;\n \t\t\t\t\tgoto error;\n \t\t\t\t} else if (!oideq(&update->old_oid, iter->oid)) {\n \t\t\t\t\tstrbuf_addf(err, \"cannot update ref '%s': \"\n@@ -1416,6 +1418,7 @@ static int write_with_updates(struct packed_ref_store *refs,\n \t\t\t\t\t\t    update->refname,\n \t\t\t\t\t\t    oid_to_hex(iter->oid),\n \t\t\t\t\t\t    oid_to_hex(&update->old_oid));\n+\t\t\t\t\tret = REF_TRANSACTION_ERROR_INCORRECT_OLD_VALUE;\n \t\t\t\t\tgoto error;\n \t\t\t\t}\n \t\t\t}\n@@ -1452,6 +1455,7 @@ static int write_with_updates(struct packed_ref_store *refs,\n \t\t\t\t\t    \"reference is missing but expected %s\",\n \t\t\t\t\t    update->refname,\n \t\t\t\t\t    oid_to_hex(&update->old_oid));\n+\t\t\t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n \t\t\t\tgoto error;\n \t\t\t}\n \t\t}\n@@ -1509,7 +1513,7 @@ static int write_with_updates(struct packed_ref_store *refs,\n \t\t\t    strerror(errno));\n \t\tstrbuf_release(&sb);\n \t\tdelete_tempfile(&refs->tempfile);\n-\t\treturn -1;\n+\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n \t}\n \n \treturn 0;\n@@ -1521,7 +1525,7 @@ static int write_with_updates(struct packed_ref_store *refs,\n error:\n \tref_iterator_free(iter);\n \tdelete_tempfile(&refs->tempfile);\n-\treturn -1;\n+\treturn ret;\n }\n \n int is_packed_transaction_needed(struct ref_store *ref_store,\n@@ -1654,7 +1658,7 @@ static int packed_transaction_prepare(struct ref_store *ref_store,\n \t\t\tREF_STORE_READ | REF_STORE_WRITE | REF_STORE_ODB,\n \t\t\t\"ref_transaction_prepare\");\n \tstruct packed_transaction_backend_data *data;\n-\tint ret = TRANSACTION_GENERIC_ERROR;\n+\tenum ref_transaction_error ret = REF_TRANSACTION_ERROR_GENERIC;\n \n \t/*\n \t * Note that we *don't* skip transactions with zero updates,\n@@ -1675,7 +1679,8 @@ static int packed_transaction_prepare(struct ref_store *ref_store,\n \t\tdata->own_lock = 1;\n \t}\n \n-\tif (write_with_updates(refs, &transaction->refnames, err))\n+\tret = write_with_updates(refs, &transaction->refnames, err);\n+\tif (ret)\n \t\tgoto failure;\n \n \ttransaction->state = REF_TRANSACTION_PREPARED;\n@@ -1707,7 +1712,7 @@ static int packed_transaction_finish(struct ref_store *ref_store,\n \t\t\tref_store,\n \t\t\tREF_STORE_READ | REF_STORE_WRITE | REF_STORE_ODB,\n \t\t\t\"ref_transaction_finish\");\n-\tint ret = TRANSACTION_GENERIC_ERROR;\n+\tint ret = REF_TRANSACTION_ERROR_GENERIC;\n \tchar *packed_refs_path;\n \n \tclear_snapshot(refs);\ndiff --git a/refs/refs-internal.h b/refs/refs-internal.h\nindex 6d3770d0cc..3f1d19abd9 100644\n--- a/refs/refs-internal.h\n+++ b/refs/refs-internal.h\n@@ -770,8 +770,9 @@ int ref_update_has_null_new_value(struct ref_update *update);\n  * If everything is OK, return 0; otherwise, write an error message to\n  * err and return -1.\n  */\n-int ref_update_check_old_target(const char *referent, struct ref_update *update,\n-\t\t\t\tstruct strbuf *err);\n+enum ref_transaction_error ref_update_check_old_target(const char *referent,\n+\t\t\t\t\t\t       struct ref_update *update,\n+\t\t\t\t\t\t       struct strbuf *err);\n \n /*\n  * Check if the ref must exist, this means that the old_oid or\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 2c1e2995de..0132b8b06a 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -1069,20 +1069,20 @@ static int queue_transaction_update(struct reftable_ref_store *refs,\n \treturn 0;\n }\n \n-static int prepare_single_update(struct reftable_ref_store *refs,\n-\t\t\t\t struct reftable_transaction_data *tx_data,\n-\t\t\t\t struct ref_transaction *transaction,\n-\t\t\t\t struct reftable_backend *be,\n-\t\t\t\t struct ref_update *u,\n-\t\t\t\t struct string_list *refnames_to_check,\n-\t\t\t\t unsigned int head_type,\n-\t\t\t\t struct strbuf *head_referent,\n-\t\t\t\t struct strbuf *referent,\n-\t\t\t\t struct strbuf *err)\n+static enum ref_transaction_error prepare_single_update(struct reftable_ref_store *refs,\n+\t\t\t\t\t\t\tstruct reftable_transaction_data *tx_data,\n+\t\t\t\t\t\t\tstruct ref_transaction *transaction,\n+\t\t\t\t\t\t\tstruct reftable_backend *be,\n+\t\t\t\t\t\t\tstruct ref_update *u,\n+\t\t\t\t\t\t\tstruct string_list *refnames_to_check,\n+\t\t\t\t\t\t\tunsigned int head_type,\n+\t\t\t\t\t\t\tstruct strbuf *head_referent,\n+\t\t\t\t\t\t\tstruct strbuf *referent,\n+\t\t\t\t\t\t\tstruct strbuf *err)\n {\n+\tenum ref_transaction_error ret = 0;\n \tstruct object_id current_oid = {0};\n \tconst char *rewritten_ref;\n-\tint ret = 0;\n \n \t/*\n \t * There is no need to reload the respective backends here as\n@@ -1093,7 +1093,7 @@ static int prepare_single_update(struct reftable_ref_store *refs,\n \t */\n \tret = backend_for(&be, refs, u->refname, &rewritten_ref, 0);\n \tif (ret)\n-\t\treturn ret;\n+\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n \n \t/* Verify that the new object ID is valid. */\n \tif ((u->flags & REF_HAVE_NEW) && !is_null_oid(&u->new_oid) &&\n@@ -1104,13 +1104,13 @@ static int prepare_single_update(struct reftable_ref_store *refs,\n \t\t\tstrbuf_addf(err,\n \t\t\t\t    _(\"trying to write ref '%s' with nonexistent object %s\"),\n \t\t\t\t    u->refname, oid_to_hex(&u->new_oid));\n-\t\t\treturn -1;\n+\t\t\treturn REF_TRANSACTION_ERROR_INVALID_NEW_VALUE;\n \t\t}\n \n \t\tif (o->type != OBJ_COMMIT && is_branch(u->refname)) {\n \t\t\tstrbuf_addf(err, _(\"trying to write non-commit object %s to branch '%s'\"),\n \t\t\t\t    oid_to_hex(&u->new_oid), u->refname);\n-\t\t\treturn -1;\n+\t\t\treturn REF_TRANSACTION_ERROR_INVALID_NEW_VALUE;\n \t\t}\n \t}\n \n@@ -1134,7 +1134,7 @@ static int prepare_single_update(struct reftable_ref_store *refs,\n \t\t\t\t    _(\"multiple updates for 'HEAD' (including one \"\n \t\t\t\t      \"via its referent '%s') are not allowed\"),\n \t\t\t\t    u->refname);\n-\t\t\treturn TRANSACTION_NAME_CONFLICT;\n+\t\t\treturn REF_TRANSACTION_ERROR_NAME_CONFLICT;\n \t\t}\n \n \t\tref_transaction_add_update(\n@@ -1147,7 +1147,7 @@ static int prepare_single_update(struct reftable_ref_store *refs,\n \tret = reftable_backend_read_ref(be, rewritten_ref,\n \t\t\t\t\t&current_oid, referent, &u->type);\n \tif (ret < 0)\n-\t\treturn ret;\n+\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n \tif (ret > 0 && !ref_update_expects_existing_old_ref(u)) {\n \t\t/*\n \t\t * The reference does not exist, and we either have no\n@@ -1168,7 +1168,7 @@ static int prepare_single_update(struct reftable_ref_store *refs,\n \t\t\tret = queue_transaction_update(refs, tx_data, u,\n \t\t\t\t\t\t       &current_oid, err);\n \t\t\tif (ret)\n-\t\t\t\treturn ret;\n+\t\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n \t\t}\n \n \t\treturn 0;\n@@ -1180,7 +1180,7 @@ static int prepare_single_update(struct reftable_ref_store *refs,\n \n \t\t\t\t   \"unable to resolve reference '%s'\"),\n \t\t\t    ref_update_original_update_refname(u), u->refname);\n-\t\treturn -1;\n+\t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n \t}\n \n \tif (u->type & REF_ISSYMREF) {\n@@ -1196,7 +1196,7 @@ static int prepare_single_update(struct reftable_ref_store *refs,\n \t\t\tif (u->flags & REF_HAVE_OLD && !resolved) {\n \t\t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n \t\t\t\t\t\t   \"error reading reference\"), u->refname);\n-\t\t\t\treturn -1;\n+\t\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n \t\t\t}\n \t\t} else {\n \t\t\tstruct ref_update *new_update;\n@@ -1211,7 +1211,7 @@ static int prepare_single_update(struct reftable_ref_store *refs,\n \t\t\t\t\t    _(\"multiple updates for '%s' (including one \"\n \t\t\t\t\t      \"via symref '%s') are not allowed\"),\n \t\t\t\t\t    referent->buf, u->refname);\n-\t\t\t\treturn TRANSACTION_NAME_CONFLICT;\n+\t\t\t\treturn REF_TRANSACTION_ERROR_NAME_CONFLICT;\n \t\t\t}\n \n \t\t\t/*\n@@ -1255,31 +1255,32 @@ static int prepare_single_update(struct reftable_ref_store *refs,\n \t\t\t\t\t   \"but is a regular ref\"),\n \t\t\t\t    ref_update_original_update_refname(u),\n \t\t\t\t    u->old_target);\n-\t\t\treturn -1;\n+\t\t\treturn REF_TRANSACTION_ERROR_EXPECTED_SYMREF;\n \t\t}\n \n-\t\tif (ref_update_check_old_target(referent->buf, u, err)) {\n-\t\t\treturn -1;\n-\t\t}\n+\t\tret = ref_update_check_old_target(referent->buf, u, err);\n+\t\tif (ret)\n+\t\t\treturn ret;\n \t} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {\n \t\tif (is_null_oid(&u->old_oid)) {\n \t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n \t\t\t\t\t   \"reference already exists\"),\n \t\t\t\t    ref_update_original_update_refname(u));\n-\t\t\treturn TRANSACTION_CREATE_EXISTS;\n-\t\t}\n-\t\telse if (is_null_oid(&current_oid))\n+\t\t\treturn REF_TRANSACTION_ERROR_CREATE_EXISTS;\n+\t\t} else if (is_null_oid(&current_oid)) {\n \t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n \t\t\t\t\t   \"reference is missing but expected %s\"),\n \t\t\t\t    ref_update_original_update_refname(u),\n \t\t\t\t    oid_to_hex(&u->old_oid));\n-\t\telse\n+\t\t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n+\t\t} else {\n \t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n \t\t\t\t\t   \"is at %s but expected %s\"),\n \t\t\t\t    ref_update_original_update_refname(u),\n \t\t\t\t    oid_to_hex(&current_oid),\n \t\t\t\t    oid_to_hex(&u->old_oid));\n-\t\treturn TRANSACTION_NAME_CONFLICT;\n+\t\t\treturn REF_TRANSACTION_ERROR_INCORRECT_OLD_VALUE;\n+\t\t}\n \t}\n \n \t/*\n@@ -1296,8 +1297,8 @@ static int prepare_single_update(struct reftable_ref_store *refs,\n \tif ((u->type & REF_ISSYMREF) ||\n \t    (u->flags & REF_LOG_ONLY) ||\n \t    (u->flags & REF_HAVE_NEW && !oideq(&current_oid, &u->new_oid)))\n-\t\treturn queue_transaction_update(refs, tx_data, u,\n-\t\t\t\t\t       &current_oid, err);\n+\t\tif (queue_transaction_update(refs, tx_data, u, &current_oid, err))\n+\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n \n \treturn 0;\n }\n@@ -1386,7 +1387,6 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n \ttransaction->state = REF_TRANSACTION_PREPARED;\n \n done:\n-\tassert(ret != REFTABLE_API_ERROR);\n \tif (ret < 0) {\n \t\tfree_transaction_data(tx_data);\n \t\ttransaction->state = REF_TRANSACTION_CLOSED;\n\n-- \n2.48.1\n\n"},{"id":"513611","messageId":"20250305-245-partially-atomic-ref-updates-v3-6-0c64e3052354@gmail.com","threadId":"63064","inReplyTo":"20250305-245-partially-atomic-ref-updates-v3-0-0c64e3052354@gmail.com","subject":"[PATCH v3 6/8] refs: implement partial reference transaction support","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-03-05T17:39:01Z","receivedAt":"2025-03-05T17:39:21Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Git's reference transactions are all-or-nothing: either all updates\nsucceed, or none do. While this atomic behavior is generally desirable,\nit can be suboptimal especially when using the reftable backend, where\nbatching multiple reference updates into a single transaction is more\nefficient than performing them sequentially.\n\nIntroduce partial transaction support with a new flag,\n'REF_TRANSACTION_ALLOW_PARTIAL'. When enabled, this flag allows\nindividual reference updates that would typically cause the entire\ntransaction to fail due to non-system-related errors to be marked as\nrejected while permitting other updates to proceed. System errors\nreferred by 'REF_TRANSACTION_ERROR_GENERIC' continue to result in the\nentire transaction failing. This approach enhances flexibility while\npreserving transactional integrity where necessary.\n\nThe implementation introduces several key components:\n\n  - Add 'rejection_err' field to struct `ref_update` to track failed\n    updates with failure reason.\n\n  - Add a new struct `ref_transaction_rejections` and a field within\n    `ref_transaction` to this struct to allow quick iteration over\n    rejected updates.\n\n  - Modify reference backends (files, packed, reftable) to handle\n    partial transactions by using `ref_transaction_set_rejected()`\n    instead of failing the entire transaction when\n    `REF_TRANSACTION_ALLOW_PARTIAL` is set.\n\n  - Add `ref_transaction_for_each_rejected_update()` to let callers\n    examine which updates were rejected and why.\n\nThis foundational change enables partial transaction support throughout\nthe reference subsystem. A following commit will expose this capability\nto users by adding a `--allow-partial` flag to 'git-update-ref(1)',\nproviding both a user-facing feature and a testable implementation.\n\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n refs.c                  | 61 +++++++++++++++++++++++++++++++++++++++++++++++++\n refs.h                  | 22 ++++++++++++++++++\n refs/files-backend.c    | 12 +++++++++-\n refs/packed-backend.c   | 27 ++++++++++++++++++++--\n refs/refs-internal.h    | 25 ++++++++++++++++++++\n refs/reftable-backend.c | 12 +++++++++-\n 6 files changed, 155 insertions(+), 4 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex 63b8050ce2..b735510c3b 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1176,6 +1176,10 @@ struct ref_transaction *ref_store_transaction_begin(struct ref_store *refs,\n \ttr->ref_store = refs;\n \ttr->flags = flags;\n \tstring_list_init_dup(&tr->refnames);\n+\n+\tif (flags & REF_TRANSACTION_ALLOW_PARTIAL)\n+\t\tCALLOC_ARRAY(tr->rejections, 1);\n+\n \treturn tr;\n }\n \n@@ -1206,11 +1210,45 @@ void ref_transaction_free(struct ref_transaction *transaction)\n \t\tfree((char *)transaction->updates[i]->old_target);\n \t\tfree(transaction->updates[i]);\n \t}\n+\n+\tif (transaction->rejections)\n+\t\tfree(transaction->rejections->update_indices);\n+\tfree(transaction->rejections);\n+\n \tstring_list_clear(&transaction->refnames, 0);\n \tfree(transaction->updates);\n \tfree(transaction);\n }\n \n+int ref_transaction_maybe_set_rejected(struct ref_transaction *transaction,\n+\t\t\t\t       size_t update_idx,\n+\t\t\t\t       enum ref_transaction_error err)\n+{\n+\tif (update_idx >= transaction->nr)\n+\t\tBUG(\"trying to set rejection on invalid update index\");\n+\n+\tif (!(transaction->flags & REF_TRANSACTION_ALLOW_PARTIAL))\n+\t\treturn 0;\n+\n+\tif (!transaction->rejections)\n+\t\tBUG(\"transaction not inititalized with partial support\");\n+\n+\t/*\n+\t * Don't accept generic errors, since these errors are not user\n+\t * input related.\n+\t */\n+\tif (err == REF_TRANSACTION_ERROR_GENERIC)\n+\t\treturn 0;\n+\n+\ttransaction->updates[update_idx]->rejection_err = err;\n+\tALLOC_GROW(transaction->rejections->update_indices,\n+\t\t   transaction->rejections->nr + 1,\n+\t\t   transaction->rejections->alloc);\n+\ttransaction->rejections->update_indices[transaction->rejections->nr++] = update_idx;\n+\n+\treturn 1;\n+}\n+\n struct ref_update *ref_transaction_add_update(\n \t\tstruct ref_transaction *transaction,\n \t\tconst char *refname, unsigned int flags,\n@@ -1236,6 +1274,7 @@ struct ref_update *ref_transaction_add_update(\n \ttransaction->updates[transaction->nr++] = update;\n \n \tupdate->flags = flags;\n+\tupdate->rejection_err = 0;\n \n \tupdate->new_target = xstrdup_or_null(new_target);\n \tupdate->old_target = xstrdup_or_null(old_target);\n@@ -2727,6 +2766,28 @@ void ref_transaction_for_each_queued_update(struct ref_transaction *transaction,\n \t}\n }\n \n+void ref_transaction_for_each_rejected_update(struct ref_transaction *transaction,\n+\t\t\t\t\t      ref_transaction_for_each_rejected_update_fn cb,\n+\t\t\t\t\t      void *cb_data)\n+{\n+\tif (!transaction->rejections)\n+\t\treturn;\n+\n+\tfor (size_t i = 0; i < transaction->rejections->nr; i++) {\n+\t\tsize_t update_index = transaction->rejections->update_indices[i];\n+\t\tstruct ref_update *update = transaction->updates[update_index];\n+\n+\t\tif (!update->rejection_err)\n+\t\t\tcontinue;\n+\n+\t\tcb(update->refname,\n+\t\t   (update->flags & REF_HAVE_OLD) ? &update->old_oid : NULL,\n+\t\t   (update->flags & REF_HAVE_NEW) ? &update->new_oid : NULL,\n+\t\t   update->old_target, update->new_target,\n+\t\t   update->rejection_err, cb_data);\n+\t}\n+}\n+\n int refs_delete_refs(struct ref_store *refs, const char *logmsg,\n \t\t     struct string_list *refnames, unsigned int flags)\n {\ndiff --git a/refs.h b/refs.h\nindex 1b9213f9ce..5e5ff9e57d 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -673,6 +673,13 @@ enum ref_transaction_flag {\n \t * either be absent or null_oid.\n \t */\n \tREF_TRANSACTION_FLAG_INITIAL = (1 << 0),\n+\n+\t/*\n+\t * The transaction mechanism by default fails all updates if any conflict\n+\t * is detected. This flag allows transactions to partially apply updates\n+\t * while rejecting updates which do not match the expected state.\n+\t */\n+\tREF_TRANSACTION_ALLOW_PARTIAL = (1 << 1),\n };\n \n /*\n@@ -903,6 +910,21 @@ void ref_transaction_for_each_queued_update(struct ref_transaction *transaction,\n \t\t\t\t\t    ref_transaction_for_each_queued_update_fn cb,\n \t\t\t\t\t    void *cb_data);\n \n+/*\n+ * Execute the given callback function for each of the reference updates which\n+ * have been rejected in the given transaction.\n+ */\n+typedef void ref_transaction_for_each_rejected_update_fn(const char *refname,\n+\t\t\t\t\t\t\t const struct object_id *old_oid,\n+\t\t\t\t\t\t\t const struct object_id *new_oid,\n+\t\t\t\t\t\t\t const char *old_target,\n+\t\t\t\t\t\t\t const char *new_target,\n+\t\t\t\t\t\t\t enum ref_transaction_error err,\n+\t\t\t\t\t\t\t void *cb_data);\n+void ref_transaction_for_each_rejected_update(struct ref_transaction *transaction,\n+\t\t\t\t\t      ref_transaction_for_each_rejected_update_fn cb,\n+\t\t\t\t\t      void *cb_data);\n+\n /*\n  * Free `*transaction` and all associated data.\n  */\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 1e1663f44b..c2fdee6013 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2852,8 +2852,15 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n \t\tret = lock_ref_for_update(refs, update, transaction,\n \t\t\t\t\t  head_ref, &refnames_to_check,\n \t\t\t\t\t  err);\n-\t\tif (ret)\n+\t\tif (ret) {\n+\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n+\t\t\t\tstrbuf_setlen(err, 0);\n+\t\t\t\tret = 0;\n+\n+\t\t\t\tcontinue;\n+\t\t\t}\n \t\t\tgoto cleanup;\n+\t\t}\n \n \t\tif (update->flags & REF_DELETING &&\n \t\t    !(update->flags & REF_LOG_ONLY) &&\n@@ -3151,6 +3158,9 @@ static int files_transaction_finish(struct ref_store *ref_store,\n \t\tstruct ref_update *update = transaction->updates[i];\n \t\tstruct ref_lock *lock = update->backend_data;\n \n+\t\tif (update->rejection_err)\n+\t\t\tcontinue;\n+\n \t\tif (update->flags & REF_NEEDS_COMMIT ||\n \t\t    update->flags & REF_LOG_ONLY) {\n \t\t\tif (parse_and_write_reflog(refs, update, lock, err)) {\ndiff --git a/refs/packed-backend.c b/refs/packed-backend.c\nindex 5458952624..bfc6135743 100644\n--- a/refs/packed-backend.c\n+++ b/refs/packed-backend.c\n@@ -1327,10 +1327,11 @@ static int packed_ref_store_remove_on_disk(struct ref_store *ref_store,\n  * remain locked when it is done.\n  */\n static enum ref_transaction_error write_with_updates(struct packed_ref_store *refs,\n-\t\t\t\t\t\t     struct string_list *updates,\n+\t\t\t\t\t\t     struct ref_transaction *transaction,\n \t\t\t\t\t\t     struct strbuf *err)\n {\n \tenum ref_transaction_error ret = REF_TRANSACTION_ERROR_GENERIC;\n+\tstruct string_list *updates = &transaction->refnames;\n \tstruct ref_iterator *iter = NULL;\n \tsize_t i;\n \tint ok;\n@@ -1411,6 +1412,13 @@ static enum ref_transaction_error write_with_updates(struct packed_ref_store *re\n \t\t\t\t\t\t    \"reference already exists\",\n \t\t\t\t\t\t    update->refname);\n \t\t\t\t\tret = REF_TRANSACTION_ERROR_CREATE_EXISTS;\n+\n+\t\t\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n+\t\t\t\t\t\tstrbuf_setlen(err, 0);\n+\t\t\t\t\t\tret = 0;\n+\t\t\t\t\t\tcontinue;\n+\t\t\t\t\t}\n+\n \t\t\t\t\tgoto error;\n \t\t\t\t} else if (!oideq(&update->old_oid, iter->oid)) {\n \t\t\t\t\tstrbuf_addf(err, \"cannot update ref '%s': \"\n@@ -1419,6 +1427,13 @@ static enum ref_transaction_error write_with_updates(struct packed_ref_store *re\n \t\t\t\t\t\t    oid_to_hex(iter->oid),\n \t\t\t\t\t\t    oid_to_hex(&update->old_oid));\n \t\t\t\t\tret = REF_TRANSACTION_ERROR_INCORRECT_OLD_VALUE;\n+\n+\t\t\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n+\t\t\t\t\t\tstrbuf_setlen(err, 0);\n+\t\t\t\t\t\tret = 0;\n+\t\t\t\t\t\tcontinue;\n+\t\t\t\t\t}\n+\n \t\t\t\t\tgoto error;\n \t\t\t\t}\n \t\t\t}\n@@ -1456,6 +1471,13 @@ static enum ref_transaction_error write_with_updates(struct packed_ref_store *re\n \t\t\t\t\t    update->refname,\n \t\t\t\t\t    oid_to_hex(&update->old_oid));\n \t\t\t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n+\n+\t\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n+\t\t\t\t\tstrbuf_setlen(err, 0);\n+\t\t\t\t\tret = 0;\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n+\n \t\t\t\tgoto error;\n \t\t\t}\n \t\t}\n@@ -1521,6 +1543,7 @@ static enum ref_transaction_error write_with_updates(struct packed_ref_store *re\n write_error:\n \tstrbuf_addf(err, \"error writing to %s: %s\",\n \t\t    get_tempfile_path(refs->tempfile), strerror(errno));\n+\tret = REF_TRANSACTION_ERROR_GENERIC;\n \n error:\n \tref_iterator_free(iter);\n@@ -1679,7 +1702,7 @@ static int packed_transaction_prepare(struct ref_store *ref_store,\n \t\tdata->own_lock = 1;\n \t}\n \n-\tret = write_with_updates(refs, &transaction->refnames, err);\n+\tret = write_with_updates(refs, transaction, err);\n \tif (ret)\n \t\tgoto failure;\n \ndiff --git a/refs/refs-internal.h b/refs/refs-internal.h\nindex 3f1d19abd9..c417aec217 100644\n--- a/refs/refs-internal.h\n+++ b/refs/refs-internal.h\n@@ -123,6 +123,11 @@ struct ref_update {\n \t */\n \tuint64_t index;\n \n+\t/*\n+\t * Used in partial transactions to mark if a given update was rejected.\n+\t */\n+\tenum ref_transaction_error rejection_err;\n+\n \t/*\n \t * If this ref_update was split off of a symref update via\n \t * split_symref_update(), then this member points at that\n@@ -142,6 +147,13 @@ int refs_read_raw_ref(struct ref_store *ref_store, const char *refname,\n \t\t      struct object_id *oid, struct strbuf *referent,\n \t\t      unsigned int *type, int *failure_errno);\n \n+/*\n+ * Mark a given update as rejected with a given reason.\n+ */\n+int ref_transaction_maybe_set_rejected(struct ref_transaction *transaction,\n+\t\t\t\t       size_t update_idx,\n+\t\t\t\t       enum ref_transaction_error err);\n+\n /*\n  * Add a ref_update with the specified properties to transaction, and\n  * return a pointer to the new object. This function does not verify\n@@ -183,6 +195,18 @@ enum ref_transaction_state {\n \tREF_TRANSACTION_CLOSED   = 2\n };\n \n+/*\n+ * Data structure to hold indices of updates which were rejected, when\n+ * partial transactions where enabled. While the updates themselves hold\n+ * the rejection error, this structure allows a transaction to iterate\n+ * only over the rejected updates.\n+ */\n+struct ref_transaction_rejections {\n+\tsize_t *update_indices;\n+\tsize_t alloc;\n+\tsize_t nr;\n+};\n+\n /*\n  * Data structure for holding a reference transaction, which can\n  * consist of checks and updates to multiple references, carried out\n@@ -195,6 +219,7 @@ struct ref_transaction {\n \tsize_t alloc;\n \tsize_t nr;\n \tenum ref_transaction_state state;\n+\tstruct ref_transaction_rejections *rejections;\n \tvoid *backend_data;\n \tunsigned int flags;\n \tuint64_t max_index;\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 0132b8b06a..dd9912d637 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -1371,8 +1371,15 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n \t\t\t\t\t    transaction->updates[i],\n \t\t\t\t\t    &refnames_to_check, head_type,\n \t\t\t\t\t    &head_referent, &referent, err);\n-\t\tif (ret)\n+\t\tif (ret) {\n+\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n+\t\t\t\tstrbuf_setlen(err, 0);\n+\t\t\t\tret = 0;\n+\n+\t\t\t\tcontinue;\n+\t\t\t}\n \t\t\tgoto done;\n+\t\t}\n \t}\n \n \tstring_list_sort(&refnames_to_check);\n@@ -1455,6 +1462,9 @@ static int write_transaction_table(struct reftable_writer *writer, void *cb_data\n \t\tstruct reftable_transaction_update *tx_update = &arg->updates[i];\n \t\tstruct ref_update *u = tx_update->update;\n \n+\t\tif (u->rejection_err)\n+\t\t\tcontinue;\n+\n \t\t/*\n \t\t * Write a reflog entry when updating a ref to point to\n \t\t * something new in either of the following cases:\n\n-- \n2.48.1\n\n"},{"id":"513613","messageId":"20250305-245-partially-atomic-ref-updates-v3-7-0c64e3052354@gmail.com","threadId":"63064","inReplyTo":"20250305-245-partially-atomic-ref-updates-v3-0-0c64e3052354@gmail.com","subject":"[PATCH v3 7/8] refs: support partial update rejections during F/D checks","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-03-05T17:39:02Z","receivedAt":"2025-03-05T17:39:21Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"The `refs_verify_refnames_available()` is used to batch check refnames\nfor F/D conflicts. While this is the more performant alternative than\nits individual version, it does not provide rejection capabilities on a\nsingle update level. For partial transactions, this would mean a\nrejection of the entire transaction whenever one reference has a F/D\nconflict.\n\nModify the function to call `ref_transaction_maybe_set_rejected()` to\ncheck if a single update can be rejected. Since this function is only\ninternally used within 'refs/' and we want to pass in a `struct\nref_transaction *` as a variable. We also move and mark\n`refs_verify_refnames_available()` to 'refs-internal.h' to be an\ninternal function.\n\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n refs.c                  | 28 +++++++++++++++++++++++++++-\n refs.h                  | 12 ------------\n refs/files-backend.c    | 27 ++++++++++++++++++---------\n refs/refs-internal.h    | 17 +++++++++++++++++\n refs/reftable-backend.c | 11 ++++++++---\n 5 files changed, 70 insertions(+), 25 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex b735510c3b..c4dccf9d8b 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2540,6 +2540,7 @@ enum ref_transaction_error refs_verify_refnames_available(struct ref_store *refs\n \t\t\t\t\t  const struct string_list *refnames,\n \t\t\t\t\t  const struct string_list *extras,\n \t\t\t\t\t  const struct string_list *skip,\n+\t\t\t\t\t  struct ref_transaction *transaction,\n \t\t\t\t\t  unsigned int initial_transaction,\n \t\t\t\t\t  struct strbuf *err)\n {\n@@ -2559,6 +2560,7 @@ enum ref_transaction_error refs_verify_refnames_available(struct ref_store *refs\n \tstrset_init(&dirnames);\n \n \tfor (size_t i = 0; i < refnames->nr; i++) {\n+\t\tconst size_t *update_idx = (size_t *)refnames->items[i].util;\n \t\tconst char *refname = refnames->items[i].string;\n \t\tconst char *extra_refname;\n \t\tstruct object_id oid;\n@@ -2598,12 +2600,26 @@ enum ref_transaction_error refs_verify_refnames_available(struct ref_store *refs\n \t\t\tif (!initial_transaction &&\n \t\t\t    !refs_read_raw_ref(refs, dirname.buf, &oid, &referent,\n \t\t\t\t\t       &type, &ignore_errno)) {\n+\t\t\t\tif (transaction && ref_transaction_maybe_set_rejected(\n+\t\t\t\t\t    transaction, *update_idx,\n+\t\t\t\t\t    REF_TRANSACTION_ERROR_NAME_CONFLICT)) {\n+\t\t\t\t\tstrset_remove(&dirnames, dirname.buf);\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n+\n \t\t\t\tstrbuf_addf(err, _(\"'%s' exists; cannot create '%s'\"),\n \t\t\t\t\t    dirname.buf, refname);\n \t\t\t\tgoto cleanup;\n \t\t\t}\n \n \t\t\tif (extras && string_list_has_string(extras, dirname.buf)) {\n+\t\t\t\tif (transaction && ref_transaction_maybe_set_rejected(\n+\t\t\t\t\t    transaction, *update_idx,\n+\t\t\t\t\t    REF_TRANSACTION_ERROR_NAME_CONFLICT)) {\n+\t\t\t\t\tstrset_remove(&dirnames, dirname.buf);\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n+\n \t\t\t\tstrbuf_addf(err, _(\"cannot process '%s' and '%s' at the same time\"),\n \t\t\t\t\t    refname, dirname.buf);\n \t\t\t\tgoto cleanup;\n@@ -2636,6 +2652,11 @@ enum ref_transaction_error refs_verify_refnames_available(struct ref_store *refs\n \t\t\t\t    string_list_has_string(skip, iter->refname))\n \t\t\t\t\tcontinue;\n \n+\t\t\t\tif (transaction && ref_transaction_maybe_set_rejected(\n+\t\t\t\t\t    transaction, *update_idx,\n+\t\t\t\t\t    REF_TRANSACTION_ERROR_NAME_CONFLICT))\n+\t\t\t\t\tcontinue;\n+\n \t\t\t\tstrbuf_addf(err, _(\"'%s' exists; cannot create '%s'\"),\n \t\t\t\t\t    iter->refname, refname);\n \t\t\t\tgoto cleanup;\n@@ -2647,6 +2668,11 @@ enum ref_transaction_error refs_verify_refnames_available(struct ref_store *refs\n \n \t\textra_refname = find_descendant_ref(dirname.buf, extras, skip);\n \t\tif (extra_refname) {\n+\t\t\tif (transaction && ref_transaction_maybe_set_rejected(\n+\t\t\t\t    transaction, *update_idx,\n+\t\t\t\t    REF_TRANSACTION_ERROR_NAME_CONFLICT))\n+\t\t\t\tcontinue;\n+\n \t\t\tstrbuf_addf(err, _(\"cannot process '%s' and '%s' at the same time\"),\n \t\t\t\t    refname, extra_refname);\n \t\t\tgoto cleanup;\n@@ -2678,7 +2704,7 @@ enum ref_transaction_error refs_verify_refname_available(\n \t};\n \n \treturn refs_verify_refnames_available(refs, &refnames, extras, skip,\n-\t\t\t\t\t      initial_transaction, err);\n+\t\t\t\t\t      NULL, initial_transaction, err);\n }\n \n struct do_for_each_reflog_help {\ndiff --git a/refs.h b/refs.h\nindex 5e5ff9e57d..938420bec4 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -147,18 +147,6 @@ enum ref_transaction_error refs_verify_refname_available(struct ref_store *refs,\n \t\t\t\t\t\t unsigned int initial_transaction,\n \t\t\t\t\t\t struct strbuf *err);\n \n-/*\n- * Same as `refs_verify_refname_available()`, but checking for a list of\n- * refnames instead of only a single item. This is more efficient in the case\n- * where one needs to check multiple refnames.\n- */\n-enum ref_transaction_error refs_verify_refnames_available(struct ref_store *refs,\n-\t\t\t\t\t  const struct string_list *refnames,\n-\t\t\t\t\t  const struct string_list *extras,\n-\t\t\t\t\t  const struct string_list *skip,\n-\t\t\t\t\t  unsigned int initial_transaction,\n-\t\t\t\t\t  struct strbuf *err);\n-\n int refs_ref_exists(struct ref_store *refs, const char *refname);\n \n int should_autocreate_reflog(enum log_refs_config log_all_ref_updates,\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex c2fdee6013..7525bf75ab 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -677,16 +677,18 @@ static void unlock_ref(struct ref_lock *lock)\n  * - Generate informative error messages in the case of failure\n  */\n static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs,\n-\t\t\t\t\t       const char *refname,\n+\t\t\t\t\t       struct ref_update *update,\n+\t\t\t\t\t       size_t update_idx,\n \t\t\t\t\t       int mustexist,\n \t\t\t\t\t       struct string_list *refnames_to_check,\n \t\t\t\t\t       const struct string_list *extras,\n \t\t\t\t\t       struct ref_lock **lock_p,\n \t\t\t\t\t       struct strbuf *referent,\n-\t\t\t\t\t       unsigned int *type,\n \t\t\t\t\t       struct strbuf *err)\n {\n \tenum ref_transaction_error ret = REF_TRANSACTION_ERROR_GENERIC;\n+\tconst char *refname = update->refname;\n+\tunsigned int *type = &update->type;\n \tstruct ref_lock *lock;\n \tstruct strbuf ref_file = STRBUF_INIT;\n \tint attempts_remaining = 3;\n@@ -785,6 +787,8 @@ static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs,\n \n \tif (files_read_raw_ref(&refs->base, refname, &lock->old_oid, referent,\n \t\t\t       type, &failure_errno)) {\n+\t\tstruct string_list_item *item;\n+\n \t\tif (failure_errno == ENOENT) {\n \t\t\tif (mustexist) {\n \t\t\t\t/* Garden variety missing reference. */\n@@ -864,7 +868,9 @@ static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs,\n \t\t * make sure there is no existing packed ref that conflicts\n \t\t * with refname. This check is deferred so that we can batch it.\n \t\t */\n-\t\tstring_list_insert(refnames_to_check, refname);\n+\t\titem = string_list_insert(refnames_to_check, refname);\n+\t\titem->util = xmalloc(sizeof(update_idx));\n+\t\tmemcpy(item->util, &update_idx, sizeof(update_idx));\n \t}\n \n \tret = 0;\n@@ -2547,6 +2553,7 @@ struct files_transaction_backend_data {\n  */\n static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *refs,\n \t\t\t\t\t\t      struct ref_update *update,\n+\t\t\t\t\t\t      size_t update_idx,\n \t\t\t\t\t\t      struct ref_transaction *transaction,\n \t\t\t\t\t\t      const char *head_ref,\n \t\t\t\t\t\t      struct string_list *refnames_to_check,\n@@ -2575,9 +2582,9 @@ static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *re\n \tif (lock) {\n \t\tlock->count++;\n \t} else {\n-\t\tret = lock_raw_ref(refs, update->refname, mustexist,\n+\t\tret = lock_raw_ref(refs, update, update_idx, mustexist,\n \t\t\t\t   refnames_to_check, &transaction->refnames,\n-\t\t\t\t   &lock, &referent, &update->type, err);\n+\t\t\t\t   &lock, &referent, err);\n \t\tif (ret) {\n \t\t\tchar *reason;\n \n@@ -2849,7 +2856,7 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n \tfor (i = 0; i < transaction->nr; i++) {\n \t\tstruct ref_update *update = transaction->updates[i];\n \n-\t\tret = lock_ref_for_update(refs, update, transaction,\n+\t\tret = lock_ref_for_update(refs, update, i, transaction,\n \t\t\t\t\t  head_ref, &refnames_to_check,\n \t\t\t\t\t  err);\n \t\tif (ret) {\n@@ -2905,7 +2912,8 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n \t * So instead, we accept the race for now.\n \t */\n \tif (refs_verify_refnames_available(refs->packed_ref_store, &refnames_to_check,\n-\t\t\t\t\t   &transaction->refnames, NULL, 0, err)) {\n+\t\t\t\t\t   &transaction->refnames, NULL, transaction,\n+\t\t\t\t\t   0, err)) {\n \t\tret = REF_TRANSACTION_ERROR_NAME_CONFLICT;\n \t\tgoto cleanup;\n \t}\n@@ -2951,7 +2959,7 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n \n cleanup:\n \tfree(head_ref);\n-\tstring_list_clear(&refnames_to_check, 0);\n+\tstring_list_clear(&refnames_to_check, 1);\n \n \tif (ret)\n \t\tfiles_transaction_cleanup(refs, transaction);\n@@ -3097,7 +3105,8 @@ static int files_transaction_finish_initial(struct files_ref_store *refs,\n \t}\n \n \tif (refs_verify_refnames_available(&refs->base, &refnames_to_check,\n-\t\t\t\t\t   &affected_refnames, NULL, 1, err)) {\n+\t\t\t\t\t   &affected_refnames, NULL, transaction,\n+\t\t\t\t\t   1, err)) {\n \t\tpacked_refs_unlock(refs->packed_ref_store);\n \t\tret = REF_TRANSACTION_ERROR_NAME_CONFLICT;\n \t\tgoto cleanup;\ndiff --git a/refs/refs-internal.h b/refs/refs-internal.h\nindex c417aec217..f0e958dc83 100644\n--- a/refs/refs-internal.h\n+++ b/refs/refs-internal.h\n@@ -805,4 +805,21 @@ enum ref_transaction_error ref_update_check_old_target(const char *referent,\n  */\n int ref_update_expects_existing_old_ref(struct ref_update *update);\n \n+/*\n+ * Same as `refs_verify_refname_available()`, but checking for a list of\n+ * refnames instead of only a single item. This is more efficient in the case\n+ * where one needs to check multiple refnames.\n+ *\n+ * If a transaction is provided with partial support, then individual updates\n+ * are marked rejected, reference backends are then in charge of not committing\n+ * those updates.\n+ */\n+enum ref_transaction_error refs_verify_refnames_available(struct ref_store *refs,\n+\t\t\t\t\t  const struct string_list *refnames,\n+\t\t\t\t\t  const struct string_list *extras,\n+\t\t\t\t\t  const struct string_list *skip,\n+\t\t\t\t\t  struct ref_transaction *transaction,\n+\t\t\t\t\t  unsigned int initial_transaction,\n+\t\t\t\t\t  struct strbuf *err);\n+\n #endif /* REFS_REFS_INTERNAL_H */\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex dd9912d637..a50e004d96 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -1074,6 +1074,7 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \t\t\t\t\t\t\tstruct ref_transaction *transaction,\n \t\t\t\t\t\t\tstruct reftable_backend *be,\n \t\t\t\t\t\t\tstruct ref_update *u,\n+\t\t\t\t\t\t\tsize_t update_idx,\n \t\t\t\t\t\t\tstruct string_list *refnames_to_check,\n \t\t\t\t\t\t\tunsigned int head_type,\n \t\t\t\t\t\t\tstruct strbuf *head_referent,\n@@ -1149,6 +1150,7 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \tif (ret < 0)\n \t\treturn REF_TRANSACTION_ERROR_GENERIC;\n \tif (ret > 0 && !ref_update_expects_existing_old_ref(u)) {\n+\t\tstruct string_list_item *item;\n \t\t/*\n \t\t * The reference does not exist, and we either have no\n \t\t * old object ID or expect the reference to not exist.\n@@ -1158,7 +1160,9 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \t\t * can output a proper error message instead of failing\n \t\t * at a later point.\n \t\t */\n-\t\tstring_list_append(refnames_to_check, u->refname);\n+\t\titem = string_list_append(refnames_to_check, u->refname);\n+\t\titem->util = xmalloc(sizeof(update_idx));\n+\t\tmemcpy(item->util, &update_idx, sizeof(update_idx));\n \n \t\t/*\n \t\t * There is no need to write the reference deletion\n@@ -1368,7 +1372,7 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n \n \tfor (i = 0; i < transaction->nr; i++) {\n \t\tret = prepare_single_update(refs, tx_data, transaction, be,\n-\t\t\t\t\t    transaction->updates[i],\n+\t\t\t\t\t    transaction->updates[i], i,\n \t\t\t\t\t    &refnames_to_check, head_type,\n \t\t\t\t\t    &head_referent, &referent, err);\n \t\tif (ret) {\n@@ -1385,6 +1389,7 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n \tstring_list_sort(&refnames_to_check);\n \tret = refs_verify_refnames_available(ref_store, &refnames_to_check,\n \t\t\t\t\t     &transaction->refnames, NULL,\n+\t\t\t\t\t     transaction,\n \t\t\t\t\t     transaction->flags & REF_TRANSACTION_FLAG_INITIAL,\n \t\t\t\t\t     err);\n \tif (ret < 0)\n@@ -1403,7 +1408,7 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n \t}\n \tstrbuf_release(&referent);\n \tstrbuf_release(&head_referent);\n-\tstring_list_clear(&refnames_to_check, 0);\n+\tstring_list_clear(&refnames_to_check, 1);\n \n \treturn ret;\n }\n\n-- \n2.48.1\n\n"},{"id":"513614","messageId":"20250305-245-partially-atomic-ref-updates-v3-8-0c64e3052354@gmail.com","threadId":"63064","inReplyTo":"20250305-245-partially-atomic-ref-updates-v3-0-0c64e3052354@gmail.com","subject":"[PATCH v3 8/8] update-ref: add --allow-partial flag for stdin mode","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-03-05T17:39:03Z","receivedAt":"2025-03-05T17:39:22Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"When updating multiple references through stdin, Git's update-ref\ncommand normally aborts the entire transaction if any single update\nfails. While this atomic behavior prevents partial updates by default,\nthere are cases where applying successful updates while reporting\nfailures is desirable.\n\nAdd a new `--allow-partial` flag that allows the transaction to continue\neven when individual reference updates fail. This flag can only be used\nin `--stdin` mode and builds upon the partial transaction support added\nto the refs subsystem. When enabled, failed updates are reported in the\nfollowing format:\n\n  rejected SP (<old-oid> | <old-target>) SP (<new-oid> | <new-target>) SP <rejection-reason> LF\n\nUpdate the documentation to reflect this change and also tests to cover\ndifferent scenarios where an update could be rejected.\n\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n Documentation/git-update-ref.adoc |  17 ++-\n builtin/update-ref.c              |  67 ++++++++++-\n t/t1400-update-ref.sh             | 233 ++++++++++++++++++++++++++++++++++++++\n 3 files changed, 309 insertions(+), 8 deletions(-)\n\ndiff --git a/Documentation/git-update-ref.adoc b/Documentation/git-update-ref.adoc\nindex 9e6935d38d..bcf38850a4 100644\n--- a/Documentation/git-update-ref.adoc\n+++ b/Documentation/git-update-ref.adoc\n@@ -7,8 +7,10 @@ git-update-ref - Update the object name stored in a ref safely\n \n SYNOPSIS\n --------\n-[verse]\n-'git update-ref' [-m <reason>] [--no-deref] (-d <ref> [<old-oid>] | [--create-reflog] <ref> <new-oid> [<old-oid>] | --stdin [-z])\n+[synopsis]\n+git update-ref [-m <reason>] [--no-deref] -d <ref> [<old-oid>]\n+\t       [-m <reason>] [--no-deref] [--create-reflog] <ref> <new-oid> [<old-oid>]\n+               [-m <reason>] [--no-deref] --stdin [-z] [--allow-partial]\n \n DESCRIPTION\n -----------\n@@ -57,6 +59,17 @@ performs all modifications together.  Specify commands of the form:\n With `--create-reflog`, update-ref will create a reflog for each ref\n even if one would not ordinarily be created.\n \n+With `--allow-partial`, update-ref continues executing the transaction even if\n+some updates fail due to invalid or incorrect user input, applying only the\n+successful updates. Errors resulting from user-provided input are treated as\n+non-system-related and do not cause the entire transaction to be aborted.\n+However, system-related errors—such as I/O failures or memory issues—will still\n+result in a full failure. Additionally, errors like F/D conflicts are batched\n+for performance optimization and will also cause a full failure. Any failed\n+updates will be reported in the following format:\n+\n+\trejected SP (<old-oid> | <old-target>) SP (<new-oid> | <new-target>) SP <rejection-reason> LF\n+\n Quote fields containing whitespace as if they were strings in C source\n code; i.e., surrounded by double-quotes and with backslash escapes.\n Use 40 \"0\" characters or the empty string to specify a zero value.  To\ndiff --git a/builtin/update-ref.c b/builtin/update-ref.c\nindex 1d541e13ad..66bd3cb44f 100644\n--- a/builtin/update-ref.c\n+++ b/builtin/update-ref.c\n@@ -5,6 +5,7 @@\n #include \"config.h\"\n #include \"gettext.h\"\n #include \"hash.h\"\n+#include \"hex.h\"\n #include \"refs.h\"\n #include \"object-name.h\"\n #include \"parse-options.h\"\n@@ -13,7 +14,7 @@\n static const char * const git_update_ref_usage[] = {\n \tN_(\"git update-ref [<options>] -d <refname> [<old-oid>]\"),\n \tN_(\"git update-ref [<options>]    <refname> <new-oid> [<old-oid>]\"),\n-\tN_(\"git update-ref [<options>] --stdin [-z]\"),\n+\tN_(\"git update-ref [<options>] --stdin [-z] [--allow-partial]\"),\n \tNULL\n };\n \n@@ -565,6 +566,49 @@ static void parse_cmd_abort(struct ref_transaction *transaction,\n \treport_ok(\"abort\");\n }\n \n+static void print_rejected_refs(const char *refname,\n+\t\t\t\tconst struct object_id *old_oid,\n+\t\t\t\tconst struct object_id *new_oid,\n+\t\t\t\tconst char *old_target,\n+\t\t\t\tconst char *new_target,\n+\t\t\t\tenum ref_transaction_error err,\n+\t\t\t\tvoid *cb_data UNUSED)\n+{\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tconst char *reason = \"\";\n+\n+\tswitch (err) {\n+\tcase REF_TRANSACTION_ERROR_NAME_CONFLICT:\n+\t\treason = \"refname conflict\";\n+\t\tbreak;\n+\tcase REF_TRANSACTION_ERROR_CREATE_EXISTS:\n+\t\treason = \"reference already exists\";\n+\t\tbreak;\n+\tcase REF_TRANSACTION_ERROR_NONEXISTENT_REF:\n+\t\treason = \"reference does not exist\";\n+\t\tbreak;\n+\tcase REF_TRANSACTION_ERROR_INCORRECT_OLD_VALUE:\n+\t\treason = \"incorrect old value provided\";\n+\t\tbreak;\n+\tcase REF_TRANSACTION_ERROR_INVALID_NEW_VALUE:\n+\t\treason = \"invalid new value provided\";\n+\t\tbreak;\n+\tcase REF_TRANSACTION_ERROR_EXPECTED_SYMREF:\n+\t\treason = \"expected symref but found regular ref\";\n+\t\tbreak;\n+\tdefault:\n+\t\treason = \"unkown failure\";\n+\t}\n+\n+\tstrbuf_addf(&sb, \"rejected %s %s %s %s\\n\", refname,\n+\t\t    new_oid ? oid_to_hex(new_oid) : new_target,\n+\t\t    old_oid ? oid_to_hex(old_oid) : old_target,\n+\t\t    reason);\n+\n+\tfwrite(sb.buf, sb.len, 1, stdout);\n+\tstrbuf_release(&sb);\n+}\n+\n static void parse_cmd_commit(struct ref_transaction *transaction,\n \t\t\t     const char *next, const char *end UNUSED)\n {\n@@ -573,6 +617,10 @@ static void parse_cmd_commit(struct ref_transaction *transaction,\n \t\tdie(\"commit: extra input: %s\", next);\n \tif (ref_transaction_commit(transaction, &error))\n \t\tdie(\"commit: %s\", error.buf);\n+\n+\tref_transaction_for_each_rejected_update(transaction,\n+\t\t\t\t\t\t print_rejected_refs, NULL);\n+\n \treport_ok(\"commit\");\n \tref_transaction_free(transaction);\n }\n@@ -609,7 +657,7 @@ static const struct parse_cmd {\n \t{ \"commit\",        parse_cmd_commit,        0, UPDATE_REFS_CLOSED },\n };\n \n-static void update_refs_stdin(void)\n+static void update_refs_stdin(unsigned int flags)\n {\n \tstruct strbuf input = STRBUF_INIT, err = STRBUF_INIT;\n \tenum update_refs_state state = UPDATE_REFS_OPEN;\n@@ -617,7 +665,7 @@ static void update_refs_stdin(void)\n \tint i, j;\n \n \ttransaction = ref_store_transaction_begin(get_main_ref_store(the_repository),\n-\t\t\t\t\t\t  0, &err);\n+\t\t\t\t\t\t  flags, &err);\n \tif (!transaction)\n \t\tdie(\"%s\", err.buf);\n \n@@ -685,7 +733,7 @@ static void update_refs_stdin(void)\n \t\t\t */\n \t\t\tstate = cmd->state;\n \t\t\ttransaction = ref_store_transaction_begin(get_main_ref_store(the_repository),\n-\t\t\t\t\t\t\t\t  0, &err);\n+\t\t\t\t\t\t\t\t  flags, &err);\n \t\t\tif (!transaction)\n \t\t\t\tdie(\"%s\", err.buf);\n \n@@ -701,6 +749,8 @@ static void update_refs_stdin(void)\n \t\t/* Commit by default if no transaction was requested. */\n \t\tif (ref_transaction_commit(transaction, &err))\n \t\t\tdie(\"%s\", err.buf);\n+\t\tref_transaction_for_each_rejected_update(transaction,\n+\t\t\t\t\t\t print_rejected_refs, NULL);\n \t\tref_transaction_free(transaction);\n \t\tbreak;\n \tcase UPDATE_REFS_STARTED:\n@@ -727,6 +777,8 @@ int cmd_update_ref(int argc,\n \tstruct object_id oid, oldoid;\n \tint delete = 0, no_deref = 0, read_stdin = 0, end_null = 0;\n \tint create_reflog = 0;\n+\tunsigned int flags = 0;\n+\n \tstruct option options[] = {\n \t\tOPT_STRING( 'm', NULL, &msg, N_(\"reason\"), N_(\"reason of the update\")),\n \t\tOPT_BOOL('d', NULL, &delete, N_(\"delete the reference\")),\n@@ -735,6 +787,8 @@ int cmd_update_ref(int argc,\n \t\tOPT_BOOL('z', NULL, &end_null, N_(\"stdin has NUL-terminated arguments\")),\n \t\tOPT_BOOL( 0 , \"stdin\", &read_stdin, N_(\"read updates from stdin\")),\n \t\tOPT_BOOL( 0 , \"create-reflog\", &create_reflog, N_(\"create a reflog\")),\n+\t\tOPT_BIT('0', \"allow-partial\", &flags, N_(\"allow partial transactions\"),\n+\t\t\tREF_TRANSACTION_ALLOW_PARTIAL),\n \t\tOPT_END(),\n \t};\n \n@@ -756,9 +810,10 @@ int cmd_update_ref(int argc,\n \t\t\tusage_with_options(git_update_ref_usage, options);\n \t\tif (end_null)\n \t\t\tline_termination = '\\0';\n-\t\tupdate_refs_stdin();\n+\t\tupdate_refs_stdin(flags);\n \t\treturn 0;\n-\t}\n+\t} else if (flags & REF_TRANSACTION_ALLOW_PARTIAL)\n+\t\tdie(\"--allow-partial can only be used with --stdin\");\n \n \tif (end_null)\n \t\tusage_with_options(git_update_ref_usage, options);\ndiff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\nindex 29045aad43..62a82f4af6 100755\n--- a/t/t1400-update-ref.sh\n+++ b/t/t1400-update-ref.sh\n@@ -2066,6 +2066,239 @@ do\n \t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n \t'\n \n+\ttest_expect_success \"stdin $type allow-partial\" '\n+\t\tgit init repo &&\n+\t\ttest_when_finished \"rm -fr repo\" &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit commit &&\n+\t\t\thead=$(git rev-parse HEAD) &&\n+\n+\t\t\tformat_command $type \"update refs/heads/ref1\" \"$head\" \"$Z\" >stdin &&\n+\t\t\tformat_command $type \"update refs/heads/ref2\" \"$head\" \"$Z\" >>stdin &&\n+\t\t\tgit update-ref $type --stdin --allow-partial <stdin &&\n+\t\t\techo $head >expect &&\n+\t\t\tgit rev-parse refs/heads/ref1 >actual &&\n+\t\t\ttest_cmp expect actual &&\n+\t\t\tgit rev-parse refs/heads/ref2 >actual &&\n+\t\t\ttest_cmp expect actual\n+\t\t)\n+\t'\n+\n+\ttest_expect_success \"stdin $type allow-partial with invalid new_oid\" '\n+\t\tgit init repo &&\n+\t\ttest_when_finished \"rm -fr repo\" &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit one &&\n+\t\t\told_head=$(git rev-parse HEAD) &&\n+\t\t\ttest_commit two &&\n+\t\t\thead=$(git rev-parse HEAD) &&\n+\t\t\tgit update-ref refs/heads/ref1 $head &&\n+\t\t\tgit update-ref refs/heads/ref2 $head &&\n+\n+\t\t\tformat_command $type \"update refs/heads/ref1\" \"$old_head\" \"$head\" >stdin &&\n+\t\t\tformat_command $type \"update refs/heads/ref2\" \"$(test_oid 001)\" \"$head\" >>stdin &&\n+\t\t\tgit update-ref $type --stdin --allow-partial <stdin >stdout &&\n+\t\t\techo $old_head >expect &&\n+\t\t\tgit rev-parse refs/heads/ref1 >actual &&\n+\t\t\ttest_cmp expect actual &&\n+\t\t\techo $head >expect &&\n+\t\t\tgit rev-parse refs/heads/ref2 >actual &&\n+\t\t\ttest_cmp expect actual &&\n+\t\t\ttest_grep -q \"invalid new value provided\" stdout\n+\t\t)\n+\t'\n+\n+\ttest_expect_success \"stdin $type allow-partial with non-commit new_oid\" '\n+\t\tgit init repo &&\n+\t\ttest_when_finished \"rm -fr repo\" &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit one &&\n+\t\t\told_head=$(git rev-parse HEAD) &&\n+\t\t\ttest_commit two &&\n+\t\t\thead=$(git rev-parse HEAD) &&\n+\t\t\thead_tree=$(git rev-parse HEAD^{tree}) &&\n+\t\t\tgit update-ref refs/heads/ref1 $head &&\n+\t\t\tgit update-ref refs/heads/ref2 $head &&\n+\n+\t\t\tformat_command $type \"update refs/heads/ref1\" \"$old_head\" \"$head\" >stdin &&\n+\t\t\tformat_command $type \"update refs/heads/ref2\" \"$head_tree\" \"$head\" >>stdin &&\n+\t\t\tgit update-ref $type --stdin --allow-partial <stdin >stdout &&\n+\t\t\techo $old_head >expect &&\n+\t\t\tgit rev-parse refs/heads/ref1 >actual &&\n+\t\t\ttest_cmp expect actual &&\n+\t\t\techo $head >expect &&\n+\t\t\tgit rev-parse refs/heads/ref2 >actual &&\n+\t\t\ttest_cmp expect actual &&\n+\t\t\ttest_grep -q \"invalid new value provided\" stdout\n+\t\t)\n+\t'\n+\n+\ttest_expect_success \"stdin $type allow-partial with non-existent ref\" '\n+\t\tgit init repo &&\n+\t\ttest_when_finished \"rm -fr repo\" &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit one &&\n+\t\t\told_head=$(git rev-parse HEAD) &&\n+\t\t\ttest_commit two &&\n+\t\t\thead=$(git rev-parse HEAD) &&\n+\t\t\tgit update-ref refs/heads/ref1 $head &&\n+\n+\t\t\tformat_command $type \"update refs/heads/ref1\" \"$old_head\" \"$head\" >stdin &&\n+\t\t\tformat_command $type \"update refs/heads/ref2\" \"$old_head\" \"$head\" >>stdin &&\n+\t\t\tgit update-ref $type --stdin --allow-partial <stdin >stdout &&\n+\t\t\techo $old_head >expect &&\n+\t\t\tgit rev-parse refs/heads/ref1 >actual &&\n+\t\t\ttest_cmp expect actual &&\n+\t\t\ttest_must_fail git rev-parse refs/heads/ref2 &&\n+\t\t\ttest_grep -q \"reference does not exist\" stdout\n+\t\t)\n+\t'\n+\n+\ttest_expect_success \"stdin $type allow-partial with dangling symref\" '\n+\t\tgit init repo &&\n+\t\ttest_when_finished \"rm -fr repo\" &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit one &&\n+\t\t\told_head=$(git rev-parse HEAD) &&\n+\t\t\ttest_commit two &&\n+\t\t\thead=$(git rev-parse HEAD) &&\n+\t\t\tgit update-ref refs/heads/ref1 $head &&\n+\t\t\tgit symbolic-ref refs/heads/ref2 refs/heads/nonexistent &&\n+\n+\t\t\tformat_command $type \"update refs/heads/ref1\" \"$old_head\" \"$head\" >stdin &&\n+\t\t\tformat_command $type \"update refs/heads/ref2\" \"$old_head\" \"$head\" >>stdin &&\n+\t\t\tgit update-ref $type --no-deref --stdin --allow-partial <stdin >stdout &&\n+\t\t\techo $old_head >expect &&\n+\t\t\tgit rev-parse refs/heads/ref1 >actual &&\n+\t\t\ttest_cmp expect actual &&\n+\t\t\techo $head >expect &&\n+\t\t\ttest_must_fail git rev-parse refs/heads/ref2 &&\n+\t\t\ttest_grep -q \"reference does not exist\" stdout\n+\t\t)\n+\t'\n+\n+\ttest_expect_success \"stdin $type allow-partial with regular ref as symref\" '\n+\t\tgit init repo &&\n+\t\ttest_when_finished \"rm -fr repo\" &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit one &&\n+\t\t\told_head=$(git rev-parse HEAD) &&\n+\t\t\ttest_commit two &&\n+\t\t\thead=$(git rev-parse HEAD) &&\n+\t\t\tgit update-ref refs/heads/ref1 $head &&\n+\t\t\tgit update-ref refs/heads/ref2 $head &&\n+\n+\t\t\tformat_command $type \"update refs/heads/ref1\" \"$old_head\" \"$head\" >stdin &&\n+\t\t\tformat_command $type \"symref-update refs/heads/ref2\" \"$old_head\" \"ref\" \"refs/heads/nonexistent\" >>stdin &&\n+\t\t\tgit update-ref $type --no-deref --stdin --allow-partial <stdin >stdout &&\n+\t\t\techo $old_head >expect &&\n+\t\t\tgit rev-parse refs/heads/ref1 >actual &&\n+\t\t\ttest_cmp expect actual &&\n+\t\t\techo $head >expect &&\n+\t\t\techo $head >expect &&\n+\t\t\tgit rev-parse refs/heads/ref2 >actual &&\n+\t\t\ttest_cmp expect actual &&\n+\t\t\ttest_grep -q \"expected symref but found regular ref\" stdout\n+\t\t)\n+\t'\n+\n+\ttest_expect_success \"stdin $type allow-partial with invalid old_oid\" '\n+\t\tgit init repo &&\n+\t\ttest_when_finished \"rm -fr repo\" &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit one &&\n+\t\t\told_head=$(git rev-parse HEAD) &&\n+\t\t\ttest_commit two &&\n+\t\t\thead=$(git rev-parse HEAD) &&\n+\t\t\tgit update-ref refs/heads/ref1 $head &&\n+\t\t\tgit update-ref refs/heads/ref2 $head &&\n+\n+\t\t\tformat_command $type \"update refs/heads/ref1\" \"$old_head\" \"$head\" >stdin &&\n+\t\t\tformat_command $type \"update refs/heads/ref2\" \"$old_head\" \"$Z\" >>stdin &&\n+\t\t\tgit update-ref $type --stdin --allow-partial <stdin >stdout &&\n+\t\t\techo $old_head >expect &&\n+\t\t\tgit rev-parse refs/heads/ref1 >actual &&\n+\t\t\ttest_cmp expect actual &&\n+\t\t\techo $head >expect &&\n+\t\t\tgit rev-parse refs/heads/ref2 >actual &&\n+\t\t\ttest_cmp expect actual &&\n+\t\t\ttest_grep -q \"reference already exists\" stdout\n+\t\t)\n+\t'\n+\n+\ttest_expect_success \"stdin $type allow-partial with incorrect old oid\" '\n+\t\tgit init repo &&\n+\t\ttest_when_finished \"rm -fr repo\" &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit one &&\n+\t\t\told_head=$(git rev-parse HEAD) &&\n+\t\t\ttest_commit two &&\n+\t\t\thead=$(git rev-parse HEAD) &&\n+\t\t\tgit update-ref refs/heads/ref1 $head &&\n+\t\t\tgit update-ref refs/heads/ref2 $head &&\n+\n+\t\t\tformat_command $type \"update refs/heads/ref1\" \"$old_head\" \"$head\" >stdin &&\n+\t\t\tformat_command $type \"update refs/heads/ref2\" \"$head\" \"$old_head\" >>stdin &&\n+\t\t\tgit update-ref $type --stdin --allow-partial <stdin >stdout &&\n+\t\t\techo $old_head >expect &&\n+\t\t\tgit rev-parse refs/heads/ref1 >actual &&\n+\t\t\ttest_cmp expect actual &&\n+\t\t\techo $head >expect &&\n+\t\t\tgit rev-parse refs/heads/ref2 >actual &&\n+\t\t\ttest_cmp expect actual &&\n+\t\t\ttest_grep -q \"incorrect old value provided\" stdout\n+\t\t)\n+\t'\n+\n+\ttest_expect_success \"stdin $type allow-partial refname conflict\" '\n+\t\tgit init repo &&\n+\t\ttest_when_finished \"rm -fr repo\" &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit one &&\n+\t\t\told_head=$(git rev-parse HEAD) &&\n+\t\t\ttest_commit two &&\n+\t\t\thead=$(git rev-parse HEAD) &&\n+\t\t\tgit update-ref refs/heads/ref/foo $head &&\n+\n+\t\t\tformat_command $type \"update refs/heads/ref/foo\" \"$old_head\" \"$head\" >stdin &&\n+\t\t\tformat_command $type \"update refs/heads/ref\" \"$old_head\" \"\" >>stdin &&\n+\t\t\tgit update-ref $type --stdin --allow-partial <stdin >stdout &&\n+\t\t\techo $old_head >expect &&\n+\t\t\tgit rev-parse refs/heads/ref/foo >actual &&\n+\t\t\ttest_cmp expect actual &&\n+\t\t\ttest_grep -q \"refname conflict\" stdout\n+\t\t)\n+\t'\n+\n+\ttest_expect_success \"stdin $type allow-partial refname conflict new ref\" '\n+\t\tgit init repo &&\n+\t\ttest_when_finished \"rm -fr repo\" &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit one &&\n+\t\t\told_head=$(git rev-parse HEAD) &&\n+\t\t\ttest_commit two &&\n+\t\t\thead=$(git rev-parse HEAD) &&\n+\t\t\tgit update-ref refs/heads/ref/foo $head &&\n+\n+\t\t\tformat_command $type \"update refs/heads/foo\" \"$old_head\" \"\" >stdin &&\n+\t\t\tformat_command $type \"update refs/heads/ref\" \"$old_head\" \"\" >>stdin &&\n+\t\t\tgit update-ref $type --stdin --allow-partial <stdin >stdout &&\n+\t\t\techo $old_head >expect &&\n+\t\t\tgit rev-parse refs/heads/foo >actual &&\n+\t\t\ttest_cmp expect actual &&\n+\t\t\ttest_grep -q \"refname conflict\" stdout\n+\t\t)\n+\t'\n done\n \n test_expect_success 'update-ref should also create reflog for HEAD' '\n\n-- \n2.48.1\n\n"},{"id":"513615","messageId":"xmqqo6yfl1js.fsf@gitster.g","threadId":"63064","inReplyTo":"20250305-245-partially-atomic-ref-updates-v3-0-0c64e3052354@gmail.com","subject":"Re: [PATCH v3 0/8] refs: introduce support for partial reference transactions","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-05T19:28:39Z","receivedAt":"2025-03-05T19:28:43Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Karthik Nayak <karthik.188@gmail.com> writes:\n\n> Git's reference updates are traditionally all or nothing - when\n> updating multiple references in a transaction, either all updates\n> succeed or none do.\n\nI am quite confused.  In the beginning (traditionally), there was no\ntransaction to speak of.  You try to update two refs at the same\ntime, we did best effort but that was never atomic.  Later we\nintroduced transactions to optionally make the changes all-or-none.\n\nSo, if you want \"I have these N updates, but I do not care if some\nof them have to fail---just make your best effort to update as many\nof them as you can\", why are you still doing a transaction?\n\nPerhaps it is merely the phrasing that makes this proposal\nconfusing.  If presented as \"non-transactional batched updates\",\nperhaps it may have been more palatable.  I dunno, but \"partial\ntransaction\" does not quite sound like a transaction, at least to\nme.\n"},{"id":"513617","messageId":"xmqqjz93kwcm.fsf@gitster.g","threadId":"63064","inReplyTo":"20250305-245-partially-atomic-ref-updates-v3-1-0c64e3052354@gmail.com","subject":"Re: [PATCH v3 1/8] refs/files: remove redundant check in split_symref_update()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-05T21:20:57Z","receivedAt":"2025-03-05T21:21:00Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Karthik Nayak <karthik.188@gmail.com> writes:\n\n> In `split_symref_update()`, there were two checks for duplicate\n> refnames:\n>\n>   - At the start, `string_list_has_string()` ensures the refname is not\n>     already in `affected_refnames`, preventing duplicates from being\n>     added.\n>\n>   - After adding the refname, another check verifies whether the newly\n>     inserted item has a `util` value.\n>\n> The second check is unnecessary because the first one guarantees that\n> `string_list_insert()` will never encounter a preexisting entry.\n>\n> Since `item->util` is only used in this context, remove the assignment and\n> simplify the surrounding code.\n\nIt was a bit unclear what \"this context\" refers to.  We lost all\nassignments to the .util member and that is a safe thing to do\nbecause ...\n\n> @@ -2843,13 +2835,7 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n>  \t\tif (update->flags & REF_LOG_ONLY)\n>  \t\t\tcontinue;\n>  \n> -\t\titem = string_list_append(&affected_refnames, update->refname);\n> -\t\t/*\n> -\t\t * We store a pointer to update in item->util, but at\n> -\t\t * the moment we never use the value of this field\n> -\t\t * except to check whether it is non-NULL.\n> -\t\t */\n> -\t\titem->util = update;\n\n... of this comment, and the \"except to check whether\" used to\nhappen in this code ...\n\n>  \t * be valid as long as affected_refnames is in use, and NOT\n>  \t * referent, which might soon be freed by our caller.\n>  \t */\n> -\titem = string_list_insert(affected_refnames, new_update->refname);\n> -\tif (item->util)\n> -\t\tBUG(\"%s unexpectedly found in affected_refnames\",\n> -\t\t    new_update->refname);\n> -\titem->util = new_update;\n\n... which the patch removed.\n\nOK.  Makes perfect sense.\n\nThanks.\n"},{"id":"513618","messageId":"xmqq5xknkup2.fsf@gitster.g","threadId":"63064","inReplyTo":"20250305-245-partially-atomic-ref-updates-v3-2-0c64e3052354@gmail.com","subject":"Re: [PATCH v3 2/8] refs: move duplicate refname update check to generic layer","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-05T21:56:41Z","receivedAt":"2025-03-05T21:56:44Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Karthik Nayak <karthik.188@gmail.com> writes:\n\n> Move the tracking of refnames in `affected_refnames` from individual\n> backends into the generic layer in 'refs.c'. This centralizes the\n> duplicate refname detection that was previously handled separately by\n> each backend.\n>\n> Make some changes to accommodate this move:\n>\n>   - Add a `string_list` field `refnames` to `ref_transaction` to contain\n>     all the references in a transaction. This field is updated whenever\n>     a new update is added via `ref_transaction_add_update`, so manual\n>     additions in reference backends are dropped.\n\nThe transaction object is the most logical place to keep track of\nwhat is involved in the transaction.  Nice.\n\n>   - Modify the backends to use this field internally as needed. The\n>     backends need to check if an update for refname already exists when\n>     splitting symrefs or adding an update for 'HEAD'.\n\nThe above reads to me as if you are saying that the files backend\nneeds to notice that it is updating \"HEAD\", notice that it is a\nsymbolic ref that points at \"refs/heads/main\", notice that \"HEAD\"\nand \"refs/heads/main\" are the two things involved in the\ntransaction, and must check if an update is already queued.\n\nBut when an update changes a symbolic ref in the sense that the\nunderlying ref gets updated through it, the need to update both the\nunderlying ref and the symbolic ref is common across backends, isn't\nit?  IOW, shouldn't \"splitting symrefs\" (which I take to mean \"ah,\nwe are updating HEAD so we need to update it and at the same time\nupdate the underlying refs/heads/main, two updates in total\") be\ndone also at the generic layer?\n\nAnd if that happens at the generic layer, should .refname member\neven be visible to backends?\n\n>   - In the reftable backend, within `reftable_be_transaction_prepare()`,\n>     move the `string_list_has_string()` check above\n>     `ref_transaction_add_update()`. Since `ref_transaction_add_update()`\n>     automatically adds the refname to `transaction->refnames`,\n>     performing the check after will always return true, so we perform\n>     the check before adding the update.\n\nThis change makes perfect tense.  It is the most natural to check\nand modify at the transaction layer the .refnames member, as it\nbelongs at the transaction layer after all.\n\n> This helps reduce duplication of functionality between the backends and\n> makes it easier to make changes in a more centralized manner.\n\nNice.\n"},{"id":"513635","messageId":"CAOLa=ZRT-VLtionwgSk2VON9Zps8KRL+XcO0ORF-ivzWZNOxUg@mail.gmail.com","threadId":"63064","inReplyTo":"xmqqo6yfl1js.fsf@gitster.g","subject":"Re: [PATCH v3 0/8] refs: introduce support for partial reference transactions","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-03-06T09:06:14Z","receivedAt":"2025-03-06T09:06:16Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Karthik Nayak <karthik.188@gmail.com> writes:\n>\n>> Git's reference updates are traditionally all or nothing - when\n>> updating multiple references in a transaction, either all updates\n>> succeed or none do.\n>\n> I am quite confused.  In the beginning (traditionally), there was no\n> transaction to speak of.  You try to update two refs at the same\n> time, we did best effort but that was never atomic.  Later we\n> introduced transactions to optionally make the changes all-or-none.\n>\n> So, if you want \"I have these N updates, but I do not care if some\n> of them have to fail---just make your best effort to update as many\n> of them as you can\", why are you still doing a transaction?\n>\n> Perhaps it is merely the phrasing that makes this proposal\n> confusing.  If presented as \"non-transactional batched updates\",\n> perhaps it may have been more palatable.  I dunno, but \"partial\n> transaction\" does not quite sound like a transaction, at least to\n> me.\n\nThat's fair. There was also some discussion earlier around this [1]. It\nis in indeed batched updates which can allow failures, but it is built\non top of the transaction infrastructure in the refs subsystem.\n\nPerhaps the best way would be to use the transaction interface under the\nhood, but present this feature as 'batched updates' to users, so there\nis no confusion between the two.\n\n[1]: 4beb0359-763d-425d-b416-ac40bda59e2e@gmail.com\n"},{"id":"513636","messageId":"CAOLa=ZTQ0MhA=rYJ7UTXReRBMJ=YA3+YqKZX05UndM4J1W9CAg@mail.gmail.com","threadId":"63064","inReplyTo":"xmqqjz93kwcm.fsf@gitster.g","subject":"Re: [PATCH v3 1/8] refs/files: remove redundant check in split_symref_update()","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-03-06T09:13:29Z","receivedAt":"2025-03-06T09:13:31Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Karthik Nayak <karthik.188@gmail.com> writes:\n>\n>> In `split_symref_update()`, there were two checks for duplicate\n>> refnames:\n>>\n>>   - At the start, `string_list_has_string()` ensures the refname is not\n>>     already in `affected_refnames`, preventing duplicates from being\n>>     added.\n>>\n>>   - After adding the refname, another check verifies whether the newly\n>>     inserted item has a `util` value.\n>>\n>> The second check is unnecessary because the first one guarantees that\n>> `string_list_insert()` will never encounter a preexisting entry.\n>>\n>> Since `item->util` is only used in this context, remove the assignment and\n>> simplify the surrounding code.\n>\n> It was a bit unclear what \"this context\" refers to.  We lost all\n> assignments to the .util member and that is a safe thing to do\n> because ...\n>\n\nDefinitely could use some clarification. Will change to:\n\n  The `item->util` field is assigned to validate that a rename doesn't\n  already exist in the list. The validation is done after the first\n  check. As this check is removed, clean up the validation and the\n  assignment of this field.\n\n>> @@ -2843,13 +2835,7 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n>>  \t\tif (update->flags & REF_LOG_ONLY)\n>>  \t\t\tcontinue;\n>>\n>> -\t\titem = string_list_append(&affected_refnames, update->refname);\n>> -\t\t/*\n>> -\t\t * We store a pointer to update in item->util, but at\n>> -\t\t * the moment we never use the value of this field\n>> -\t\t * except to check whether it is non-NULL.\n>> -\t\t */\n>> -\t\titem->util = update;\n>\n> ... of this comment, and the \"except to check whether\" used to\n> happen in this code ...\n>\n>>  \t * be valid as long as affected_refnames is in use, and NOT\n>>  \t * referent, which might soon be freed by our caller.\n>>  \t */\n>> -\titem = string_list_insert(affected_refnames, new_update->refname);\n>> -\tif (item->util)\n>> -\t\tBUG(\"%s unexpectedly found in affected_refnames\",\n>> -\t\t    new_update->refname);\n>> -\titem->util = new_update;\n>\n> ... which the patch removed.\n>\n> OK.  Makes perfect sense.\n>\n> Thanks.\n\nThanks!\n"},{"id":"513637","messageId":"CAOLa=ZSW9TaD5_-9oQ97=hZXinZUGAkLOSeyDsg-YrTiOOorvw@mail.gmail.com","threadId":"63064","inReplyTo":"xmqq5xknkup2.fsf@gitster.g","subject":"Re: [PATCH v3 2/8] refs: move duplicate refname update check to generic layer","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-03-06T09:46:21Z","receivedAt":"2025-03-06T09:46:23Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Karthik Nayak <karthik.188@gmail.com> writes:\n>\n>> Move the tracking of refnames in `affected_refnames` from individual\n>> backends into the generic layer in 'refs.c'. This centralizes the\n>> duplicate refname detection that was previously handled separately by\n>> each backend.\n>>\n>> Make some changes to accommodate this move:\n>>\n>>   - Add a `string_list` field `refnames` to `ref_transaction` to contain\n>>     all the references in a transaction. This field is updated whenever\n>>     a new update is added via `ref_transaction_add_update`, so manual\n>>     additions in reference backends are dropped.\n>\n> The transaction object is the most logical place to keep track of\n> what is involved in the transaction.  Nice.\n>\n>>   - Modify the backends to use this field internally as needed. The\n>>     backends need to check if an update for refname already exists when\n>>     splitting symrefs or adding an update for 'HEAD'.\n>\n> The above reads to me as if you are saying that the files backend\n> needs to notice that it is updating \"HEAD\", notice that it is a\n> symbolic ref that points at \"refs/heads/main\", notice that \"HEAD\"\n> and \"refs/heads/main\" are the two things involved in the\n> transaction, and must check if an update is already queued.\n>\n> But when an update changes a symbolic ref in the sense that the\n> underlying ref gets updated through it, the need to update both the\n> underlying ref and the symbolic ref is common across backends, isn't\n> it?  IOW, shouldn't \"splitting symrefs\" (which I take to mean \"ah,\n> we are updating HEAD so we need to update it and at the same time\n> update the underlying refs/heads/main, two updates in total\") be\n> done also at the generic layer?\n\nYup that is correct, in the files backend, we do this via the\n'split_symref_update()' function and in the reftable backend it is\ndirectly handled in the 'reftable_be_transaction_prepare()' function.\n\nI don't have a reason for why I didn't undertake that too in this\nseries. Mostly I think I didn't observe it. But it something that\ncan/should be done in the future.\n\n>\n> And if that happens at the generic layer, should .refname member\n> even be visible to backends?\n>\n\nIt shouldn't be necessary anymore with that change. I think this is good\nstep in that direction.\n\n>>   - In the reftable backend, within `reftable_be_transaction_prepare()`,\n>>     move the `string_list_has_string()` check above\n>>     `ref_transaction_add_update()`. Since `ref_transaction_add_update()`\n>>     automatically adds the refname to `transaction->refnames`,\n>>     performing the check after will always return true, so we perform\n>>     the check before adding the update.\n>\n> This change makes perfect tense.  It is the most natural to check\n> and modify at the transaction layer the .refnames member, as it\n> belongs at the transaction layer after all.\n>\n>> This helps reduce duplication of functionality between the backends and\n>> makes it easier to make changes in a more centralized manner.\n>\n> Nice.\n"},{"id":"513786","messageId":"20250307195057.GA3675279@coredump.intra.peff.net","threadId":"63064","inReplyTo":"20250305-245-partially-atomic-ref-updates-v3-6-0c64e3052354@gmail.com","subject":"Re: [PATCH v3 6/8] refs: implement partial reference transaction support","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-03-07T19:50:57Z","receivedAt":"2025-03-07T19:51:06Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, Mar 05, 2025 at 06:39:01PM +0100, Karthik Nayak wrote:\n\n> @@ -1456,6 +1471,13 @@ static enum ref_transaction_error write_with_updates(struct packed_ref_store *re\n>  \t\t\t\t\t    update->refname,\n>  \t\t\t\t\t    oid_to_hex(&update->old_oid));\n>  \t\t\t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n> +\n> +\t\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n> +\t\t\t\t\tstrbuf_setlen(err, 0);\n> +\t\t\t\t\tret = 0;\n> +\t\t\t\t\tcontinue;\n> +\t\t\t\t}\n> +\n>  \t\t\t\tgoto error;\n>  \t\t\t}\n>  \t\t}\n\nThis new code isn't reachable, since we return in the lines shown in the\ndiff context.\n\nShould it have been \"ret = REF_TRANSACTION_ERROR\"... in the first place?\nI think the \"goto error\" was already unreachable, so possibly the error\nis in an earlier patch. (I didn't look; Coverity flagged this in the\nfinal state in 'jch').\n\n-Peff\n"},{"id":"513788","messageId":"20250307195740.GA3675378@coredump.intra.peff.net","threadId":"63064","inReplyTo":"20250305-245-partially-atomic-ref-updates-v3-6-0c64e3052354@gmail.com","subject":"Re: [PATCH v3 6/8] refs: implement partial reference transaction support","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-03-07T19:57:40Z","receivedAt":"2025-03-07T19:57:42Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, Mar 05, 2025 at 06:39:01PM +0100, Karthik Nayak wrote:\n\n> diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\n> index 0132b8b06a..dd9912d637 100644\n> --- a/refs/reftable-backend.c\n> +++ b/refs/reftable-backend.c\n> @@ -1371,8 +1371,15 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n>  \t\t\t\t\t    transaction->updates[i],\n>  \t\t\t\t\t    &refnames_to_check, head_type,\n>  \t\t\t\t\t    &head_referent, &referent, err);\n> -\t\tif (ret)\n> +\t\tif (ret) {\n> +\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n> +\t\t\t\tstrbuf_setlen(err, 0);\n> +\t\t\t\tret = 0;\n> +\n> +\t\t\t\tcontinue;\n> +\t\t\t}\n>  \t\t\tgoto done;\n> +\t\t}\n>  \t}\n>  \n>  \tstring_list_sort(&refnames_to_check);\n\nCoverity complains that this \"ret = 0\" is a dead store. I think it's\nright, because either:\n\n  1. Our continue loops again, and we overwrite \"ret\" with the next call\n     to prepare_single_update().\n\n  2. We leave the loop (because this is the final entry in the\n     transaction update array), and then we overwrite \"ret\" with the\n     result of refs_verify_refnames_available().\n\nBut it may be better to leave it in place as a defensive measure against\nthe rest of the function changing.\n\n-Peff\n"},{"id":"513792","messageId":"xmqq34foefh8.fsf@gitster.g","threadId":"63064","inReplyTo":"20250307195057.GA3675279@coredump.intra.peff.net","subject":"Re: [PATCH v3 6/8] refs: implement partial reference transaction support","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-07T20:46:27Z","receivedAt":"2025-03-07T20:46:29Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> On Wed, Mar 05, 2025 at 06:39:01PM +0100, Karthik Nayak wrote:\n>\n>> @@ -1456,6 +1471,13 @@ static enum ref_transaction_error write_with_updates(struct packed_ref_store *re\n>>  \t\t\t\t\t    update->refname,\n>>  \t\t\t\t\t    oid_to_hex(&update->old_oid));\n>>  \t\t\t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n>> +\n>> +\t\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n>> +\t\t\t\t\tstrbuf_setlen(err, 0);\n>> +\t\t\t\t\tret = 0;\n>> +\t\t\t\t\tcontinue;\n>> +\t\t\t\t}\n>> +\n>>  \t\t\t\tgoto error;\n>>  \t\t\t}\n>>  \t\t}\n>\n> This new code isn't reachable, since we return in the lines shown in the\n> diff context.\n>\n> Should it have been \"ret = REF_TRANSACTION_ERROR\"... in the first place?\n> I think the \"goto error\" was already unreachable, so possibly the error\n> is in an earlier patch. (I didn't look; Coverity flagged this in the\n> final state in 'jch').\n\nSorry about that.  It shows that I lack the bandwidth necessary to\ngo through fine toothed comb on all the topics I queue.  Perhaps I\nshould be more selective and queue only the ones I personally had\nenough bandwidth to look over (or have seen clear \"I looked each and\nevery line of this series with fine toothed comb, put reviewed-by:\nme\" messages sent by trusted reviewers) while ignoring others?\n\nI dunno.\n\nThanks.\n"},{"id":"513793","messageId":"xmqqy0xgd0sn.fsf@gitster.g","threadId":"63064","inReplyTo":"xmqq34foefh8.fsf@gitster.g","subject":"Re: [PATCH v3 6/8] refs: implement partial reference transaction support","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-07T20:48:56Z","receivedAt":"2025-03-07T20:48:59Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Sorry about that.  It shows that I lack the bandwidth necessary to\n> go through fine toothed comb on all the topics I queue.  Perhaps I\n> should be more selective and queue only the ones I personally had\n> enough bandwidth to look over (or have seen clear \"I looked each and\n> every line of this series with fine toothed comb, put reviewed-by:\n> me\" messages sent by trusted reviewers) while ignoring others?\n\nI forgot a third category.  I should be able to queue series by\nthose who have track record of being meticulous and not have made\nsilly mistakes without reading each and every line.\n"},{"id":"513795","messageId":"CAOLa=ZRXZqE3ezA_ync1Z68sZfquSK2xCOKogNHc8unNcJpvbw@mail.gmail.com","threadId":"63064","inReplyTo":"20250307195057.GA3675279@coredump.intra.peff.net","subject":"Re: [PATCH v3 6/8] refs: implement partial reference transaction support","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-03-07T21:02:02Z","receivedAt":"2025-03-07T21:02:03Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> On Wed, Mar 05, 2025 at 06:39:01PM +0100, Karthik Nayak wrote:\n>\n>> @@ -1456,6 +1471,13 @@ static enum ref_transaction_error write_with_updates(struct packed_ref_store *re\n>>  \t\t\t\t\t    update->refname,\n>>  \t\t\t\t\t    oid_to_hex(&update->old_oid));\n>>  \t\t\t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n>> +\n>> +\t\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n>> +\t\t\t\t\tstrbuf_setlen(err, 0);\n>> +\t\t\t\t\tret = 0;\n>> +\t\t\t\t\tcontinue;\n>> +\t\t\t\t}\n>> +\n>>  \t\t\t\tgoto error;\n>>  \t\t\t}\n>>  \t\t}\n>\n> This new code isn't reachable, since we return in the lines shown in the\n> diff context.\n>\n> Should it have been \"ret = REF_TRANSACTION_ERROR\"... in the first place?\n> I think the \"goto error\" was already unreachable, so possibly the error\n> is in an earlier patch. (I didn't look; Coverity flagged this in the\n> final state in 'jch').\n>\n> -Peff\n\nIt should have bee `ret = REF_TRANSACTION_ERROR_NONEXISTENT_REF` and it\nshould have been in the previous commit!\n\nThanks for reporting!\n"},{"id":"513796","messageId":"CAOLa=ZTSb9c=Cb=OEhoXuavKHhamhV4rAQsiJMHVnBEBbtwH3A@mail.gmail.com","threadId":"63064","inReplyTo":"xmqq34foefh8.fsf@gitster.g","subject":"Re: [PATCH v3 6/8] refs: implement partial reference transaction support","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-03-07T21:05:05Z","receivedAt":"2025-03-07T21:05:06Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Jeff King <peff@peff.net> writes:\n>\n>> On Wed, Mar 05, 2025 at 06:39:01PM +0100, Karthik Nayak wrote:\n>>\n>>> @@ -1456,6 +1471,13 @@ static enum ref_transaction_error write_with_updates(struct packed_ref_store *re\n>>>  \t\t\t\t\t    update->refname,\n>>>  \t\t\t\t\t    oid_to_hex(&update->old_oid));\n>>>  \t\t\t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n>>> +\n>>> +\t\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n>>> +\t\t\t\t\tstrbuf_setlen(err, 0);\n>>> +\t\t\t\t\tret = 0;\n>>> +\t\t\t\t\tcontinue;\n>>> +\t\t\t\t}\n>>> +\n>>>  \t\t\t\tgoto error;\n>>>  \t\t\t}\n>>>  \t\t}\n>>\n>> This new code isn't reachable, since we return in the lines shown in the\n>> diff context.\n>>\n>> Should it have been \"ret = REF_TRANSACTION_ERROR\"... in the first place?\n>> I think the \"goto error\" was already unreachable, so possibly the error\n>> is in an earlier patch. (I didn't look; Coverity flagged this in the\n>> final state in 'jch').\n>\n> Sorry about that.  It shows that I lack the bandwidth necessary to\n> go through fine toothed comb on all the topics I queue.  Perhaps I\n> should be more selective and queue only the ones I personally had\n> enough bandwidth to look over (or have seen clear \"I looked each and\n> every line of this series with fine toothed comb, put reviewed-by:\n> me\" messages sent by trusted reviewers) while ignoring others?\n>\n> I dunno.\n>\n> Thanks.\n\nApologies, I see that this was also present in the previous version.\nDefinitely a miss on my side. I'll see how it was missed in the tests\nand add one if necessary!\n\nThanks!\n"},{"id":"513797","messageId":"CAOLa=ZTqqEYX4UBweEfZiHM4Fb=gQr1oe5PA66j8n+nS_fymRw@mail.gmail.com","threadId":"63064","inReplyTo":"20250307195740.GA3675378@coredump.intra.peff.net","subject":"Re: [PATCH v3 6/8] refs: implement partial reference transaction support","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-03-07T21:07:48Z","receivedAt":"2025-03-07T21:07:51Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> On Wed, Mar 05, 2025 at 06:39:01PM +0100, Karthik Nayak wrote:\n>\n>> diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\n>> index 0132b8b06a..dd9912d637 100644\n>> --- a/refs/reftable-backend.c\n>> +++ b/refs/reftable-backend.c\n>> @@ -1371,8 +1371,15 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\n>>  \t\t\t\t\t    transaction->updates[i],\n>>  \t\t\t\t\t    &refnames_to_check, head_type,\n>>  \t\t\t\t\t    &head_referent, &referent, err);\n>> -\t\tif (ret)\n>> +\t\tif (ret) {\n>> +\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n>> +\t\t\t\tstrbuf_setlen(err, 0);\n>> +\t\t\t\tret = 0;\n>> +\n>> +\t\t\t\tcontinue;\n>> +\t\t\t}\n>>  \t\t\tgoto done;\n>> +\t\t}\n>>  \t}\n>>\n>>  \tstring_list_sort(&refnames_to_check);\n>\n> Coverity complains that this \"ret = 0\" is a dead store. I think it's\n> right, because either:\n>\n>   1. Our continue loops again, and we overwrite \"ret\" with the next call\n>      to prepare_single_update().\n>\n>   2. We leave the loop (because this is the final entry in the\n>      transaction update array), and then we overwrite \"ret\" with the\n>      result of refs_verify_refnames_available().\n>\n> But it may be better to leave it in place as a defensive measure against\n> the rest of the function changing.\n>\n\nYes agreed with your analysis, and also your inference. So I'll let this stay.\nThanks for reporting!\n\n> -Peff\n"},{"id":"513806","messageId":"20250307225444.GA42758@coredump.intra.peff.net","threadId":"63064","inReplyTo":"xmqq34foefh8.fsf@gitster.g","subject":"[PATCH] config.mak.dev: enable -Wunreachable-code","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-03-07T22:54:44Z","receivedAt":"2025-03-07T22:54:46Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Fri, Mar 07, 2025 at 12:46:27PM -0800, Junio C Hamano wrote:\n\n> Jeff King <peff@peff.net> writes:\n> \n> > On Wed, Mar 05, 2025 at 06:39:01PM +0100, Karthik Nayak wrote:\n> >\n> >> @@ -1456,6 +1471,13 @@ static enum ref_transaction_error write_with_updates(struct packed_ref_store *re\n> >>  \t\t\t\t\t    update->refname,\n> >>  \t\t\t\t\t    oid_to_hex(&update->old_oid));\n> >>  \t\t\t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n> >> +\n> >> +\t\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n> >> +\t\t\t\t\tstrbuf_setlen(err, 0);\n> >> +\t\t\t\t\tret = 0;\n> >> +\t\t\t\t\tcontinue;\n> >> +\t\t\t\t}\n> >> +\n> >>  \t\t\t\tgoto error;\n> >>  \t\t\t}\n> >>  \t\t}\n> >\n> > This new code isn't reachable, since we return in the lines shown in the\n> > diff context.\n> >\n> > Should it have been \"ret = REF_TRANSACTION_ERROR\"... in the first place?\n> > I think the \"goto error\" was already unreachable, so possibly the error\n> > is in an earlier patch. (I didn't look; Coverity flagged this in the\n> > final state in 'jch').\n> \n> Sorry about that.  It shows that I lack the bandwidth necessary to\n> go through fine toothed comb on all the topics I queue.  Perhaps I\n> should be more selective and queue only the ones I personally had\n> enough bandwidth to look over (or have seen clear \"I looked each and\n> every line of this series with fine toothed comb, put reviewed-by:\n> me\" messages sent by trusted reviewers) while ignoring others?\n\nEh, I would not worry about it too much. Things get missed, and that is\nwhy we have many layers of reviews, static analysis, and ultimately\nusers to help us find bugs. ;)\n\nI was disappointed that the compiler didn't complain, though. Maybe we\nshould do this:\n\n-- >8 --\nSubject: [PATCH] config.mak.dev: enable -Wunreachable-code\n\nHaving the compiler point out unreachable code can help avoid bugs, like\nthe one discussed in:\n\n  https://lore.kernel.org/git/20250307195057.GA3675279@coredump.intra.peff.net/\n\nIn that case it was found by Coverity, but finding it earlier saves\neverybody time and effort.\n\nWe can use -Wunreachable-code to get some help from the compiler here.\nInterestingly, this is a noop in gcc. It was a real warning up until gcc\n4.x, when it was removed for being too flaky, but they left the\ncommand-line option to avoid breaking users. See:\n\n  https://stackoverflow.com/questions/17249934/why-does-gcc-not-warn-for-unreachable-code\n\nHowever, clang does implement this option, and it finds the case\nmentioned above (and no other cases within the code base). And since we\nrun clang in several of our CI jobs, that's enough to get an early\nwarning of breakage.\n\nWe could enable it only for clang, but since gcc is happy to ignore it,\nit's simpler to just turn it on for all developer builds.\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\nYou can see it in action (merged into 'jch') here:\n\n  https://github.com/peff/git/actions/runs/13729842188\n\nwhere all of the clang jobs fail.\n\n config.mak.dev | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git a/config.mak.dev b/config.mak.dev\nindex 0fd8cc4d35..95b7bc46ae 100644\n--- a/config.mak.dev\n+++ b/config.mak.dev\n@@ -39,6 +39,7 @@ DEVELOPER_CFLAGS += -Wunused\n DEVELOPER_CFLAGS += -Wvla\n DEVELOPER_CFLAGS += -Wwrite-strings\n DEVELOPER_CFLAGS += -fno-common\n+DEVELOPER_CFLAGS += -Wunreachable-code\n \n ifneq ($(filter clang4,$(COMPILER_FEATURES)),)\n DEVELOPER_CFLAGS += -Wtautological-constant-out-of-range-compare\n-- \n2.49.0.rc1.380.g53e738dd21\n\n"},{"id":"513808","messageId":"xmqqzfhwbev1.fsf@gitster.g","threadId":"63064","inReplyTo":"20250307225444.GA42758@coredump.intra.peff.net","subject":"Re: [PATCH] config.mak.dev: enable -Wunreachable-code","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-07T23:28:02Z","receivedAt":"2025-03-07T23:28:06Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> I was disappointed that the compiler didn't complain, though. Maybe we\n> should do this:\n\nIndeed.  It would have helped us if it were already there in place.\n\n> -- >8 --\n> Subject: [PATCH] config.mak.dev: enable -Wunreachable-code\n>\n> Having the compiler point out unreachable code can help avoid bugs, like\n> the one discussed in:\n>\n>   https://lore.kernel.org/git/20250307195057.GA3675279@coredump.intra.peff.net/\n>\n> In that case it was found by Coverity, but finding it earlier saves\n> everybody time and effort.\n>\n> We can use -Wunreachable-code to get some help from the compiler here.\n> Interestingly, this is a noop in gcc. It was a real warning up until gcc\n> 4.x, when it was removed for being too flaky, but they left the\n> command-line option to avoid breaking users. See:\n>\n>   https://stackoverflow.com/questions/17249934/why-does-gcc-not-warn-for-unreachable-code\n\nWow, now they leave their users confused, making them wondering why\ntheir command line option does not do anything useful ;-)\n\n> However, clang does implement this option, and it finds the case\n> mentioned above (and no other cases within the code base). And since we\n> run clang in several of our CI jobs, that's enough to get an early\n> warning of breakage.\n\nYes, this is great.\n\nThanks.\n"},{"id":"513826","messageId":"20250308032309.GA584028@coredump.intra.peff.net","threadId":"63064","inReplyTo":"20250307225444.GA42758@coredump.intra.peff.net","subject":"Re: [PATCH] config.mak.dev: enable -Wunreachable-code","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-03-08T03:23:09Z","receivedAt":"2025-03-08T03:23:11Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Fri, Mar 07, 2025 at 05:54:45PM -0500, Jeff King wrote:\n\n> However, clang does implement this option, and it finds the case\n> mentioned above (and no other cases within the code base). And since we\n> run clang in several of our CI jobs, that's enough to get an early\n> warning of breakage.\n\nHmph, this might be more trouble than it is worth.\n\nAfter correcting the problem in the refs code, the osx CI builds (and\nonly those) now fail with:\n\n  run-command.c:519:3: error: code will never be executed [-Werror,-Wunreachable-code]\n                  die_errno(\"sigfillset\");\n                  ^~~~~~~~~\n\nThe code in question is just:\n\n  if (sigfillset(&all))\n\tdie_errno(\"sigfillset\");\n\nSo I have to imagine that the issue is that sigfillset() on that\nplatform is an inline or macro that will never return an error, and the\ncompiler can see that. But since POSIX says this can fail (though I'd\nimagine it's unlikely on most platforms), we should check in the general\ncase.\n\nSo I don't see how to solve it short of:\n\n#ifdef SIGFILLSET_CANNOT_FAIL\n\tsigfillset(&all);\n#else\n\tif (sigfillset(&all))\n\t\tdie_errno(\"sigfillset\");\n#endif\n\nwhich is rather ugly. It's only used in one spot, so the damage doesn't\ngo too far, but I don't love the idea of getting surprised by the\ncompiler over-analyzing system functions (and having to add Makefile\nknobs to support it).\n\nI guess a knob-less version is:\n\n  errno = 0;\n  sigfillset(&all); /* don't check return value! only errno */\n  if (errno)\n\tdie_errno(\"sigfillset\");\n\nwhich is subtle, to say the least.\n\n-Peff\n"},{"id":"513904","messageId":"xmqqfrjkao75.fsf@gitster.g","threadId":"63064","inReplyTo":"20250308032309.GA584028@coredump.intra.peff.net","subject":"Re: [PATCH] config.mak.dev: enable -Wunreachable-code","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-10T15:40:46Z","receivedAt":"2025-03-10T15:40:49Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> On Fri, Mar 07, 2025 at 05:54:45PM -0500, Jeff King wrote:\n>\n>> However, clang does implement this option, and it finds the case\n>> mentioned above (and no other cases within the code base). And since we\n>> run clang in several of our CI jobs, that's enough to get an early\n>> warning of breakage.\n>\n> Hmph, this might be more trouble than it is worth.\n>\n> After correcting the problem in the refs code, the osx CI builds (and\n> only those) now fail with:\n>\n>   run-command.c:519:3: error: code will never be executed [-Werror,-Wunreachable-code]\n>                   die_errno(\"sigfillset\");\n>                   ^~~~~~~~~\n> ...\n> I guess a knob-less version is:\n>\n>   errno = 0;\n>   sigfillset(&all); /* don't check return value! only errno */\n>   if (errno)\n> \tdie_errno(\"sigfillset\");\n>\n> which is subtle, to say the least.\n\nBah.  This is just as horrible as some other warnings that are not\nenabled by default.  I guess we should just be more vigilant X-<.\n\nThanks.\n"},{"id":"513917","messageId":"20250310160440.GA26189@coredump.intra.peff.net","threadId":"63064","inReplyTo":"xmqqfrjkao75.fsf@gitster.g","subject":"Re: [PATCH] config.mak.dev: enable -Wunreachable-code","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-03-10T16:04:40Z","receivedAt":"2025-03-10T16:04:49Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Mon, Mar 10, 2025 at 08:40:46AM -0700, Junio C Hamano wrote:\n\n> Jeff King <peff@peff.net> writes:\n> \n> > On Fri, Mar 07, 2025 at 05:54:45PM -0500, Jeff King wrote:\n> >\n> >> However, clang does implement this option, and it finds the case\n> >> mentioned above (and no other cases within the code base). And since we\n> >> run clang in several of our CI jobs, that's enough to get an early\n> >> warning of breakage.\n> >\n> > Hmph, this might be more trouble than it is worth.\n> >\n> > After correcting the problem in the refs code, the osx CI builds (and\n> > only those) now fail with:\n> >\n> >   run-command.c:519:3: error: code will never be executed [-Werror,-Wunreachable-code]\n> >                   die_errno(\"sigfillset\");\n> >                   ^~~~~~~~~\n> > ...\n> > I guess a knob-less version is:\n> >\n> >   errno = 0;\n> >   sigfillset(&all); /* don't check return value! only errno */\n> >   if (errno)\n> > \tdie_errno(\"sigfillset\");\n> >\n> > which is subtle, to say the least.\n> \n> Bah.  This is just as horrible as some other warnings that are not\n> enabled by default.  I guess we should just be more vigilant X-<.\n\nYeah. We could perhaps live with hacking around this one specific spot.\nBut there's an open question of how often these kinds of false positives\nwill come up.\n\nMaybe not often, if there is only one instance in the current code base.\nOr maybe a lot, but we wouldn't know because we haven't had the warning\nenabled.\n\nI guess another option is to enable it in _one_ CI job that uses clang\non Linux (maybe linux-sha256?) and see how often it is helpful or\nharmful.\n\n-Peff\n"},{"id":"513929","messageId":"xmqqsenk7mab.fsf@gitster.g","threadId":"63064","inReplyTo":"20250310160440.GA26189@coredump.intra.peff.net","subject":"Re: [PATCH] config.mak.dev: enable -Wunreachable-code","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-10T18:50:20Z","receivedAt":"2025-03-10T18:50:23Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> Maybe not often, if there is only one instance in the current code base.\n> Or maybe a lot, but we wouldn't know because we haven't had the warning\n> enabled.\n>\n> I guess another option is to enable it in _one_ CI job that uses clang\n> on Linux (maybe linux-sha256?) and see how often it is helpful or\n> harmful.\n\nThe reason why you said Linux rather than macOS is because the\nsingle instance we know about would not have to be worked around if\nwe did it that way?\n\nI am OK with that.  \n\nThanks.\n"},{"id":"514269","messageId":"20250314161010.GA8522@coredump.intra.peff.net","threadId":"63064","inReplyTo":"xmqqsenk7mab.fsf@gitster.g","subject":"Re: [PATCH] config.mak.dev: enable -Wunreachable-code","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-03-14T16:10:10Z","receivedAt":"2025-03-14T16:10:19Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Mon, Mar 10, 2025 at 11:50:20AM -0700, Junio C Hamano wrote:\n\n> Jeff King <peff@peff.net> writes:\n> \n> > Maybe not often, if there is only one instance in the current code base.\n> > Or maybe a lot, but we wouldn't know because we haven't had the warning\n> > enabled.\n> >\n> > I guess another option is to enable it in _one_ CI job that uses clang\n> > on Linux (maybe linux-sha256?) and see how often it is helpful or\n> > harmful.\n> \n> The reason why you said Linux rather than macOS is because the\n> single instance we know about would not have to be worked around if\n> we did it that way?\n> \n> I am OK with that.\n\nYes, exactly. I started to prepare a patch for that, but then I realized\nI'd probably be adding support in config.mak.dev. So we could also just\nhandle it automatically there, skipping the flag on macOS.\n\nThat would use the flag in more situations (blocking the known-bad case,\nrather than enabling it in a known-good one). It might hit more false\npositives, but I'd rather experiment in that direction and see if\nanybody setting DEVELOPER=1 complains. After all, in either case it is\nstill a big question of whether this is the only false positive we'll\nsee, or if this is opening up a can of worms. So I consider it all\nkind-of exploratory.\n\nSo that patch could look like this (on top of what you've queued already\nin jk/use-wunreachable-code-for-devs).\n\n-- >8 --\nSubject: [PATCH] config.mak.dev: disable -Wunreachable-code on macOS\n\nWe've seen false positives here related to calling sigfillset(); even\nthough POSIX specifies that it may return an error, it transparently (to\nthe compiler) always returns success on macOS. As a result, the compiler\nflags the error path in something like:\n\n  if (sigfillset(&set))\n\tdie(...);\n\nas unreachable (which it is on this platform, but not in the general\ncase). We could work around it, but let's just disable the warning on\nthis platform. There are plenty of CI jobs that will still trigger it\n(e.g., all of the linux+clang jobs).\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\nIt's possible FreeBSD might share the same problem, but their manpage\ndoes not seem to have the same \"it always returns 0\" language. But we\nmight need to expand this list if people report more problems.\n\n config.mak.dev | 5 +++++\n 1 file changed, 5 insertions(+)\n\ndiff --git a/config.mak.dev b/config.mak.dev\nindex 95b7bc46ae..30dcd0c175 100644\n--- a/config.mak.dev\n+++ b/config.mak.dev\n@@ -39,7 +39,12 @@ DEVELOPER_CFLAGS += -Wunused\n DEVELOPER_CFLAGS += -Wvla\n DEVELOPER_CFLAGS += -Wwrite-strings\n DEVELOPER_CFLAGS += -fno-common\n+\n+# There are false positives for unreachable code related to system\n+# functions on macOS.\n+ifneq ($(uname_S),Darwin)\n DEVELOPER_CFLAGS += -Wunreachable-code\n+endif\n \n ifneq ($(filter clang4,$(COMPILER_FEATURES)),)\n DEVELOPER_CFLAGS += -Wtautological-constant-out-of-range-compare\n-- \n2.49.0.rc2.384.gf2d6285ccb\n\n"},{"id":"514270","messageId":"20250314161347.GA9440@coredump.intra.peff.net","threadId":"63064","inReplyTo":"20250314161010.GA8522@coredump.intra.peff.net","subject":"Re: [PATCH] config.mak.dev: enable -Wunreachable-code","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-03-14T16:13:47Z","receivedAt":"2025-03-14T16:13:49Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Fri, Mar 14, 2025 at 12:10:10PM -0400, Jeff King wrote:\n\n> So that patch could look like this (on top of what you've queued already\n> in jk/use-wunreachable-code-for-devs).\n> \n> -- >8 --\n> Subject: [PATCH] config.mak.dev: disable -Wunreachable-code on macOS\n> [...]\n> ---\n> It's possible FreeBSD might share the same problem, but their manpage\n> does not seem to have the same \"it always returns 0\" language. But we\n> might need to expand this list if people report more problems.\n\nAnd I'm still a bit tempted to instead actually silence this one false\npositive, and keep the warning enabled everywhere. That would help\nFreeBSD (if it indeed does have the same issue) and let macOS benefit\nfrom the warning (most code paths would be covered on Linux anyway, but\nthere could be platform-specific ones).\n\nAnd that patch would look like this (again, on top of what you've\nalready queued, and replacing the patch I'm replying to):\n\n-- >8 --\nSubject: [PATCH] run-command: use errno to check for sigfillset() error\n\nSince enabling -Wunreachable-code, builds with clang on macOS now fail,\ncomplaining that the die_errno() call in:\n\n  if (sigfillset(&all))\n\tdie_errno(\"sigfillset\");\n\nis unreachable. On that platform the manpage documents that sigfillset()\nalways returns success, and presumably the implementation is a macro or\ninline function that does so in a way that is transparent to the\ncompiler.\n\nBut we should continue to check on other platforms, since POSIX says it\nmay return an error.\n\nWe could solve this with a compile-time knob to split the two cases\n(assuming success on macOS and checking for the error elsewhere). But we\ncan also work around it more directly by relying on errno to check the\noutcome (since POSIX dictates that errno will be set on error). And that\nworks around the compiler's cleverness, since it doesn't know the\nsemantics of errno (though I suppose if sigfillset() is simple enough,\nit could perhaps realize that no writes to errno are possible; however\nthis does seem to work in practice).\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n run-command.c | 10 +++++++++-\n 1 file changed, 9 insertions(+), 1 deletion(-)\n\ndiff --git a/run-command.c b/run-command.c\nindex 402138b8b5..d527c46175 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -515,7 +515,15 @@ static void atfork_prepare(struct atfork_state *as)\n {\n \tsigset_t all;\n \n-\tif (sigfillset(&all))\n+\t/*\n+\t * Do not use the return value of sigfillset(). It is transparently 0\n+\t * on some platforms, meaning a clever compiler may complain that\n+\t * the conditional body is dead code. Instead, check for error via\n+\t * errno, which outsmarts the compiler.\n+\t */\n+\terrno = 0;\n+\tsigfillset(&all);\n+\tif (errno)\n \t\tdie_errno(\"sigfillset\");\n #ifdef NO_PTHREADS\n \tif (sigprocmask(SIG_SETMASK, &all, &as->old))\n-- \n2.49.0.rc2.384.gf2d6285ccb\n\n"},{"id":"514273","messageId":"xmqqldt7ildk.fsf@gitster.g","threadId":"63064","inReplyTo":"20250314161010.GA8522@coredump.intra.peff.net","subject":"Re: [PATCH] config.mak.dev: enable -Wunreachable-code","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-14T17:15:51Z","receivedAt":"2025-03-14T17:15:53Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> That would use the flag in more situations (blocking the known-bad case,\n> rather than enabling it in a known-good one). It might hit more false\n> positives, but I'd rather experiment in that direction and see if\n> anybody setting DEVELOPER=1 complains.\n\nGood.\n\n> After all, in either case it is\n> still a big question of whether this is the only false positive we'll\n> see, or if this is opening up a can of worms. So I consider it all\n> kind-of exploratory.\n\nAgain, good.\n\n> So that patch could look like this (on top of what you've queued already\n> in jk/use-wunreachable-code-for-devs).\n\n> +\n> +# There are false positives for unreachable code related to system\n> +# functions on macOS.\n> +ifneq ($(uname_S),Darwin)\n>  DEVELOPER_CFLAGS += -Wunreachable-code\n> +endif\n\nOne possible downside of this is that we would not know when their\ncompiler stops giving the \"false positive\" and becomes as usuable as\nother platforms (oh, it came out unintendedly harsh---it could be\nthat the situation is that their compiler is doing the right thing,\nand the right thing is a bit inconvenient for this codebase).\n\nUnless diligent volunteers with macOS step up to do trial builds\nwith the option when they notice that their toolchain or OS header\nfiles got upgraded, that is.\n\nBut other than that, I am fine with this.  Let's have this for some\ntime to see how much problems (false positives) our newly added code\nwould get to judge if it is worth our time to deal with them.\n\nThanks.\n"},{"id":"514279","messageId":"xmqqv7sbh698.fsf@gitster.g","threadId":"63064","inReplyTo":"20250314161347.GA9440@coredump.intra.peff.net","subject":"Re: [PATCH] config.mak.dev: enable -Wunreachable-code","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-14T17:27:47Z","receivedAt":"2025-03-14T17:27:49Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> -- >8 --\n> Subject: [PATCH] run-command: use errno to check for sigfillset() error\n>\n> Since enabling -Wunreachable-code, builds with clang on macOS now fail,\n> complaining that the die_errno() call in:\n>\n>   if (sigfillset(&all))\n> \tdie_errno(\"sigfillset\");\n>\n> is unreachable. On that platform the manpage documents that sigfillset()\n> always returns success, and presumably the implementation is a macro or\n> inline function that does so in a way that is transparent to the\n> compiler.\n\nWould it work to instead do this here\n\n\tif (sigfillset(&all) || false_but_compiler_does_not_know_it)\n\t\tdie_error(\"sigfillset\");\n\nwith\n\n\textern int false_but_compiler_does_not_know_it;\n\nin <git-compat-util.h>?  And a standalone .c file with its\ndefinition\n\n\t#include <git-compat-util.h>\n\tint false_but_compiler_does_not_know_it;\n\nand nothing else, linked into libgit.a?\n\nI am hoping that such a false-positive would come from conditionals\nthat are known to be compiler to be always taken (or never taken),\nso eventually we can mark such an expression with a macro, e.g.\n\n\tif (CAN_BE_TAKEN(sigfilllset(&all))\n\t\tdie_error(\"sigfillset\");\n\nBecause in this particular case we _can_ rely on errno, so the patch\nwe see here is perfectly fine by me, but a more generic approach\nlike the above would make it unnecessary to\n\n - have a 4-line comment\n - come up with workaround\n\nsuitable for each such places we need to work around compiler\nsmarta^hness.\n\n> But we should continue to check on other platforms, since POSIX says it\n> may return an error.\n>\n> We could solve this with a compile-time knob to split the two cases\n> (assuming success on macOS and checking for the error elsewhere). But we\n> can also work around it more directly by relying on errno to check the\n> outcome (since POSIX dictates that errno will be set on error). And that\n> works around the compiler's cleverness, since it doesn't know the\n> semantics of errno (though I suppose if sigfillset() is simple enough,\n> it could perhaps realize that no writes to errno are possible; however\n> this does seem to work in practice).\n>\n> Signed-off-by: Jeff King <peff@peff.net>\n> ---\n>  run-command.c | 10 +++++++++-\n>  1 file changed, 9 insertions(+), 1 deletion(-)\n>\n> diff --git a/run-command.c b/run-command.c\n> index 402138b8b5..d527c46175 100644\n> --- a/run-command.c\n> +++ b/run-command.c\n> @@ -515,7 +515,15 @@ static void atfork_prepare(struct atfork_state *as)\n>  {\n>  \tsigset_t all;\n>  \n> -\tif (sigfillset(&all))\n> +\t/*\n> +\t * Do not use the return value of sigfillset(). It is transparently 0\n> +\t * on some platforms, meaning a clever compiler may complain that\n> +\t * the conditional body is dead code. Instead, check for error via\n> +\t * errno, which outsmarts the compiler.\n> +\t */\n> +\terrno = 0;\n> +\tsigfillset(&all);\n> +\tif (errno)\n>  \t\tdie_errno(\"sigfillset\");\n>  #ifdef NO_PTHREADS\n>  \tif (sigprocmask(SIG_SETMASK, &all, &as->old))\n"},{"id":"514285","messageId":"xmqqr02zfr3r.fsf@gitster.g","threadId":"63064","inReplyTo":"xmqqv7sbh698.fsf@gitster.g","subject":"Re: [PATCH] config.mak.dev: enable -Wunreachable-code","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-14T17:40:24Z","receivedAt":"2025-03-14T17:40:27Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Jeff King <peff@peff.net> writes:\n>\n>> -- >8 --\n>> Subject: [PATCH] run-command: use errno to check for sigfillset() error\n>>\n>> Since enabling -Wunreachable-code, builds with clang on macOS now fail,\n>> complaining that the die_errno() call in:\n>>\n>>   if (sigfillset(&all))\n>> \tdie_errno(\"sigfillset\");\n>>\n>> is unreachable. On that platform the manpage documents that sigfillset()\n>> always returns success, and presumably the implementation is a macro or\n>> inline function that does so in a way that is transparent to the\n>> compiler.\n>\n> Would it work to instead do this here\n> ...\n\nI forgot to say a more important thing.  Between the \"let's excempt\ndevelopers on macOS\" and the \"let's see how far we can go with the\nwarning turned on everywhere and wack-a-mole this particular one\nwith errno check\" patches, I prefer the latter at least for a short\nterm.\n\nThanks.\n"},{"id":"514286","messageId":"Z9Rqx6CuXB_6JQoL@pks.im","threadId":"63064","inReplyTo":"xmqqr02zfr3r.fsf@gitster.g","subject":"Re: [PATCH] config.mak.dev: enable -Wunreachable-code","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-03-14T17:43:35Z","receivedAt":"2025-03-14T17:43:41Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Mar 14, 2025 at 10:40:24AM -0700, Junio C Hamano wrote:\n> Junio C Hamano <gitster@pobox.com> writes:\n> \n> > Jeff King <peff@peff.net> writes:\n> >\n> >> -- >8 --\n> >> Subject: [PATCH] run-command: use errno to check for sigfillset() error\n> >>\n> >> Since enabling -Wunreachable-code, builds with clang on macOS now fail,\n> >> complaining that the die_errno() call in:\n> >>\n> >>   if (sigfillset(&all))\n> >> \tdie_errno(\"sigfillset\");\n> >>\n> >> is unreachable. On that platform the manpage documents that sigfillset()\n> >> always returns success, and presumably the implementation is a macro or\n> >> inline function that does so in a way that is transparent to the\n> >> compiler.\n> >\n> > Would it work to instead do this here\n> > ...\n> \n> I forgot to say a more important thing.  Between the \"let's excempt\n> developers on macOS\" and the \"let's see how far we can go with the\n> warning turned on everywhere and wack-a-mole this particular one\n> with errno check\" patches, I prefer the latter at least for a short\n> term.\n\nYeah, I'm also in favor of generally enabling the warning and seeing\nwhether it will end up being a pain or not. This particular edge case\nhere is ugly, but it's manageable and may protect us from mistakes in\nother places going forward.\n\nIf we do so, could we please also include the following patch for Meson?\n\nThanks!\n\nPatrick\n\ndiff --git a/meson.build b/meson.build\nindex efe2871c9d..a0a602864a 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -721,6 +721,7 @@ if get_option('warning_level') in ['2','3', 'everything'] and compiler.get_argum\n     '-Woverflow',\n     '-Wpointer-arith',\n     '-Wstrict-prototypes',\n+    '-Wunreachable-code',\n     '-Wunused',\n     '-Wvla',\n     '-Wwrite-strings',\n"},{"id":"514293","messageId":"20250314185325.GC578421@coredump.intra.peff.net","threadId":"63064","inReplyTo":"xmqqr02zfr3r.fsf@gitster.g","subject":"Re: [PATCH] config.mak.dev: enable -Wunreachable-code","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-03-14T18:53:25Z","receivedAt":"2025-03-14T18:53:26Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Fri, Mar 14, 2025 at 10:40:24AM -0700, Junio C Hamano wrote:\n\n> >> -- >8 --\n> >> Subject: [PATCH] run-command: use errno to check for sigfillset() error\n> >>\n> >> Since enabling -Wunreachable-code, builds with clang on macOS now fail,\n> >> complaining that the die_errno() call in:\n> >>\n> >>   if (sigfillset(&all))\n> >> \tdie_errno(\"sigfillset\");\n> >>\n> >> is unreachable. On that platform the manpage documents that sigfillset()\n> >> always returns success, and presumably the implementation is a macro or\n> >> inline function that does so in a way that is transparent to the\n> >> compiler.\n> >\n> > Would it work to instead do this here\n> > ...\n> \n> I forgot to say a more important thing.  Between the \"let's excempt\n> developers on macOS\" and the \"let's see how far we can go with the\n> warning turned on everywhere and wack-a-mole this particular one\n> with errno check\" patches, I prefer the latter at least for a short\n> term.\n\nThat's my gut feeling, too. I wasn't sure how people would feel about\nactually touching the code (whereas the other patches were purely\nturning compiler knobs). It may turn into wack-a-mole, but finding out\nis part of the experiment.\n\nYour CAN_BE_TAKEN() approach is certainly less subtle, and can be\napplied in a more general way. If this is the only spot needed it may be\noverkill, but the readability improvement alone probably makes it\nworthwhile.\n\nDo you want to turn that into a patch?\n\n-Peff\n"},{"id":"514300","messageId":"xmqq7c4rfl3o.fsf@gitster.g","threadId":"63064","inReplyTo":"20250314185325.GC578421@coredump.intra.peff.net","subject":"Re: [PATCH] config.mak.dev: enable -Wunreachable-code","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-14T19:50:03Z","receivedAt":"2025-03-14T19:50:07Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> Your CAN_BE_TAKEN() approach is certainly less subtle, and can be\n> applied in a more general way. If this is the only spot needed it may be\n> overkill, but the readability improvement alone probably makes it\n> worthwhile.\n>\n> Do you want to turn that into a patch?\n\nYes, but after I come up with a better name.  CAN_BE_TAKEN may be OK\nfor if/while but not good enough for switch() for example.  \"Do not\nopmimize out because, despite your beliefs, this expression is ...\"\nis what we want to convey.  \n\n Makefile          |  1 +\n git-compat-util.h |  9 +++++++++\n meson.build       |  1 +\n run-command.c     | 12 +++++-------\n 4 files changed, 16 insertions(+), 7 deletions(-)\n\ndiff --git c/Makefile w/Makefile\nindex 97e8385b66..2158bf6916 100644\n--- c/Makefile\n+++ w/Makefile\n@@ -1018,6 +1018,7 @@ LIB_OBJS += ewah/ewah_bitmap.o\n LIB_OBJS += ewah/ewah_io.o\n LIB_OBJS += ewah/ewah_rlw.o\n LIB_OBJS += exec-cmd.o\n+LIB_OBJS += fbtcdnki.o\n LIB_OBJS += fetch-negotiator.o\n LIB_OBJS += fetch-pack.o\n LIB_OBJS += fmt-merge-msg.o\ndiff --git c/git-compat-util.h w/git-compat-util.h\nindex e283c46c6f..63a3ef6b70 100644\n--- c/git-compat-util.h\n+++ w/git-compat-util.h\n@@ -1593,4 +1593,13 @@ static inline void *container_of_or_null_offset(void *ptr, size_t offset)\n \t((uintptr_t)&(ptr)->member - (uintptr_t)(ptr))\n #endif /* !__GNUC__ */\n \n+/*\n+ * Prevent an overly clever compiler from optimizing an expression\n+ * out, triggering a false positive when building with the\n+ * -Wunreachable-code option. false_but_the_compiler_does_not_know_it_\n+ * is defined in a compilation unit separate from where the macro is\n+ * used, initialized to 0, and never modified.\n+ */\n+#define NOT_A_CONST(expr) ((expr) || false_but_the_compiler_does_not_know_it_)\n+extern int false_but_the_compiler_does_not_know_it_;\n #endif\ndiff --git c/meson.build w/meson.build\nindex f60f3f49e4..ce642dcf65 100644\n--- c/meson.build\n+++ w/meson.build\n@@ -282,6 +282,7 @@ libgit_sources = [\n   'ewah/ewah_io.c',\n   'ewah/ewah_rlw.c',\n   'exec-cmd.c',\n+  'fbtcdnki.c',\n   'fetch-negotiator.c',\n   'fetch-pack.c',\n   'fmt-merge-msg.c',\ndiff --git c/run-command.c w/run-command.c\nindex d527c46175..535c73a059 100644\n--- c/run-command.c\n+++ w/run-command.c\n@@ -516,14 +516,12 @@ static void atfork_prepare(struct atfork_state *as)\n \tsigset_t all;\n \n \t/*\n-\t * Do not use the return value of sigfillset(). It is transparently 0\n-\t * on some platforms, meaning a clever compiler may complain that\n-\t * the conditional body is dead code. Instead, check for error via\n-\t * errno, which outsmarts the compiler.\n+\t * POSIX says sitfillset() can fail, but an overly clever\n+\t * compiler can see through the header files and decide\n+\t * it cannot fail on a particular platform it is compiling for,\n+\t * triggering -Wunreachable-code false positive.\n \t */\n-\terrno = 0;\n-\tsigfillset(&all);\n-\tif (errno)\n+\tif (NOT_A_CONST(sigfillset(&all)))\n \t\tdie_errno(\"sigfillset\");\n #ifdef NO_PTHREADS\n \tif (sigprocmask(SIG_SETMASK, &all, &as->old))\n\n\n"},{"id":"514320","messageId":"20250314210909.3776678-1-gitster@pobox.com","threadId":"63064","inReplyTo":"20250307225444.GA42758@coredump.intra.peff.net","subject":"[PATCH v2 0/3] -Wunreachable-code","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-14T21:09:06Z","receivedAt":"2025-03-14T21:09:12Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"So here is a recap.  The first one has meson.build change from\nPatrick squashed in, the second \"errno\" based one was what made\nme write the last one, and is kept as-is.  The third one introduces\nNOT_A_CONST() marking to an expression to tell the compiler not to\nbe overly aggressive to optimize it out.\n\nJeff King (2):\n  config.mak.dev: enable -Wunreachable-code\n  run-command: use errno to check for sigfillset() error\n\nJunio C Hamano (1):\n  git-compat-util: add NOT_A_CONST macro and use it in atfork_prepare()\n\n Makefile          | 1 +\n config.mak.dev    | 1 +\n git-compat-util.h | 9 +++++++++\n meson.build       | 2 ++\n run-command.c     | 8 +++++++-\n 5 files changed, 20 insertions(+), 1 deletion(-)\n\n-- \n2.49.0-188-g35fcca2323\n\n"},{"id":"514318","messageId":"20250314210909.3776678-2-gitster@pobox.com","threadId":"63064","inReplyTo":"20250314210909.3776678-1-gitster@pobox.com","subject":"[PATCH v2 1/3] config.mak.dev: enable -Wunreachable-code","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-14T21:09:07Z","receivedAt":"2025-03-14T21:09:13Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"From: Jeff King <peff@peff.net>\n\nHaving the compiler point out unreachable code can help avoid bugs, like\nthe one discussed in:\n\n  https://lore.kernel.org/git/20250307195057.GA3675279@coredump.intra.peff.net/\n\nIn that case it was found by Coverity, but finding it earlier saves\neverybody time and effort.\n\nWe can use -Wunreachable-code to get some help from the compiler here.\nInterestingly, this is a noop in gcc. It was a real warning up until gcc\n4.x, when it was removed for being too flaky, but they left the\ncommand-line option to avoid breaking users. See:\n\n  https://stackoverflow.com/questions/17249934/why-does-gcc-not-warn-for-unreachable-code\n\nHowever, clang does implement this option, and it finds the case\nmentioned above (and no other cases within the code base). And since we\nrun clang in several of our CI jobs, that's enough to get an early\nwarning of breakage.\n\nWe could enable it only for clang, but since gcc is happy to ignore it,\nit's simpler to just turn it on for all developer builds.\n\nSigned-off-by: Jeff King <peff@peff.net>\n[jc: squashed meson.build change sent by Patrick]\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n config.mak.dev | 1 +\n meson.build    | 1 +\n 2 files changed, 2 insertions(+)\n\ndiff --git a/config.mak.dev b/config.mak.dev\nindex 0fd8cc4d35..95b7bc46ae 100644\n--- a/config.mak.dev\n+++ b/config.mak.dev\n@@ -39,6 +39,7 @@ DEVELOPER_CFLAGS += -Wunused\n DEVELOPER_CFLAGS += -Wvla\n DEVELOPER_CFLAGS += -Wwrite-strings\n DEVELOPER_CFLAGS += -fno-common\n+DEVELOPER_CFLAGS += -Wunreachable-code\n \n ifneq ($(filter clang4,$(COMPILER_FEATURES)),)\n DEVELOPER_CFLAGS += -Wtautological-constant-out-of-range-compare\ndiff --git a/meson.build b/meson.build\nindex 0064eb64f5..f60f3f49e4 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -697,6 +697,7 @@ if get_option('warning_level') in ['2','3', 'everything'] and compiler.get_argum\n     '-Woverflow',\n     '-Wpointer-arith',\n     '-Wstrict-prototypes',\n+    '-Wunreachable-code',\n     '-Wunused',\n     '-Wvla',\n     '-Wwrite-strings',\n-- \n2.49.0-188-g35fcca2323\n\n"},{"id":"514319","messageId":"20250314210909.3776678-3-gitster@pobox.com","threadId":"63064","inReplyTo":"20250314210909.3776678-1-gitster@pobox.com","subject":"[PATCH v2 2/3] run-command: use errno to check for sigfillset() error","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-14T21:09:08Z","receivedAt":"2025-03-14T21:09:15Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"From: Jeff King <peff@peff.net>\n\nSince enabling -Wunreachable-code, builds with clang on macOS now fail,\ncomplaining that the die_errno() call in:\n\n  if (sigfillset(&all))\n\tdie_errno(\"sigfillset\");\n\nis unreachable. On that platform the manpage documents that sigfillset()\nalways returns success, and presumably the implementation is a macro or\ninline function that does so in a way that is transparent to the\ncompiler.\n\nBut we should continue to check on other platforms, since POSIX says it\nmay return an error.\n\nWe could solve this with a compile-time knob to split the two cases\n(assuming success on macOS and checking for the error elsewhere). But we\ncan also work around it more directly by relying on errno to check the\noutcome (since POSIX dictates that errno will be set on error). And that\nworks around the compiler's cleverness, since it doesn't know the\nsemantics of errno (though I suppose if sigfillset() is simple enough,\nit could perhaps realize that no writes to errno are possible; however\nthis does seem to work in practice).\n\nSigned-off-by: Jeff King <peff@peff.net>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n run-command.c | 10 +++++++++-\n 1 file changed, 9 insertions(+), 1 deletion(-)\n\ndiff --git a/run-command.c b/run-command.c\nindex 402138b8b5..d527c46175 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -515,7 +515,15 @@ static void atfork_prepare(struct atfork_state *as)\n {\n \tsigset_t all;\n \n-\tif (sigfillset(&all))\n+\t/*\n+\t * Do not use the return value of sigfillset(). It is transparently 0\n+\t * on some platforms, meaning a clever compiler may complain that\n+\t * the conditional body is dead code. Instead, check for error via\n+\t * errno, which outsmarts the compiler.\n+\t */\n+\terrno = 0;\n+\tsigfillset(&all);\n+\tif (errno)\n \t\tdie_errno(\"sigfillset\");\n #ifdef NO_PTHREADS\n \tif (sigprocmask(SIG_SETMASK, &all, &as->old))\n-- \n2.49.0-188-g35fcca2323\n\n"},{"id":"514321","messageId":"20250314210909.3776678-4-gitster@pobox.com","threadId":"63064","inReplyTo":"20250314210909.3776678-1-gitster@pobox.com","subject":"[PATCH v2 3/3] git-compat-util: add NOT_A_CONST macro and use it in atfork_prepare()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-14T21:09:09Z","receivedAt":"2025-03-14T21:09:17Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Our hope is that the number of code paths that falsely trigger\nwarnings with the -Wunreachable-code compilation option are small,\nand they can be worked around case-by-case basis, like we just did\nin the previous commit.  If we need such a workaround a bit more\noften, however, we may benefit from a more generic and descriptive\nfacility that helps document the cases we need such workarounds.\n\n    Side note: if we need the workaround all over the place, it\n    simply means -Wunreachable-code is not a good tool for us to\n    save engineering effort to catch mistakes.  We are still\n    exploring if it helps us, so let's assume that it is not the\n    case.\n\nIntroduce NOT_A_CONST() macro, with which, the developer can tell\nthe compiler:\n\n    Do not optimize this expression out, because, despite whatever\n    you are told by the system headers, this expression should *not*\n    be treated as a constant.\n\nand use it as a replacement for the workaround we used that was\nsomewhat specific to the sigfillset case.  If the compiler already\nknows that the call to sigfillset() cannot fail on a particular\nplatform it is compiling for and declares that the if() condition\nwould not hold, it is plausible that the next version of the\ncompiler may learn that sigfillset() that never fails would not\ntouch errno and decide that in this sequence:\n\n\terrno = 0;\n\tsigfillset(&all)\n\tif (errno)\n\t\tdie_errno(\"sigfillset\");\n\nthe if() statement will never trigger.  Marking that the value\nreturned by sigfillset() cannot be a constant would document our\nintention better and would not break with such a new version of\ncompiler that is even more \"clever\".  With the marco, the above\nsequence can be rewritten:\n\n\tif (NOT_A_CONST(sigfillset(&all)))\n\t\tdie_errno(\"sigfillset\");\n\nwhich looks almost like other innocuous annotations we have,\ne.g. UNUSED.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Makefile          |  1 +\n git-compat-util.h |  9 +++++++++\n meson.build       |  1 +\n run-command.c     | 12 +++++-------\n 4 files changed, 16 insertions(+), 7 deletions(-)\n\ndiff --git a/Makefile b/Makefile\nindex 97e8385b66..2158bf6916 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1018,6 +1018,7 @@ LIB_OBJS += ewah/ewah_bitmap.o\n LIB_OBJS += ewah/ewah_io.o\n LIB_OBJS += ewah/ewah_rlw.o\n LIB_OBJS += exec-cmd.o\n+LIB_OBJS += fbtcdnki.o\n LIB_OBJS += fetch-negotiator.o\n LIB_OBJS += fetch-pack.o\n LIB_OBJS += fmt-merge-msg.o\ndiff --git a/git-compat-util.h b/git-compat-util.h\nindex e283c46c6f..63a3ef6b70 100644\n--- a/git-compat-util.h\n+++ b/git-compat-util.h\n@@ -1593,4 +1593,13 @@ static inline void *container_of_or_null_offset(void *ptr, size_t offset)\n \t((uintptr_t)&(ptr)->member - (uintptr_t)(ptr))\n #endif /* !__GNUC__ */\n \n+/*\n+ * Prevent an overly clever compiler from optimizing an expression\n+ * out, triggering a false positive when building with the\n+ * -Wunreachable-code option. false_but_the_compiler_does_not_know_it_\n+ * is defined in a compilation unit separate from where the macro is\n+ * used, initialized to 0, and never modified.\n+ */\n+#define NOT_A_CONST(expr) ((expr) || false_but_the_compiler_does_not_know_it_)\n+extern int false_but_the_compiler_does_not_know_it_;\n #endif\ndiff --git a/meson.build b/meson.build\nindex f60f3f49e4..ce642dcf65 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -282,6 +282,7 @@ libgit_sources = [\n   'ewah/ewah_io.c',\n   'ewah/ewah_rlw.c',\n   'exec-cmd.c',\n+  'fbtcdnki.c',\n   'fetch-negotiator.c',\n   'fetch-pack.c',\n   'fmt-merge-msg.c',\ndiff --git a/run-command.c b/run-command.c\nindex d527c46175..535c73a059 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -516,14 +516,12 @@ static void atfork_prepare(struct atfork_state *as)\n \tsigset_t all;\n \n \t/*\n-\t * Do not use the return value of sigfillset(). It is transparently 0\n-\t * on some platforms, meaning a clever compiler may complain that\n-\t * the conditional body is dead code. Instead, check for error via\n-\t * errno, which outsmarts the compiler.\n+\t * POSIX says sitfillset() can fail, but an overly clever\n+\t * compiler can see through the header files and decide\n+\t * it cannot fail on a particular platform it is compiling for,\n+\t * triggering -Wunreachable-code false positive.\n \t */\n-\terrno = 0;\n-\tsigfillset(&all);\n-\tif (errno)\n+\tif (NOT_A_CONST(sigfillset(&all)))\n \t\tdie_errno(\"sigfillset\");\n #ifdef NO_PTHREADS\n \tif (sigprocmask(SIG_SETMASK, &all, &as->old))\n-- \n2.49.0-188-g35fcca2323\n\n"},{"id":"514328","messageId":"xmqqecyzdz4t.fsf@gitster.g","threadId":"63064","inReplyTo":"20250314210909.3776678-4-gitster@pobox.com","subject":"Re: [PATCH v2 3/3] git-compat-util: add NOT_A_CONST macro and use it in atfork_prepare()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-14T22:29:54Z","receivedAt":"2025-03-14T22:29:59Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Sorry, one new file was left out of the patch.  Here is a quick fix\n(I am not rerolling the earlier 2 steps).\n\n---- >8 ----\nOur hope is that the number of code paths that falsely trigger\nwarnings with the -Wunreachable-code compilation option are small,\nand they can be worked around case-by-case basis, like we just did\nin the previous commit.  If we need such a workaround a bit more\noften, however, we may benefit from a more generic and descriptive\nfacility that helps document the cases we need such workarounds.\n\n    Side note: if we need the workaround all over the place, it\n    simply means -Wunreachable-code is not a good tool for us to\n    save engineering effort to catch mistakes.  We are still\n    exploring if it helps us, so let's assume that it is not the\n    case.\n\nIntroduce NOT_A_CONST() macro, with which, the developer can tell\nthe compiler:\n\n    Do not optimize this expression out, because, despite whatever\n    you are told by the system headers, this expression should *not*\n    be treated as a constant.\n\nand use it as a replacement for the workaround we used that was\nsomewhat specific to the sigfillset case.  If the compiler already\nknows that the call to sigfillset() cannot fail on a particular\nplatform it is compiling for and declares that the if() condition\nwould not hold, it is plausible that the next version of the\ncompiler may learn that sigfillset() that never fails would not\ntouch errno and decide that in this sequence:\n\n\terrno = 0;\n\tsigfillset(&all)\n\tif (errno)\n\t\tdie_errno(\"sigfillset\");\n\nthe if() statement will never trigger.  Marking that the value\nreturned by sigfillset() cannot be a constant would document our\nintention better and would not break with such a new version of\ncompiler that is even more \"clever\".  With the marco, the above\nsequence can be rewritten:\n\n\tif (NOT_A_CONST(sigfillset(&all)))\n\t\tdie_errno(\"sigfillset\");\n\nwhich looks almost like other innocuous annotations we have,\ne.g. UNUSED.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Makefile          |  1 +\n fbtcdnki.c        |  2 ++\n git-compat-util.h |  9 +++++++++\n meson.build       |  1 +\n run-command.c     | 12 +++++-------\n 5 files changed, 18 insertions(+), 7 deletions(-)\n create mode 100644 fbtcdnki.c\n\ndiff --git a/Makefile b/Makefile\nindex 97e8385b66..2158bf6916 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1018,6 +1018,7 @@ LIB_OBJS += ewah/ewah_bitmap.o\n LIB_OBJS += ewah/ewah_io.o\n LIB_OBJS += ewah/ewah_rlw.o\n LIB_OBJS += exec-cmd.o\n+LIB_OBJS += fbtcdnki.o\n LIB_OBJS += fetch-negotiator.o\n LIB_OBJS += fetch-pack.o\n LIB_OBJS += fmt-merge-msg.o\ndiff --git a/fbtcdnki.c b/fbtcdnki.c\nnew file mode 100644\nindex 0000000000..1da3ffc2f5\n--- /dev/null\n+++ b/fbtcdnki.c\n@@ -0,0 +1,2 @@\n+#include <git-compat-util.h>\n+int false_but_the_compiler_does_not_know_it_;\ndiff --git a/git-compat-util.h b/git-compat-util.h\nindex e283c46c6f..63a3ef6b70 100644\n--- a/git-compat-util.h\n+++ b/git-compat-util.h\n@@ -1593,4 +1593,13 @@ static inline void *container_of_or_null_offset(void *ptr, size_t offset)\n \t((uintptr_t)&(ptr)->member - (uintptr_t)(ptr))\n #endif /* !__GNUC__ */\n \n+/*\n+ * Prevent an overly clever compiler from optimizing an expression\n+ * out, triggering a false positive when building with the\n+ * -Wunreachable-code option. false_but_the_compiler_does_not_know_it_\n+ * is defined in a compilation unit separate from where the macro is\n+ * used, initialized to 0, and never modified.\n+ */\n+#define NOT_A_CONST(expr) ((expr) || false_but_the_compiler_does_not_know_it_)\n+extern int false_but_the_compiler_does_not_know_it_;\n #endif\ndiff --git a/meson.build b/meson.build\nindex f60f3f49e4..ce642dcf65 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -282,6 +282,7 @@ libgit_sources = [\n   'ewah/ewah_io.c',\n   'ewah/ewah_rlw.c',\n   'exec-cmd.c',\n+  'fbtcdnki.c',\n   'fetch-negotiator.c',\n   'fetch-pack.c',\n   'fmt-merge-msg.c',\ndiff --git a/run-command.c b/run-command.c\nindex d527c46175..535c73a059 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -516,14 +516,12 @@ static void atfork_prepare(struct atfork_state *as)\n \tsigset_t all;\n \n \t/*\n-\t * Do not use the return value of sigfillset(). It is transparently 0\n-\t * on some platforms, meaning a clever compiler may complain that\n-\t * the conditional body is dead code. Instead, check for error via\n-\t * errno, which outsmarts the compiler.\n+\t * POSIX says sitfillset() can fail, but an overly clever\n+\t * compiler can see through the header files and decide\n+\t * it cannot fail on a particular platform it is compiling for,\n+\t * triggering -Wunreachable-code false positive.\n \t */\n-\terrno = 0;\n-\tsigfillset(&all);\n-\tif (errno)\n+\tif (NOT_A_CONST(sigfillset(&all)))\n \t\tdie_errno(\"sigfillset\");\n #ifdef NO_PTHREADS\n \tif (sigprocmask(SIG_SETMASK, &all, &as->old))\n-- \n2.49.0-188-g35fcca2323\n\n"},{"id":"514426","messageId":"20250317180014.GA704553@coredump.intra.peff.net","threadId":"63064","inReplyTo":"xmqqecyzdz4t.fsf@gitster.g","subject":"Re: [PATCH v2 3/3] git-compat-util: add NOT_A_CONST macro and use it in atfork_prepare()","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-03-17T18:00:14Z","receivedAt":"2025-03-17T18:00:15Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Fri, Mar 14, 2025 at 03:29:54PM -0700, Junio C Hamano wrote:\n\n> ---- >8 ----\n> Our hope is that the number of code paths that falsely trigger\n> warnings with the -Wunreachable-code compilation option are small,\n> and they can be worked around case-by-case basis, like we just did\n> in the previous commit.  If we need such a workaround a bit more\n> often, however, we may benefit from a more generic and descriptive\n> facility that helps document the cases we need such workarounds.\n> \n>     Side note: if we need the workaround all over the place, it\n>     simply means -Wunreachable-code is not a good tool for us to\n>     save engineering effort to catch mistakes.  We are still\n>     exploring if it helps us, so let's assume that it is not the\n>     case.\n\nYup, I very much agree with this, especially the side note. (I'd\nprobably have just dropped patch 2 and gone straight here, but I don't\nmind leaving it in as documentation of that other direction).\n\n> Introduce NOT_A_CONST() macro, with which, the developer can tell\n> the compiler:\n> \n>     Do not optimize this expression out, because, despite whatever\n>     you are told by the system headers, this expression should *not*\n>     be treated as a constant.\n\nThis is definitely better than the other name. I might spell it out\nas \"NOT_A_CONSTANT\", just because \"const\" to me is a variable annotation\n(for something that _could_ change, but we are not allowed to). Whereas\n\"constant\" is something defined to a single value in the program. Maybe\nsplitting hairs, but as somebody who read NOT_A_CONST(foo) I might\nexpect it to be casting away \"const\" or something.\n\n> --- a/Makefile\n> +++ b/Makefile\n> @@ -1018,6 +1018,7 @@ LIB_OBJS += ewah/ewah_bitmap.o\n>  LIB_OBJS += ewah/ewah_io.o\n>  LIB_OBJS += ewah/ewah_rlw.o\n>  LIB_OBJS += exec-cmd.o\n> +LIB_OBJS += fbtcdnki.o\n\nThat name is a mouthful, for sure. The long name is really an\nimplementation detail. Would calling it not-constant.c or something be\nmore descriptive? (Yes, the macro itself does not appear in the file,\nbut hopefully it links the two semantically in the reader's head).\n\nI almost want to suggest a name like \"compiler-tricks.c\", but part of\nthe point of this particular trick is that there's nothing else in its\ntranslation unit. So later when somebody adds another trick, it cannot\nuse this macro. ;)\n\n> +/*\n> + * Prevent an overly clever compiler from optimizing an expression\n> + * out, triggering a false positive when building with the\n> + * -Wunreachable-code option. false_but_the_compiler_does_not_know_it_\n> + * is defined in a compilation unit separate from where the macro is\n> + * used, initialized to 0, and never modified.\n> + */\n> +#define NOT_A_CONST(expr) ((expr) || false_but_the_compiler_does_not_know_it_)\n> +extern int false_but_the_compiler_does_not_know_it_;\n\nGood explanation. I do wonder if we'd eventually see a compiler that\nreaches across translation units to optimize, but I'd hope we probably\nbought ourselves a decade or two.\n\n> diff --git a/run-command.c b/run-command.c\n> index d527c46175..535c73a059 100644\n> --- a/run-command.c\n> +++ b/run-command.c\n> @@ -516,14 +516,12 @@ static void atfork_prepare(struct atfork_state *as)\n>  \tsigset_t all;\n>  \n>  \t/*\n> -\t * Do not use the return value of sigfillset(). It is transparently 0\n> -\t * on some platforms, meaning a clever compiler may complain that\n> -\t * the conditional body is dead code. Instead, check for error via\n> -\t * errno, which outsmarts the compiler.\n> +\t * POSIX says sitfillset() can fail, but an overly clever\n> +\t * compiler can see through the header files and decide\n> +\t * it cannot fail on a particular platform it is compiling for,\n> +\t * triggering -Wunreachable-code false positive.\n>  \t */\n> -\terrno = 0;\n> -\tsigfillset(&all);\n> -\tif (errno)\n> +\tif (NOT_A_CONST(sigfillset(&all)))\n>  \t\tdie_errno(\"sigfillset\");\n\nAnd this looks much nicer and more descriptive. You could probably even\nget away without the comment, but I certainly do not mind it.\n\ns/sitfillset/sigfillset/ in your comment text, though.\n\n-Peff\n"},{"id":"514436","messageId":"Z9iUe3Hg30W5LFSZ@nand.local","threadId":"63064","inReplyTo":"20250314210909.3776678-3-gitster@pobox.com","subject":"Re: [PATCH v2 2/3] run-command: use errno to check for sigfillset() error","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2025-03-17T21:30:35Z","receivedAt":"2025-03-17T21:30:38Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Fri, Mar 14, 2025 at 02:09:08PM -0700, Junio C Hamano wrote:\n> From: Jeff King <peff@peff.net>\n>\n> Since enabling -Wunreachable-code, builds with clang on macOS now fail,\n> complaining that the die_errno() call in:\n>\n>   if (sigfillset(&all))\n> \tdie_errno(\"sigfillset\");\n\nHmm. Would it have made sense to swap the order of this and the first\npatch so we don't have a DEVELOPER=1 breakage (for macOS with Clang) in\nhistory?\n\nI think it's too late now since this topic is already on 'next', but it\noccurred to me idly while reading this patch.\n\nThanks,\nTaylor\n"},{"id":"514466","messageId":"xmqqwmcn5k11.fsf@gitster.g","threadId":"63064","inReplyTo":"Z9iUe3Hg30W5LFSZ@nand.local","subject":"Re: [PATCH v2 2/3] run-command: use errno to check for sigfillset() error","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-17T23:12:26Z","receivedAt":"2025-03-17T23:12:28Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Taylor Blau <me@ttaylorr.com> writes:\n\n> On Fri, Mar 14, 2025 at 02:09:08PM -0700, Junio C Hamano wrote:\n>> From: Jeff King <peff@peff.net>\n>>\n>> Since enabling -Wunreachable-code, builds with clang on macOS now fail,\n>> complaining that the die_errno() call in:\n>>\n>>   if (sigfillset(&all))\n>> \tdie_errno(\"sigfillset\");\n>\n> Hmm. Would it have made sense to swap the order of this and the first\n> patch so we don't have a DEVELOPER=1 breakage (for macOS with Clang) in\n> history?\n>\n> I think it's too late now since this topic is already on 'next', but it\n> occurred to me idly while reading this patch.\n\nI thought db1d1f5d (config.mak.dev: enable -Wunreachable-code,\n2025-03-14) aka jk/use-wunreachable-code-for-devs~2 is still out of\n'next'?\n"},{"id":"514469","messageId":"20250317235329.809302-1-gitster@pobox.com","threadId":"63064","inReplyTo":"20250314210909.3776678-1-gitster@pobox.com","subject":"[PATCH v3 0/3] -Wunreachable-code","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-17T23:53:26Z","receivedAt":"2025-03-17T23:53:32Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"As Taylor noticed, we can still help macOS users by first dealing\nwith the false positive in the code, and then flip the warning\noption for developers on.\n\n [1/3] run-command: use errno to check for sigfillset() error\n\n This was our first \"workaround\" that is very specific to the code\n that gets falsely flagged by the compiler.\n\n [2/3] git-compat-util: add NOT_CONSTANT macro and use it in atfork_prepare()\n\n This adds a more generic way to work around a false positive from\n -Wunreachable-code to prevent compilers from optimize away\n expressions that are used in conditionals, and rewrite the earlier\n workaround with it.\n\n [3/3] config.mak.dev: enable -Wunreachable-code\n\n Now we worked around known false positive of -Wunreachable-code,\n we force it upon our developers, including macOS ones.\n\nThis is totally offtopic, but I often find the short-log (list of\ncommits, grouped by author) in the cover letter very awkward to work\nwith.  Between v2 and v3, aside from the NOT_CONSTANT() improvements\nin the patch [2/3] that used to be [3/3], one large change is the\nreordering of the patches but that is not seen in the shortlog (I\nran \"git log --oneline -reverse\" to prepare the list of commits in\nthe order they are applied to describe them in the above list).\n\nJeff King (2):\n  run-command: use errno to check for sigfillset() error\n  config.mak.dev: enable -Wunreachable-code\n\nJunio C Hamano (1):\n  git-compat-util: add NOT_CONSTANT macro and use it in atfork_prepare()\n\n Makefile                         | 1 +\n compiler-tricks/not-a-constant.c | 2 ++\n config.mak.dev                   | 1 +\n git-compat-util.h                | 9 +++++++++\n meson.build                      | 2 ++\n run-command.c                    | 8 +++++++-\n 6 files changed, 22 insertions(+), 1 deletion(-)\n create mode 100644 compiler-tricks/not-a-constant.c\n\n-- \n2.49.0-207-gc8924421c3\n\n"},{"id":"514470","messageId":"20250317235329.809302-2-gitster@pobox.com","threadId":"63064","inReplyTo":"20250317235329.809302-1-gitster@pobox.com","subject":"[PATCH v3 1/3] run-command: use errno to check for sigfillset() error","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-17T23:53:27Z","receivedAt":"2025-03-17T23:53:33Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"From: Jeff King <peff@peff.net>\n\nSince enabling -Wunreachable-code, builds with clang on macOS now fail,\ncomplaining that the die_errno() call in:\n\n  if (sigfillset(&all))\n\tdie_errno(\"sigfillset\");\n\nis unreachable. On that platform the manpage documents that sigfillset()\nalways returns success, and presumably the implementation is a macro or\ninline function that does so in a way that is transparent to the\ncompiler.\n\nBut we should continue to check on other platforms, since POSIX says it\nmay return an error.\n\nWe could solve this with a compile-time knob to split the two cases\n(assuming success on macOS and checking for the error elsewhere). But we\ncan also work around it more directly by relying on errno to check the\noutcome (since POSIX dictates that errno will be set on error). And that\nworks around the compiler's cleverness, since it doesn't know the\nsemantics of errno (though I suppose if sigfillset() is simple enough,\nit could perhaps realize that no writes to errno are possible; however\nthis does seem to work in practice).\n\nSigned-off-by: Jeff King <peff@peff.net>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n run-command.c | 10 +++++++++-\n 1 file changed, 9 insertions(+), 1 deletion(-)\n\ndiff --git a/run-command.c b/run-command.c\nindex 402138b8b5..d527c46175 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -515,7 +515,15 @@ static void atfork_prepare(struct atfork_state *as)\n {\n \tsigset_t all;\n \n-\tif (sigfillset(&all))\n+\t/*\n+\t * Do not use the return value of sigfillset(). It is transparently 0\n+\t * on some platforms, meaning a clever compiler may complain that\n+\t * the conditional body is dead code. Instead, check for error via\n+\t * errno, which outsmarts the compiler.\n+\t */\n+\terrno = 0;\n+\tsigfillset(&all);\n+\tif (errno)\n \t\tdie_errno(\"sigfillset\");\n #ifdef NO_PTHREADS\n \tif (sigprocmask(SIG_SETMASK, &all, &as->old))\n-- \n2.49.0-207-gc8924421c3\n\n"},{"id":"514471","messageId":"20250317235329.809302-3-gitster@pobox.com","threadId":"63064","inReplyTo":"20250317235329.809302-1-gitster@pobox.com","subject":"[PATCH v3 2/3] git-compat-util: add NOT_CONSTANT macro and use it in atfork_prepare()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-17T23:53:28Z","receivedAt":"2025-03-17T23:53:35Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Our hope is that the number of code paths that falsely trigger\nwarnings with the -Wunreachable-code compilation option are small,\nand they can be worked around case-by-case basis, like we just did\nin the previous commit.  If we need such a workaround a bit more\noften, however, we may benefit from a more generic and descriptive\nfacility that helps document the cases we need such workarounds.\n\n    Side note: if we need the workaround all over the place, it\n    simply means -Wunreachable-code is not a good tool for us to\n    save engineering effort to catch mistakes.  We are still\n    exploring if it helps us, so let's assume that it is not the\n    case.\n\nIntroduce NOT_CONSTANT() macro, with which, the developer can tell\nthe compiler:\n\n    Do not optimize this expression out, because, despite whatever\n    you are told by the system headers, this expression should *not*\n    be treated as a constant.\n\nand use it as a replacement for the workaround we used that was\nsomewhat specific to the sigfillset case.  If the compiler already\nknows that the call to sigfillset() cannot fail on a particular\nplatform it is compiling for and declares that the if() condition\nwould not hold, it is plausible that the next version of the\ncompiler may learn that sigfillset() that never fails would not\ntouch errno and decide that in this sequence:\n\n\terrno = 0;\n\tsigfillset(&all)\n\tif (errno)\n\t\tdie_errno(\"sigfillset\");\n\nthe if() statement will never trigger.  Marking that the value\nreturned by sigfillset() cannot be a constant would document our\nintention better and would not break with such a new version of\ncompiler that is even more \"clever\".  With the marco, the above\nsequence can be rewritten:\n\n\tif (NOT_CONSTANT(sigfillset(&all)))\n\t\tdie_errno(\"sigfillset\");\n\nwhich looks almost like other innocuous annotations we have,\ne.g. UNUSED.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Makefile                         |  1 +\n compiler-tricks/not-a-constant.c |  2 ++\n git-compat-util.h                |  9 +++++++++\n meson.build                      |  1 +\n run-command.c                    | 12 +++++-------\n 5 files changed, 18 insertions(+), 7 deletions(-)\n create mode 100644 compiler-tricks/not-a-constant.c\n\ndiff --git a/Makefile b/Makefile\nindex 97e8385b66..605e2d7f61 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -985,6 +985,7 @@ LIB_OBJS += compat/nonblock.o\n LIB_OBJS += compat/obstack.o\n LIB_OBJS += compat/terminal.o\n LIB_OBJS += compat/zlib-uncompress2.o\n+LIB_OBJS += compiler-tricks/not-a-constant.o\n LIB_OBJS += config.o\n LIB_OBJS += connect.o\n LIB_OBJS += connected.o\ndiff --git a/compiler-tricks/not-a-constant.c b/compiler-tricks/not-a-constant.c\nnew file mode 100644\nindex 0000000000..1da3ffc2f5\n--- /dev/null\n+++ b/compiler-tricks/not-a-constant.c\n@@ -0,0 +1,2 @@\n+#include <git-compat-util.h>\n+int false_but_the_compiler_does_not_know_it_;\ndiff --git a/git-compat-util.h b/git-compat-util.h\nindex e283c46c6f..f6a149827b 100644\n--- a/git-compat-util.h\n+++ b/git-compat-util.h\n@@ -1593,4 +1593,13 @@ static inline void *container_of_or_null_offset(void *ptr, size_t offset)\n \t((uintptr_t)&(ptr)->member - (uintptr_t)(ptr))\n #endif /* !__GNUC__ */\n \n+/*\n+ * Prevent an overly clever compiler from optimizing an expression\n+ * out, triggering a false positive when building with the\n+ * -Wunreachable-code option. false_but_the_compiler_does_not_know_it_\n+ * is defined in a compilation unit separate from where the macro is\n+ * used, initialized to 0, and never modified.\n+ */\n+#define NOT_CONSTANT(expr) ((expr) || false_but_the_compiler_does_not_know_it_)\n+extern int false_but_the_compiler_does_not_know_it_;\n #endif\ndiff --git a/meson.build b/meson.build\nindex 0064eb64f5..373524dad2 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -249,6 +249,7 @@ libgit_sources = [\n   'compat/obstack.c',\n   'compat/terminal.c',\n   'compat/zlib-uncompress2.c',\n+  'compiler-tricks/not-a-constant.c',\n   'config.c',\n   'connect.c',\n   'connected.c',\ndiff --git a/run-command.c b/run-command.c\nindex d527c46175..b74fd08056 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -516,14 +516,12 @@ static void atfork_prepare(struct atfork_state *as)\n \tsigset_t all;\n \n \t/*\n-\t * Do not use the return value of sigfillset(). It is transparently 0\n-\t * on some platforms, meaning a clever compiler may complain that\n-\t * the conditional body is dead code. Instead, check for error via\n-\t * errno, which outsmarts the compiler.\n+\t * POSIX says sigfillset() can fail, but an overly clever\n+\t * compiler can see through the header files and decide\n+\t * it cannot fail on a particular platform it is compiling for,\n+\t * triggering -Wunreachable-code false positive.\n \t */\n-\terrno = 0;\n-\tsigfillset(&all);\n-\tif (errno)\n+\tif (NOT_CONSTANT(sigfillset(&all)))\n \t\tdie_errno(\"sigfillset\");\n #ifdef NO_PTHREADS\n \tif (sigprocmask(SIG_SETMASK, &all, &as->old))\n-- \n2.49.0-207-gc8924421c3\n\n"},{"id":"514472","messageId":"20250317235329.809302-4-gitster@pobox.com","threadId":"63064","inReplyTo":"20250317235329.809302-1-gitster@pobox.com","subject":"[PATCH v3 3/3] config.mak.dev: enable -Wunreachable-code","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-17T23:53:29Z","receivedAt":"2025-03-17T23:53:36Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"From: Jeff King <peff@peff.net>\n\nHaving the compiler point out unreachable code can help avoid bugs, like\nthe one discussed in:\n\n  https://lore.kernel.org/git/20250307195057.GA3675279@coredump.intra.peff.net/\n\nIn that case it was found by Coverity, but finding it earlier saves\neverybody time and effort.\n\nWe can use -Wunreachable-code to get some help from the compiler here.\nInterestingly, this is a noop in gcc. It was a real warning up until gcc\n4.x, when it was removed for being too flaky, but they left the\ncommand-line option to avoid breaking users. See:\n\n  https://stackoverflow.com/questions/17249934/why-does-gcc-not-warn-for-unreachable-code\n\nHowever, clang does implement this option, and it finds the case\nmentioned above (and no other cases within the code base). And since we\nrun clang in several of our CI jobs, that's enough to get an early\nwarning of breakage.\n\nWe could enable it only for clang, but since gcc is happy to ignore it,\nit's simpler to just turn it on for all developer builds.\n\nSigned-off-by: Jeff King <peff@peff.net>\n[jc: squashed meson.build change sent by Patrick]\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n config.mak.dev | 1 +\n meson.build    | 1 +\n 2 files changed, 2 insertions(+)\n\ndiff --git a/config.mak.dev b/config.mak.dev\nindex 0fd8cc4d35..95b7bc46ae 100644\n--- a/config.mak.dev\n+++ b/config.mak.dev\n@@ -39,6 +39,7 @@ DEVELOPER_CFLAGS += -Wunused\n DEVELOPER_CFLAGS += -Wvla\n DEVELOPER_CFLAGS += -Wwrite-strings\n DEVELOPER_CFLAGS += -fno-common\n+DEVELOPER_CFLAGS += -Wunreachable-code\n \n ifneq ($(filter clang4,$(COMPILER_FEATURES)),)\n DEVELOPER_CFLAGS += -Wtautological-constant-out-of-range-compare\ndiff --git a/meson.build b/meson.build\nindex 373524dad2..fdccc59945 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -698,6 +698,7 @@ if get_option('warning_level') in ['2','3', 'everything'] and compiler.get_argum\n     '-Woverflow',\n     '-Wpointer-arith',\n     '-Wstrict-prototypes',\n+    '-Wunreachable-code',\n     '-Wunused',\n     '-Wvla',\n     '-Wwrite-strings',\n-- \n2.49.0-207-gc8924421c3\n\n"},{"id":"514473","messageId":"20250318001835.GA1470172@coredump.intra.peff.net","threadId":"63064","inReplyTo":"20250317235329.809302-1-gitster@pobox.com","subject":"Re: [PATCH v3 0/3] -Wunreachable-code","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-03-18T00:18:35Z","receivedAt":"2025-03-18T00:18:38Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Mon, Mar 17, 2025 at 04:53:26PM -0700, Junio C Hamano wrote:\n\n> As Taylor noticed, we can still help macOS users by first dealing\n> with the false positive in the code, and then flip the warning\n> option for developers on.\n\nYeah, this is worth doing.\n\n> This is totally offtopic, but I often find the short-log (list of\n> commits, grouped by author) in the cover letter very awkward to work\n> with.  Between v2 and v3, aside from the NOT_CONSTANT() improvements\n> in the patch [2/3] that used to be [3/3], one large change is the\n> reordering of the patches but that is not seen in the shortlog (I\n> ran \"git log --oneline -reverse\" to prepare the list of commits in\n> the order they are applied to describe them in the above list).\n> \n> Jeff King (2):\n>   run-command: use errno to check for sigfillset() error\n>   config.mak.dev: enable -Wunreachable-code\n> \n> Junio C Hamano (1):\n>   git-compat-util: add NOT_CONSTANT macro and use it in atfork_prepare()\n\nThe re-ordering does appear in the range-diff, if you provide one. But I\nagree that the organized-by-name shortlog does not make much sense for\nmost series. As a reviewer, I care most about the patches, not the\nauthors.\n\nI make my cover letters with something like this (part of a larger\nscript):\n\n    git format-patch --stdout origin..$topic |\n    perl -lne '\n      if (/^Subject: (.*)/) {\n        $subject = $1;\n      }\n      elsif ($subject && /^\\s+(.*)/) {\n        $subject .= \" $1\";\n      }\n      elsif ($subject) {\n        print $subject;\n        $subject = undef;\n      }\n    ' |\n    sed -e 's/\\[PATCH /[/' \\\n        -e 's/]/]:/' \\\n        -e 's/^/  /'\n\nwhich yields something like (for the older version of this series):\n\n  [1/3]: config.mak.dev: enable -Wunreachable-code\n  [2/3]: run-command: use errno to check for sigfillset() error\n  [3/3]: git-compat-util: add NOT_A_CONST macro and use it in atfork_prepare()\n\nHaving the correct order and the matching numbering next to each one\nmakes it much easier if you're going to comment on them inline.\n\nThe perl in the script above is required to handle rfc822 wrapping /\nline continuation. I never bothered to implement rfc2047 unquoting. I\ndon't tend to use non-ascii chars in my subject lines. ;)\n\nIt would be nice if we had a format-patch option to avoid quoting and\nwrapping in order to make text processing like this easier.\n\n-Peff\n"},{"id":"514474","messageId":"20250318002012.GB1470172@coredump.intra.peff.net","threadId":"63064","inReplyTo":"20250317235329.809302-3-gitster@pobox.com","subject":"Re: [PATCH v3 2/3] git-compat-util: add NOT_CONSTANT macro and use it in atfork_prepare()","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-03-18T00:20:12Z","receivedAt":"2025-03-18T00:20:13Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Mon, Mar 17, 2025 at 04:53:28PM -0700, Junio C Hamano wrote:\n\n> Introduce NOT_CONSTANT() macro, with which, the developer can tell\n> the compiler:\n\nThis name looks great to me.\n\n>  compiler-tricks/not-a-constant.c |  2 ++\n\nAnd this is much better, too. ;) I see you dropped the \"a\" in the macro\nname; I don't know if it matters much to do it here, too.\n\n-Peff\n"},{"id":"514477","messageId":"xmqqh63r5gi0.fsf@gitster.g","threadId":"63064","inReplyTo":"20250318002012.GB1470172@coredump.intra.peff.net","subject":"Re: [PATCH v3 2/3] git-compat-util: add NOT_CONSTANT macro and use it in atfork_prepare()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-18T00:28:39Z","receivedAt":"2025-03-18T00:28:41Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> On Mon, Mar 17, 2025 at 04:53:28PM -0700, Junio C Hamano wrote:\n>\n>> Introduce NOT_CONSTANT() macro, with which, the developer can tell\n>> the compiler:\n>\n> This name looks great to me.\n>\n>>  compiler-tricks/not-a-constant.c |  2 ++\n>\n> And this is much better, too. ;) I see you dropped the \"a\" in the macro\n> name; I don't know if it matters much to do it here, too.\n\nGood eyes.\n"},{"id":"514478","messageId":"xmqqcyef5g5g.fsf@gitster.g","threadId":"63064","inReplyTo":"xmqqwmcn5k11.fsf@gitster.g","subject":"Re: [PATCH v2 2/3] run-command: use errno to check for sigfillset() error","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-18T00:36:11Z","receivedAt":"2025-03-18T00:36:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Taylor Blau <me@ttaylorr.com> writes:\n>\n>> On Fri, Mar 14, 2025 at 02:09:08PM -0700, Junio C Hamano wrote:\n>>> From: Jeff King <peff@peff.net>\n>>>\n>>> Since enabling -Wunreachable-code, builds with clang on macOS now fail,\n>>> complaining that the die_errno() call in:\n>>>\n>>>   if (sigfillset(&all))\n>>> \tdie_errno(\"sigfillset\");\n>>\n>> Hmm. Would it have made sense to swap the order of this and the first\n>> patch so we don't have a DEVELOPER=1 breakage (for macOS with Clang) in\n>> history?\n>>\n>> I think it's too late now since this topic is already on 'next', but it\n>> occurred to me idly while reading this patch.\n>\n> I thought db1d1f5d (config.mak.dev: enable -Wunreachable-code,\n> 2025-03-14) aka jk/use-wunreachable-code-for-devs~2 is still out of\n> 'next'?\n\nAh, I did revert an earlier one-commit topic out of 'next'.  Perhaps\nI didn't tell What's cooking about it.\n\n"},{"id":"514559","messageId":"20250318220453.1937685-1-calvinwan@google.com","threadId":"63064","inReplyTo":"20250317235329.809302-3-gitster@pobox.com","subject":"Re: [PATCH v3 2/3] git-compat-util: add NOT_CONSTANT macro and use it in atfork_prepare()","fromName":"Calvin Wan","fromEmail":"calvinwan@google.com","sentAt":"2025-03-18T22:04:53Z","receivedAt":"2025-03-18T22:05:14Z","isPatch":true,"sender":{"key":"calvinwan@google.com","avatar":"https://avatars.githubusercontent.com/u/92547554?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n> Our hope is that the number of code paths that falsely trigger\n> warnings with the -Wunreachable-code compilation option are small,\n> and they can be worked around case-by-case basis, like we just did\n> in the previous commit.  If we need such a workaround a bit more\n> often, however, we may benefit from a more generic and descriptive\n> facility that helps document the cases we need such workarounds.\n> \n>     Side note: if we need the workaround all over the place, it\n>     simply means -Wunreachable-code is not a good tool for us to\n>     save engineering effort to catch mistakes.  We are still\n>     exploring if it helps us, so let's assume that it is not the\n>     case.\n> \n> Introduce NOT_CONSTANT() macro, with which, the developer can tell\n> the compiler:\n> \n>     Do not optimize this expression out, because, despite whatever\n>     you are told by the system headers, this expression should *not*\n>     be treated as a constant.\n> \n> and use it as a replacement for the workaround we used that was\n> somewhat specific to the sigfillset case.  If the compiler already\n> knows that the call to sigfillset() cannot fail on a particular\n> platform it is compiling for and declares that the if() condition\n> would not hold, it is plausible that the next version of the\n> compiler may learn that sigfillset() that never fails would not\n> touch errno and decide that in this sequence:\n> \n> \terrno = 0;\n> \tsigfillset(&all)\n> \tif (errno)\n> \t\tdie_errno(\"sigfillset\");\n> \n> the if() statement will never trigger.  Marking that the value\n> returned by sigfillset() cannot be a constant would document our\n> intention better and would not break with such a new version of\n> compiler that is even more \"clever\".  With the marco, the above\n> sequence can be rewritten:\n> \n> \tif (NOT_CONSTANT(sigfillset(&all)))\n> \t\tdie_errno(\"sigfillset\");\n> \n> which looks almost like other innocuous annotations we have,\n> e.g. UNUSED.\n> \n> Signed-off-by: Junio C Hamano <gitster@pobox.com>\n> ---\n>  Makefile                         |  1 +\n>  compiler-tricks/not-a-constant.c |  2 ++\n>  git-compat-util.h                |  9 +++++++++\n>  meson.build                      |  1 +\n>  run-command.c                    | 12 +++++-------\n>  5 files changed, 18 insertions(+), 7 deletions(-)\n>  create mode 100644 compiler-tricks/not-a-constant.c\n> \n> diff --git a/Makefile b/Makefile\n> index 97e8385b66..605e2d7f61 100644\n> --- a/Makefile\n> +++ b/Makefile\n> @@ -985,6 +985,7 @@ LIB_OBJS += compat/nonblock.o\n>  LIB_OBJS += compat/obstack.o\n>  LIB_OBJS += compat/terminal.o\n>  LIB_OBJS += compat/zlib-uncompress2.o\n> +LIB_OBJS += compiler-tricks/not-a-constant.o\n\nThe name is correctly added here, but in `next,` this name is set to\n`compiler-tricks/not-constant.o`.\n"},{"id":"514565","messageId":"CAFySSZC=qxs43ahZSRUeFxFTXDq905OdZ8-4beGUoaScaj=ugg@mail.gmail.com","threadId":"63064","inReplyTo":"20250318220453.1937685-1-calvinwan@google.com","subject":"Re: [PATCH v3 2/3] git-compat-util: add NOT_CONSTANT macro and use it in atfork_prepare()","fromName":"Calvin Wan","fromEmail":"calvinwan@google.com","sentAt":"2025-03-18T22:26:30Z","receivedAt":"2025-03-18T22:26:42Z","isPatch":true,"sender":{"key":"calvinwan@google.com","avatar":"https://avatars.githubusercontent.com/u/92547554?v=4"},"body":"On Tue, Mar 18, 2025 at 3:05 PM Calvin Wan <calvinwan@google.com> wrote:\n>\n> Junio C Hamano <gitster@pobox.com> writes:\n> > Our hope is that the number of code paths that falsely trigger\n> > @@ -985,6 +985,7 @@ LIB_OBJS += compat/nonblock.o\n> >  LIB_OBJS += compat/obstack.o\n> >  LIB_OBJS += compat/terminal.o\n> >  LIB_OBJS += compat/zlib-uncompress2.o\n> > +LIB_OBJS += compiler-tricks/not-a-constant.o\n>\n> The name is correctly added here, but in `next,` this name is set to\n> `compiler-tricks/not-constant.o`.\n\nApologies you can ignore this -- we needed to add a reference to the new folder\ninternally so this was a red herring for our broken build.\n"},{"id":"514585","messageId":"xmqqh63pzyg3.fsf@gitster.g","threadId":"63064","inReplyTo":"CAFySSZC=qxs43ahZSRUeFxFTXDq905OdZ8-4beGUoaScaj=ugg@mail.gmail.com","subject":"Re: [PATCH v3 2/3] git-compat-util: add NOT_CONSTANT macro and use it in atfork_prepare()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-18T23:55:08Z","receivedAt":"2025-03-18T23:55:10Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Calvin Wan <calvinwan@google.com> writes:\n\n> On Tue, Mar 18, 2025 at 3:05 PM Calvin Wan <calvinwan@google.com> wrote:\n>>\n>> Junio C Hamano <gitster@pobox.com> writes:\n>> > Our hope is that the number of code paths that falsely trigger\n>> > @@ -985,6 +985,7 @@ LIB_OBJS += compat/nonblock.o\n>> >  LIB_OBJS += compat/obstack.o\n>> >  LIB_OBJS += compat/terminal.o\n>> >  LIB_OBJS += compat/zlib-uncompress2.o\n>> > +LIB_OBJS += compiler-tricks/not-a-constant.o\n>>\n>> The name is correctly added here, but in `next,` this name is set to\n>> `compiler-tricks/not-constant.o`.\n>\n> Apologies you can ignore this -- we needed to add a reference to the new folder\n> internally so this was a red herring for our broken build.\n\nSorry, I may not have sent a reroll to the list for the version that\nwent into 'next'.  It should have lost \"a\" from not-constant\nconsistently everywhere.\n\nThanks for being eagle-eyed.\n"},{"id":"519597","messageId":"20250603212934.uojo22zjcuf6yfic@glandium.org","threadId":"63064","inReplyTo":"20250308032309.GA584028@coredump.intra.peff.net","subject":"Re: [PATCH] config.mak.dev: enable -Wunreachable-code","fromName":"Mike Hommey","fromEmail":"mh@glandium.org","sentAt":"2025-06-03T21:29:34Z","receivedAt":"2025-06-03T21:50:35Z","isPatch":true,"sender":{"key":"mh@glandium.org","avatar":"https://avatars.githubusercontent.com/u/1038527?v=4"},"body":"On Fri, Mar 07, 2025 at 10:23:09PM -0500, Jeff King wrote:\n> On Fri, Mar 07, 2025 at 05:54:45PM -0500, Jeff King wrote:\n> \n> > However, clang does implement this option, and it finds the case\n> > mentioned above (and no other cases within the code base). And since we\n> > run clang in several of our CI jobs, that's enough to get an early\n> > warning of breakage.\n> \n> Hmph, this might be more trouble than it is worth.\n> \n> After correcting the problem in the refs code, the osx CI builds (and\n> only those) now fail with:\n> \n>   run-command.c:519:3: error: code will never be executed [-Werror,-Wunreachable-code]\n>                   die_errno(\"sigfillset\");\n>                   ^~~~~~~~~\n> \n> The code in question is just:\n> \n>   if (sigfillset(&all))\n> \tdie_errno(\"sigfillset\");\n> \n> So I have to imagine that the issue is that sigfillset() on that\n> platform is an inline or macro that will never return an error, and the\n> compiler can see that. But since POSIX says this can fail (though I'd\n> imagine it's unlikely on most platforms), we should check in the general\n> case.\n> \n> So I don't see how to solve it short of:\n> \n> #ifdef SIGFILLSET_CANNOT_FAIL\n> \tsigfillset(&all);\n> #else\n> \tif (sigfillset(&all))\n> \t\tdie_errno(\"sigfillset\");\n> #endif\n\nThere is a similar problem with this code in refs/files-backend.c:\n\n\t\t\tif (!create_ref_symlink(lock, update->new_target))\n\t\t\t\tcontinue;\n\nWhere create_ref_symlink is defined as such:\n\n#ifdef NO_SYMLINK_HEAD\n#define create_ref_symlink(a, b) (-1)\n#else\nstatic int create_ref_symlink(struct ref_lock *lock, const char *target)\n{\n...\n#endif\n\nAnd NO_SYMLINK_HEAD is defined on Windows.\n\nMike\n"},{"id":"519598","messageId":"xmqqh60wh42f.fsf@gitster.g","threadId":"63064","inReplyTo":"20250603212934.uojo22zjcuf6yfic@glandium.org","subject":"Re: [PATCH] config.mak.dev: enable -Wunreachable-code","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-06-03T22:07:36Z","receivedAt":"2025-06-03T22:07:39Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Mike Hommey <mh@glandium.org> writes:\n\n> There is a similar problem with this code in refs/files-backend.c:\n>\n> \t\t\tif (!create_ref_symlink(lock, update->new_target))\n> \t\t\t\tcontinue;\n>\n> Where create_ref_symlink is defined as such:\n>\n> #ifdef NO_SYMLINK_HEAD\n> #define create_ref_symlink(a, b) (-1)\n> #else\n> static int create_ref_symlink(struct ref_lock *lock, const char *target)\n> {\n> ...\n> #endif\n>\n> And NO_SYMLINK_HEAD is defined on Windows.\n\nWould the NOT_CONSTANT() trick we ended up using for the original\n\"sigfillset\" thing solve your issue as well?\n\n"},{"id":"519608","messageId":"20250603223750.sjp5rw56ajehaaqe@glandium.org","threadId":"63064","inReplyTo":"xmqqh60wh42f.fsf@gitster.g","subject":"Re: [PATCH] config.mak.dev: enable -Wunreachable-code","fromName":"Mike Hommey","fromEmail":"mh@glandium.org","sentAt":"2025-06-03T22:37:50Z","receivedAt":"2025-06-03T22:38:02Z","isPatch":true,"sender":{"key":"mh@glandium.org","avatar":"https://avatars.githubusercontent.com/u/1038527?v=4"},"body":"On Tue, Jun 03, 2025 at 03:07:36PM -0700, Junio C Hamano wrote:\n> Mike Hommey <mh@glandium.org> writes:\n> \n> > There is a similar problem with this code in refs/files-backend.c:\n> >\n> > \t\t\tif (!create_ref_symlink(lock, update->new_target))\n> > \t\t\t\tcontinue;\n> >\n> > Where create_ref_symlink is defined as such:\n> >\n> > #ifdef NO_SYMLINK_HEAD\n> > #define create_ref_symlink(a, b) (-1)\n> > #else\n> > static int create_ref_symlink(struct ref_lock *lock, const char *target)\n> > {\n> > ...\n> > #endif\n> >\n> > And NO_SYMLINK_HEAD is defined on Windows.\n> \n> Would the NOT_CONSTANT() trick we ended up using for the original\n> \"sigfillset\" thing solve your issue as well?\n\n   if (NOT_CONSTANT(!create_ref_symlink(lock, update->new_target)))\n\nindeed works around it.\n\nMike\n"},{"id":"519617","messageId":"20250603230821.qig7g4zsenunlkbh@glandium.org","threadId":"63064","inReplyTo":"20250603223750.sjp5rw56ajehaaqe@glandium.org","subject":"Re: [PATCH] config.mak.dev: enable -Wunreachable-code","fromName":"Mike Hommey","fromEmail":"mh@glandium.org","sentAt":"2025-06-03T23:08:21Z","receivedAt":"2025-06-03T23:08:26Z","isPatch":true,"sender":{"key":"mh@glandium.org","avatar":"https://avatars.githubusercontent.com/u/1038527?v=4"},"body":"On Wed, Jun 04, 2025 at 07:37:50AM +0900, Mike Hommey wrote:\n> On Tue, Jun 03, 2025 at 03:07:36PM -0700, Junio C Hamano wrote:\n> > Mike Hommey <mh@glandium.org> writes:\n> > \n> > > There is a similar problem with this code in refs/files-backend.c:\n> > >\n> > > \t\t\tif (!create_ref_symlink(lock, update->new_target))\n> > > \t\t\t\tcontinue;\n> > >\n> > > Where create_ref_symlink is defined as such:\n> > >\n> > > #ifdef NO_SYMLINK_HEAD\n> > > #define create_ref_symlink(a, b) (-1)\n> > > #else\n> > > static int create_ref_symlink(struct ref_lock *lock, const char *target)\n> > > {\n> > > ...\n> > > #endif\n> > >\n> > > And NO_SYMLINK_HEAD is defined on Windows.\n> > \n> > Would the NOT_CONSTANT() trick we ended up using for the original\n> > \"sigfillset\" thing solve your issue as well?\n> \n>    if (NOT_CONSTANT(!create_ref_symlink(lock, update->new_target)))\n> \n> indeed works around it.\n\nI sent it as a patch along with other warning fixes.\n\nMike\n"}]}