{"thread":{"id":"64825","subject":"[PATCH v2 1/7] refs: drop unnecessary header includes","startedAt":"2026-01-16T21:27:12Z","lastAt":"2026-01-19T16:11:11Z","messageCount":12,"participants":["Karthik Nayak","Phillip Wood","SZEDER Gábor"],"isPatch":true,"patchVersion":2,"patchTotal":7},"messages":[{"id":"534090","messageId":"20260116-633-regression-lost-diagnostic-message-when-pushing-non-commit-objects-to-refs-heads-v2-0-925a0e9c7f32@gmail.com","threadId":"64825","inReplyTo":"20260114-633-regression-lost-diagnostic-message-when-pushing-non-commit-objects-to-refs-heads-v1-0-f5f8b173c501@gmail.com","subject":"[PATCH v2 0/7] refs: provide detailed error messages when using batched update","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-01-16T21:27:05Z","receivedAt":"2026-01-16T21:27:12Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"The refs namespace uses an error buffer to capture details about failed\nreference updates. However when we added batched update support to\nreference transactions, these messages were never propagated, instead\nonly an error code pertaining to the type of failure was propagated.\n\nCurrently, there are three regions which utilize batched updates:\n\n  - git update-ref --batch-updates\n  - git fetch\n  - git receive-pack\n\nWhile 'git update-ref --batch-updates' was a newly introduced flag, both\n'git fetch' and 'git receive-pack' were pre-existing. Before using\nbatched updates, they provided more detailed error messages to the user,\nbut this changed with the introduction of batched updates. This is a\nregression in their workings.\n\nThis patch series fixes this, by passing the detailed error message and\nutilizing it whenever available. The regression was reported by Elijah\nNewren [1] and based on the patch submitted by Jeff King [2].\n\n[1]: https://lore.kernel.org/all/CABPp-BGL2tJR4dPidQuFcp-X0_VkVTknCY-0Zgo=jHVGv_P=wA@mail.gmail.com/\n[2]: https://lore.kernel.org/all/20251224081214.GA1879908@coredump.intra.peff.net/\n\n---\nChanges in v2:\n- Updates to the commit messages to be more descriptive.\n- Instead of passing the char pointer for the error description, pass\n  the 'strbuf' itself. This makes the API a lot cleaner to deal with.\n  Also avoids having to remember to reset the strbuf after usage.\n- Chalk out a separate commit for using a 'goto next_ref' in\n  `refs_verify_refnames_available()`. This makes the intention much\n  clearer.\n- For git-update-ref(1), keep the existing implementation as is and only\n  output the detailed error message to stderr.\n- For git-receive-pack(1), use 'rp_error()' for detailed error message\n  while keeping the current implementation as is.\n- Added a separate patch to handle missing information in git-fetch(1)'s\n  status table. This involves delaying updates to the end, where update\n  success/failure information is available. I'm not too confident about\n  this approach though, we could also drop it from the series and I\n  could pick that up independently. This is still 1.19 ± 0.02 times\n  faster than non-batched version (v2.50.0) in the files backend.\n- Link to v1: https://patch.msgid.link/20260114-633-regression-lost-diagnostic-message-when-pushing-non-commit-objects-to-refs-heads-v1-0-f5f8b173c501@gmail.com\n\n---\n builtin/fetch.c         | 188 ++++++++++++++++++++++++++++++++++++++++--------\n builtin/receive-pack.c  |   7 +-\n builtin/update-ref.c    |   7 +-\n refs.c                  |  48 +++++++------\n refs.h                  |   1 +\n refs/files-backend.c    |   5 +-\n refs/packed-backend.c   |  12 ++--\n refs/refs-internal.h    |   4 +-\n refs/reftable-backend.c |   5 +-\n t/t1400-update-ref.sh   |  71 ++++++++++--------\n t/t5510-fetch.sh        |   8 +--\n t/t5516-fetch-push.sh   |  16 +++++\n t/t5574-fetch-output.sh |  16 ++---\n 13 files changed, 280 insertions(+), 108 deletions(-)\n\nKarthik Nayak (7):\n      refs: drop unnecessary header includes\n      refs: skip to next ref when current ref is rejected\n      refs: add rejection detail to the callback function\n      update-ref: utilize rejected error details if available\n      fetch: utilize rejected ref error details\n      receive-pack: utilize rejected ref error details\n      fetch: delay user information post committing of transaction\n\nRange-diff versus v1:\n\n1:  806ec3de6e ! 1:  75b7b2f83d refs: remove unused header\n    @@ Metadata\n     Author: Karthik Nayak <karthik.188@gmail.com>\n     \n      ## Commit message ##\n    -    refs: remove unused header\n    +    refs: drop unnecessary header includes\n     \n    -    Some of the headers in 'refs.c' are no longer required, let's remove\n    -    them.\n    +    The 'sigchain.h' header isn't being used and can be removed.\n    +\n    +    Similarly, 'run-command.h' serves no direct purpose here. While it gets pulled in transitively through 'hook.h', we can still drop the explicit include for clarity.\n     \n         Signed-off-by: Karthik Nayak <karthik.188@gmail.com>\n     \n2:  6ba3b9da56 ! 2:  507906091c refs: attach rejection details to updates\n    @@ Metadata\n     Author: Karthik Nayak <karthik.188@gmail.com>\n     \n      ## Commit message ##\n    -    refs: attach rejection details to updates\n    +    refs: skip to next ref when current ref is rejected\n     \n    -    The implementation of batched updates in 23fc8e4f61 (refs: implement\n    -    batch reference update support, 2025-04-08) added rejection error codes\n    -    to each reference update. This allowed batching of updates, however\n    -    while each rejection is linked to a rejection code, the already present\n    -    user readable error message is simply dropped.\n    +    In `refs_verify_refnames_available()` we have two nested loops: the\n    +    outer loop iterates over all references to check, while the inner loop\n    +    checks for filesystem conflicts for a given ref by breaking down its\n    +    path.\n     \n    -    Make necessary changes to ensure that the rejection detail is also added\n    -    to the reference update. In upcoming commits, we'll utilize this field\n    -    to provide better error message to users, namely in:\n    +    With batched updates, when we detect a filesystem conflict, we mark the\n    +    update as rejected and execute 'continue'. However, this only skips to\n    +    the next iteration of the inner loop, not the outer loop as intended.\n    +    This causes the same reference to be repeatedly rejected. Fix this by\n    +    using a goto statement to skip to the next reference in the outer loop.\n     \n    -      - git update-ref --batch-updates\n    -      - git fetch\n    -      - git receive-pack\n    -\n    -    We move the error message creation right above\n    -    `ref_transaction_maybe_set_rejected()`, so that the error message is\n    -    available and also reset the error message if utilized to avoid\n    -    un-expected concatination.\n    -\n    -    Co-authored-by: Jeff King <peff@peff.net>\n         Signed-off-by: Karthik Nayak <karthik.188@gmail.com>\n     \n      ## refs.c ##\n    @@ refs.c: void ref_transaction_free(struct ref_transaction *transaction)\n      \t\t\t\t       size_t update_idx,\n     -\t\t\t\t       enum ref_transaction_error err)\n     +\t\t\t\t       enum ref_transaction_error err,\n    -+\t\t\t\t       const char *details)\n    ++\t\t\t\t       struct strbuf *details)\n      {\n      \tif (update_idx >= transaction->nr)\n      \t\tBUG(\"trying to set rejection on invalid update index\");\n    @@ refs.c: int ref_transaction_maybe_set_rejected(struct ref_transaction *transacti\n      \t\t\t   transaction->updates[update_idx]->refname, 0);\n      \n      \ttransaction->updates[update_idx]->rejection_err = err;\n    -+\tif (details)\n    -+\t\ttransaction->updates[update_idx]->rejection_details = xstrdup(details);\n    ++\ttransaction->updates[update_idx]->rejection_details = strbuf_detach(details, NULL);\n      \tALLOC_GROW(transaction->rejections->update_indices,\n      \t\t   transaction->rejections->nr + 1,\n      \t\t   transaction->rejections->alloc);\n    @@ refs.c: enum ref_transaction_error refs_verify_refnames_available(struct ref_sto\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\t    REF_TRANSACTION_ERROR_NAME_CONFLICT, err->buf)) {\n    ++\t\t\t\t\t    REF_TRANSACTION_ERROR_NAME_CONFLICT, err)) {\n      \t\t\t\t\tstrset_remove(&dirnames, dirname.buf);\n      \t\t\t\t\tstrset_add(&conflicting_dirnames, dirname.buf);\n     -\t\t\t\t\tcontinue;\n    -+\t\t\t\t\tstrbuf_reset(err);\n    -+\t\t\t\t\tgoto next;\n    ++\t\t\t\t\tgoto next_ref;\n      \t\t\t\t}\n      \n     -\t\t\t\tstrbuf_addf(err, _(\"'%s' exists; cannot create '%s'\"),\n    @@ refs.c: enum ref_transaction_error refs_verify_refnames_available(struct ref_sto\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\t    REF_TRANSACTION_ERROR_NAME_CONFLICT, err->buf)) {\n    ++\t\t\t\t\t    REF_TRANSACTION_ERROR_NAME_CONFLICT, err)) {\n      \t\t\t\t\tstrset_remove(&dirnames, dirname.buf);\n     -\t\t\t\t\tcontinue;\n    -+\t\t\t\t\tstrbuf_reset(err);\n    -+\t\t\t\t\tgoto next;\n    ++\t\t\t\t\tgoto next_ref;\n      \t\t\t\t}\n      \n     -\t\t\t\tstrbuf_addf(err, _(\"cannot process '%s' and '%s' at the same time\"),\n    @@ refs.c: enum ref_transaction_error refs_verify_refnames_available(struct ref_sto\n      \t\t\t}\n      \t\t}\n     @@ refs.c: enum ref_transaction_error refs_verify_refnames_available(struct ref_store *refs\n    + \t\t\t\tif (skip &&\n      \t\t\t\t    string_list_has_string(skip, iter->ref.name))\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->ref.name, refname);\n    -+\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    -+\t\t\t\t\t    REF_TRANSACTION_ERROR_NAME_CONFLICT, err->buf)) {\n    -+\t\t\t\t\tstrbuf_reset(err);\n    -+\t\t\t\t\tgoto next;\n    -+\t\t\t\t}\n    ++\t\t\t\t\t    REF_TRANSACTION_ERROR_NAME_CONFLICT, err))\n    ++\t\t\t\t\tgoto next_ref;\n      \n     -\t\t\t\tstrbuf_addf(err, _(\"'%s' exists; cannot create '%s'\"),\n     -\t\t\t\t\t    iter->ref.name, refname);\n    @@ refs.c: enum ref_transaction_error refs_verify_refnames_available(struct ref_sto\n      \t\t\t\t    transaction, *update_idx,\n     -\t\t\t\t    REF_TRANSACTION_ERROR_NAME_CONFLICT))\n     -\t\t\t\tcontinue;\n    -+\t\t\t\t    REF_TRANSACTION_ERROR_NAME_CONFLICT, err->buf)) {\n    -+\t\t\t\tstrbuf_reset(err);\n    -+\t\t\t\tgoto next;\n    -+\t\t\t}\n    ++\t\t\t\t    REF_TRANSACTION_ERROR_NAME_CONFLICT, err))\n    ++\t\t\t\tgoto next_ref;\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      \t\t}\n    -+next:;\n    ++next_ref:;\n      \t}\n      \n      \tret = 0;\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\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n    +-\t\t\t\tstrbuf_reset(err);\n     +\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i,\n    -+\t\t\t\t\t\t\t       ret, err->buf)) {\n    - \t\t\t\tstrbuf_reset(err);\n    ++\t\t\t\t\t\t\t       ret, err)) {\n      \t\t\t\tret = 0;\n    - \n    +-\n    + \t\t\t\tcontinue;\n    + \t\t\t}\n    + \t\t\tgoto cleanup;\n     \n      ## refs/packed-backend.c ##\n     @@ refs/packed-backend.c: static enum ref_transaction_error write_with_updates(struct packed_ref_store *re\n    @@ refs/packed-backend.c: static enum ref_transaction_error write_with_updates(stru\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_reset(err);\n     +\t\t\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i,\n    -+\t\t\t\t\t\t\t\t\t       ret, err->buf)) {\n    - \t\t\t\t\t\tstrbuf_reset(err);\n    ++\t\t\t\t\t\t\t\t\t       ret, err)) {\n      \t\t\t\t\t\tret = 0;\n      \t\t\t\t\t\tcontinue;\n    + \t\t\t\t\t}\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(&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_reset(err);\n     +\t\t\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i,\n    -+\t\t\t\t\t\t\t\t\t       ret, err->buf)) {\n    - \t\t\t\t\t\tstrbuf_reset(err);\n    ++\t\t\t\t\t\t\t\t\t       ret, err)) {\n      \t\t\t\t\t\tret = 0;\n      \t\t\t\t\t\tcontinue;\n    + \t\t\t\t\t}\n     @@ refs/packed-backend.c: static enum ref_transaction_error write_with_updates(struct packed_ref_store *re\n      \t\t\t\t\t    oid_to_hex(&update->old_oid));\n      \t\t\t\tret = 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_reset(err);\n     +\t\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i,\n    -+\t\t\t\t\t\t\t\t       ret, err->buf)) {\n    - \t\t\t\t\tstrbuf_reset(err);\n    ++\t\t\t\t\t\t\t\t       ret, err)) {\n      \t\t\t\t\tret = 0;\n      \t\t\t\t\tcontinue;\n    + \t\t\t\t}\n     \n      ## refs/refs-internal.h ##\n     @@ refs/refs-internal.h: struct ref_update {\n    @@ refs/refs-internal.h: int refs_read_raw_ref(struct ref_store *ref_store, const c\n      \t\t\t\t       size_t update_idx,\n     -\t\t\t\t       enum ref_transaction_error err);\n     +\t\t\t\t       enum ref_transaction_error err,\n    -+\t\t\t\t       const char *details);\n    ++\t\t\t\t       struct strbuf *details);\n      \n      /*\n       * Add a ref_update with the specified properties to transaction, and\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\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n    +-\t\t\t\tstrbuf_reset(err);\n     +\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i,\n    -+\t\t\t\t\t\t\t       ret, err->buf)) {\n    - \t\t\t\tstrbuf_reset(err);\n    ++\t\t\t\t\t\t\t       ret, err)) {\n      \t\t\t\tret = 0;\n    - \n    +-\n    + \t\t\t\tcontinue;\n    + \t\t\t}\n    + \t\t\tgoto done;\n3:  76f199b434 ! 3:  78d6220027 refs: add rejection detail to the callback function\n    @@ Commit message\n         field is unused, but will be integrated in the upcoming commits.\n     \n         Co-authored-by: Jeff King <peff@peff.net>\n    +    Signed-off-by: Jeff King <peff@peff.net>\n         Signed-off-by: Karthik Nayak <karthik.188@gmail.com>\n     \n      ## builtin/fetch.c ##\n4:  c05216bf9c < -:  ---------- update-ref: utilize rejected error details if available\n-:  ---------- > 4:  6ca8a03f74 update-ref: utilize rejected error details if available\n5:  bdfef1b20f = 5:  289282031d fetch: utilize rejected ref error details\n6:  08b74e8077 ! 6:  d555777da0 receive-pack: utilize rejected ref error details\n    @@ Commit message\n         messages for failed referenced updates, the users were provided generic\n         error messages based on the error type.\n     \n    -    Similar to the previous commit, switch to using detailed error messages\n    -    if present for failed reference updates to fix this regression.\n    -\n    -    One downside of this is that the messages can be very verbose, for e.g.\n    -    in the files backend, when trying to write a non-commit object to a\n    -    branch, you would see:\n    +    Now that the updates also contain detailed error message, propagate\n    +    those to the client via 'rp_error'. The detailed error messages can be\n    +    very verbose, for e.g. in the files backend, when trying to write a\n    +    non-commit object to a branch, you would see:\n     \n            ! [remote rejected] 3eaec9ccf3a53f168362a6b3fdeb73426fb9813d ->\n            branch (cannot update ref 'refs/heads/branch': trying to write\n    @@ Commit message\n     \n         Reported-by: Elijah Newren <newren@gmail.com>\n         Co-authored-by: Jeff King <peff@peff.net>\n    +    Signed-off-by: Jeff King <peff@peff.net>\n         Signed-off-by: Karthik Nayak <karthik.188@gmail.com>\n     \n      ## builtin/receive-pack.c ##\n    @@ builtin/receive-pack.c: static void ref_transaction_rejection_handler(const char\n      {\n      \tstruct strmap *failed_refs = cb_data;\n      \n    --\tstrmap_put(failed_refs, refname, (char *)ref_transaction_error_msg(err));\n    -+\tif (!details)\n    -+\t\tdetails = ref_transaction_error_msg(err);\n    ++\tif (details)\n    ++\t\trp_error(\"%s\", details);\n     +\n    -+\tstrmap_put(failed_refs, refname, (char *)details);\n    + \tstrmap_put(failed_refs, refname, (char *)ref_transaction_error_msg(err));\n      }\n      \n    - static void execute_commands_non_atomic(struct command *commands,\n     @@ builtin/receive-pack.c: static void execute_commands_non_atomic(struct command *commands,\n      \t\t}\n      \n-:  ---------- > 7:  640d09d408 fetch: delay user information post committing of transaction\n\n\nbase-commit: 8745eae506f700657882b9e32b2aa00f234a6fb6\nchange-id: 20260113-633-regression-lost-diagnostic-message-when-pushing-non-commit-objects-to-refs-heads-17786b20894a\n\nThanks\n- Karthik\n\n"},{"id":"534089","messageId":"20260116-633-regression-lost-diagnostic-message-when-pushing-non-commit-objects-to-refs-heads-v2-1-925a0e9c7f32@gmail.com","threadId":"64825","inReplyTo":"20260116-633-regression-lost-diagnostic-message-when-pushing-non-commit-objects-to-refs-heads-v2-0-925a0e9c7f32@gmail.com","subject":"[PATCH v2 1/7] refs: drop unnecessary header includes","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-01-16T21:27:06Z","receivedAt":"2026-01-16T21:27:13Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"The 'sigchain.h' header isn't being used and can be removed.\n\nSimilarly, 'run-command.h' serves no direct purpose here. While it gets pulled in transitively through 'hook.h', we can still drop the explicit include for clarity.\n\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n refs.c | 2 --\n 1 file changed, 2 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex e06e0cb072..965b232a06 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -15,7 +15,6 @@\n #include \"iterator.h\"\n #include \"refs.h\"\n #include \"refs/refs-internal.h\"\n-#include \"run-command.h\"\n #include \"hook.h\"\n #include \"object-name.h\"\n #include \"odb.h\"\n@@ -26,7 +25,6 @@\n #include \"strvec.h\"\n #include \"repo-settings.h\"\n #include \"setup.h\"\n-#include \"sigchain.h\"\n #include \"date.h\"\n #include \"commit.h\"\n #include \"wildmatch.h\"\n\n-- \n2.51.2\n\n"},{"id":"534091","messageId":"20260116-633-regression-lost-diagnostic-message-when-pushing-non-commit-objects-to-refs-heads-v2-2-925a0e9c7f32@gmail.com","threadId":"64825","inReplyTo":"20260116-633-regression-lost-diagnostic-message-when-pushing-non-commit-objects-to-refs-heads-v2-0-925a0e9c7f32@gmail.com","subject":"[PATCH v2 2/7] refs: skip to next ref when current ref is rejected","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-01-16T21:27:07Z","receivedAt":"2026-01-16T21:27:14Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"In `refs_verify_refnames_available()` we have two nested loops: the\nouter loop iterates over all references to check, while the inner loop\nchecks for filesystem conflicts for a given ref by breaking down its\npath.\n\nWith batched updates, when we detect a filesystem conflict, we mark the\nupdate as rejected and execute 'continue'. However, this only skips to\nthe next iteration of the inner loop, not the outer loop as intended.\nThis causes the same reference to be repeatedly rejected. Fix this by\nusing a goto statement to skip to the next reference in the outer loop.\n\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n refs.c                  | 44 ++++++++++++++++++++++++++------------------\n refs/files-backend.c    |  5 ++---\n refs/packed-backend.c   | 12 ++++++------\n refs/refs-internal.h    |  4 +++-\n refs/reftable-backend.c |  5 ++---\n 5 files changed, 39 insertions(+), 31 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex 965b232a06..3459d0e4e5 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1222,6 +1222,7 @@ void ref_transaction_free(struct ref_transaction *transaction)\n \t\tfree(transaction->updates[i]->committer_info);\n \t\tfree((char *)transaction->updates[i]->new_target);\n \t\tfree((char *)transaction->updates[i]->old_target);\n+\t\tfree((char *)transaction->updates[i]->rejection_details);\n \t\tfree(transaction->updates[i]);\n \t}\n \n@@ -1236,7 +1237,8 @@ void ref_transaction_free(struct ref_transaction *transaction)\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+\t\t\t\t       enum ref_transaction_error err,\n+\t\t\t\t       struct strbuf *details)\n {\n \tif (update_idx >= transaction->nr)\n \t\tBUG(\"trying to set rejection on invalid update index\");\n@@ -1262,6 +1264,7 @@ int ref_transaction_maybe_set_rejected(struct ref_transaction *transaction,\n \t\t\t   transaction->updates[update_idx]->refname, 0);\n \n \ttransaction->updates[update_idx]->rejection_err = err;\n+\ttransaction->updates[update_idx]->rejection_details = strbuf_detach(details, NULL);\n \tALLOC_GROW(transaction->rejections->update_indices,\n \t\t   transaction->rejections->nr + 1,\n \t\t   transaction->rejections->alloc);\n@@ -2657,30 +2660,33 @@ enum ref_transaction_error refs_verify_refnames_available(struct ref_store *refs\n \t\t\tif (!initial_transaction &&\n \t\t\t    (strset_contains(&conflicting_dirnames, dirname.buf) ||\n \t\t\t     !refs_read_raw_ref(refs, dirname.buf, &oid, &referent,\n-\t\t\t\t\t\t       &type, &ignore_errno))) {\n+\t\t\t\t\t\t&type, &ignore_errno))) {\n+\n+\t\t\t\tstrbuf_addf(err, _(\"'%s' exists; cannot create '%s'\"),\n+\t\t\t\t\t    dirname.buf, refname);\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\t    REF_TRANSACTION_ERROR_NAME_CONFLICT, err)) {\n \t\t\t\t\tstrset_remove(&dirnames, dirname.buf);\n \t\t\t\t\tstrset_add(&conflicting_dirnames, dirname.buf);\n-\t\t\t\t\tcontinue;\n+\t\t\t\t\tgoto next_ref;\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\tstrbuf_addf(err, _(\"cannot process '%s' and '%s' at the same time\"),\n+\t\t\t\t\t    refname, dirname.buf);\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\t    REF_TRANSACTION_ERROR_NAME_CONFLICT, err)) {\n \t\t\t\t\tstrset_remove(&dirnames, dirname.buf);\n-\t\t\t\t\tcontinue;\n+\t\t\t\t\tgoto next_ref;\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 \t\t\t}\n \t\t}\n@@ -2710,14 +2716,14 @@ enum ref_transaction_error refs_verify_refnames_available(struct ref_store *refs\n \t\t\t\tif (skip &&\n \t\t\t\t    string_list_has_string(skip, iter->ref.name))\n \t\t\t\t\tcontinue;\n+\t\t\t\tstrbuf_addf(err, _(\"'%s' exists; cannot create '%s'\"),\n+\t\t\t\t\t    iter->ref.name, refname);\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+\t\t\t\t\t    REF_TRANSACTION_ERROR_NAME_CONFLICT, err))\n+\t\t\t\t\tgoto next_ref;\n \n-\t\t\t\tstrbuf_addf(err, _(\"'%s' exists; cannot create '%s'\"),\n-\t\t\t\t\t    iter->ref.name, refname);\n \t\t\t\tgoto cleanup;\n \t\t\t}\n \n@@ -2727,15 +2733,17 @@ 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\tstrbuf_addf(err, _(\"cannot process '%s' and '%s' at the same time\"),\n+\t\t\t\t    refname, extra_refname);\n+\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+\t\t\t\t    REF_TRANSACTION_ERROR_NAME_CONFLICT, err))\n+\t\t\t\tgoto next_ref;\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 \t\t}\n+next_ref:;\n \t}\n \n \tret = 0;\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 6f6f76a8d8..6790d8bf53 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2983,10 +2983,9 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n \t\t\t\t\t  head_ref, &refnames_to_check,\n \t\t\t\t\t  err);\n \t\tif (ret) {\n-\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n-\t\t\t\tstrbuf_reset(err);\n+\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i,\n+\t\t\t\t\t\t\t       ret, err)) {\n \t\t\t\tret = 0;\n-\n \t\t\t\tcontinue;\n \t\t\t}\n \t\t\tgoto cleanup;\ndiff --git a/refs/packed-backend.c b/refs/packed-backend.c\nindex 4ea0c12299..59b3ecb9d6 100644\n--- a/refs/packed-backend.c\n+++ b/refs/packed-backend.c\n@@ -1437,8 +1437,8 @@ static enum ref_transaction_error write_with_updates(struct packed_ref_store *re\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_reset(err);\n+\t\t\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i,\n+\t\t\t\t\t\t\t\t\t       ret, err)) {\n \t\t\t\t\t\tret = 0;\n \t\t\t\t\t\tcontinue;\n \t\t\t\t\t}\n@@ -1452,8 +1452,8 @@ static enum ref_transaction_error write_with_updates(struct packed_ref_store *re\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_reset(err);\n+\t\t\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i,\n+\t\t\t\t\t\t\t\t\t       ret, err)) {\n \t\t\t\t\t\tret = 0;\n \t\t\t\t\t\tcontinue;\n \t\t\t\t\t}\n@@ -1496,8 +1496,8 @@ static enum ref_transaction_error write_with_updates(struct packed_ref_store *re\n \t\t\t\t\t    oid_to_hex(&update->old_oid));\n \t\t\t\tret = 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_reset(err);\n+\t\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i,\n+\t\t\t\t\t\t\t\t       ret, err)) {\n \t\t\t\t\tret = 0;\n \t\t\t\t\tcontinue;\n \t\t\t\t}\ndiff --git a/refs/refs-internal.h b/refs/refs-internal.h\nindex c7d2a6e50b..191a25683f 100644\n--- a/refs/refs-internal.h\n+++ b/refs/refs-internal.h\n@@ -128,6 +128,7 @@ struct ref_update {\n \t * was rejected.\n \t */\n \tenum ref_transaction_error rejection_err;\n+\tconst char *rejection_details;\n \n \t/*\n \t * If this ref_update was split off of a symref update via\n@@ -153,7 +154,8 @@ int refs_read_raw_ref(struct ref_store *ref_store, const char *refname,\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+\t\t\t\t       enum ref_transaction_error err,\n+\t\t\t\t       struct strbuf *details);\n \n /*\n  * Add a ref_update with the specified properties to transaction, and\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 4319a4eacb..0e2648e36c 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -1401,10 +1401,9 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,\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\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n-\t\t\t\tstrbuf_reset(err);\n+\t\t\tif (ref_transaction_maybe_set_rejected(transaction, i,\n+\t\t\t\t\t\t\t       ret, err)) {\n \t\t\t\tret = 0;\n-\n \t\t\t\tcontinue;\n \t\t\t}\n \t\t\tgoto done;\n\n-- \n2.51.2\n\n"},{"id":"534092","messageId":"20260116-633-regression-lost-diagnostic-message-when-pushing-non-commit-objects-to-refs-heads-v2-3-925a0e9c7f32@gmail.com","threadId":"64825","inReplyTo":"20260116-633-regression-lost-diagnostic-message-when-pushing-non-commit-objects-to-refs-heads-v2-0-925a0e9c7f32@gmail.com","subject":"[PATCH v2 3/7] refs: add rejection detail to the callback function","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-01-16T21:27:08Z","receivedAt":"2026-01-16T21:27:15Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"The previous commit started storing the rejection details alongside the\nerror code for rejected updates. Pass this along to the callback\nfunction `ref_transaction_for_each_rejected_update()`. Currently the\nfield is unused, but will be integrated in the upcoming commits.\n\nCo-authored-by: Jeff King <peff@peff.net>\nSigned-off-by: Jeff King <peff@peff.net>\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n builtin/fetch.c        | 1 +\n builtin/receive-pack.c | 1 +\n builtin/update-ref.c   | 1 +\n refs.c                 | 2 +-\n refs.h                 | 1 +\n 5 files changed, 5 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 288d3772ea..d427adea61 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1649,6 +1649,7 @@ static void ref_transaction_rejection_handler(const char *refname,\n \t\t\t\t\t      const char *old_target UNUSED,\n \t\t\t\t\t      const char *new_target UNUSED,\n \t\t\t\t\t      enum ref_transaction_error err,\n+\t\t\t\t\t      const char *details UNUSED,\n \t\t\t\t\t      void *cb_data)\n {\n \tstruct ref_rejection_data *data = cb_data;\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex ef1f77be8c..94d3e73cee 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1813,6 +1813,7 @@ static void ref_transaction_rejection_handler(const char *refname,\n \t\t\t\t\t      const char *old_target UNUSED,\n \t\t\t\t\t      const char *new_target UNUSED,\n \t\t\t\t\t      enum ref_transaction_error err,\n+\t\t\t\t\t      const char *details UNUSED,\n \t\t\t\t\t      void *cb_data)\n {\n \tstruct strmap *failed_refs = cb_data;\ndiff --git a/builtin/update-ref.c b/builtin/update-ref.c\nindex 195437e7c6..0046a87c57 100644\n--- a/builtin/update-ref.c\n+++ b/builtin/update-ref.c\n@@ -573,6 +573,7 @@ static void print_rejected_refs(const char *refname,\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\tconst char *details UNUSED,\n \t\t\t\tvoid *cb_data UNUSED)\n {\n \tstruct strbuf sb = STRBUF_INIT;\ndiff --git a/refs.c b/refs.c\nindex 3459d0e4e5..e754159c21 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2872,7 +2872,7 @@ void ref_transaction_for_each_rejected_update(struct ref_transaction *transactio\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\t   update->rejection_err, update->rejection_details, cb_data);\n \t}\n }\n \ndiff --git a/refs.h b/refs.h\nindex d9051bbb04..4fbe3da924 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -975,6 +975,7 @@ typedef void ref_transaction_for_each_rejected_update_fn(const char *refname,\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 const char *details,\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\n-- \n2.51.2\n\n"},{"id":"534093","messageId":"20260116-633-regression-lost-diagnostic-message-when-pushing-non-commit-objects-to-refs-heads-v2-4-925a0e9c7f32@gmail.com","threadId":"64825","inReplyTo":"20260116-633-regression-lost-diagnostic-message-when-pushing-non-commit-objects-to-refs-heads-v2-0-925a0e9c7f32@gmail.com","subject":"[PATCH v2 4/7] update-ref: utilize rejected error details if available","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-01-16T21:27:09Z","receivedAt":"2026-01-16T21:27:16Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"When git-update-ref(1) received the '--update-ref' flag, the error\ndetails generated in the refs namespace wasn't propagated with failed\nupdates. Instead only an error code pertaining to the type of rejection\nwas noted.\n\nThis missed detailed error message which the user can act upon. The\nprevious commits added the required code to propagate these detailed\nerror messages from the refs namespace. Now that additional details are\navailable, let's output this additional details to stderr. This allows\nusers to have additional information over the already present machine\nparsable output.\n\nWhile we're here, improve the existing tests for the machine parsable\noutput by checking for the entire output string and not just the\nrejection reason.\n\nReported-by: Elijah Newren <newren@gmail.com>\nCo-authored-by: Jeff King <peff@peff.net>\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n builtin/update-ref.c  |  8 +++---\n t/t1400-update-ref.sh | 71 ++++++++++++++++++++++++++++++---------------------\n 2 files changed, 47 insertions(+), 32 deletions(-)\n\ndiff --git a/builtin/update-ref.c b/builtin/update-ref.c\nindex 0046a87c57..2d68c40ecb 100644\n--- a/builtin/update-ref.c\n+++ b/builtin/update-ref.c\n@@ -573,16 +573,18 @@ static void print_rejected_refs(const char *refname,\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\tconst char *details UNUSED,\n+\t\t\t\tconst char *details,\n \t\t\t\tvoid *cb_data UNUSED)\n {\n \tstruct strbuf sb = STRBUF_INIT;\n-\tconst char *reason = ref_transaction_error_msg(err);\n+\n+\tif (details && *details)\n+\t\terror(\"%s\", details);\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+\t\t    ref_transaction_error_msg(err));\n \n \tfwrite(sb.buf, sb.len, 1, stdout);\n \tstrbuf_release(&sb);\ndiff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\nindex db7f5444da..db6585b8d8 100755\n--- a/t/t1400-update-ref.sh\n+++ b/t/t1400-update-ref.sh\n@@ -2093,14 +2093,15 @@ do\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 --batch-updates <stdin >stdout &&\n+\t\t\tgit update-ref $type --stdin --batch-updates <stdin >stdout 2>err &&\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\ttest_grep \"rejected refs/heads/ref2 $(test_oid 001) $head invalid new value provided\" stdout &&\n+\t\t\ttest_grep \"trying to write ref ${SQ}refs/heads/ref2${SQ} with nonexistent object\" err\n \t\t)\n \t'\n \n@@ -2119,14 +2120,15 @@ do\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 --batch-updates <stdin >stdout &&\n+\t\t\tgit update-ref $type --stdin --batch-updates <stdin >stdout 2>err &&\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\ttest_grep \"rejected refs/heads/ref2 $head_tree $head invalid new value provided\" stdout &&\n+\t\t\ttest_grep \"trying to write non-commit object $head_tree to branch ${SQ}refs/heads/ref2${SQ}\" err\n \t\t)\n \t'\n \n@@ -2143,12 +2145,13 @@ do\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 --batch-updates <stdin >stdout &&\n+\t\t\tgit update-ref $type --stdin --batch-updates <stdin >stdout 2>err &&\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\ttest_grep \"rejected refs/heads/ref2 $old_head $head reference does not exist\" stdout &&\n+\t\t\ttest_grep \"cannot lock ref ${SQ}refs/heads/ref2${SQ}: unable to resolve reference ${SQ}refs/heads/ref2${SQ}\" err\n \t\t)\n \t'\n \n@@ -2166,13 +2169,14 @@ do\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 --batch-updates <stdin >stdout &&\n+\t\t\tgit update-ref $type --no-deref --stdin --batch-updates <stdin >stdout 2>err &&\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\ttest_grep \"rejected refs/heads/ref2 $old_head $head reference does not exist\" stdout &&\n+\t\t\ttest_grep \"cannot lock ref ${SQ}refs/heads/ref2${SQ}: reference is missing but expected $head\" err\n \t\t)\n \t'\n \n@@ -2190,7 +2194,7 @@ do\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 --batch-updates <stdin >stdout &&\n+\t\t\tgit update-ref $type --no-deref --stdin --batch-updates <stdin >stdout 2>err &&\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@@ -2198,7 +2202,8 @@ do\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\ttest_grep \"rejected refs/heads/ref2 $ZERO_OID $ZERO_OID expected symref but found regular ref\" stdout &&\n+\t\t\ttest_grep \"cannot lock ref ${SQ}refs/heads/ref2${SQ}: expected symref with target ${SQ}refs/heads/nonexistent${SQ}: but is a regular ref\" err\n \t\t)\n \t'\n \n@@ -2216,14 +2221,15 @@ do\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 --batch-updates <stdin >stdout &&\n+\t\t\tgit update-ref $type --stdin --batch-updates <stdin >stdout 2>err &&\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\ttest_grep \"rejected refs/heads/ref2 $old_head $ZERO_OID reference already exists\" stdout &&\n+\t\t\ttest_grep \"cannot lock ref ${SQ}refs/heads/ref2${SQ}: reference already exists\" err\n \t\t)\n \t'\n \n@@ -2241,14 +2247,15 @@ do\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 --batch-updates <stdin >stdout &&\n+\t\t\tgit update-ref $type --stdin --batch-updates <stdin >stdout 2>err &&\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\ttest_grep \"rejected refs/heads/ref2 $head $old_head incorrect old value provided\" stdout &&\n+\t\t\ttest_grep \"cannot lock ref ${SQ}refs/heads/ref2${SQ}: is at $head but expected $old_head\" err\n \t\t)\n \t'\n \n@@ -2264,12 +2271,13 @@ do\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 --batch-updates <stdin >stdout &&\n+\t\t\tformat_command $type \"update refs/heads/ref\" \"$old_head\" \"$ZERO_OID\" >>stdin &&\n+\t\t\tgit update-ref $type --stdin --batch-updates <stdin >stdout 2>err &&\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\ttest_grep \"rejected refs/heads/ref $old_head $ZERO_OID refname conflict\" stdout &&\n+\t\t\ttest_grep \"${SQ}refs/heads/ref/foo${SQ} exists; cannot create ${SQ}refs/heads/ref${SQ}\" err\n \t\t)\n \t'\n \n@@ -2284,13 +2292,14 @@ do\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 --batch-updates <stdin >stdout &&\n+\t\t\tformat_command $type \"update refs/heads/foo\" \"$old_head\" \"$ZERO_OID\" >stdin &&\n+\t\t\tformat_command $type \"update refs/heads/ref\" \"$old_head\" \"$ZERO_OID\" >>stdin &&\n+\t\t\tgit update-ref $type --stdin --batch-updates <stdin >stdout 2>err &&\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\ttest_grep \"rejected refs/heads/ref $old_head $ZERO_OID refname conflict\" stdout &&\n+\t\t\ttest_grep \"${SQ}refs/heads/ref/foo${SQ} exists; cannot create ${SQ}refs/heads/ref${SQ}\" err\n \t\t)\n \t'\n \n@@ -2309,14 +2318,15 @@ do\n \t\t\t\tformat_command $type \"create refs/heads/ref\" \"$old_head\" &&\n \t\t\t\tformat_command $type \"create refs/heads/Foo\" \"$old_head\"\n \t\t\t} >stdin &&\n-\t\t\tgit update-ref $type --stdin --batch-updates <stdin >stdout &&\n+\t\t\tgit update-ref $type --stdin --batch-updates <stdin >stdout 2>err &&\n \n \t\t\techo $head >expect &&\n \t\t\tgit rev-parse refs/heads/foo >actual &&\n \t\t\techo $old_head >expect &&\n \t\t\tgit rev-parse refs/heads/ref >actual &&\n \t\t\ttest_cmp expect actual &&\n-\t\t\ttest_grep -q \"reference conflict due to case-insensitive filesystem\" stdout\n+\t\t\ttest_grep \"rejected refs/heads/Foo $old_head $ZERO_OID reference conflict due to case-insensitive filesystem\" stdout &&\n+\t\t\ttest_grep -e \"cannot lock ref ${SQ}refs/heads/Foo${SQ}: Unable to create\" -e \"Foo.lock\" err\n \t\t)\n \t'\n \n@@ -2357,8 +2367,9 @@ do\n \t\t\tgit symbolic-ref refs/heads/symbolic refs/heads/non-existent &&\n \n \t\t\tformat_command $type \"delete refs/heads/symbolic\" \"$head\" >stdin &&\n-\t\t\tgit update-ref $type --stdin --batch-updates <stdin >stdout &&\n-\t\t\ttest_grep \"reference does not exist\" stdout\n+\t\t\tgit update-ref $type --stdin --batch-updates <stdin >stdout 2>err &&\n+\t\t\ttest_grep \"rejected refs/heads/non-existent $ZERO_OID $head reference does not exist\" stdout &&\n+\t\t\ttest_grep \"cannot lock ref ${SQ}refs/heads/symbolic${SQ}: unable to resolve reference ${SQ}refs/heads/non-existent${SQ}\" err\n \t\t)\n \t'\n \n@@ -2373,8 +2384,9 @@ do\n \t\t\thead=$(git rev-parse HEAD) &&\n \n \t\t\tformat_command $type \"delete refs/heads/new-branch\" \"$head\" >stdin &&\n-\t\t\tgit update-ref $type --stdin --batch-updates <stdin >stdout &&\n-\t\t\ttest_grep \"incorrect old value provided\" stdout\n+\t\t\tgit update-ref $type --stdin --batch-updates <stdin >stdout 2>err &&\n+\t\t\ttest_grep \"rejected refs/heads/new-branch $ZERO_OID $head incorrect old value provided\" stdout &&\n+\t\t\ttest_grep \"cannot lock ref ${SQ}refs/heads/new-branch${SQ}: is at $(git rev-parse new-branch) but expected $head\" err\n \t\t)\n \t'\n \n@@ -2387,8 +2399,9 @@ do\n \t\t\thead=$(git rev-parse HEAD) &&\n \n \t\t\tformat_command $type \"delete refs/heads/non-existent\" \"$head\" >stdin &&\n-\t\t\tgit update-ref $type --stdin --batch-updates <stdin >stdout &&\n-\t\t\ttest_grep \"reference does not exist\" stdout\n+\t\t\tgit update-ref $type --stdin --batch-updates <stdin >stdout 2>err &&\n+\t\t\ttest_grep \"rejected refs/heads/non-existent $ZERO_OID $head reference does not exist\" stdout &&\n+\t\t\ttest_grep \"cannot lock ref ${SQ}refs/heads/non-existent${SQ}: unable to resolve reference ${SQ}refs/heads/non-existent${SQ}\" err\n \t\t)\n \t'\n done\n\n-- \n2.51.2\n\n"},{"id":"534094","messageId":"20260116-633-regression-lost-diagnostic-message-when-pushing-non-commit-objects-to-refs-heads-v2-5-925a0e9c7f32@gmail.com","threadId":"64825","inReplyTo":"20260116-633-regression-lost-diagnostic-message-when-pushing-non-commit-objects-to-refs-heads-v2-0-925a0e9c7f32@gmail.com","subject":"[PATCH v2 5/7] fetch: utilize rejected ref error details","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-01-16T21:27:10Z","receivedAt":"2026-01-16T21:27:17Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"In 0e358de64a (fetch: use batched reference updates, 2025-05-19),\ngit-fetch(1) switched to using batched reference updates. This also\nintroduced a regression wherein instead of providing detailed error\nmessages for failed referenced updates, the users were provided generic\nerror messages based on the error type.\n\nSimilar to the previous commit, switch to using detailed error messages\nif present for failed reference updates to fix this regression.\n\nReported-by: Elijah Newren <newren@gmail.com>\nCo-authored-by: Jeff King <peff@peff.net>\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n builtin/fetch.c  | 10 ++++++----\n t/t5510-fetch.sh |  8 ++++----\n 2 files changed, 10 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex d427adea61..49495be0b6 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1649,7 +1649,7 @@ static void ref_transaction_rejection_handler(const char *refname,\n \t\t\t\t\t      const char *old_target UNUSED,\n \t\t\t\t\t      const char *new_target UNUSED,\n \t\t\t\t\t      enum ref_transaction_error err,\n-\t\t\t\t\t      const char *details UNUSED,\n+\t\t\t\t\t      const char *details,\n \t\t\t\t\t      void *cb_data)\n {\n \tstruct ref_rejection_data *data = cb_data;\n@@ -1674,9 +1674,11 @@ static void ref_transaction_rejection_handler(const char *refname,\n \t\t\t\"branches\"), data->remote_name);\n \t\tdata->conflict_msg_shown = true;\n \t} else {\n-\t\tconst char *reason = ref_transaction_error_msg(err);\n-\n-\t\terror(_(\"fetching ref %s failed: %s\"), refname, reason);\n+\t\tif (details)\n+\t\t\terror(\"%s\", details);\n+\t\telse\n+\t\t\terror(_(\"fetching ref %s failed: %s\"),\n+\t\t\t      refname, ref_transaction_error_msg(err));\n \t}\n \n \t*data->retcode = 1;\ndiff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\nindex ce1c23684e..c69afb5a60 100755\n--- a/t/t5510-fetch.sh\n+++ b/t/t5510-fetch.sh\n@@ -1516,7 +1516,7 @@ test_expect_success REFFILES 'existing reference lock in repo' '\n \t\tgit remote add origin ../base &&\n \t\ttouch refs/heads/foo.lock &&\n \t\ttest_must_fail git fetch -f origin \"refs/heads/*:refs/heads/*\" 2>err &&\n-\t\ttest_grep \"error: fetching ref refs/heads/foo failed: reference already exists\" err &&\n+\t\ttest_grep -e \"error: cannot lock ref ${SQ}refs/heads/foo${SQ}: Unable to create\" -e \"refs/heads/foo.lock${SQ}: File exists.\" err &&\n \t\tgit rev-parse refs/heads/main >expect &&\n \t\tgit rev-parse refs/heads/branch >actual &&\n \t\ttest_cmp expect actual\n@@ -1530,7 +1530,7 @@ test_expect_success CASE_INSENSITIVE_FS,REFFILES 'F/D conflict on case insensiti\n \t\tcd case_insensitive &&\n \t\tgit remote add origin -- ../case_sensitive_fd &&\n \t\ttest_must_fail git fetch -f origin \"refs/heads/*:refs/heads/*\" 2>err &&\n-\t\ttest_grep \"failed: refname conflict\" err &&\n+\t\ttest_grep \"cannot process ${SQ}refs/remotes/origin/foo${SQ} and ${SQ}refs/remotes/origin/foo/bar${SQ} at the same time\" err &&\n \t\tgit rev-parse refs/heads/main >expect &&\n \t\tgit rev-parse refs/heads/foo/bar >actual &&\n \t\ttest_cmp expect actual\n@@ -1544,7 +1544,7 @@ test_expect_success CASE_INSENSITIVE_FS,REFFILES 'D/F conflict on case insensiti\n \t\tcd case_insensitive &&\n \t\tgit remote add origin -- ../case_sensitive_df &&\n \t\ttest_must_fail git fetch -f origin \"refs/heads/*:refs/heads/*\" 2>err &&\n-\t\ttest_grep \"failed: refname conflict\" err &&\n+\t\ttest_grep \"cannot lock ref ${SQ}refs/remotes/origin/foo${SQ}: there is a non-empty directory ${SQ}./refs/remotes/origin/foo${SQ} blocking reference ${SQ}refs/remotes/origin/foo${SQ}\" err &&\n \t\tgit rev-parse refs/heads/main >expect &&\n \t\tgit rev-parse refs/heads/Foo/bar >actual &&\n \t\ttest_cmp expect actual\n@@ -1658,7 +1658,7 @@ test_expect_success REFFILES \"FETCH_HEAD is updated even if ref updates fail\" '\n \t\tgit remote add origin ../base &&\n \t\t>refs/heads/foo.lock &&\n \t\ttest_must_fail git fetch -f origin \"refs/heads/*:refs/heads/*\" 2>err &&\n-\t\ttest_grep \"error: fetching ref refs/heads/foo failed: reference already exists\" err &&\n+\t\ttest_grep -e \"error: cannot lock ref ${SQ}refs/heads/foo${SQ}: Unable to create\" -e \"refs/heads/foo.lock${SQ}: File exists.\" err &&\n \t\ttest_grep \"branch ${SQ}branch${SQ} of ../base\" FETCH_HEAD &&\n \t\ttest_grep \"branch ${SQ}foo${SQ} of ../base\" FETCH_HEAD\n \t)\n\n-- \n2.51.2\n\n"},{"id":"534095","messageId":"20260116-633-regression-lost-diagnostic-message-when-pushing-non-commit-objects-to-refs-heads-v2-6-925a0e9c7f32@gmail.com","threadId":"64825","inReplyTo":"20260116-633-regression-lost-diagnostic-message-when-pushing-non-commit-objects-to-refs-heads-v2-0-925a0e9c7f32@gmail.com","subject":"[PATCH v2 6/7] receive-pack: utilize rejected ref error details","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-01-16T21:27:11Z","receivedAt":"2026-01-16T21:27:18Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"In 9d2962a7c4 (receive-pack: use batched reference updates, 2025-05-19),\ngit-receive-pack(1) switched to using batched reference updates. This also\nintroduced a regression wherein instead of providing detailed error\nmessages for failed referenced updates, the users were provided generic\nerror messages based on the error type.\n\nNow that the updates also contain detailed error message, propagate\nthose to the client via 'rp_error'. The detailed error messages can be\nvery verbose, for e.g. in the files backend, when trying to write a\nnon-commit object to a branch, you would see:\n\n   ! [remote rejected] 3eaec9ccf3a53f168362a6b3fdeb73426fb9813d ->\n   branch (cannot update ref 'refs/heads/branch': trying to write\n   non-commit object 3eaec9ccf3a53f168362a6b3fdeb73426fb9813d to branch\n   'refs/heads/branch')\n\nHere the refname is repeated multiple times due to how error messages\nare propagated and filled over the code stack. This potentially can be\ncleaned up in a future commit.\n\nReported-by: Elijah Newren <newren@gmail.com>\nCo-authored-by: Jeff King <peff@peff.net>\nSigned-off-by: Jeff King <peff@peff.net>\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n builtin/receive-pack.c |  8 ++++++--\n t/t5516-fetch-push.sh  | 15 +++++++++++++++\n 2 files changed, 21 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 94d3e73cee..70e04b3efb 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1813,11 +1813,14 @@ static void ref_transaction_rejection_handler(const char *refname,\n \t\t\t\t\t      const char *old_target UNUSED,\n \t\t\t\t\t      const char *new_target UNUSED,\n \t\t\t\t\t      enum ref_transaction_error err,\n-\t\t\t\t\t      const char *details UNUSED,\n+\t\t\t\t\t      const char *details,\n \t\t\t\t\t      void *cb_data)\n {\n \tstruct strmap *failed_refs = cb_data;\n \n+\tif (details)\n+\t\trp_error(\"%s\", details);\n+\n \tstrmap_put(failed_refs, refname, (char *)ref_transaction_error_msg(err));\n }\n \n@@ -1884,6 +1887,7 @@ static void execute_commands_non_atomic(struct command *commands,\n \t\t}\n \n \t\tref_transaction_for_each_rejected_update(transaction,\n+\n \t\t\t\t\t\t\t ref_transaction_rejection_handler,\n \t\t\t\t\t\t\t &failed_refs);\n \n@@ -1895,7 +1899,7 @@ static void execute_commands_non_atomic(struct command *commands,\n \t\t\tif (reported_error)\n \t\t\t\tcmd->error_string = reported_error;\n \t\t\telse if (strmap_contains(&failed_refs, cmd->ref_name))\n-\t\t\t\tcmd->error_string = strmap_get(&failed_refs, cmd->ref_name);\n+\t\t\t\tcmd->error_string = cmd->error_string_owned = xstrdup(strmap_get(&failed_refs, cmd->ref_name));\n \t\t}\n \n \tcleanup:\ndiff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh\nindex 46926e7bbd..45595991c8 100755\n--- a/t/t5516-fetch-push.sh\n+++ b/t/t5516-fetch-push.sh\n@@ -1882,4 +1882,19 @@ test_expect_success 'push with F/D conflict with deletion and creation' '\n \tgit push testrepo :refs/heads/branch/conflict refs/heads/branch\n '\n \n+test_expect_success 'pushing non-commit objects should report error' '\n+\ttest_when_finished \"rm -rf dest repo\" &&\n+\tgit init dest &&\n+\tgit init repo &&\n+\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit --annotate test &&\n+\n+\t\ttagsha=$(git rev-parse test^{tag}) &&\n+\t\ttest_must_fail git push ../dest \"$tagsha:refs/heads/branch\" 2>err &&\n+\t\ttest_grep \"trying to write non-commit object $tagsha to branch ${SQ}refs/heads/branch${SQ}\" err\n+\t)\n+'\n+\n test_done\n\n-- \n2.51.2\n\n"},{"id":"534096","messageId":"20260116-633-regression-lost-diagnostic-message-when-pushing-non-commit-objects-to-refs-heads-v2-7-925a0e9c7f32@gmail.com","threadId":"64825","inReplyTo":"20260116-633-regression-lost-diagnostic-message-when-pushing-non-commit-objects-to-refs-heads-v2-0-925a0e9c7f32@gmail.com","subject":"[PATCH v2 7/7] fetch: delay user information post committing of transaction","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-01-16T21:27:12Z","receivedAt":"2026-01-16T21:27:19Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"In Git 2.50 and earlier, we would display failure codes and error\nmessage as part of the status display:\n\n  $ git fetch . v1.0.0:refs/heads/foo\n    error: cannot update ref 'refs/heads/foo': trying to write non-commit object f665776185ad074b236c00751d666da7d1977dbe to branch 'refs/heads/foo'\n    From .\n     ! [new tag]               v1.0.0     -> foo  (unable to update local ref)\n\nWith the addition of batched updates, this information is no longer\nshown to the user:\n\n  $ git fetch . v1.0.0:refs/heads/foo\n    From .\n     * [new tag]               v1.0.0     -> foo\n    error: cannot update ref 'refs/heads/foo': trying to write non-commit object f665776185ad074b236c00751d666da7d1977dbe to branch 'refs/heads/foo'\n\nSince reference updates are batched and processed together at the end,\ninformation around the outcome is not available during individual\nreference parsing.\n\nTo overcome this, collate and delay the output to the end. Introduce\n`ref_update_display_info` which will hold individual update's\ninformation and also whether the update failed or succeeded. This\nfinally allows us to iterate over all such updates and print them to the\nuser. While this brings back the functionality, it does change the order\nof the output. Modify the tests to reflect this.\n\nUsing an strmap does add some overhead to 'git-fetch(1)', but from\nbenchmarking this seems to be not too bad:\n\n  Benchmark 1: fetch: many refs (refformat = files, refcount = 1000, revision = master)\n    Time (mean ± σ):      51.9 ms ±   2.5 ms    [User: 15.6 ms, System: 36.9 ms]\n    Range (min … max):    47.4 ms …  58.3 ms    41 runs\n\n  Benchmark 2: fetch: many refs (refformat = files, refcount = 1000, revision = HEAD)\n    Time (mean ± σ):      53.0 ms ±   1.8 ms    [User: 17.6 ms, System: 36.0 ms]\n    Range (min … max):    49.4 ms …  57.6 ms    40 runs\n\n  Summary\n    fetch: many refs (refformat = files, refcount = 1000, revision = master) ran\n      1.02 ± 0.06 times faster than fetch: many refs (refformat = files, refcount = 1000, revision = HEAD)\n\nAnother approach would be to move the status printing logic to be\nhandled post the transaction being committed. That however would require\nadding an iterator to the ref transaction that tracks both the outcome\n(success/failure) and the original refspec information for each update,\nwhich is more involved infrastructure work compared to the strmap\napproach here.\n\nReported-by: Jeff King <peff@peff.net>\nSigned-off-by: Karthik Nayak <karthik.188@gmail.com>\n---\n builtin/fetch.c         | 179 ++++++++++++++++++++++++++++++++++++++++--------\n t/t5516-fetch-push.sh   |   1 +\n t/t5574-fetch-output.sh |  16 ++---\n 3 files changed, 161 insertions(+), 35 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 49495be0b6..afe5d321d1 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -861,12 +861,77 @@ static void display_ref_update(struct display_state *display_state, char code,\n \tfputs(display_state->buf.buf, f);\n }\n \n+struct ref_update_display_info {\n+\tbool failed;\n+\tchar success_code;\n+\tchar fail_code;\n+\tconst char *summary;\n+\tconst char *fail_detail;\n+\tconst char *success_detail;\n+\tconst char *remote;\n+\tconst char *local;\n+\tstruct object_id old_oid;\n+\tstruct object_id new_oid;\n+};\n+\n+static struct ref_update_display_info *ref_update_display_info_new(\n+\t\t\t\t\t\tchar success_code,\n+\t\t\t\t\t\tchar fail_code,\n+\t\t\t\t\t\tconst char *summary,\n+\t\t\t\t\t\tconst char *success_detail,\n+\t\t\t\t\t\tconst char *fail_detail,\n+\t\t\t\t\t\tconst char *remote,\n+\t\t\t\t\t\tconst struct object_id *old_oid,\n+\t\t\t\t\t\tconst struct object_id *new_oid)\n+{\n+\tstruct ref_update_display_info *info;\n+\tCALLOC_ARRAY(info, 1);\n+\n+\tinfo->success_code = success_code;\n+\tinfo->fail_code = fail_code;\n+\tinfo->summary = xstrdup(summary);\n+\tinfo->success_detail = xstrdup_or_null(success_detail);\n+\tinfo->fail_detail = xstrdup_or_null(fail_detail);\n+\tinfo->remote = xstrdup(remote);\n+\n+\toidcpy(&info->old_oid, old_oid);\n+\toidcpy(&info->new_oid, new_oid);\n+\n+\treturn info;\n+}\n+\n+static void ref_update_display_info_set_failed(struct ref_update_display_info *info)\n+{\n+\tinfo->failed = true;\n+}\n+\n+static void ref_update_display_info_free(struct ref_update_display_info *info)\n+{\n+\tfree((char *)info->summary);\n+\tfree((char *)info->success_detail);\n+\tfree((char *)info->fail_detail);\n+\tfree((char *)info->remote);\n+}\n+\n+static void ref_update_display_info_display(struct ref_update_display_info *info,\n+\t\t\t\t\t    struct display_state *display_state,\n+\t\t\t\t\t    const char *refname, int summary_width)\n+{\n+\tdisplay_ref_update(display_state,\n+\t\t\t   info->failed ? info->fail_code : info->success_code,\n+\t\t\t   info->summary,\n+\t\t\t   info->failed ? info->fail_detail : info->success_detail,\n+\t\t\t   info->remote, refname, &info->old_oid,\n+\t\t\t   &info->new_oid, summary_width);\n+}\n+\n static int update_local_ref(struct ref *ref,\n \t\t\t    struct ref_transaction *transaction,\n \t\t\t    struct display_state *display_state,\n \t\t\t    const struct ref *remote_ref,\n \t\t\t    int summary_width,\n-\t\t\t    const struct fetch_config *config)\n+\t\t\t    const struct fetch_config *config,\n+\t\t\t    struct strmap *delayed_ref_display)\n {\n \tstruct commit *current = NULL, *updated;\n \tint fast_forward = 0;\n@@ -900,12 +965,19 @@ static int update_local_ref(struct ref *ref,\n \tif (!is_null_oid(&ref->old_oid) &&\n \t    starts_with(ref->name, \"refs/tags/\")) {\n \t\tif (force || ref->force) {\n+\t\t\tstruct ref_update_display_info *info;\n \t\t\tint r;\n+\n \t\t\tr = s_update_ref(\"updating tag\", ref, transaction, 0);\n-\t\t\tdisplay_ref_update(display_state, r ? '!' : 't', _(\"[tag update]\"),\n-\t\t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t\t   remote_ref->name, ref->name,\n-\t\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n+\n+\t\t\tinfo = ref_update_display_info_new('t', '!', _(\"[tag update]\"), NULL,\n+\t\t\t\t\t\t\t   _(\"unable to update local ref\"),\n+\t\t\t\t\t\t\t   remote_ref->name, &ref->old_oid,\n+\t\t\t\t\t\t\t   &ref->new_oid);\n+\t\t\tif (r)\n+\t\t\t\tref_update_display_info_set_failed(info);\n+\t\t\tstrmap_put(delayed_ref_display, ref->name, info);\n+\n \t\t\treturn r;\n \t\t} else {\n \t\t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n@@ -921,6 +993,7 @@ static int update_local_ref(struct ref *ref,\n \tupdated = lookup_commit_reference_gently(the_repository,\n \t\t\t\t\t\t &ref->new_oid, 1);\n \tif (!current || !updated) {\n+\t\tstruct ref_update_display_info *info;\n \t\tconst char *msg;\n \t\tconst char *what;\n \t\tint r;\n@@ -941,10 +1014,15 @@ static int update_local_ref(struct ref *ref,\n \t\t}\n \n \t\tr = s_update_ref(msg, ref, transaction, 0);\n-\t\tdisplay_ref_update(display_state, r ? '!' : '*', what,\n-\t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t   remote_ref->name, ref->name,\n-\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n+\n+\t\tinfo = ref_update_display_info_new('*', '!', what, NULL,\n+\t\t\t\t\t\t   _(\"unable to update local ref\"),\n+\t\t\t\t\t\t   remote_ref->name, &ref->old_oid,\n+\t\t\t\t\t\t   &ref->new_oid);\n+\t\tif (r)\n+\t\t\tref_update_display_info_set_failed(info);\n+\t\tstrmap_put(delayed_ref_display, ref->name, info);\n+\n \t\treturn r;\n \t}\n \n@@ -960,6 +1038,7 @@ static int update_local_ref(struct ref *ref,\n \t}\n \n \tif (fast_forward) {\n+\t\tstruct ref_update_display_info *info;\n \t\tstruct strbuf quickref = STRBUF_INIT;\n \t\tint r;\n \n@@ -967,23 +1046,36 @@ static int update_local_ref(struct ref *ref,\n \t\tstrbuf_addstr(&quickref, \"..\");\n \t\tstrbuf_add_unique_abbrev(&quickref, &ref->new_oid, DEFAULT_ABBREV);\n \t\tr = s_update_ref(\"fast-forward\", ref, transaction, 1);\n-\t\tdisplay_ref_update(display_state, r ? '!' : ' ', quickref.buf,\n-\t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t   remote_ref->name, ref->name,\n-\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n+\n+\t\tinfo = ref_update_display_info_new(' ', '!', quickref.buf, NULL,\n+\t\t\t\t\t\t   _(\"unable to update local ref\"),\n+\t\t\t\t\t\t   remote_ref->name, &ref->old_oid,\n+\t\t\t\t\t\t   &ref->new_oid);\n+\t\tif (r)\n+\t\t\tref_update_display_info_set_failed(info);\n+\t\tstrmap_put(delayed_ref_display, ref->name, info);\n+\n \t\tstrbuf_release(&quickref);\n \t\treturn r;\n \t} else if (force || ref->force) {\n+\t\tstruct ref_update_display_info *info;\n \t\tstruct strbuf quickref = STRBUF_INIT;\n \t\tint r;\n+\n \t\tstrbuf_add_unique_abbrev(&quickref, &current->object.oid, DEFAULT_ABBREV);\n \t\tstrbuf_addstr(&quickref, \"...\");\n \t\tstrbuf_add_unique_abbrev(&quickref, &ref->new_oid, DEFAULT_ABBREV);\n \t\tr = s_update_ref(\"forced-update\", ref, transaction, 1);\n-\t\tdisplay_ref_update(display_state, r ? '!' : '+', quickref.buf,\n-\t\t\t\t   r ? _(\"unable to update local ref\") : _(\"forced update\"),\n-\t\t\t\t   remote_ref->name, ref->name,\n-\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n+\n+\t\tinfo = ref_update_display_info_new('+', '!', quickref.buf,\n+\t\t\t\t\t\t   _(\"forced update\"),\n+\t\t\t\t\t\t   _(\"unable to update local ref\"),\n+\t\t\t\t\t\t   remote_ref->name, &ref->old_oid,\n+\t\t\t\t\t\t   &ref->new_oid);\n+\t\tif (r)\n+\t\t\tref_update_display_info_set_failed(info);\n+\t\tstrmap_put(delayed_ref_display, ref->name, info);\n+\n \t\tstrbuf_release(&quickref);\n \t\treturn r;\n \t} else {\n@@ -1103,7 +1195,8 @@ static int store_updated_refs(struct display_state *display_state,\n \t\t\t      int connectivity_checked,\n \t\t\t      struct ref_transaction *transaction, struct ref *ref_map,\n \t\t\t      struct fetch_head *fetch_head,\n-\t\t\t      const struct fetch_config *config)\n+\t\t\t      const struct fetch_config *config,\n+\t\t\t      struct strmap *delayed_ref_display)\n {\n \tint rc = 0;\n \tstruct strbuf note = STRBUF_INIT;\n@@ -1219,7 +1312,8 @@ static int store_updated_refs(struct display_state *display_state,\n \n \t\t\tif (ref) {\n \t\t\t\trc |= update_local_ref(ref, transaction, display_state,\n-\t\t\t\t\t\t       rm, summary_width, config);\n+\t\t\t\t\t\t       rm, summary_width, config,\n+\t\t\t\t\t\t       delayed_ref_display);\n \t\t\t\tfree(ref);\n \t\t\t} else if (write_fetch_head || dry_run) {\n \t\t\t\t/*\n@@ -1300,7 +1394,8 @@ static int fetch_and_consume_refs(struct display_state *display_state,\n \t\t\t\t  struct ref_transaction *transaction,\n \t\t\t\t  struct ref *ref_map,\n \t\t\t\t  struct fetch_head *fetch_head,\n-\t\t\t\t  const struct fetch_config *config)\n+\t\t\t\t  const struct fetch_config *config,\n+\t\t\t\t  struct strmap *delayed_ref_display)\n {\n \tint connectivity_checked = 1;\n \tint ret;\n@@ -1322,7 +1417,8 @@ static int fetch_and_consume_refs(struct display_state *display_state,\n \n \ttrace2_region_enter(\"fetch\", \"consume_refs\", the_repository);\n \tret = store_updated_refs(display_state, connectivity_checked,\n-\t\t\t\t transaction, ref_map, fetch_head, config);\n+\t\t\t\t transaction, ref_map, fetch_head, config,\n+\t\t\t\t delayed_ref_display);\n \ttrace2_region_leave(\"fetch\", \"consume_refs\", the_repository);\n \n out:\n@@ -1493,7 +1589,8 @@ static int backfill_tags(struct display_state *display_state,\n \t\t\t struct ref_transaction *transaction,\n \t\t\t struct ref *ref_map,\n \t\t\t struct fetch_head *fetch_head,\n-\t\t\t const struct fetch_config *config)\n+\t\t\t const struct fetch_config *config,\n+\t\t\t struct strmap *delayed_ref_display)\n {\n \tint retcode, cannot_reuse;\n \n@@ -1515,7 +1612,7 @@ static int backfill_tags(struct display_state *display_state,\n \ttransport_set_option(transport, TRANS_OPT_DEPTH, \"0\");\n \ttransport_set_option(transport, TRANS_OPT_DEEPEN_RELATIVE, NULL);\n \tretcode = fetch_and_consume_refs(display_state, transport, transaction, ref_map,\n-\t\t\t\t\t fetch_head, config);\n+\t\t\t\t\t fetch_head, config, delayed_ref_display);\n \n \tif (gsecondary) {\n \t\ttransport_disconnect(gsecondary);\n@@ -1641,6 +1738,7 @@ struct ref_rejection_data {\n \tbool conflict_msg_shown;\n \tbool case_sensitive_msg_shown;\n \tconst char *remote_name;\n+\tstruct strmap *delayed_ref_display;\n };\n \n static void ref_transaction_rejection_handler(const char *refname,\n@@ -1653,6 +1751,7 @@ static void ref_transaction_rejection_handler(const char *refname,\n \t\t\t\t\t      void *cb_data)\n {\n \tstruct ref_rejection_data *data = cb_data;\n+\tstruct ref_update_display_info *info;\n \n \tif (err == REF_TRANSACTION_ERROR_CASE_CONFLICT && ignore_case &&\n \t    !data->case_sensitive_msg_shown) {\n@@ -1681,6 +1780,10 @@ static void ref_transaction_rejection_handler(const char *refname,\n \t\t\t      refname, ref_transaction_error_msg(err));\n \t}\n \n+\tinfo = strmap_get(data->delayed_ref_display, refname);\n+\tif (info)\n+\t\tref_update_display_info_set_failed(info);\n+\n \t*data->retcode = 1;\n }\n \n@@ -1690,6 +1793,7 @@ static void ref_transaction_rejection_handler(const char *refname,\n  */\n static int commit_ref_transaction(struct ref_transaction **transaction,\n \t\t\t\t  bool is_atomic, const char *remote_name,\n+\t\t\t\t  struct strmap *delayed_ref_display,\n \t\t\t\t  struct strbuf *err)\n {\n \tint retcode = ref_transaction_commit(*transaction, err);\n@@ -1701,6 +1805,7 @@ static int commit_ref_transaction(struct ref_transaction **transaction,\n \t\t\t.conflict_msg_shown = 0,\n \t\t\t.remote_name = remote_name,\n \t\t\t.retcode = &retcode,\n+\t\t\t.delayed_ref_display = delayed_ref_display,\n \t\t};\n \n \t\tref_transaction_for_each_rejected_update(*transaction,\n@@ -1729,6 +1834,10 @@ static int do_fetch(struct transport *transport,\n \tstruct fetch_head fetch_head = { 0 };\n \tstruct strbuf err = STRBUF_INIT;\n \tint do_set_head = 0;\n+\tstruct strmap delayed_ref_display = STRMAP_INIT;\n+\tint summary_width = 0;\n+\tstruct strmap_entry *e;\n+\tstruct hashmap_iter iter;\n \n \tif (tags == TAGS_DEFAULT) {\n \t\tif (transport->remote->fetch_tags == 2)\n@@ -1853,7 +1962,7 @@ static int do_fetch(struct transport *transport,\n \t}\n \n \tif (fetch_and_consume_refs(&display_state, transport, transaction, ref_map,\n-\t\t\t\t   &fetch_head, config)) {\n+\t\t\t\t   &fetch_head, config, &delayed_ref_display)) {\n \t\tretcode = 1;\n \t\tgoto cleanup;\n \t}\n@@ -1876,7 +1985,7 @@ static int do_fetch(struct transport *transport,\n \t\t\t * the transaction and don't commit anything.\n \t\t\t */\n \t\t\tif (backfill_tags(&display_state, transport, transaction, tags_ref_map,\n-\t\t\t\t\t  &fetch_head, config))\n+\t\t\t\t\t  &fetch_head, config, &delayed_ref_display))\n \t\t\t\tretcode = 1;\n \t\t}\n \n@@ -1886,8 +1995,12 @@ static int do_fetch(struct transport *transport,\n \tif (retcode)\n \t\tgoto cleanup;\n \n+\tif (verbosity >= 0)\n+\t\tsummary_width = transport_summary_width(ref_map);\n+\n \tretcode = commit_ref_transaction(&transaction, atomic_fetch,\n-\t\t\t\t\t transport->remote->name, &err);\n+\t\t\t\t\t transport->remote->name,\n+\t\t\t\t\t &delayed_ref_display, &err);\n \t/*\n \t * With '--atomic', bail out if the transaction fails. Without '--atomic',\n \t * continue to fetch head and perform other post-fetch operations.\n@@ -1965,7 +2078,17 @@ static int do_fetch(struct transport *transport,\n \t */\n \tif (retcode && !atomic_fetch && transaction)\n \t\tcommit_ref_transaction(&transaction, false,\n-\t\t\t\t       transport->remote->name, &err);\n+\t\t\t\t       transport->remote->name,\n+\t\t\t\t       &delayed_ref_display, &err);\n+\n+\t/*\n+\t * Clear any pending information that needs to be shown to the user.\n+\t */\n+\tstrmap_for_each_entry(&delayed_ref_display, &iter, e) {\n+\t\tstruct ref_update_display_info *info = e->value;\n+\t\tref_update_display_info_display(info, &display_state, e->key, summary_width);\n+\t\tref_update_display_info_free(info);\n+\t}\n \n \tif (retcode) {\n \t\tif (err.len) {\n@@ -1980,6 +2103,8 @@ static int do_fetch(struct transport *transport,\n \n \tif (transaction)\n \t\tref_transaction_free(transaction);\n+\n+\tstrmap_clear(&delayed_ref_display, 1);\n \tdisplay_state_release(&display_state);\n \tclose_fetch_head(&fetch_head);\n \tstrbuf_release(&err);\ndiff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh\nindex 45595991c8..29e2f17608 100755\n--- a/t/t5516-fetch-push.sh\n+++ b/t/t5516-fetch-push.sh\n@@ -1893,6 +1893,7 @@ test_expect_success 'pushing non-commit objects should report error' '\n \n \t\ttagsha=$(git rev-parse test^{tag}) &&\n \t\ttest_must_fail git push ../dest \"$tagsha:refs/heads/branch\" 2>err &&\n+\t\ttest_grep \"! \\[remote rejected\\] $tagsha -> branch (invalid new value provided)\" err &&\n \t\ttest_grep \"trying to write non-commit object $tagsha to branch ${SQ}refs/heads/branch${SQ}\" err\n \t)\n '\ndiff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\nindex 5883839a04..22bfc0c74d 100755\n--- a/t/t5574-fetch-output.sh\n+++ b/t/t5574-fetch-output.sh\n@@ -40,8 +40,8 @@ test_expect_success 'fetch aligned output' '\n \t\tgrep -e \"->\" actual | cut -c 22- >../actual\n \t) &&\n \tcat >expect <<-\\EOF &&\n-\tmain                 -> origin/main\n \tlooooooooooooong-tag -> looooooooooooong-tag\n+\tmain                 -> origin/main\n \tEOF\n \ttest_cmp expect actual\n '\n@@ -55,8 +55,8 @@ test_expect_success 'fetch compact output' '\n \t\tgrep -e \"->\" actual | cut -c 22- >../actual\n \t) &&\n \tcat >expect <<-\\EOF &&\n-\tmain       -> origin/*\n \textraaa    -> *\n+\tmain       -> origin/*\n \tEOF\n \ttest_cmp expect actual\n '\n@@ -103,15 +103,15 @@ do\n \t\tcat >expect <<-EOF &&\n \t\t- $MAIN_OLD $ZERO_OID refs/forced/deleted-branch\n \t\t- $MAIN_OLD $ZERO_OID refs/unforced/deleted-branch\n-\t\t  $MAIN_OLD $FAST_FORWARD_NEW refs/unforced/fast-forward\n \t\t! $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/unforced/force-updated\n+\t\t* $ZERO_OID $MAIN_OLD refs/forced/new-branch\n+\t\t* $ZERO_OID $MAIN_OLD refs/remotes/origin/new-branch\n+\t\t+ $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/remotes/origin/force-updated\n+\t\t  $MAIN_OLD $FAST_FORWARD_NEW refs/unforced/fast-forward\n \t\t* $ZERO_OID $MAIN_OLD refs/unforced/new-branch\n \t\t  $MAIN_OLD $FAST_FORWARD_NEW refs/forced/fast-forward\n-\t\t+ $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/forced/force-updated\n-\t\t* $ZERO_OID $MAIN_OLD refs/forced/new-branch\n \t\t  $MAIN_OLD $FAST_FORWARD_NEW refs/remotes/origin/fast-forward\n-\t\t+ $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/remotes/origin/force-updated\n-\t\t* $ZERO_OID $MAIN_OLD refs/remotes/origin/new-branch\n+\t\t+ $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/forced/force-updated\n \t\tEOF\n \n \t\t# Change the URL of the repository to fetch different references.\n@@ -179,8 +179,8 @@ test_expect_success 'fetch porcelain overrides fetch.output config' '\n \tnew_commit=$(git rev-parse HEAD) &&\n \n \tcat >expect <<-EOF &&\n-\t  $old_commit $new_commit refs/remotes/origin/config-override\n \t* $ZERO_OID $new_commit refs/tags/new-commit\n+\t  $old_commit $new_commit refs/remotes/origin/config-override\n \tEOF\n \n \tgit -C porcelain -c fetch.output=compact fetch --porcelain >stdout 2>stderr &&\n\n-- \n2.51.2\n\n"},{"id":"534111","messageId":"0082426c-a945-4f2e-969e-897e1aeaed66@gmail.com","threadId":"64825","inReplyTo":"20260116-633-regression-lost-diagnostic-message-when-pushing-non-commit-objects-to-refs-heads-v2-7-925a0e9c7f32@gmail.com","subject":"Re: [PATCH v2 7/7] fetch: delay user information post committing of transaction","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-01-17T13:56:22Z","receivedAt":"2026-01-17T13:56:26Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Karthik\n\nOn 16/01/2026 21:27, Karthik Nayak wrote:\n> In Git 2.50 and earlier, we would display failure codes and error\n> message as part of the status display:\n> \n>    $ git fetch . v1.0.0:refs/heads/foo\n>      error: cannot update ref 'refs/heads/foo': trying to write non-commit object f665776185ad074b236c00751d666da7d1977dbe to branch 'refs/heads/foo'\n>      From .\n>       ! [new tag]               v1.0.0     -> foo  (unable to update local ref)\n> \n> With the addition of batched updates, this information is no longer\n> shown to the user:\n> \n>    $ git fetch . v1.0.0:refs/heads/foo\n>      From .\n>       * [new tag]               v1.0.0     -> foo\n>      error: cannot update ref 'refs/heads/foo': trying to write non-commit object f665776185ad074b236c00751d666da7d1977dbe to branch 'refs/heads/foo'\n> \n> Since reference updates are batched and processed together at the end,\n> information around the outcome is not available during individual\n> reference parsing.\n> \n> To overcome this, collate and delay the output to the end. Introduce\n> `ref_update_display_info` which will hold individual update's\n> information and also whether the update failed or succeeded. This\n> finally allows us to iterate over all such updates and print them to the\n> user. While this brings back the functionality, it does change the order\n> of the output. Modify the tests to reflect this.\n\nIt is unfortunate that a fix for a regression the the messages changes \nthe order of those messages. It is doubly unfortunate that the new order \ndepends on the implementation of strmap_for_each() which may change in \nthe future. I think you can avoid this by appending each update to an \narray in update_local_ref() and adding the errors to a separate strmap \nin ref_transaction_rejection_handler(). Then when you come to print the \nmassages, loop over the array and for each update lookup the ref in the \nstrmap to see if it failed before printing the appropriate message.\n\nThanks\n\nPhillip\n\n> Using an strmap does add some overhead to 'git-fetch(1)', but from\n> benchmarking this seems to be not too bad:\n> \n>    Benchmark 1: fetch: many refs (refformat = files, refcount = 1000, revision = master)\n>      Time (mean ± σ):      51.9 ms ±   2.5 ms    [User: 15.6 ms, System: 36.9 ms]\n>      Range (min … max):    47.4 ms …  58.3 ms    41 runs\n> \n>    Benchmark 2: fetch: many refs (refformat = files, refcount = 1000, revision = HEAD)\n>      Time (mean ± σ):      53.0 ms ±   1.8 ms    [User: 17.6 ms, System: 36.0 ms]\n>      Range (min … max):    49.4 ms …  57.6 ms    40 runs\n> \n>    Summary\n>      fetch: many refs (refformat = files, refcount = 1000, revision = master) ran\n>        1.02 ± 0.06 times faster than fetch: many refs (refformat = files, refcount = 1000, revision = HEAD)\n> \n> Another approach would be to move the status printing logic to be\n> handled post the transaction being committed. That however would require\n> adding an iterator to the ref transaction that tracks both the outcome\n> (success/failure) and the original refspec information for each update,\n> which is more involved infrastructure work compared to the strmap\n> approach here.\n> \n> Reported-by: Jeff King <peff@peff.net>\n> Signed-off-by: Karthik Nayak <karthik.188@gmail.com>\n> ---\n>   builtin/fetch.c         | 179 ++++++++++++++++++++++++++++++++++++++++--------\n>   t/t5516-fetch-push.sh   |   1 +\n>   t/t5574-fetch-output.sh |  16 ++---\n>   3 files changed, 161 insertions(+), 35 deletions(-)\n> \n> diff --git a/builtin/fetch.c b/builtin/fetch.c\n> index 49495be0b6..afe5d321d1 100644\n> --- a/builtin/fetch.c\n> +++ b/builtin/fetch.c\n> @@ -861,12 +861,77 @@ static void display_ref_update(struct display_state *display_state, char code,\n>   \tfputs(display_state->buf.buf, f);\n>   }\n>   \n> +struct ref_update_display_info {\n> +\tbool failed;\n> +\tchar success_code;\n> +\tchar fail_code;\n> +\tconst char *summary;\n> +\tconst char *fail_detail;\n> +\tconst char *success_detail;\n> +\tconst char *remote;\n> +\tconst char *local;\n> +\tstruct object_id old_oid;\n> +\tstruct object_id new_oid;\n> +};\n> +\n> +static struct ref_update_display_info *ref_update_display_info_new(\n> +\t\t\t\t\t\tchar success_code,\n> +\t\t\t\t\t\tchar fail_code,\n> +\t\t\t\t\t\tconst char *summary,\n> +\t\t\t\t\t\tconst char *success_detail,\n> +\t\t\t\t\t\tconst char *fail_detail,\n> +\t\t\t\t\t\tconst char *remote,\n> +\t\t\t\t\t\tconst struct object_id *old_oid,\n> +\t\t\t\t\t\tconst struct object_id *new_oid)\n> +{\n> +\tstruct ref_update_display_info *info;\n> +\tCALLOC_ARRAY(info, 1);\n> +\n> +\tinfo->success_code = success_code;\n> +\tinfo->fail_code = fail_code;\n> +\tinfo->summary = xstrdup(summary);\n> +\tinfo->success_detail = xstrdup_or_null(success_detail);\n> +\tinfo->fail_detail = xstrdup_or_null(fail_detail);\n> +\tinfo->remote = xstrdup(remote);\n> +\n> +\toidcpy(&info->old_oid, old_oid);\n> +\toidcpy(&info->new_oid, new_oid);\n> +\n> +\treturn info;\n> +}\n> +\n> +static void ref_update_display_info_set_failed(struct ref_update_display_info *info)\n> +{\n> +\tinfo->failed = true;\n> +}\n> +\n> +static void ref_update_display_info_free(struct ref_update_display_info *info)\n> +{\n> +\tfree((char *)info->summary);\n> +\tfree((char *)info->success_detail);\n> +\tfree((char *)info->fail_detail);\n> +\tfree((char *)info->remote);\n> +}\n> +\n> +static void ref_update_display_info_display(struct ref_update_display_info *info,\n> +\t\t\t\t\t    struct display_state *display_state,\n> +\t\t\t\t\t    const char *refname, int summary_width)\n> +{\n> +\tdisplay_ref_update(display_state,\n> +\t\t\t   info->failed ? info->fail_code : info->success_code,\n> +\t\t\t   info->summary,\n> +\t\t\t   info->failed ? info->fail_detail : info->success_detail,\n> +\t\t\t   info->remote, refname, &info->old_oid,\n> +\t\t\t   &info->new_oid, summary_width);\n> +}\n> +\n>   static int update_local_ref(struct ref *ref,\n>   \t\t\t    struct ref_transaction *transaction,\n>   \t\t\t    struct display_state *display_state,\n>   \t\t\t    const struct ref *remote_ref,\n>   \t\t\t    int summary_width,\n> -\t\t\t    const struct fetch_config *config)\n> +\t\t\t    const struct fetch_config *config,\n> +\t\t\t    struct strmap *delayed_ref_display)\n>   {\n>   \tstruct commit *current = NULL, *updated;\n>   \tint fast_forward = 0;\n> @@ -900,12 +965,19 @@ static int update_local_ref(struct ref *ref,\n>   \tif (!is_null_oid(&ref->old_oid) &&\n>   \t    starts_with(ref->name, \"refs/tags/\")) {\n>   \t\tif (force || ref->force) {\n> +\t\t\tstruct ref_update_display_info *info;\n>   \t\t\tint r;\n> +\n>   \t\t\tr = s_update_ref(\"updating tag\", ref, transaction, 0);\n> -\t\t\tdisplay_ref_update(display_state, r ? '!' : 't', _(\"[tag update]\"),\n> -\t\t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n> -\t\t\t\t\t   remote_ref->name, ref->name,\n> -\t\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n> +\n> +\t\t\tinfo = ref_update_display_info_new('t', '!', _(\"[tag update]\"), NULL,\n> +\t\t\t\t\t\t\t   _(\"unable to update local ref\"),\n> +\t\t\t\t\t\t\t   remote_ref->name, &ref->old_oid,\n> +\t\t\t\t\t\t\t   &ref->new_oid);\n> +\t\t\tif (r)\n> +\t\t\t\tref_update_display_info_set_failed(info);\n> +\t\t\tstrmap_put(delayed_ref_display, ref->name, info);\n> +\n>   \t\t\treturn r;\n>   \t\t} else {\n>   \t\t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n> @@ -921,6 +993,7 @@ static int update_local_ref(struct ref *ref,\n>   \tupdated = lookup_commit_reference_gently(the_repository,\n>   \t\t\t\t\t\t &ref->new_oid, 1);\n>   \tif (!current || !updated) {\n> +\t\tstruct ref_update_display_info *info;\n>   \t\tconst char *msg;\n>   \t\tconst char *what;\n>   \t\tint r;\n> @@ -941,10 +1014,15 @@ static int update_local_ref(struct ref *ref,\n>   \t\t}\n>   \n>   \t\tr = s_update_ref(msg, ref, transaction, 0);\n> -\t\tdisplay_ref_update(display_state, r ? '!' : '*', what,\n> -\t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n> -\t\t\t\t   remote_ref->name, ref->name,\n> -\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n> +\n> +\t\tinfo = ref_update_display_info_new('*', '!', what, NULL,\n> +\t\t\t\t\t\t   _(\"unable to update local ref\"),\n> +\t\t\t\t\t\t   remote_ref->name, &ref->old_oid,\n> +\t\t\t\t\t\t   &ref->new_oid);\n> +\t\tif (r)\n> +\t\t\tref_update_display_info_set_failed(info);\n> +\t\tstrmap_put(delayed_ref_display, ref->name, info);\n> +\n>   \t\treturn r;\n>   \t}\n>   \n> @@ -960,6 +1038,7 @@ static int update_local_ref(struct ref *ref,\n>   \t}\n>   \n>   \tif (fast_forward) {\n> +\t\tstruct ref_update_display_info *info;\n>   \t\tstruct strbuf quickref = STRBUF_INIT;\n>   \t\tint r;\n>   \n> @@ -967,23 +1046,36 @@ static int update_local_ref(struct ref *ref,\n>   \t\tstrbuf_addstr(&quickref, \"..\");\n>   \t\tstrbuf_add_unique_abbrev(&quickref, &ref->new_oid, DEFAULT_ABBREV);\n>   \t\tr = s_update_ref(\"fast-forward\", ref, transaction, 1);\n> -\t\tdisplay_ref_update(display_state, r ? '!' : ' ', quickref.buf,\n> -\t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n> -\t\t\t\t   remote_ref->name, ref->name,\n> -\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n> +\n> +\t\tinfo = ref_update_display_info_new(' ', '!', quickref.buf, NULL,\n> +\t\t\t\t\t\t   _(\"unable to update local ref\"),\n> +\t\t\t\t\t\t   remote_ref->name, &ref->old_oid,\n> +\t\t\t\t\t\t   &ref->new_oid);\n> +\t\tif (r)\n> +\t\t\tref_update_display_info_set_failed(info);\n> +\t\tstrmap_put(delayed_ref_display, ref->name, info);\n> +\n>   \t\tstrbuf_release(&quickref);\n>   \t\treturn r;\n>   \t} else if (force || ref->force) {\n> +\t\tstruct ref_update_display_info *info;\n>   \t\tstruct strbuf quickref = STRBUF_INIT;\n>   \t\tint r;\n> +\n>   \t\tstrbuf_add_unique_abbrev(&quickref, &current->object.oid, DEFAULT_ABBREV);\n>   \t\tstrbuf_addstr(&quickref, \"...\");\n>   \t\tstrbuf_add_unique_abbrev(&quickref, &ref->new_oid, DEFAULT_ABBREV);\n>   \t\tr = s_update_ref(\"forced-update\", ref, transaction, 1);\n> -\t\tdisplay_ref_update(display_state, r ? '!' : '+', quickref.buf,\n> -\t\t\t\t   r ? _(\"unable to update local ref\") : _(\"forced update\"),\n> -\t\t\t\t   remote_ref->name, ref->name,\n> -\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n> +\n> +\t\tinfo = ref_update_display_info_new('+', '!', quickref.buf,\n> +\t\t\t\t\t\t   _(\"forced update\"),\n> +\t\t\t\t\t\t   _(\"unable to update local ref\"),\n> +\t\t\t\t\t\t   remote_ref->name, &ref->old_oid,\n> +\t\t\t\t\t\t   &ref->new_oid);\n> +\t\tif (r)\n> +\t\t\tref_update_display_info_set_failed(info);\n> +\t\tstrmap_put(delayed_ref_display, ref->name, info);\n> +\n>   \t\tstrbuf_release(&quickref);\n>   \t\treturn r;\n>   \t} else {\n> @@ -1103,7 +1195,8 @@ static int store_updated_refs(struct display_state *display_state,\n>   \t\t\t      int connectivity_checked,\n>   \t\t\t      struct ref_transaction *transaction, struct ref *ref_map,\n>   \t\t\t      struct fetch_head *fetch_head,\n> -\t\t\t      const struct fetch_config *config)\n> +\t\t\t      const struct fetch_config *config,\n> +\t\t\t      struct strmap *delayed_ref_display)\n>   {\n>   \tint rc = 0;\n>   \tstruct strbuf note = STRBUF_INIT;\n> @@ -1219,7 +1312,8 @@ static int store_updated_refs(struct display_state *display_state,\n>   \n>   \t\t\tif (ref) {\n>   \t\t\t\trc |= update_local_ref(ref, transaction, display_state,\n> -\t\t\t\t\t\t       rm, summary_width, config);\n> +\t\t\t\t\t\t       rm, summary_width, config,\n> +\t\t\t\t\t\t       delayed_ref_display);\n>   \t\t\t\tfree(ref);\n>   \t\t\t} else if (write_fetch_head || dry_run) {\n>   \t\t\t\t/*\n> @@ -1300,7 +1394,8 @@ static int fetch_and_consume_refs(struct display_state *display_state,\n>   \t\t\t\t  struct ref_transaction *transaction,\n>   \t\t\t\t  struct ref *ref_map,\n>   \t\t\t\t  struct fetch_head *fetch_head,\n> -\t\t\t\t  const struct fetch_config *config)\n> +\t\t\t\t  const struct fetch_config *config,\n> +\t\t\t\t  struct strmap *delayed_ref_display)\n>   {\n>   \tint connectivity_checked = 1;\n>   \tint ret;\n> @@ -1322,7 +1417,8 @@ static int fetch_and_consume_refs(struct display_state *display_state,\n>   \n>   \ttrace2_region_enter(\"fetch\", \"consume_refs\", the_repository);\n>   \tret = store_updated_refs(display_state, connectivity_checked,\n> -\t\t\t\t transaction, ref_map, fetch_head, config);\n> +\t\t\t\t transaction, ref_map, fetch_head, config,\n> +\t\t\t\t delayed_ref_display);\n>   \ttrace2_region_leave(\"fetch\", \"consume_refs\", the_repository);\n>   \n>   out:\n> @@ -1493,7 +1589,8 @@ static int backfill_tags(struct display_state *display_state,\n>   \t\t\t struct ref_transaction *transaction,\n>   \t\t\t struct ref *ref_map,\n>   \t\t\t struct fetch_head *fetch_head,\n> -\t\t\t const struct fetch_config *config)\n> +\t\t\t const struct fetch_config *config,\n> +\t\t\t struct strmap *delayed_ref_display)\n>   {\n>   \tint retcode, cannot_reuse;\n>   \n> @@ -1515,7 +1612,7 @@ static int backfill_tags(struct display_state *display_state,\n>   \ttransport_set_option(transport, TRANS_OPT_DEPTH, \"0\");\n>   \ttransport_set_option(transport, TRANS_OPT_DEEPEN_RELATIVE, NULL);\n>   \tretcode = fetch_and_consume_refs(display_state, transport, transaction, ref_map,\n> -\t\t\t\t\t fetch_head, config);\n> +\t\t\t\t\t fetch_head, config, delayed_ref_display);\n>   \n>   \tif (gsecondary) {\n>   \t\ttransport_disconnect(gsecondary);\n> @@ -1641,6 +1738,7 @@ struct ref_rejection_data {\n>   \tbool conflict_msg_shown;\n>   \tbool case_sensitive_msg_shown;\n>   \tconst char *remote_name;\n> +\tstruct strmap *delayed_ref_display;\n>   };\n>   \n>   static void ref_transaction_rejection_handler(const char *refname,\n> @@ -1653,6 +1751,7 @@ static void ref_transaction_rejection_handler(const char *refname,\n>   \t\t\t\t\t      void *cb_data)\n>   {\n>   \tstruct ref_rejection_data *data = cb_data;\n> +\tstruct ref_update_display_info *info;\n>   \n>   \tif (err == REF_TRANSACTION_ERROR_CASE_CONFLICT && ignore_case &&\n>   \t    !data->case_sensitive_msg_shown) {\n> @@ -1681,6 +1780,10 @@ static void ref_transaction_rejection_handler(const char *refname,\n>   \t\t\t      refname, ref_transaction_error_msg(err));\n>   \t}\n>   \n> +\tinfo = strmap_get(data->delayed_ref_display, refname);\n> +\tif (info)\n> +\t\tref_update_display_info_set_failed(info);\n> +\n>   \t*data->retcode = 1;\n>   }\n>   \n> @@ -1690,6 +1793,7 @@ static void ref_transaction_rejection_handler(const char *refname,\n>    */\n>   static int commit_ref_transaction(struct ref_transaction **transaction,\n>   \t\t\t\t  bool is_atomic, const char *remote_name,\n> +\t\t\t\t  struct strmap *delayed_ref_display,\n>   \t\t\t\t  struct strbuf *err)\n>   {\n>   \tint retcode = ref_transaction_commit(*transaction, err);\n> @@ -1701,6 +1805,7 @@ static int commit_ref_transaction(struct ref_transaction **transaction,\n>   \t\t\t.conflict_msg_shown = 0,\n>   \t\t\t.remote_name = remote_name,\n>   \t\t\t.retcode = &retcode,\n> +\t\t\t.delayed_ref_display = delayed_ref_display,\n>   \t\t};\n>   \n>   \t\tref_transaction_for_each_rejected_update(*transaction,\n> @@ -1729,6 +1834,10 @@ static int do_fetch(struct transport *transport,\n>   \tstruct fetch_head fetch_head = { 0 };\n>   \tstruct strbuf err = STRBUF_INIT;\n>   \tint do_set_head = 0;\n> +\tstruct strmap delayed_ref_display = STRMAP_INIT;\n> +\tint summary_width = 0;\n> +\tstruct strmap_entry *e;\n> +\tstruct hashmap_iter iter;\n>   \n>   \tif (tags == TAGS_DEFAULT) {\n>   \t\tif (transport->remote->fetch_tags == 2)\n> @@ -1853,7 +1962,7 @@ static int do_fetch(struct transport *transport,\n>   \t}\n>   \n>   \tif (fetch_and_consume_refs(&display_state, transport, transaction, ref_map,\n> -\t\t\t\t   &fetch_head, config)) {\n> +\t\t\t\t   &fetch_head, config, &delayed_ref_display)) {\n>   \t\tretcode = 1;\n>   \t\tgoto cleanup;\n>   \t}\n> @@ -1876,7 +1985,7 @@ static int do_fetch(struct transport *transport,\n>   \t\t\t * the transaction and don't commit anything.\n>   \t\t\t */\n>   \t\t\tif (backfill_tags(&display_state, transport, transaction, tags_ref_map,\n> -\t\t\t\t\t  &fetch_head, config))\n> +\t\t\t\t\t  &fetch_head, config, &delayed_ref_display))\n>   \t\t\t\tretcode = 1;\n>   \t\t}\n>   \n> @@ -1886,8 +1995,12 @@ static int do_fetch(struct transport *transport,\n>   \tif (retcode)\n>   \t\tgoto cleanup;\n>   \n> +\tif (verbosity >= 0)\n> +\t\tsummary_width = transport_summary_width(ref_map);\n> +\n>   \tretcode = commit_ref_transaction(&transaction, atomic_fetch,\n> -\t\t\t\t\t transport->remote->name, &err);\n> +\t\t\t\t\t transport->remote->name,\n> +\t\t\t\t\t &delayed_ref_display, &err);\n>   \t/*\n>   \t * With '--atomic', bail out if the transaction fails. Without '--atomic',\n>   \t * continue to fetch head and perform other post-fetch operations.\n> @@ -1965,7 +2078,17 @@ static int do_fetch(struct transport *transport,\n>   \t */\n>   \tif (retcode && !atomic_fetch && transaction)\n>   \t\tcommit_ref_transaction(&transaction, false,\n> -\t\t\t\t       transport->remote->name, &err);\n> +\t\t\t\t       transport->remote->name,\n> +\t\t\t\t       &delayed_ref_display, &err);\n> +\n> +\t/*\n> +\t * Clear any pending information that needs to be shown to the user.\n> +\t */\n> +\tstrmap_for_each_entry(&delayed_ref_display, &iter, e) {\n> +\t\tstruct ref_update_display_info *info = e->value;\n> +\t\tref_update_display_info_display(info, &display_state, e->key, summary_width);\n> +\t\tref_update_display_info_free(info);\n> +\t}\n>   \n>   \tif (retcode) {\n>   \t\tif (err.len) {\n> @@ -1980,6 +2103,8 @@ static int do_fetch(struct transport *transport,\n>   \n>   \tif (transaction)\n>   \t\tref_transaction_free(transaction);\n> +\n> +\tstrmap_clear(&delayed_ref_display, 1);\n>   \tdisplay_state_release(&display_state);\n>   \tclose_fetch_head(&fetch_head);\n>   \tstrbuf_release(&err);\n> diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh\n> index 45595991c8..29e2f17608 100755\n> --- a/t/t5516-fetch-push.sh\n> +++ b/t/t5516-fetch-push.sh\n> @@ -1893,6 +1893,7 @@ test_expect_success 'pushing non-commit objects should report error' '\n>   \n>   \t\ttagsha=$(git rev-parse test^{tag}) &&\n>   \t\ttest_must_fail git push ../dest \"$tagsha:refs/heads/branch\" 2>err &&\n> +\t\ttest_grep \"! \\[remote rejected\\] $tagsha -> branch (invalid new value provided)\" err &&\n>   \t\ttest_grep \"trying to write non-commit object $tagsha to branch ${SQ}refs/heads/branch${SQ}\" err\n>   \t)\n>   '\n> diff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\n> index 5883839a04..22bfc0c74d 100755\n> --- a/t/t5574-fetch-output.sh\n> +++ b/t/t5574-fetch-output.sh\n> @@ -40,8 +40,8 @@ test_expect_success 'fetch aligned output' '\n>   \t\tgrep -e \"->\" actual | cut -c 22- >../actual\n>   \t) &&\n>   \tcat >expect <<-\\EOF &&\n> -\tmain                 -> origin/main\n>   \tlooooooooooooong-tag -> looooooooooooong-tag\n> +\tmain                 -> origin/main\n>   \tEOF\n>   \ttest_cmp expect actual\n>   '\n> @@ -55,8 +55,8 @@ test_expect_success 'fetch compact output' '\n>   \t\tgrep -e \"->\" actual | cut -c 22- >../actual\n>   \t) &&\n>   \tcat >expect <<-\\EOF &&\n> -\tmain       -> origin/*\n>   \textraaa    -> *\n> +\tmain       -> origin/*\n>   \tEOF\n>   \ttest_cmp expect actual\n>   '\n> @@ -103,15 +103,15 @@ do\n>   \t\tcat >expect <<-EOF &&\n>   \t\t- $MAIN_OLD $ZERO_OID refs/forced/deleted-branch\n>   \t\t- $MAIN_OLD $ZERO_OID refs/unforced/deleted-branch\n> -\t\t  $MAIN_OLD $FAST_FORWARD_NEW refs/unforced/fast-forward\n>   \t\t! $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/unforced/force-updated\n> +\t\t* $ZERO_OID $MAIN_OLD refs/forced/new-branch\n> +\t\t* $ZERO_OID $MAIN_OLD refs/remotes/origin/new-branch\n> +\t\t+ $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/remotes/origin/force-updated\n> +\t\t  $MAIN_OLD $FAST_FORWARD_NEW refs/unforced/fast-forward\n>   \t\t* $ZERO_OID $MAIN_OLD refs/unforced/new-branch\n>   \t\t  $MAIN_OLD $FAST_FORWARD_NEW refs/forced/fast-forward\n> -\t\t+ $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/forced/force-updated\n> -\t\t* $ZERO_OID $MAIN_OLD refs/forced/new-branch\n>   \t\t  $MAIN_OLD $FAST_FORWARD_NEW refs/remotes/origin/fast-forward\n> -\t\t+ $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/remotes/origin/force-updated\n> -\t\t* $ZERO_OID $MAIN_OLD refs/remotes/origin/new-branch\n> +\t\t+ $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/forced/force-updated\n>   \t\tEOF\n>   \n>   \t\t# Change the URL of the repository to fetch different references.\n> @@ -179,8 +179,8 @@ test_expect_success 'fetch porcelain overrides fetch.output config' '\n>   \tnew_commit=$(git rev-parse HEAD) &&\n>   \n>   \tcat >expect <<-EOF &&\n> -\t  $old_commit $new_commit refs/remotes/origin/config-override\n>   \t* $ZERO_OID $new_commit refs/tags/new-commit\n> +\t  $old_commit $new_commit refs/remotes/origin/config-override\n>   \tEOF\n>   \n>   \tgit -C porcelain -c fetch.output=compact fetch --porcelain >stdout 2>stderr &&\n> \n\n"},{"id":"534139","messageId":"aWzM+qePiaRWz5/f@szeder.dev","threadId":"64825","inReplyTo":"20260116-633-regression-lost-diagnostic-message-when-pushing-non-commit-objects-to-refs-heads-v2-1-925a0e9c7f32@gmail.com","subject":"Re: [PATCH v2 1/7] refs: drop unnecessary header includes","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2026-01-18T12:07:22Z","receivedAt":"2026-01-18T12:07:35Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Fri, Jan 16, 2026 at 10:27:06PM +0100, Karthik Nayak wrote:\n> The 'sigchain.h' header isn't being used and can be removed.\n> \n> Similarly, 'run-command.h' serves no direct purpose here. While it gets pulled in transitively through 'hook.h', we can still drop the explicit include for clarity.\n\nThe need for these #includes went away fairly recently, I think in\n26238496a7 (hook: provide stdin via callback, 2025-12-26) and\n7a7717427e (reference-transaction: use hook API instead of\nrun-command, 2025-12-26), which were merged in f406b89552 (Merge\nbranch 'ar/run-command-hook', 2026-01-06).  Unfortunately, that topic\nhad some regressions and therefore was reverted in a3d1f391d3 (Revert\n\"Merge branch 'ar/run-command-hook'\", 2026-01-15), and as a result\nmerging this topic into seen resulted in a merge commit 180b93f7ba\n(Merge branch 'kn/ref-batch-output-error-reporting-fix' into jch,\n2026-01-16) that can't be built.\n\nI think this patch should be dropped from this series, and these\n#includes should be removed in that other topic.\n\nhttps://public-inbox.org/git/20251226122334.16687-1-adrian.ratiu@collabora.com/T/#u\n\n> Signed-off-by: Karthik Nayak <karthik.188@gmail.com>\n> ---\n>  refs.c | 2 --\n>  1 file changed, 2 deletions(-)\n> \n> diff --git a/refs.c b/refs.c\n> index e06e0cb072..965b232a06 100644\n> --- a/refs.c\n> +++ b/refs.c\n> @@ -15,7 +15,6 @@\n>  #include \"iterator.h\"\n>  #include \"refs.h\"\n>  #include \"refs/refs-internal.h\"\n> -#include \"run-command.h\"\n>  #include \"hook.h\"\n>  #include \"object-name.h\"\n>  #include \"odb.h\"\n> @@ -26,7 +25,6 @@\n>  #include \"strvec.h\"\n>  #include \"repo-settings.h\"\n>  #include \"setup.h\"\n> -#include \"sigchain.h\"\n>  #include \"date.h\"\n>  #include \"commit.h\"\n>  #include \"wildmatch.h\"\n> \n> -- \n> 2.51.2\n> \n"},{"id":"534177","messageId":"CAOLa=ZTuD=bcO2pin6CWYFrc9Lwnq5JWy99cUGt8bOYkGh8vHw@mail.gmail.com","threadId":"64825","inReplyTo":"aWzM+qePiaRWz5/f@szeder.dev","subject":"Re: [PATCH v2 1/7] refs: drop unnecessary header includes","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-01-19T08:53:07Z","receivedAt":"2026-01-19T08:53:10Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"SZEDER Gábor <szeder.dev@gmail.com> writes:\n\n> On Fri, Jan 16, 2026 at 10:27:06PM +0100, Karthik Nayak wrote:\n>> The 'sigchain.h' header isn't being used and can be removed.\n>>\n>> Similarly, 'run-command.h' serves no direct purpose here. While it gets pulled in transitively through 'hook.h', we can still drop the explicit include for clarity.\n>\n> The need for these #includes went away fairly recently, I think in\n> 26238496a7 (hook: provide stdin via callback, 2025-12-26) and\n> 7a7717427e (reference-transaction: use hook API instead of\n> run-command, 2025-12-26), which were merged in f406b89552 (Merge\n> branch 'ar/run-command-hook', 2026-01-06).  Unfortunately, that topic\n> had some regressions and therefore was reverted in a3d1f391d3 (Revert\n> \"Merge branch 'ar/run-command-hook'\", 2026-01-15), and as a result\n> merging this topic into seen resulted in a merge commit 180b93f7ba\n> (Merge branch 'kn/ref-batch-output-error-reporting-fix' into jch,\n> 2026-01-16) that can't be built.\n>\n> I think this patch should be dropped from this series, and these\n> #includes should be removed in that other topic.\n>\n> https://public-inbox.org/git/20251226122334.16687-1-adrian.ratiu@collabora.com/T/#u\n>\n\nSounds good, thanks for letting me know. I'll drop it from the next\nversion!\n"},{"id":"534183","messageId":"CAOLa=ZRwErG0wBb8ia7NbfnSOmWcx2_7WS0vL2rJTtXeJaJ9kA@mail.gmail.com","threadId":"64825","inReplyTo":"0082426c-a945-4f2e-969e-897e1aeaed66@gmail.com","subject":"Re: [PATCH v2 7/7] fetch: delay user information post committing of transaction","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-01-19T16:11:07Z","receivedAt":"2026-01-19T16:11:11Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> Hi Karthik\n>\n> On 16/01/2026 21:27, Karthik Nayak wrote:\n>> In Git 2.50 and earlier, we would display failure codes and error\n>> message as part of the status display:\n>>\n>>    $ git fetch . v1.0.0:refs/heads/foo\n>>      error: cannot update ref 'refs/heads/foo': trying to write non-commit object f665776185ad074b236c00751d666da7d1977dbe to branch 'refs/heads/foo'\n>>      From .\n>>       ! [new tag]               v1.0.0     -> foo  (unable to update local ref)\n>>\n>> With the addition of batched updates, this information is no longer\n>> shown to the user:\n>>\n>>    $ git fetch . v1.0.0:refs/heads/foo\n>>      From .\n>>       * [new tag]               v1.0.0     -> foo\n>>      error: cannot update ref 'refs/heads/foo': trying to write non-commit object f665776185ad074b236c00751d666da7d1977dbe to branch 'refs/heads/foo'\n>>\n>> Since reference updates are batched and processed together at the end,\n>> information around the outcome is not available during individual\n>> reference parsing.\n>>\n>> To overcome this, collate and delay the output to the end. Introduce\n>> `ref_update_display_info` which will hold individual update's\n>> information and also whether the update failed or succeeded. This\n>> finally allows us to iterate over all such updates and print them to the\n>> user. While this brings back the functionality, it does change the order\n>> of the output. Modify the tests to reflect this.\n>\n> It is unfortunate that a fix for a regression the the messages changes\n> the order of those messages. It is doubly unfortunate that the new order\n> depends on the implementation of strmap_for_each() which may change in\n> the future. I think you can avoid this by appending each update to an\n> array in update_local_ref() and adding the errors to a separate strmap\n> in ref_transaction_rejection_handler(). Then when you come to print the\n> massages, loop over the array and for each update lookup the ref in the\n> strmap to see if it failed before printing the appropriate message.\n>\n> Thanks\n>\n> Phillip\n>\n\nYes, I think there is merit in the approach you suggested, it ensures\nthat all messages are delayed (avoiding the split between displaying a\nfew at the beginning vs some at the end) and that they retain the order.\nI have a version cooking locally which does this and works correctly.\nI'll send it in with my next version.\n\nThanks,\nKarthik\n"}]}