threads / patch / 64834

v3, 6 partsrefs: skip to next ref when current ref is rejected

Subject: [PATCH v3 1/6] refs: skip to next ref when current ref is rejected

## tl;dr

11 messages between Jan 20, 2026 and Jan 22, 2026. Diffs are folded; open one to read it.

replies: 10people: 3as markdown or json

Karthik Nayak· Jan 20, 2026, 09:59 UTC · lore

[PATCH v3 0/6] refs: provide detailed error messages when using batched update

The refs namespace uses an error buffer to capture details about failed reference updates. However when we added batched update support to reference transactions, these messages were never propagated, instead only an error code pertaining to the type of failure was propagated.

Currently, there are three regions which utilize batched updates:
  - git update-ref --batch-updates
  - git fetch
  - git receive-pack

While 'git update-ref --batch-updates' was a newly introduced flag, both 'git fetch' and 'git receive-pack' were pre-existing. Before using batched updates, they provided more detailed error messages to the user, but this changed with the introduction of batched updates. This is a regression in their workings.

This patch series fixes this, by passing the detailed error message and utilizing it whenever available. The regression was reported by Elijah Newren [1] and based on the patch submitted by Jeff King [2].

[1]: https://lore.kernel.org/all/CABPp-BGL2tJR4dPidQuFcp-X0_VkVTknCY-0Zgo=jHVGv_P=wA@mail.gmail.com/ [2]: https://lore.kernel.org/all/20251224081214.GA1879908@coredump.intra.peff.net/

---
Changes in v3:
- Drop the first commit.
- For the last commit, where we delay 'git fetch' status information,
  delay all information to the end. Also use a list to compliment the
  existing strmap, this ensures that the order is maintained.
- Link to v2: https://patch.msgid.link/20260116-633-regression-lost-diagnostic-message-when-pushing-non-commit-objects-to-refs-heads-v2-0-925a0e9c7f32@gmail.com
Changes in v2:
- Updates to the commit messages to be more descriptive.
- Instead of passing the char pointer for the error description, pass
  the 'strbuf' itself. This makes the API a lot cleaner to deal with.
  Also avoids having to remember to reset the strbuf after usage.
- Chalk out a separate commit for using a 'goto next_ref' in
  `refs_verify_refnames_available()`. This makes the intention much
  clearer.
- For git-update-ref(1), keep the existing implementation as is and only
  output the detailed error message to stderr.
- For git-receive-pack(1), use 'rp_error()' for detailed error message
  while keeping the current implementation as is.
- Added a separate patch to handle missing information in git-fetch(1)'s
  status table. This involves delaying updates to the end, where update
  success/failure information is available. I'm not too confident about
  this approach though, we could also drop it from the series and I
  could pick that up independently. This is still 1.19 ± 0.02 times
  faster than non-batched version (v2.50.0) in the files backend.
- 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
---
 builtin/fetch.c         | 259 +++++++++++++++++++++++++++++++++++++-----------
 builtin/receive-pack.c  |   7 +-
 builtin/update-ref.c    |   7 +-
 refs.c                  |  46 +++++----
 refs.h                  |   1 +
 refs/files-backend.c    |   5 +-
 refs/packed-backend.c   |  12 +--
 refs/refs-internal.h    |   4 +-
 refs/reftable-backend.c |   5 +-
 t/t1400-update-ref.sh   |  71 +++++++------
 t/t5510-fetch.sh        |   8 +-
 t/t5516-fetch-push.sh   |  16 +++
 12 files changed, 316 insertions(+), 125 deletions(-)
Karthik Nayak (6):
      refs: skip to next ref when current ref is rejected
      refs: add rejection detail to the callback function
      update-ref: utilize rejected error details if available
      fetch: utilize rejected ref error details
      receive-pack: utilize rejected ref error details
      fetch: delay user information post committing of transaction
Range-diff versus v2:
1:  7592b0a9aa < -:  ---------- refs: drop unnecessary header includes
2:  97095095bc = 1:  dbabb9a172 refs: skip to next ref when current ref is rejected
3:  2dadab77a2 = 2:  b0ab39a262 refs: add rejection detail to the callback function
4:  007c6d58c1 = 3:  2b323bddbc update-ref: utilize rejected error details if available
5:  0d0b8b75c8 = 4:  8bf3d986f4 fetch: utilize rejected ref error details
6:  b9348b5ae3 = 5:  5dab402570 receive-pack: utilize rejected ref error details
7:  d90420903f ! 6:  596762e6b5 fetch: delay user information post committing of transaction
    @@ Commit message
         `ref_update_display_info` which will hold individual update's
         information and also whether the update failed or succeeded. This
         finally allows us to iterate over all such updates and print them to the
    -    user. While this brings back the functionality, it does change the order
    -    of the output. Modify the tests to reflect this.
    +    user.
     
    -    Using an strmap does add some overhead to 'git-fetch(1)', but from
    -    benchmarking this seems to be not too bad:
    +    Using an dynamic array and strmap does add some overhead to
    +    'git-fetch(1)', but from benchmarking this seems to be not too bad:
     
           Benchmark 1: fetch: many refs (refformat = files, refcount = 1000, revision = master)
    -        Time (mean ± σ):      51.9 ms ±   2.5 ms    [User: 15.6 ms, System: 36.9 ms]
    -        Range (min … max):    47.4 ms …  58.3 ms    41 runs
    +        Time (mean ± σ):      42.6 ms ±   1.2 ms    [User: 13.1 ms, System: 29.8 ms]
    +        Range (min … max):    40.1 ms …  45.8 ms    47 runs
     
           Benchmark 2: fetch: many refs (refformat = files, refcount = 1000, revision = HEAD)
    -        Time (mean ± σ):      53.0 ms ±   1.8 ms    [User: 17.6 ms, System: 36.0 ms]
    -        Range (min … max):    49.4 ms …  57.6 ms    40 runs
    +        Time (mean ± σ):      43.1 ms ±   1.2 ms    [User: 12.7 ms, System: 30.7 ms]
    +        Range (min … max):    40.5 ms …  45.8 ms    48 runs
     
           Summary
             fetch: many refs (refformat = files, refcount = 1000, revision = master) ran
    -          1.02 ± 0.06 times faster than fetch: many refs (refformat = files, refcount = 1000, revision = HEAD)
    +          1.01 ± 0.04 times faster than fetch: many refs (refformat = files, refcount = 1000, revision = HEAD)
     
         Another approach would be to move the status printing logic to be
         handled post the transaction being committed. That however would require
    @@ Commit message
         which is more involved infrastructure work compared to the strmap
         approach here.
     
    +    Helped-by: Phillip Wood <phillip.wood123@gmail.com>
         Reported-by: Jeff King <peff@peff.net>
         Signed-off-by: Karthik Nayak <karthik.188@gmail.com>
     
    @@ builtin/fetch.c: static void display_ref_update(struct display_state *display_st
     +	const char *summary;
     +	const char *fail_detail;
     +	const char *success_detail;
    ++	const char *ref;
     +	const char *remote;
    -+	const char *local;
     +	struct object_id old_oid;
     +	struct object_id new_oid;
     +};
     +
    -+static struct ref_update_display_info *ref_update_display_info_new(
    -+						char success_code,
    -+						char fail_code,
    -+						const char *summary,
    -+						const char *success_detail,
    -+						const char *fail_detail,
    -+						const char *remote,
    -+						const struct object_id *old_oid,
    -+						const struct object_id *new_oid)
    ++static struct ref_update_display_info *ref_update_display_info_append(
    ++					   struct ref_update_display_info **list,
    ++					   size_t *count,
    ++					   char success_code,
    ++					   char fail_code,
    ++					   const char *summary,
    ++					   const char *success_detail,
    ++					   const char *fail_detail,
    ++					   const char *ref,
    ++					   const char *remote,
    ++					   const struct object_id *old_oid,
    ++					   const struct object_id *new_oid)
     +{
     +	struct ref_update_display_info *info;
    -+	CALLOC_ARRAY(info, 1);
    ++	size_t index = *count;
     +
    ++	(*count)++;
    ++	REALLOC_ARRAY(*list, *count);
    ++
    ++	info = &(*list)[index];
    ++
    ++	info->failed = false;
     +	info->success_code = success_code;
     +	info->fail_code = fail_code;
     +	info->summary = xstrdup(summary);
     +	info->success_detail = xstrdup_or_null(success_detail);
     +	info->fail_detail = xstrdup_or_null(fail_detail);
     +	info->remote = xstrdup(remote);
    ++	info->ref = xstrdup(ref);
     +
     +	oidcpy(&info->old_oid, old_oid);
     +	oidcpy(&info->new_oid, new_oid);
    @@ builtin/fetch.c: static void display_ref_update(struct display_state *display_st
     +	free((char *)info->success_detail);
     +	free((char *)info->fail_detail);
     +	free((char *)info->remote);
    ++	free((char *)info->ref);
     +}
     +
     +static void ref_update_display_info_display(struct ref_update_display_info *info,
     +					    struct display_state *display_state,
    -+					    const char *refname, int summary_width)
    ++					    int summary_width)
     +{
     +	display_ref_update(display_state,
     +			   info->failed ? info->fail_code : info->success_code,
     +			   info->summary,
     +			   info->failed ? info->fail_detail : info->success_detail,
    -+			   info->remote, refname, &info->old_oid,
    ++			   info->remote, info->ref, &info->old_oid,
     +			   &info->new_oid, summary_width);
     +}
     +
      static int update_local_ref(struct ref *ref,
      			    struct ref_transaction *transaction,
    - 			    struct display_state *display_state,
    +-			    struct display_state *display_state,
      			    const struct ref *remote_ref,
    - 			    int summary_width,
    +-			    int summary_width,
     -			    const struct fetch_config *config)
     +			    const struct fetch_config *config,
    -+			    struct strmap *delayed_ref_display)
    ++			    struct ref_update_display_info **display_list,
    ++			    size_t *display_count)
      {
      	struct commit *current = NULL, *updated;
      	int fast_forward = 0;
     @@ builtin/fetch.c: static int update_local_ref(struct ref *ref,
    + 
    + 	if (oideq(&ref->old_oid, &ref->new_oid)) {
    + 		if (verbosity > 0)
    +-			display_ref_update(display_state, '=', _("[up to date]"), NULL,
    +-					   remote_ref->name, ref->name,
    +-					   &ref->old_oid, &ref->new_oid, summary_width);
    ++			ref_update_display_info_append(display_list, display_count,
    ++						       '=', '=', _("[up to date]"),
    ++						       NULL, NULL, ref->name,
    ++						       remote_ref->name, &ref->old_oid,
    ++						       &ref->new_oid);
    + 		return 0;
    + 	}
    + 
    + 	if (!update_head_ok &&
    + 	    !is_null_oid(&ref->old_oid) &&
    + 	    branch_checked_out(ref->name)) {
    ++		struct ref_update_display_info *info;
    + 		/*
    + 		 * If this is the head, and it's not okay to update
    + 		 * the head, and the old value of the head isn't empty...
    + 		 */
    +-		display_ref_update(display_state, '!', _("[rejected]"),
    +-				   _("can't fetch into checked-out branch"),
    +-				   remote_ref->name, ref->name,
    +-				   &ref->old_oid, &ref->new_oid, summary_width);
    ++		info = ref_update_display_info_append(display_list, display_count,
    ++						      '!', '!', _("[rejected]"),
    ++						      NULL, _("can't fetch into checked-out branch"),
    ++						      ref->name, remote_ref->name,
    ++						      &ref->old_oid, &ref->new_oid);
    ++		ref_update_display_info_set_failed(info);
    + 		return 1;
    + 	}
    + 
      	if (!is_null_oid(&ref->old_oid) &&
      	    starts_with(ref->name, "refs/tags/")) {
    ++		struct ref_update_display_info *info;
    ++
      		if (force || ref->force) {
    -+			struct ref_update_display_info *info;
      			int r;
     +
      			r = s_update_ref("updating tag", ref, transaction, 0);
    @@ builtin/fetch.c: static int update_local_ref(struct ref *ref,
     -					   remote_ref->name, ref->name,
     -					   &ref->old_oid, &ref->new_oid, summary_width);
     +
    -+			info = ref_update_display_info_new('t', '!', _("[tag update]"), NULL,
    -+							   _("unable to update local ref"),
    -+							   remote_ref->name, &ref->old_oid,
    -+							   &ref->new_oid);
    ++			info = ref_update_display_info_append(display_list, display_count,
    ++							      't', '!', _("[tag update]"), NULL,
    ++							      _("unable to update local ref"),
    ++							      ref->name, remote_ref->name,
    ++							      &ref->old_oid, &ref->new_oid);
     +			if (r)
     +				ref_update_display_info_set_failed(info);
    -+			strmap_put(delayed_ref_display, ref->name, info);
     +
      			return r;
      		} else {
    - 			display_ref_update(display_state, '!', _("[rejected]"),
    +-			display_ref_update(display_state, '!', _("[rejected]"),
    +-					   _("would clobber existing tag"),
    +-					   remote_ref->name, ref->name,
    +-					   &ref->old_oid, &ref->new_oid, summary_width);
    ++			info = ref_update_display_info_append(display_list, display_count,
    ++							      '!', '!', _("[rejected]"), NULL,
    ++							      _("would clobber existing tag"),
    ++							      ref->name, remote_ref->name,
    ++							      &ref->old_oid, &ref->new_oid);
    ++			ref_update_display_info_set_failed(info);
    + 			return 1;
    + 		}
    + 	}
     @@ builtin/fetch.c: static int update_local_ref(struct ref *ref,
      	updated = lookup_commit_reference_gently(the_repository,
      						 &ref->new_oid, 1);
    @@ builtin/fetch.c: static int update_local_ref(struct ref *ref,
     -				   remote_ref->name, ref->name,
     -				   &ref->old_oid, &ref->new_oid, summary_width);
     +
    -+		info = ref_update_display_info_new('*', '!', what, NULL,
    -+						   _("unable to update local ref"),
    -+						   remote_ref->name, &ref->old_oid,
    -+						   &ref->new_oid);
    ++		info = ref_update_display_info_append(display_list, display_count,
    ++						      '*', '!', what, NULL,
    ++						      _("unable to update local ref"),
    ++						      ref->name, remote_ref->name,
    ++						      &ref->old_oid, &ref->new_oid);
     +		if (r)
     +			ref_update_display_info_set_failed(info);
    -+		strmap_put(delayed_ref_display, ref->name, info);
     +
      		return r;
      	}
    @@ builtin/fetch.c: static int update_local_ref(struct ref *ref,
     -				   remote_ref->name, ref->name,
     -				   &ref->old_oid, &ref->new_oid, summary_width);
     +
    -+		info = ref_update_display_info_new(' ', '!', quickref.buf, NULL,
    -+						   _("unable to update local ref"),
    -+						   remote_ref->name, &ref->old_oid,
    -+						   &ref->new_oid);
    ++		info = ref_update_display_info_append(display_list, display_count,
    ++						      ' ', '!', quickref.buf, NULL,
    ++						      _("unable to update local ref"),
    ++						      ref->name, remote_ref->name,
    ++						      &ref->old_oid, &ref->new_oid);
     +		if (r)
     +			ref_update_display_info_set_failed(info);
    -+		strmap_put(delayed_ref_display, ref->name, info);
     +
      		strbuf_release(&quickref);
      		return r;
    @@ builtin/fetch.c: static int update_local_ref(struct ref *ref,
     -				   remote_ref->name, ref->name,
     -				   &ref->old_oid, &ref->new_oid, summary_width);
     +
    -+		info = ref_update_display_info_new('+', '!', quickref.buf,
    -+						   _("forced update"),
    -+						   _("unable to update local ref"),
    -+						   remote_ref->name, &ref->old_oid,
    -+						   &ref->new_oid);
    ++		info = ref_update_display_info_append(display_list, display_count,
    ++						      '+', '!', quickref.buf, _("forced update"),
    ++						      _("unable to update local ref"),
    ++						      ref->name, remote_ref->name,
    ++						      &ref->old_oid, &ref->new_oid);
    ++
     +		if (r)
     +			ref_update_display_info_set_failed(info);
    -+		strmap_put(delayed_ref_display, ref->name, info);
     +
      		strbuf_release(&quickref);
      		return r;
      	} else {
    +-		display_ref_update(display_state, '!', _("[rejected]"), _("non-fast-forward"),
    +-				   remote_ref->name, ref->name,
    +-				   &ref->old_oid, &ref->new_oid, summary_width);
    ++		struct ref_update_display_info *info;
    ++		info = ref_update_display_info_append(display_list, display_count,
    ++						      '!', '!', _("[rejected]"), NULL,
    ++						      _("non-fast-forward"),
    ++						      ref->name, remote_ref->name,
    ++						      &ref->old_oid, &ref->new_oid);
    ++		ref_update_display_info_set_failed(info);
    + 		return 1;
    + 	}
    + }
     @@ builtin/fetch.c: static int store_updated_refs(struct display_state *display_state,
      			      int connectivity_checked,
      			      struct ref_transaction *transaction, struct ref *ref_map,
      			      struct fetch_head *fetch_head,
     -			      const struct fetch_config *config)
     +			      const struct fetch_config *config,
    -+			      struct strmap *delayed_ref_display)
    ++			      struct ref_update_display_info **display_list,
    ++			      size_t *display_count)
      {
      	int rc = 0;
      	struct strbuf note = STRBUF_INIT;
    + 	const char *what, *kind;
    + 	struct ref *rm;
    + 	int want_status;
    +-	int summary_width = 0;
    +-
    +-	if (verbosity >= 0)
    +-		summary_width = transport_summary_width(ref_map);
    + 
    + 	if (!connectivity_checked) {
    + 		struct check_connected_options opt = CHECK_CONNECTED_INIT;
     @@ builtin/fetch.c: static int store_updated_refs(struct display_state *display_state,
    + 					  display_state->url_len);
      
      			if (ref) {
    - 				rc |= update_local_ref(ref, transaction, display_state,
    +-				rc |= update_local_ref(ref, transaction, display_state,
     -						       rm, summary_width, config);
    -+						       rm, summary_width, config,
    -+						       delayed_ref_display);
    ++				rc |= update_local_ref(ref, transaction, rm,
    ++						       config, display_list,
    ++						       display_count);
      				free(ref);
      			} else if (write_fetch_head || dry_run) {
      				/*
    +@@ builtin/fetch.c: static int store_updated_refs(struct display_state *display_state,
    + 				 * would be written to FETCH_HEAD, if --dry-run
    + 				 * is set).
    + 				 */
    +-				display_ref_update(display_state, '*',
    +-						   *kind ? kind : "branch", NULL,
    +-						   rm->name,
    +-						   "FETCH_HEAD",
    +-						   &rm->new_oid, &rm->old_oid,
    +-						   summary_width);
    ++
    ++				ref_update_display_info_append(display_list, display_count,
    ++							       '*', '*', *kind ? kind : "branch",
    ++							       NULL, NULL, "FETCH_HEAD", rm->name,
    ++							       &rm->new_oid, &rm->old_oid);
    + 			}
    + 		}
    + 	}
     @@ builtin/fetch.c: static int fetch_and_consume_refs(struct display_state *display_state,
      				  struct ref_transaction *transaction,
      				  struct ref *ref_map,
      				  struct fetch_head *fetch_head,
     -				  const struct fetch_config *config)
     +				  const struct fetch_config *config,
    -+				  struct strmap *delayed_ref_display)
    ++				  struct ref_update_display_info **display_list,
    ++				  size_t *display_count)
      {
      	int connectivity_checked = 1;
      	int ret;
    @@ builtin/fetch.c: static int fetch_and_consume_refs(struct display_state *display
      	ret = store_updated_refs(display_state, connectivity_checked,
     -				 transaction, ref_map, fetch_head, config);
     +				 transaction, ref_map, fetch_head, config,
    -+				 delayed_ref_display);
    ++				 display_list, display_count);
      	trace2_region_leave("fetch", "consume_refs", the_repository);
      
      out:
    @@ builtin/fetch.c: static int backfill_tags(struct display_state *display_state,
      			 struct fetch_head *fetch_head,
     -			 const struct fetch_config *config)
     +			 const struct fetch_config *config,
    -+			 struct strmap *delayed_ref_display)
    ++			 struct ref_update_display_info **display_list,
    ++			 size_t *display_count)
      {
      	int retcode, cannot_reuse;
      
    @@ builtin/fetch.c: static int backfill_tags(struct display_state *display_state,
      	transport_set_option(transport, TRANS_OPT_DEEPEN_RELATIVE, NULL);
      	retcode = fetch_and_consume_refs(display_state, transport, transaction, ref_map,
     -					 fetch_head, config);
    -+					 fetch_head, config, delayed_ref_display);
    ++					 fetch_head, config, display_list, display_count);
      
      	if (gsecondary) {
      		transport_disconnect(gsecondary);
    @@ builtin/fetch.c: struct ref_rejection_data {
      	bool conflict_msg_shown;
      	bool case_sensitive_msg_shown;
      	const char *remote_name;
    -+	struct strmap *delayed_ref_display;
    ++	struct strmap *rejected_refs;
      };
      
      static void ref_transaction_rejection_handler(const char *refname,
    -@@ builtin/fetch.c: static void ref_transaction_rejection_handler(const char *refname,
    - 					      void *cb_data)
    - {
    - 	struct ref_rejection_data *data = cb_data;
    -+	struct ref_update_display_info *info;
    - 
    - 	if (err == REF_TRANSACTION_ERROR_CASE_CONFLICT && ignore_case &&
    - 	    !data->case_sensitive_msg_shown) {
     @@ builtin/fetch.c: static void ref_transaction_rejection_handler(const char *refname,
      			      refname, ref_transaction_error_msg(err));
      	}
      
    -+	info = strmap_get(data->delayed_ref_display, refname);
    -+	if (info)
    -+		ref_update_display_info_set_failed(info);
    -+
    ++	strmap_put(data->rejected_refs, refname, NULL);
      	*data->retcode = 1;
      }
      
    @@ builtin/fetch.c: static void ref_transaction_rejection_handler(const char *refna
       */
      static int commit_ref_transaction(struct ref_transaction **transaction,
      				  bool is_atomic, const char *remote_name,
    -+				  struct strmap *delayed_ref_display,
    ++				  struct strmap *rejected_refs,
      				  struct strbuf *err)
      {
      	int retcode = ref_transaction_commit(*transaction, err);
    @@ builtin/fetch.c: static int commit_ref_transaction(struct ref_transaction **tran
      			.conflict_msg_shown = 0,
      			.remote_name = remote_name,
      			.retcode = &retcode,
    -+			.delayed_ref_display = delayed_ref_display,
    ++			.rejected_refs = rejected_refs,
      		};
      
      		ref_transaction_for_each_rejected_update(*transaction,
    @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
      	struct fetch_head fetch_head = { 0 };
      	struct strbuf err = STRBUF_INIT;
      	int do_set_head = 0;
    -+	struct strmap delayed_ref_display = STRMAP_INIT;
    ++	struct ref_update_display_info *display_list = NULL;
    ++	struct strmap rejected_refs = STRMAP_INIT;
    ++	size_t display_count = 0;
     +	int summary_width = 0;
    -+	struct strmap_entry *e;
    -+	struct hashmap_iter iter;
      
      	if (tags == TAGS_DEFAULT) {
      		if (transport->remote->fetch_tags == 2)
    @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
      
      	if (fetch_and_consume_refs(&display_state, transport, transaction, ref_map,
     -				   &fetch_head, config)) {
    -+				   &fetch_head, config, &delayed_ref_display)) {
    ++				   &fetch_head, config, &display_list, &display_count)) {
      		retcode = 1;
      		goto cleanup;
      	}
    @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
      			 */
      			if (backfill_tags(&display_state, transport, transaction, tags_ref_map,
     -					  &fetch_head, config))
    -+					  &fetch_head, config, &delayed_ref_display))
    ++					  &fetch_head, config, &display_list, &display_count))
      				retcode = 1;
      		}
      
    @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
      	retcode = commit_ref_transaction(&transaction, atomic_fetch,
     -					 transport->remote->name, &err);
     +					 transport->remote->name,
    -+					 &delayed_ref_display, &err);
    ++					 &rejected_refs, &err);
      	/*
      	 * With '--atomic', bail out if the transaction fails. Without '--atomic',
      	 * continue to fetch head and perform other post-fetch operations.
    @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
      		commit_ref_transaction(&transaction, false,
     -				       transport->remote->name, &err);
     +				       transport->remote->name,
    -+				       &delayed_ref_display, &err);
    ++				       &rejected_refs, &err);
     +
    -+	/*
    -+	 * Clear any pending information that needs to be shown to the user.
    -+	 */
    -+	strmap_for_each_entry(&delayed_ref_display, &iter, e) {
    -+		struct ref_update_display_info *info = e->value;
    -+		ref_update_display_info_display(info, &display_state, e->key, summary_width);
    ++	for (size_t i = 0; i < display_count; i++) {
    ++		struct ref_update_display_info *info = &display_list[i];
    ++
    ++		if (!info->failed && strmap_contains(&rejected_refs, info->ref))
    ++			ref_update_display_info_set_failed(info);
    ++		ref_update_display_info_display(info, &display_state, summary_width);
     +		ref_update_display_info_free(info);
     +	}
      
    @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
      	if (transaction)
      		ref_transaction_free(transaction);
     +
    -+	strmap_clear(&delayed_ref_display, 1);
    ++	free(display_list);
    ++	strmap_clear(&rejected_refs, 0);
      	display_state_release(&display_state);
      	close_fetch_head(&fetch_head);
      	strbuf_release(&err);
    @@ t/t5516-fetch-push.sh: test_expect_success 'pushing non-commit objects should re
      		test_grep "trying to write non-commit object $tagsha to branch ${SQ}refs/heads/branch${SQ}" err
      	)
      '
    -
    - ## t/t5574-fetch-output.sh ##
    -@@ t/t5574-fetch-output.sh: test_expect_success 'fetch aligned output' '
    - 		grep -e "->" actual | cut -c 22- >../actual
    - 	) &&
    - 	cat >expect <<-\EOF &&
    --	main                 -> origin/main
    - 	looooooooooooong-tag -> looooooooooooong-tag
    -+	main                 -> origin/main
    - 	EOF
    - 	test_cmp expect actual
    - '
    -@@ t/t5574-fetch-output.sh: test_expect_success 'fetch compact output' '
    - 		grep -e "->" actual | cut -c 22- >../actual
    - 	) &&
    - 	cat >expect <<-\EOF &&
    --	main       -> origin/*
    - 	extraaa    -> *
    -+	main       -> origin/*
    - 	EOF
    - 	test_cmp expect actual
    - '
    -@@ t/t5574-fetch-output.sh: do
    - 		cat >expect <<-EOF &&
    - 		- $MAIN_OLD $ZERO_OID refs/forced/deleted-branch
    - 		- $MAIN_OLD $ZERO_OID refs/unforced/deleted-branch
    --		  $MAIN_OLD $FAST_FORWARD_NEW refs/unforced/fast-forward
    - 		! $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/unforced/force-updated
    -+		* $ZERO_OID $MAIN_OLD refs/forced/new-branch
    -+		* $ZERO_OID $MAIN_OLD refs/remotes/origin/new-branch
    -+		+ $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/remotes/origin/force-updated
    -+		  $MAIN_OLD $FAST_FORWARD_NEW refs/unforced/fast-forward
    - 		* $ZERO_OID $MAIN_OLD refs/unforced/new-branch
    - 		  $MAIN_OLD $FAST_FORWARD_NEW refs/forced/fast-forward
    --		+ $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/forced/force-updated
    --		* $ZERO_OID $MAIN_OLD refs/forced/new-branch
    - 		  $MAIN_OLD $FAST_FORWARD_NEW refs/remotes/origin/fast-forward
    --		+ $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/remotes/origin/force-updated
    --		* $ZERO_OID $MAIN_OLD refs/remotes/origin/new-branch
    -+		+ $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/forced/force-updated
    - 		EOF
    - 
    - 		# Change the URL of the repository to fetch different references.
    -@@ t/t5574-fetch-output.sh: test_expect_success 'fetch porcelain overrides fetch.output config' '
    - 	new_commit=$(git rev-parse HEAD) &&
    - 
    - 	cat >expect <<-EOF &&
    --	  $old_commit $new_commit refs/remotes/origin/config-override
    - 	* $ZERO_OID $new_commit refs/tags/new-commit
    -+	  $old_commit $new_commit refs/remotes/origin/config-override
    - 	EOF
    - 
    - 	git -C porcelain -c fetch.output=compact fetch --porcelain >stdout 2>stderr &&

base-commit: 8745eae506f700657882b9e32b2aa00f234a6fb6 change-id: 20260113-633-regression-lost-diagnostic-message-when-pushing-non-commit-objects-to-refs-heads-17786b20894a

Thanks
- Karthik
Karthik Nayak· Jan 20, 2026, 09:59 UTC · re: Karthik Nayak · lore

In `refs_verify_refnames_available()` we have two nested loops: the outer loop iterates over all references to check, while the inner loop checks for filesystem conflicts for a given ref by breaking down its path.

With batched updates, when we detect a filesystem conflict, we mark the update as rejected and execute 'continue'. However, this only skips to the next iteration of the inner loop, not the outer loop as intended. This causes the same reference to be repeatedly rejected. Fix this by using a goto statement to skip to the next reference in the outer loop.

Signed-off-by: Karthik Nayak <karthik.188@gmail.com>
---
 refs.c                  | 44 ++++++++++++++++++++++++++------------------
 refs/files-backend.c    |  5 ++---
 refs/packed-backend.c   | 12 ++++++------
 refs/refs-internal.h    |  4 +++-
 refs/reftable-backend.c |  5 ++---
 5 files changed, 39 insertions(+), 31 deletions(-)
Show changes to 5 files +39 −31

refs.c, refs/files-backend.c, refs/packed-backend.c, refs/refs-internal.h, refs/reftable-backend.c

diff --git a/refs.c b/refs.c
index e06e0cb072..53919c3d22 100644
--- a/refs.c
+++ b/refs.c
@@ -1224,6 +1224,7 @@ void ref_transaction_free(struct ref_transaction *transaction)
 		free(transaction->updates[i]->committer_info);
 		free((char *)transaction->updates[i]->new_target);
 		free((char *)transaction->updates[i]->old_target);
+		free((char *)transaction->updates[i]->rejection_details);
 		free(transaction->updates[i]);
 	}
 
@@ -1238,7 +1239,8 @@ void ref_transaction_free(struct ref_transaction *transaction)
 
 int ref_transaction_maybe_set_rejected(struct ref_transaction *transaction,
 				       size_t update_idx,
-				       enum ref_transaction_error err)
+				       enum ref_transaction_error err,
+				       struct strbuf *details)
 {
 	if (update_idx >= transaction->nr)
 		BUG("trying to set rejection on invalid update index");
@@ -1264,6 +1266,7 @@ int ref_transaction_maybe_set_rejected(struct ref_transaction *transaction,
 			   transaction->updates[update_idx]->refname, 0);
 
 	transaction->updates[update_idx]->rejection_err = err;
+	transaction->updates[update_idx]->rejection_details = strbuf_detach(details, NULL);
 	ALLOC_GROW(transaction->rejections->update_indices,
 		   transaction->rejections->nr + 1,
 		   transaction->rejections->alloc);
@@ -2659,30 +2662,33 @@ enum ref_transaction_error refs_verify_refnames_available(struct ref_store *refs
 			if (!initial_transaction &&
 			    (strset_contains(&conflicting_dirnames, dirname.buf) ||
 			     !refs_read_raw_ref(refs, dirname.buf, &oid, &referent,
-						       &type, &ignore_errno))) {
+						&type, &ignore_errno))) {
+
+				strbuf_addf(err, _("'%s' exists; cannot create '%s'"),
+					    dirname.buf, refname);
+
 				if (transaction && ref_transaction_maybe_set_rejected(
 					    transaction, *update_idx,
-					    REF_TRANSACTION_ERROR_NAME_CONFLICT)) {
+					    REF_TRANSACTION_ERROR_NAME_CONFLICT, err)) {
 					strset_remove(&dirnames, dirname.buf);
 					strset_add(&conflicting_dirnames, dirname.buf);
-					continue;
+					goto next_ref;
 				}
 
-				strbuf_addf(err, _("'%s' exists; cannot create '%s'"),
-					    dirname.buf, refname);
 				goto cleanup;
 			}
 
 			if (extras && string_list_has_string(extras, dirname.buf)) {
+				strbuf_addf(err, _("cannot process '%s' and '%s' at the same time"),
+					    refname, dirname.buf);
+
 				if (transaction && ref_transaction_maybe_set_rejected(
 					    transaction, *update_idx,
-					    REF_TRANSACTION_ERROR_NAME_CONFLICT)) {
+					    REF_TRANSACTION_ERROR_NAME_CONFLICT, err)) {
 					strset_remove(&dirnames, dirname.buf);
-					continue;
+					goto next_ref;
 				}
 
-				strbuf_addf(err, _("cannot process '%s' and '%s' at the same time"),
-					    refname, dirname.buf);
 				goto cleanup;
 			}
 		}
@@ -2712,14 +2718,14 @@ enum ref_transaction_error refs_verify_refnames_available(struct ref_store *refs
 				if (skip &&
 				    string_list_has_string(skip, iter->ref.name))
 					continue;
+				strbuf_addf(err, _("'%s' exists; cannot create '%s'"),
+					    iter->ref.name, refname);
 
 				if (transaction && ref_transaction_maybe_set_rejected(
 					    transaction, *update_idx,
-					    REF_TRANSACTION_ERROR_NAME_CONFLICT))
-					continue;
+					    REF_TRANSACTION_ERROR_NAME_CONFLICT, err))
+					goto next_ref;
 
-				strbuf_addf(err, _("'%s' exists; cannot create '%s'"),
-					    iter->ref.name, refname);
 				goto cleanup;
 			}
 
@@ -2729,15 +2735,17 @@ enum ref_transaction_error refs_verify_refnames_available(struct ref_store *refs
 
 		extra_refname = find_descendant_ref(dirname.buf, extras, skip);
 		if (extra_refname) {
+			strbuf_addf(err, _("cannot process '%s' and '%s' at the same time"),
+				    refname, extra_refname);
+
 			if (transaction && ref_transaction_maybe_set_rejected(
 				    transaction, *update_idx,
-				    REF_TRANSACTION_ERROR_NAME_CONFLICT))
-				continue;
+				    REF_TRANSACTION_ERROR_NAME_CONFLICT, err))
+				goto next_ref;
 
-			strbuf_addf(err, _("cannot process '%s' and '%s' at the same time"),
-				    refname, extra_refname);
 			goto cleanup;
 		}
+next_ref:;
 	}
 
 	ret = 0;
diff --git a/refs/files-backend.c b/refs/files-backend.c
index 6f6f76a8d8..6790d8bf53 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -2983,10 +2983,9 @@ static int files_transaction_prepare(struct ref_store *ref_store,
 					  head_ref, &refnames_to_check,
 					  err);
 		if (ret) {
-			if (ref_transaction_maybe_set_rejected(transaction, i, ret)) {
-				strbuf_reset(err);
+			if (ref_transaction_maybe_set_rejected(transaction, i,
+							       ret, err)) {
 				ret = 0;
-
 				continue;
 			}
 			goto cleanup;
diff --git a/refs/packed-backend.c b/refs/packed-backend.c
index 4ea0c12299..59b3ecb9d6 100644
--- a/refs/packed-backend.c
+++ b/refs/packed-backend.c
@@ -1437,8 +1437,8 @@ static enum ref_transaction_error write_with_updates(struct packed_ref_store *re
 						    update->refname);
 					ret = REF_TRANSACTION_ERROR_CREATE_EXISTS;
 
-					if (ref_transaction_maybe_set_rejected(transaction, i, ret)) {
-						strbuf_reset(err);
+					if (ref_transaction_maybe_set_rejected(transaction, i,
+									       ret, err)) {
 						ret = 0;
 						continue;
 					}
@@ -1452,8 +1452,8 @@ static enum ref_transaction_error write_with_updates(struct packed_ref_store *re
 						    oid_to_hex(&update->old_oid));
 					ret = REF_TRANSACTION_ERROR_INCORRECT_OLD_VALUE;
 
-					if (ref_transaction_maybe_set_rejected(transaction, i, ret)) {
-						strbuf_reset(err);
+					if (ref_transaction_maybe_set_rejected(transaction, i,
+									       ret, err)) {
 						ret = 0;
 						continue;
 					}
@@ -1496,8 +1496,8 @@ static enum ref_transaction_error write_with_updates(struct packed_ref_store *re
 					    oid_to_hex(&update->old_oid));
 				ret = REF_TRANSACTION_ERROR_NONEXISTENT_REF;
 
-				if (ref_transaction_maybe_set_rejected(transaction, i, ret)) {
-					strbuf_reset(err);
+				if (ref_transaction_maybe_set_rejected(transaction, i,
+								       ret, err)) {
 					ret = 0;
 					continue;
 				}
diff --git a/refs/refs-internal.h b/refs/refs-internal.h
index c7d2a6e50b..191a25683f 100644
--- a/refs/refs-internal.h
+++ b/refs/refs-internal.h
@@ -128,6 +128,7 @@ struct ref_update {
 	 * was rejected.
 	 */
 	enum ref_transaction_error rejection_err;
+	const char *rejection_details;
 
 	/*
 	 * If this ref_update was split off of a symref update via
@@ -153,7 +154,8 @@ int refs_read_raw_ref(struct ref_store *ref_store, const char *refname,
  */
 int ref_transaction_maybe_set_rejected(struct ref_transaction *transaction,
 				       size_t update_idx,
-				       enum ref_transaction_error err);
+				       enum ref_transaction_error err,
+				       struct strbuf *details);
 
 /*
  * Add a ref_update with the specified properties to transaction, and
diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
index 4319a4eacb..0e2648e36c 100644
--- a/refs/reftable-backend.c
+++ b/refs/reftable-backend.c
@@ -1401,10 +1401,9 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 					    &refnames_to_check, head_type,
 					    &head_referent, &referent, err);
 		if (ret) {
-			if (ref_transaction_maybe_set_rejected(transaction, i, ret)) {
-				strbuf_reset(err);
+			if (ref_transaction_maybe_set_rejected(transaction, i,
+							       ret, err)) {
 				ret = 0;
-
 				continue;
 			}
 			goto done;
-- 
2.51.2
Karthik Nayak· Jan 20, 2026, 09:59 UTC · re: Karthik Nayak · lore

[PATCH v3 2/6] refs: add rejection detail to the callback function

The previous commit started storing the rejection details alongside the error code for rejected updates. Pass this along to the callback function `ref_transaction_for_each_rejected_update()`. Currently the field is unused, but will be integrated in the upcoming commits.

Co-authored-by: Jeff King <peff@peff.net>
Signed-off-by: Jeff King <peff@peff.net>
Signed-off-by: Karthik Nayak <karthik.188@gmail.com>
---
 builtin/fetch.c        | 1 +
 builtin/receive-pack.c | 1 +
 builtin/update-ref.c   | 1 +
 refs.c                 | 2 +-
 refs.h                 | 1 +
 5 files changed, 5 insertions(+), 1 deletion(-)
Show changes to 5 files +5 −1

builtin/fetch.c, builtin/receive-pack.c, builtin/update-ref.c, refs.c, refs.h

diff --git a/builtin/fetch.c b/builtin/fetch.c
index 288d3772ea..d427adea61 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1649,6 +1649,7 @@ static void ref_transaction_rejection_handler(const char *refname,
 					      const char *old_target UNUSED,
 					      const char *new_target UNUSED,
 					      enum ref_transaction_error err,
+					      const char *details UNUSED,
 					      void *cb_data)
 {
 	struct ref_rejection_data *data = cb_data;
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index ef1f77be8c..94d3e73cee 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -1813,6 +1813,7 @@ static void ref_transaction_rejection_handler(const char *refname,
 					      const char *old_target UNUSED,
 					      const char *new_target UNUSED,
 					      enum ref_transaction_error err,
+					      const char *details UNUSED,
 					      void *cb_data)
 {
 	struct strmap *failed_refs = cb_data;
diff --git a/builtin/update-ref.c b/builtin/update-ref.c
index 195437e7c6..0046a87c57 100644
--- a/builtin/update-ref.c
+++ b/builtin/update-ref.c
@@ -573,6 +573,7 @@ static void print_rejected_refs(const char *refname,
 				const char *old_target,
 				const char *new_target,
 				enum ref_transaction_error err,
+				const char *details UNUSED,
 				void *cb_data UNUSED)
 {
 	struct strbuf sb = STRBUF_INIT;
diff --git a/refs.c b/refs.c
index 53919c3d22..c85c3d2c8b 100644
--- a/refs.c
+++ b/refs.c
@@ -2874,7 +2874,7 @@ void ref_transaction_for_each_rejected_update(struct ref_transaction *transactio
 		   (update->flags & REF_HAVE_OLD) ? &update->old_oid : NULL,
 		   (update->flags & REF_HAVE_NEW) ? &update->new_oid : NULL,
 		   update->old_target, update->new_target,
-		   update->rejection_err, cb_data);
+		   update->rejection_err, update->rejection_details, cb_data);
 	}
 }
 
diff --git a/refs.h b/refs.h
index d9051bbb04..4fbe3da924 100644
--- a/refs.h
+++ b/refs.h
@@ -975,6 +975,7 @@ typedef void ref_transaction_for_each_rejected_update_fn(const char *refname,
 							 const char *old_target,
 							 const char *new_target,
 							 enum ref_transaction_error err,
+							 const char *details,
 							 void *cb_data);
 void ref_transaction_for_each_rejected_update(struct ref_transaction *transaction,
 					      ref_transaction_for_each_rejected_update_fn cb,
-- 
2.51.2
Karthik Nayak· Jan 20, 2026, 09:59 UTC · re: Karthik Nayak · lore

[PATCH v3 3/6] update-ref: utilize rejected error details if available

When git-update-ref(1) received the '--update-ref' flag, the error details generated in the refs namespace wasn't propagated with failed updates. Instead only an error code pertaining to the type of rejection was noted.

This missed detailed error message which the user can act upon. The previous commits added the required code to propagate these detailed error messages from the refs namespace. Now that additional details are available, let's output this additional details to stderr. This allows users to have additional information over the already present machine parsable output.

While we're here, improve the existing tests for the machine parsable output by checking for the entire output string and not just the rejection reason.

Reported-by: Elijah Newren <newren@gmail.com>
Co-authored-by: Jeff King <peff@peff.net>
Signed-off-by: Karthik Nayak <karthik.188@gmail.com>
---
 builtin/update-ref.c  |  8 +++---
 t/t1400-update-ref.sh | 71 ++++++++++++++++++++++++++++++---------------------
 2 files changed, 47 insertions(+), 32 deletions(-)
Show changes to 2 files +47 −32

builtin/update-ref.c, t/t1400-update-ref.sh

diff --git a/builtin/update-ref.c b/builtin/update-ref.c
index 0046a87c57..2d68c40ecb 100644
--- a/builtin/update-ref.c
+++ b/builtin/update-ref.c
@@ -573,16 +573,18 @@ static void print_rejected_refs(const char *refname,
 				const char *old_target,
 				const char *new_target,
 				enum ref_transaction_error err,
-				const char *details UNUSED,
+				const char *details,
 				void *cb_data UNUSED)
 {
 	struct strbuf sb = STRBUF_INIT;
-	const char *reason = ref_transaction_error_msg(err);
+
+	if (details && *details)
+		error("%s", details);
 
 	strbuf_addf(&sb, "rejected %s %s %s %s\n", refname,
 		    new_oid ? oid_to_hex(new_oid) : new_target,
 		    old_oid ? oid_to_hex(old_oid) : old_target,
-		    reason);
+		    ref_transaction_error_msg(err));
 
 	fwrite(sb.buf, sb.len, 1, stdout);
 	strbuf_release(&sb);
diff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh
index db7f5444da..db6585b8d8 100755
--- a/t/t1400-update-ref.sh
+++ b/t/t1400-update-ref.sh
@@ -2093,14 +2093,15 @@ do
 
 			format_command $type "update refs/heads/ref1" "$old_head" "$head" >stdin &&
 			format_command $type "update refs/heads/ref2" "$(test_oid 001)" "$head" >>stdin &&
-			git update-ref $type --stdin --batch-updates <stdin >stdout &&
+			git update-ref $type --stdin --batch-updates <stdin >stdout 2>err &&
 			echo $old_head >expect &&
 			git rev-parse refs/heads/ref1 >actual &&
 			test_cmp expect actual &&
 			echo $head >expect &&
 			git rev-parse refs/heads/ref2 >actual &&
 			test_cmp expect actual &&
-			test_grep -q "invalid new value provided" stdout
+			test_grep "rejected refs/heads/ref2 $(test_oid 001) $head invalid new value provided" stdout &&
+			test_grep "trying to write ref ${SQ}refs/heads/ref2${SQ} with nonexistent object" err
 		)
 	'
 
@@ -2119,14 +2120,15 @@ do
 
 			format_command $type "update refs/heads/ref1" "$old_head" "$head" >stdin &&
 			format_command $type "update refs/heads/ref2" "$head_tree" "$head" >>stdin &&
-			git update-ref $type --stdin --batch-updates <stdin >stdout &&
+			git update-ref $type --stdin --batch-updates <stdin >stdout 2>err &&
 			echo $old_head >expect &&
 			git rev-parse refs/heads/ref1 >actual &&
 			test_cmp expect actual &&
 			echo $head >expect &&
 			git rev-parse refs/heads/ref2 >actual &&
 			test_cmp expect actual &&
-			test_grep -q "invalid new value provided" stdout
+			test_grep "rejected refs/heads/ref2 $head_tree $head invalid new value provided" stdout &&
+			test_grep "trying to write non-commit object $head_tree to branch ${SQ}refs/heads/ref2${SQ}" err
 		)
 	'
 
@@ -2143,12 +2145,13 @@ do
 
 			format_command $type "update refs/heads/ref1" "$old_head" "$head" >stdin &&
 			format_command $type "update refs/heads/ref2" "$old_head" "$head" >>stdin &&
-			git update-ref $type --stdin --batch-updates <stdin >stdout &&
+			git update-ref $type --stdin --batch-updates <stdin >stdout 2>err &&
 			echo $old_head >expect &&
 			git rev-parse refs/heads/ref1 >actual &&
 			test_cmp expect actual &&
 			test_must_fail git rev-parse refs/heads/ref2 &&
-			test_grep -q "reference does not exist" stdout
+			test_grep "rejected refs/heads/ref2 $old_head $head reference does not exist" stdout &&
+			test_grep "cannot lock ref ${SQ}refs/heads/ref2${SQ}: unable to resolve reference ${SQ}refs/heads/ref2${SQ}" err
 		)
 	'
 
@@ -2166,13 +2169,14 @@ do
 
 			format_command $type "update refs/heads/ref1" "$old_head" "$head" >stdin &&
 			format_command $type "update refs/heads/ref2" "$old_head" "$head" >>stdin &&
-			git update-ref $type --no-deref --stdin --batch-updates <stdin >stdout &&
+			git update-ref $type --no-deref --stdin --batch-updates <stdin >stdout 2>err &&
 			echo $old_head >expect &&
 			git rev-parse refs/heads/ref1 >actual &&
 			test_cmp expect actual &&
 			echo $head >expect &&
 			test_must_fail git rev-parse refs/heads/ref2 &&
-			test_grep -q "reference does not exist" stdout
+			test_grep "rejected refs/heads/ref2 $old_head $head reference does not exist" stdout &&
+			test_grep "cannot lock ref ${SQ}refs/heads/ref2${SQ}: reference is missing but expected $head" err
 		)
 	'
 
@@ -2190,7 +2194,7 @@ do
 
 			format_command $type "update refs/heads/ref1" "$old_head" "$head" >stdin &&
 			format_command $type "symref-update refs/heads/ref2" "$old_head" "ref" "refs/heads/nonexistent" >>stdin &&
-			git update-ref $type --no-deref --stdin --batch-updates <stdin >stdout &&
+			git update-ref $type --no-deref --stdin --batch-updates <stdin >stdout 2>err &&
 			echo $old_head >expect &&
 			git rev-parse refs/heads/ref1 >actual &&
 			test_cmp expect actual &&
@@ -2198,7 +2202,8 @@ do
 			echo $head >expect &&
 			git rev-parse refs/heads/ref2 >actual &&
 			test_cmp expect actual &&
-			test_grep -q "expected symref but found regular ref" stdout
+			test_grep "rejected refs/heads/ref2 $ZERO_OID $ZERO_OID expected symref but found regular ref" stdout &&
+			test_grep "cannot lock ref ${SQ}refs/heads/ref2${SQ}: expected symref with target ${SQ}refs/heads/nonexistent${SQ}: but is a regular ref" err
 		)
 	'
 
@@ -2216,14 +2221,15 @@ do
 
 			format_command $type "update refs/heads/ref1" "$old_head" "$head" >stdin &&
 			format_command $type "update refs/heads/ref2" "$old_head" "$Z" >>stdin &&
-			git update-ref $type --stdin --batch-updates <stdin >stdout &&
+			git update-ref $type --stdin --batch-updates <stdin >stdout 2>err &&
 			echo $old_head >expect &&
 			git rev-parse refs/heads/ref1 >actual &&
 			test_cmp expect actual &&
 			echo $head >expect &&
 			git rev-parse refs/heads/ref2 >actual &&
 			test_cmp expect actual &&
-			test_grep -q "reference already exists" stdout
+			test_grep "rejected refs/heads/ref2 $old_head $ZERO_OID reference already exists" stdout &&
+			test_grep "cannot lock ref ${SQ}refs/heads/ref2${SQ}: reference already exists" err
 		)
 	'
 
@@ -2241,14 +2247,15 @@ do
 
 			format_command $type "update refs/heads/ref1" "$old_head" "$head" >stdin &&
 			format_command $type "update refs/heads/ref2" "$head" "$old_head" >>stdin &&
-			git update-ref $type --stdin --batch-updates <stdin >stdout &&
+			git update-ref $type --stdin --batch-updates <stdin >stdout 2>err &&
 			echo $old_head >expect &&
 			git rev-parse refs/heads/ref1 >actual &&
 			test_cmp expect actual &&
 			echo $head >expect &&
 			git rev-parse refs/heads/ref2 >actual &&
 			test_cmp expect actual &&
-			test_grep -q "incorrect old value provided" stdout
+			test_grep "rejected refs/heads/ref2 $head $old_head incorrect old value provided" stdout &&
+			test_grep "cannot lock ref ${SQ}refs/heads/ref2${SQ}: is at $head but expected $old_head" err
 		)
 	'
 
@@ -2264,12 +2271,13 @@ do
 			git update-ref refs/heads/ref/foo $head &&
 
 			format_command $type "update refs/heads/ref/foo" "$old_head" "$head" >stdin &&
-			format_command $type "update refs/heads/ref" "$old_head" "" >>stdin &&
-			git update-ref $type --stdin --batch-updates <stdin >stdout &&
+			format_command $type "update refs/heads/ref" "$old_head" "$ZERO_OID" >>stdin &&
+			git update-ref $type --stdin --batch-updates <stdin >stdout 2>err &&
 			echo $old_head >expect &&
 			git rev-parse refs/heads/ref/foo >actual &&
 			test_cmp expect actual &&
-			test_grep -q "refname conflict" stdout
+			test_grep "rejected refs/heads/ref $old_head $ZERO_OID refname conflict" stdout &&
+			test_grep "${SQ}refs/heads/ref/foo${SQ} exists; cannot create ${SQ}refs/heads/ref${SQ}" err
 		)
 	'
 
@@ -2284,13 +2292,14 @@ do
 			head=$(git rev-parse HEAD) &&
 			git update-ref refs/heads/ref/foo $head &&
 
-			format_command $type "update refs/heads/foo" "$old_head" "" >stdin &&
-			format_command $type "update refs/heads/ref" "$old_head" "" >>stdin &&
-			git update-ref $type --stdin --batch-updates <stdin >stdout &&
+			format_command $type "update refs/heads/foo" "$old_head" "$ZERO_OID" >stdin &&
+			format_command $type "update refs/heads/ref" "$old_head" "$ZERO_OID" >>stdin &&
+			git update-ref $type --stdin --batch-updates <stdin >stdout 2>err &&
 			echo $old_head >expect &&
 			git rev-parse refs/heads/foo >actual &&
 			test_cmp expect actual &&
-			test_grep -q "refname conflict" stdout
+			test_grep "rejected refs/heads/ref $old_head $ZERO_OID refname conflict" stdout &&
+			test_grep "${SQ}refs/heads/ref/foo${SQ} exists; cannot create ${SQ}refs/heads/ref${SQ}" err
 		)
 	'
 
@@ -2309,14 +2318,15 @@ do
 				format_command $type "create refs/heads/ref" "$old_head" &&
 				format_command $type "create refs/heads/Foo" "$old_head"
 			} >stdin &&
-			git update-ref $type --stdin --batch-updates <stdin >stdout &&
+			git update-ref $type --stdin --batch-updates <stdin >stdout 2>err &&
 
 			echo $head >expect &&
 			git rev-parse refs/heads/foo >actual &&
 			echo $old_head >expect &&
 			git rev-parse refs/heads/ref >actual &&
 			test_cmp expect actual &&
-			test_grep -q "reference conflict due to case-insensitive filesystem" stdout
+			test_grep "rejected refs/heads/Foo $old_head $ZERO_OID reference conflict due to case-insensitive filesystem" stdout &&
+			test_grep -e "cannot lock ref ${SQ}refs/heads/Foo${SQ}: Unable to create" -e "Foo.lock" err
 		)
 	'
 
@@ -2357,8 +2367,9 @@ do
 			git symbolic-ref refs/heads/symbolic refs/heads/non-existent &&
 
 			format_command $type "delete refs/heads/symbolic" "$head" >stdin &&
-			git update-ref $type --stdin --batch-updates <stdin >stdout &&
-			test_grep "reference does not exist" stdout
+			git update-ref $type --stdin --batch-updates <stdin >stdout 2>err &&
+			test_grep "rejected refs/heads/non-existent $ZERO_OID $head reference does not exist" stdout &&
+			test_grep "cannot lock ref ${SQ}refs/heads/symbolic${SQ}: unable to resolve reference ${SQ}refs/heads/non-existent${SQ}" err
 		)
 	'
 
@@ -2373,8 +2384,9 @@ do
 			head=$(git rev-parse HEAD) &&
 
 			format_command $type "delete refs/heads/new-branch" "$head" >stdin &&
-			git update-ref $type --stdin --batch-updates <stdin >stdout &&
-			test_grep "incorrect old value provided" stdout
+			git update-ref $type --stdin --batch-updates <stdin >stdout 2>err &&
+			test_grep "rejected refs/heads/new-branch $ZERO_OID $head incorrect old value provided" stdout &&
+			test_grep "cannot lock ref ${SQ}refs/heads/new-branch${SQ}: is at $(git rev-parse new-branch) but expected $head" err
 		)
 	'
 
@@ -2387,8 +2399,9 @@ do
 			head=$(git rev-parse HEAD) &&
 
 			format_command $type "delete refs/heads/non-existent" "$head" >stdin &&
-			git update-ref $type --stdin --batch-updates <stdin >stdout &&
-			test_grep "reference does not exist" stdout
+			git update-ref $type --stdin --batch-updates <stdin >stdout 2>err &&
+			test_grep "rejected refs/heads/non-existent $ZERO_OID $head reference does not exist" stdout &&
+			test_grep "cannot lock ref ${SQ}refs/heads/non-existent${SQ}: unable to resolve reference ${SQ}refs/heads/non-existent${SQ}" err
 		)
 	'
 done
-- 
2.51.2
Karthik Nayak· Jan 20, 2026, 09:59 UTC · re: Karthik Nayak · lore

[PATCH v3 4/6] fetch: utilize rejected ref error details

In 0e358de64a (fetch: use batched reference updates, 2025-05-19), git-fetch(1) switched to using batched reference updates. This also introduced a regression wherein instead of providing detailed error messages for failed referenced updates, the users were provided generic error messages based on the error type.

Similar to the previous commit, switch to using detailed error messages if present for failed reference updates to fix this regression.

Reported-by: Elijah Newren <newren@gmail.com>
Co-authored-by: Jeff King <peff@peff.net>
Signed-off-by: Karthik Nayak <karthik.188@gmail.com>
---
 builtin/fetch.c  | 10 ++++++----
 t/t5510-fetch.sh |  8 ++++----
 2 files changed, 10 insertions(+), 8 deletions(-)
Show changes to 2 files +10 −8

builtin/fetch.c, t/t5510-fetch.sh

diff --git a/builtin/fetch.c b/builtin/fetch.c
index d427adea61..49495be0b6 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1649,7 +1649,7 @@ static void ref_transaction_rejection_handler(const char *refname,
 					      const char *old_target UNUSED,
 					      const char *new_target UNUSED,
 					      enum ref_transaction_error err,
-					      const char *details UNUSED,
+					      const char *details,
 					      void *cb_data)
 {
 	struct ref_rejection_data *data = cb_data;
@@ -1674,9 +1674,11 @@ static void ref_transaction_rejection_handler(const char *refname,
 			"branches"), data->remote_name);
 		data->conflict_msg_shown = true;
 	} else {
-		const char *reason = ref_transaction_error_msg(err);
-
-		error(_("fetching ref %s failed: %s"), refname, reason);
+		if (details)
+			error("%s", details);
+		else
+			error(_("fetching ref %s failed: %s"),
+			      refname, ref_transaction_error_msg(err));
 	}
 
 	*data->retcode = 1;
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index ce1c23684e..c69afb5a60 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -1516,7 +1516,7 @@ test_expect_success REFFILES 'existing reference lock in repo' '
 		git remote add origin ../base &&
 		touch refs/heads/foo.lock &&
 		test_must_fail git fetch -f origin "refs/heads/*:refs/heads/*" 2>err &&
-		test_grep "error: fetching ref refs/heads/foo failed: reference already exists" err &&
+		test_grep -e "error: cannot lock ref ${SQ}refs/heads/foo${SQ}: Unable to create" -e "refs/heads/foo.lock${SQ}: File exists." err &&
 		git rev-parse refs/heads/main >expect &&
 		git rev-parse refs/heads/branch >actual &&
 		test_cmp expect actual
@@ -1530,7 +1530,7 @@ test_expect_success CASE_INSENSITIVE_FS,REFFILES 'F/D conflict on case insensiti
 		cd case_insensitive &&
 		git remote add origin -- ../case_sensitive_fd &&
 		test_must_fail git fetch -f origin "refs/heads/*:refs/heads/*" 2>err &&
-		test_grep "failed: refname conflict" err &&
+		test_grep "cannot process ${SQ}refs/remotes/origin/foo${SQ} and ${SQ}refs/remotes/origin/foo/bar${SQ} at the same time" err &&
 		git rev-parse refs/heads/main >expect &&
 		git rev-parse refs/heads/foo/bar >actual &&
 		test_cmp expect actual
@@ -1544,7 +1544,7 @@ test_expect_success CASE_INSENSITIVE_FS,REFFILES 'D/F conflict on case insensiti
 		cd case_insensitive &&
 		git remote add origin -- ../case_sensitive_df &&
 		test_must_fail git fetch -f origin "refs/heads/*:refs/heads/*" 2>err &&
-		test_grep "failed: refname conflict" err &&
+		test_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 &&
 		git rev-parse refs/heads/main >expect &&
 		git rev-parse refs/heads/Foo/bar >actual &&
 		test_cmp expect actual
@@ -1658,7 +1658,7 @@ test_expect_success REFFILES "FETCH_HEAD is updated even if ref updates fail" '
 		git remote add origin ../base &&
 		>refs/heads/foo.lock &&
 		test_must_fail git fetch -f origin "refs/heads/*:refs/heads/*" 2>err &&
-		test_grep "error: fetching ref refs/heads/foo failed: reference already exists" err &&
+		test_grep -e "error: cannot lock ref ${SQ}refs/heads/foo${SQ}: Unable to create" -e "refs/heads/foo.lock${SQ}: File exists." err &&
 		test_grep "branch ${SQ}branch${SQ} of ../base" FETCH_HEAD &&
 		test_grep "branch ${SQ}foo${SQ} of ../base" FETCH_HEAD
 	)
-- 
2.51.2
Karthik Nayak· Jan 20, 2026, 09:59 UTC · re: Karthik Nayak · lore

[PATCH v3 5/6] receive-pack: utilize rejected ref error details

In 9d2962a7c4 (receive-pack: use batched reference updates, 2025-05-19), git-receive-pack(1) switched to using batched reference updates. This also introduced a regression wherein instead of providing detailed error messages for failed referenced updates, the users were provided generic error messages based on the error type.

Now that the updates also contain detailed error message, propagate those to the client via 'rp_error'. The detailed error messages can be very verbose, for e.g. in the files backend, when trying to write a non-commit object to a branch, you would see:

   ! [remote rejected] 3eaec9ccf3a53f168362a6b3fdeb73426fb9813d ->
   branch (cannot update ref 'refs/heads/branch': trying to write
   non-commit object 3eaec9ccf3a53f168362a6b3fdeb73426fb9813d to branch
   'refs/heads/branch')

Here the refname is repeated multiple times due to how error messages are propagated and filled over the code stack. This potentially can be cleaned up in a future commit.

Reported-by: Elijah Newren <newren@gmail.com>
Co-authored-by: Jeff King <peff@peff.net>
Signed-off-by: Jeff King <peff@peff.net>
Signed-off-by: Karthik Nayak <karthik.188@gmail.com>
---
 builtin/receive-pack.c |  8 ++++++--
 t/t5516-fetch-push.sh  | 15 +++++++++++++++
 2 files changed, 21 insertions(+), 2 deletions(-)
Show changes to 2 files +21 −2

builtin/receive-pack.c, t/t5516-fetch-push.sh

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 94d3e73cee..70e04b3efb 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -1813,11 +1813,14 @@ static void ref_transaction_rejection_handler(const char *refname,
 					      const char *old_target UNUSED,
 					      const char *new_target UNUSED,
 					      enum ref_transaction_error err,
-					      const char *details UNUSED,
+					      const char *details,
 					      void *cb_data)
 {
 	struct strmap *failed_refs = cb_data;
 
+	if (details)
+		rp_error("%s", details);
+
 	strmap_put(failed_refs, refname, (char *)ref_transaction_error_msg(err));
 }
 
@@ -1884,6 +1887,7 @@ static void execute_commands_non_atomic(struct command *commands,
 		}
 
 		ref_transaction_for_each_rejected_update(transaction,
+
 							 ref_transaction_rejection_handler,
 							 &failed_refs);
 
@@ -1895,7 +1899,7 @@ static void execute_commands_non_atomic(struct command *commands,
 			if (reported_error)
 				cmd->error_string = reported_error;
 			else if (strmap_contains(&failed_refs, cmd->ref_name))
-				cmd->error_string = strmap_get(&failed_refs, cmd->ref_name);
+				cmd->error_string = cmd->error_string_owned = xstrdup(strmap_get(&failed_refs, cmd->ref_name));
 		}
 
 	cleanup:
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 46926e7bbd..45595991c8 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1882,4 +1882,19 @@ test_expect_success 'push with F/D conflict with deletion and creation' '
 	git push testrepo :refs/heads/branch/conflict refs/heads/branch
 '
 
+test_expect_success 'pushing non-commit objects should report error' '
+	test_when_finished "rm -rf dest repo" &&
+	git init dest &&
+	git init repo &&
+
+	(
+		cd repo &&
+		test_commit --annotate test &&
+
+		tagsha=$(git rev-parse test^{tag}) &&
+		test_must_fail git push ../dest "$tagsha:refs/heads/branch" 2>err &&
+		test_grep "trying to write non-commit object $tagsha to branch ${SQ}refs/heads/branch${SQ}" err
+	)
+'
+
 test_done
-- 
2.51.2
Karthik Nayak· Jan 20, 2026, 09:59 UTC · re: Karthik Nayak · lore

[PATCH v3 6/6] fetch: delay user information post committing of transaction

In Git 2.50 and earlier, we would display failure codes and error message as part of the status display:

  $ git fetch . v1.0.0:refs/heads/foo
    error: cannot update ref 'refs/heads/foo': trying to write non-commit object f665776185ad074b236c00751d666da7d1977dbe to branch 'refs/heads/foo'
    From .
     ! [new tag]               v1.0.0     -> foo  (unable to update local ref)

With the addition of batched updates, this information is no longer shown to the user:

  $ git fetch . v1.0.0:refs/heads/foo
    From .
     * [new tag]               v1.0.0     -> foo
    error: cannot update ref 'refs/heads/foo': trying to write non-commit object f665776185ad074b236c00751d666da7d1977dbe to branch 'refs/heads/foo'

Since reference updates are batched and processed together at the end, information around the outcome is not available during individual reference parsing.

To overcome this, collate and delay the output to the end. Introduce `ref_update_display_info` which will hold individual update's information and also whether the update failed or succeeded. This finally allows us to iterate over all such updates and print them to the user.

Using an dynamic array and strmap does add some overhead to 'git-fetch(1)', but from benchmarking this seems to be not too bad:

  Benchmark 1: fetch: many refs (refformat = files, refcount = 1000, revision = master)
    Time (mean ± σ):      42.6 ms ±   1.2 ms    [User: 13.1 ms, System: 29.8 ms]
    Range (min … max):    40.1 ms …  45.8 ms    47 runs
  Benchmark 2: fetch: many refs (refformat = files, refcount = 1000, revision = HEAD)
    Time (mean ± σ):      43.1 ms ±   1.2 ms    [User: 12.7 ms, System: 30.7 ms]
    Range (min … max):    40.5 ms …  45.8 ms    48 runs
  Summary
    fetch: many refs (refformat = files, refcount = 1000, revision = master) ran
      1.01 ± 0.04 times faster than fetch: many refs (refformat = files, refcount = 1000, revision = HEAD)

Another approach would be to move the status printing logic to be handled post the transaction being committed. That however would require adding an iterator to the ref transaction that tracks both the outcome (success/failure) and the original refspec information for each update, which is more involved infrastructure work compared to the strmap approach here.

Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Reported-by: Jeff King <peff@peff.net>
Signed-off-by: Karthik Nayak <karthik.188@gmail.com>
---
 builtin/fetch.c       | 250 +++++++++++++++++++++++++++++++++++++++-----------
 t/t5516-fetch-push.sh |   1 +
 2 files changed, 197 insertions(+), 54 deletions(-)
Show changes to 2 files +197 −54

builtin/fetch.c, t/t5516-fetch-push.sh

diff --git a/builtin/fetch.c b/builtin/fetch.c
index 49495be0b6..3a3f1d8914 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -861,12 +861,87 @@ static void display_ref_update(struct display_state *display_state, char code,
 	fputs(display_state->buf.buf, f);
 }
 
+struct ref_update_display_info {
+	bool failed;
+	char success_code;
+	char fail_code;
+	const char *summary;
+	const char *fail_detail;
+	const char *success_detail;
+	const char *ref;
+	const char *remote;
+	struct object_id old_oid;
+	struct object_id new_oid;
+};
+
+static struct ref_update_display_info *ref_update_display_info_append(
+					   struct ref_update_display_info **list,
+					   size_t *count,
+					   char success_code,
+					   char fail_code,
+					   const char *summary,
+					   const char *success_detail,
+					   const char *fail_detail,
+					   const char *ref,
+					   const char *remote,
+					   const struct object_id *old_oid,
+					   const struct object_id *new_oid)
+{
+	struct ref_update_display_info *info;
+	size_t index = *count;
+
+	(*count)++;
+	REALLOC_ARRAY(*list, *count);
+
+	info = &(*list)[index];
+
+	info->failed = false;
+	info->success_code = success_code;
+	info->fail_code = fail_code;
+	info->summary = xstrdup(summary);
+	info->success_detail = xstrdup_or_null(success_detail);
+	info->fail_detail = xstrdup_or_null(fail_detail);
+	info->remote = xstrdup(remote);
+	info->ref = xstrdup(ref);
+
+	oidcpy(&info->old_oid, old_oid);
+	oidcpy(&info->new_oid, new_oid);
+
+	return info;
+}
+
+static void ref_update_display_info_set_failed(struct ref_update_display_info *info)
+{
+	info->failed = true;
+}
+
+static void ref_update_display_info_free(struct ref_update_display_info *info)
+{
+	free((char *)info->summary);
+	free((char *)info->success_detail);
+	free((char *)info->fail_detail);
+	free((char *)info->remote);
+	free((char *)info->ref);
+}
+
+static void ref_update_display_info_display(struct ref_update_display_info *info,
+					    struct display_state *display_state,
+					    int summary_width)
+{
+	display_ref_update(display_state,
+			   info->failed ? info->fail_code : info->success_code,
+			   info->summary,
+			   info->failed ? info->fail_detail : info->success_detail,
+			   info->remote, info->ref, &info->old_oid,
+			   &info->new_oid, summary_width);
+}
+
 static int update_local_ref(struct ref *ref,
 			    struct ref_transaction *transaction,
-			    struct display_state *display_state,
 			    const struct ref *remote_ref,
-			    int summary_width,
-			    const struct fetch_config *config)
+			    const struct fetch_config *config,
+			    struct ref_update_display_info **display_list,
+			    size_t *display_count)
 {
 	struct commit *current = NULL, *updated;
 	int fast_forward = 0;
@@ -877,41 +952,56 @@ static int update_local_ref(struct ref *ref,
 
 	if (oideq(&ref->old_oid, &ref->new_oid)) {
 		if (verbosity > 0)
-			display_ref_update(display_state, '=', _("[up to date]"), NULL,
-					   remote_ref->name, ref->name,
-					   &ref->old_oid, &ref->new_oid, summary_width);
+			ref_update_display_info_append(display_list, display_count,
+						       '=', '=', _("[up to date]"),
+						       NULL, NULL, ref->name,
+						       remote_ref->name, &ref->old_oid,
+						       &ref->new_oid);
 		return 0;
 	}
 
 	if (!update_head_ok &&
 	    !is_null_oid(&ref->old_oid) &&
 	    branch_checked_out(ref->name)) {
+		struct ref_update_display_info *info;
 		/*
 		 * If this is the head, and it's not okay to update
 		 * the head, and the old value of the head isn't empty...
 		 */
-		display_ref_update(display_state, '!', _("[rejected]"),
-				   _("can't fetch into checked-out branch"),
-				   remote_ref->name, ref->name,
-				   &ref->old_oid, &ref->new_oid, summary_width);
+		info = ref_update_display_info_append(display_list, display_count,
+						      '!', '!', _("[rejected]"),
+						      NULL, _("can't fetch into checked-out branch"),
+						      ref->name, remote_ref->name,
+						      &ref->old_oid, &ref->new_oid);
+		ref_update_display_info_set_failed(info);
 		return 1;
 	}
 
 	if (!is_null_oid(&ref->old_oid) &&
 	    starts_with(ref->name, "refs/tags/")) {
+		struct ref_update_display_info *info;
+
 		if (force || ref->force) {
 			int r;
+
 			r = s_update_ref("updating tag", ref, transaction, 0);
-			display_ref_update(display_state, r ? '!' : 't', _("[tag update]"),
-					   r ? _("unable to update local ref") : NULL,
-					   remote_ref->name, ref->name,
-					   &ref->old_oid, &ref->new_oid, summary_width);
+
+			info = ref_update_display_info_append(display_list, display_count,
+							      't', '!', _("[tag update]"), NULL,
+							      _("unable to update local ref"),
+							      ref->name, remote_ref->name,
+							      &ref->old_oid, &ref->new_oid);
+			if (r)
+				ref_update_display_info_set_failed(info);
+
 			return r;
 		} else {
-			display_ref_update(display_state, '!', _("[rejected]"),
-					   _("would clobber existing tag"),
-					   remote_ref->name, ref->name,
-					   &ref->old_oid, &ref->new_oid, summary_width);
+			info = ref_update_display_info_append(display_list, display_count,
+							      '!', '!', _("[rejected]"), NULL,
+							      _("would clobber existing tag"),
+							      ref->name, remote_ref->name,
+							      &ref->old_oid, &ref->new_oid);
+			ref_update_display_info_set_failed(info);
 			return 1;
 		}
 	}
@@ -921,6 +1011,7 @@ static int update_local_ref(struct ref *ref,
 	updated = lookup_commit_reference_gently(the_repository,
 						 &ref->new_oid, 1);
 	if (!current || !updated) {
+		struct ref_update_display_info *info;
 		const char *msg;
 		const char *what;
 		int r;
@@ -941,10 +1032,15 @@ static int update_local_ref(struct ref *ref,
 		}
 
 		r = s_update_ref(msg, ref, transaction, 0);
-		display_ref_update(display_state, r ? '!' : '*', what,
-				   r ? _("unable to update local ref") : NULL,
-				   remote_ref->name, ref->name,
-				   &ref->old_oid, &ref->new_oid, summary_width);
+
+		info = ref_update_display_info_append(display_list, display_count,
+						      '*', '!', what, NULL,
+						      _("unable to update local ref"),
+						      ref->name, remote_ref->name,
+						      &ref->old_oid, &ref->new_oid);
+		if (r)
+			ref_update_display_info_set_failed(info);
+
 		return r;
 	}
 
@@ -960,6 +1056,7 @@ static int update_local_ref(struct ref *ref,
 	}
 
 	if (fast_forward) {
+		struct ref_update_display_info *info;
 		struct strbuf quickref = STRBUF_INIT;
 		int r;
 
@@ -967,29 +1064,46 @@ static int update_local_ref(struct ref *ref,
 		strbuf_addstr(&quickref, "..");
 		strbuf_add_unique_abbrev(&quickref, &ref->new_oid, DEFAULT_ABBREV);
 		r = s_update_ref("fast-forward", ref, transaction, 1);
-		display_ref_update(display_state, r ? '!' : ' ', quickref.buf,
-				   r ? _("unable to update local ref") : NULL,
-				   remote_ref->name, ref->name,
-				   &ref->old_oid, &ref->new_oid, summary_width);
+
+		info = ref_update_display_info_append(display_list, display_count,
+						      ' ', '!', quickref.buf, NULL,
+						      _("unable to update local ref"),
+						      ref->name, remote_ref->name,
+						      &ref->old_oid, &ref->new_oid);
+		if (r)
+			ref_update_display_info_set_failed(info);
+
 		strbuf_release(&quickref);
 		return r;
 	} else if (force || ref->force) {
+		struct ref_update_display_info *info;
 		struct strbuf quickref = STRBUF_INIT;
 		int r;
+
 		strbuf_add_unique_abbrev(&quickref, &current->object.oid, DEFAULT_ABBREV);
 		strbuf_addstr(&quickref, "...");
 		strbuf_add_unique_abbrev(&quickref, &ref->new_oid, DEFAULT_ABBREV);
 		r = s_update_ref("forced-update", ref, transaction, 1);
-		display_ref_update(display_state, r ? '!' : '+', quickref.buf,
-				   r ? _("unable to update local ref") : _("forced update"),
-				   remote_ref->name, ref->name,
-				   &ref->old_oid, &ref->new_oid, summary_width);
+
+		info = ref_update_display_info_append(display_list, display_count,
+						      '+', '!', quickref.buf, _("forced update"),
+						      _("unable to update local ref"),
+						      ref->name, remote_ref->name,
+						      &ref->old_oid, &ref->new_oid);
+
+		if (r)
+			ref_update_display_info_set_failed(info);
+
 		strbuf_release(&quickref);
 		return r;
 	} else {
-		display_ref_update(display_state, '!', _("[rejected]"), _("non-fast-forward"),
-				   remote_ref->name, ref->name,
-				   &ref->old_oid, &ref->new_oid, summary_width);
+		struct ref_update_display_info *info;
+		info = ref_update_display_info_append(display_list, display_count,
+						      '!', '!', _("[rejected]"), NULL,
+						      _("non-fast-forward"),
+						      ref->name, remote_ref->name,
+						      &ref->old_oid, &ref->new_oid);
+		ref_update_display_info_set_failed(info);
 		return 1;
 	}
 }
@@ -1103,17 +1217,15 @@ static int store_updated_refs(struct display_state *display_state,
 			      int connectivity_checked,
 			      struct ref_transaction *transaction, struct ref *ref_map,
 			      struct fetch_head *fetch_head,
-			      const struct fetch_config *config)
+			      const struct fetch_config *config,
+			      struct ref_update_display_info **display_list,
+			      size_t *display_count)
 {
 	int rc = 0;
 	struct strbuf note = STRBUF_INIT;
 	const char *what, *kind;
 	struct ref *rm;
 	int want_status;
-	int summary_width = 0;
-
-	if (verbosity >= 0)
-		summary_width = transport_summary_width(ref_map);
 
 	if (!connectivity_checked) {
 		struct check_connected_options opt = CHECK_CONNECTED_INIT;
@@ -1218,8 +1330,9 @@ static int store_updated_refs(struct display_state *display_state,
 					  display_state->url_len);
 
 			if (ref) {
-				rc |= update_local_ref(ref, transaction, display_state,
-						       rm, summary_width, config);
+				rc |= update_local_ref(ref, transaction, rm,
+						       config, display_list,
+						       display_count);
 				free(ref);
 			} else if (write_fetch_head || dry_run) {
 				/*
@@ -1227,12 +1340,11 @@ static int store_updated_refs(struct display_state *display_state,
 				 * would be written to FETCH_HEAD, if --dry-run
 				 * is set).
 				 */
-				display_ref_update(display_state, '*',
-						   *kind ? kind : "branch", NULL,
-						   rm->name,
-						   "FETCH_HEAD",
-						   &rm->new_oid, &rm->old_oid,
-						   summary_width);
+
+				ref_update_display_info_append(display_list, display_count,
+							       '*', '*', *kind ? kind : "branch",
+							       NULL, NULL, "FETCH_HEAD", rm->name,
+							       &rm->new_oid, &rm->old_oid);
 			}
 		}
 	}
@@ -1300,7 +1412,9 @@ static int fetch_and_consume_refs(struct display_state *display_state,
 				  struct ref_transaction *transaction,
 				  struct ref *ref_map,
 				  struct fetch_head *fetch_head,
-				  const struct fetch_config *config)
+				  const struct fetch_config *config,
+				  struct ref_update_display_info **display_list,
+				  size_t *display_count)
 {
 	int connectivity_checked = 1;
 	int ret;
@@ -1322,7 +1436,8 @@ static int fetch_and_consume_refs(struct display_state *display_state,
 
 	trace2_region_enter("fetch", "consume_refs", the_repository);
 	ret = store_updated_refs(display_state, connectivity_checked,
-				 transaction, ref_map, fetch_head, config);
+				 transaction, ref_map, fetch_head, config,
+				 display_list, display_count);
 	trace2_region_leave("fetch", "consume_refs", the_repository);
 
 out:
@@ -1493,7 +1608,9 @@ static int backfill_tags(struct display_state *display_state,
 			 struct ref_transaction *transaction,
 			 struct ref *ref_map,
 			 struct fetch_head *fetch_head,
-			 const struct fetch_config *config)
+			 const struct fetch_config *config,
+			 struct ref_update_display_info **display_list,
+			 size_t *display_count)
 {
 	int retcode, cannot_reuse;
 
@@ -1515,7 +1632,7 @@ static int backfill_tags(struct display_state *display_state,
 	transport_set_option(transport, TRANS_OPT_DEPTH, "0");
 	transport_set_option(transport, TRANS_OPT_DEEPEN_RELATIVE, NULL);
 	retcode = fetch_and_consume_refs(display_state, transport, transaction, ref_map,
-					 fetch_head, config);
+					 fetch_head, config, display_list, display_count);
 
 	if (gsecondary) {
 		transport_disconnect(gsecondary);
@@ -1641,6 +1758,7 @@ struct ref_rejection_data {
 	bool conflict_msg_shown;
 	bool case_sensitive_msg_shown;
 	const char *remote_name;
+	struct strmap *rejected_refs;
 };
 
 static void ref_transaction_rejection_handler(const char *refname,
@@ -1681,6 +1799,7 @@ static void ref_transaction_rejection_handler(const char *refname,
 			      refname, ref_transaction_error_msg(err));
 	}
 
+	strmap_put(data->rejected_refs, refname, NULL);
 	*data->retcode = 1;
 }
 
@@ -1690,6 +1809,7 @@ static void ref_transaction_rejection_handler(const char *refname,
  */
 static int commit_ref_transaction(struct ref_transaction **transaction,
 				  bool is_atomic, const char *remote_name,
+				  struct strmap *rejected_refs,
 				  struct strbuf *err)
 {
 	int retcode = ref_transaction_commit(*transaction, err);
@@ -1701,6 +1821,7 @@ static int commit_ref_transaction(struct ref_transaction **transaction,
 			.conflict_msg_shown = 0,
 			.remote_name = remote_name,
 			.retcode = &retcode,
+			.rejected_refs = rejected_refs,
 		};
 
 		ref_transaction_for_each_rejected_update(*transaction,
@@ -1729,6 +1850,10 @@ static int do_fetch(struct transport *transport,
 	struct fetch_head fetch_head = { 0 };
 	struct strbuf err = STRBUF_INIT;
 	int do_set_head = 0;
+	struct ref_update_display_info *display_list = NULL;
+	struct strmap rejected_refs = STRMAP_INIT;
+	size_t display_count = 0;
+	int summary_width = 0;
 
 	if (tags == TAGS_DEFAULT) {
 		if (transport->remote->fetch_tags == 2)
@@ -1853,7 +1978,7 @@ static int do_fetch(struct transport *transport,
 	}
 
 	if (fetch_and_consume_refs(&display_state, transport, transaction, ref_map,
-				   &fetch_head, config)) {
+				   &fetch_head, config, &display_list, &display_count)) {
 		retcode = 1;
 		goto cleanup;
 	}
@@ -1876,7 +2001,7 @@ static int do_fetch(struct transport *transport,
 			 * the transaction and don't commit anything.
 			 */
 			if (backfill_tags(&display_state, transport, transaction, tags_ref_map,
-					  &fetch_head, config))
+					  &fetch_head, config, &display_list, &display_count))
 				retcode = 1;
 		}
 
@@ -1886,8 +2011,12 @@ static int do_fetch(struct transport *transport,
 	if (retcode)
 		goto cleanup;
 
+	if (verbosity >= 0)
+		summary_width = transport_summary_width(ref_map);
+
 	retcode = commit_ref_transaction(&transaction, atomic_fetch,
-					 transport->remote->name, &err);
+					 transport->remote->name,
+					 &rejected_refs, &err);
 	/*
 	 * With '--atomic', bail out if the transaction fails. Without '--atomic',
 	 * continue to fetch head and perform other post-fetch operations.
@@ -1965,7 +2094,17 @@ static int do_fetch(struct transport *transport,
 	 */
 	if (retcode && !atomic_fetch && transaction)
 		commit_ref_transaction(&transaction, false,
-				       transport->remote->name, &err);
+				       transport->remote->name,
+				       &rejected_refs, &err);
+
+	for (size_t i = 0; i < display_count; i++) {
+		struct ref_update_display_info *info = &display_list[i];
+
+		if (!info->failed && strmap_contains(&rejected_refs, info->ref))
+			ref_update_display_info_set_failed(info);
+		ref_update_display_info_display(info, &display_state, summary_width);
+		ref_update_display_info_free(info);
+	}
 
 	if (retcode) {
 		if (err.len) {
@@ -1980,6 +2119,9 @@ static int do_fetch(struct transport *transport,
 
 	if (transaction)
 		ref_transaction_free(transaction);
+
+	free(display_list);
+	strmap_clear(&rejected_refs, 0);
 	display_state_release(&display_state);
 	close_fetch_head(&fetch_head);
 	strbuf_release(&err);
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 45595991c8..29e2f17608 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1893,6 +1893,7 @@ test_expect_success 'pushing non-commit objects should report error' '
 
 		tagsha=$(git rev-parse test^{tag}) &&
 		test_must_fail git push ../dest "$tagsha:refs/heads/branch" 2>err &&
+		test_grep "! \[remote rejected\] $tagsha -> branch (invalid new value provided)" err &&
 		test_grep "trying to write non-commit object $tagsha to branch ${SQ}refs/heads/branch${SQ}" err
 	)
 '
-- 
2.51.2
Phillip Wood· Jan 21, 2026, 16:21 UTC · re: Karthik Nayak · lore

Re: [PATCH v3 6/6] fetch: delay user information post committing of transaction

Hi Karthik
On 20/01/2026 09:59, Karthik Nayak wrote:
Show 12 quoted lines
> +struct ref_update_display_info {
> +	bool failed;
> +	char success_code;
> +	char fail_code;
> +	const char *summary;
> +	const char *fail_detail;
> +	const char *success_detail;
> +	const char *ref;
> +	const char *remote;
> +	struct object_id old_oid;
> +	struct object_id new_oid;
> +};
I was expecting that we'd pass around a struct like
struct ref_update_display_info_array {
	size_t alloc, nr;
	ref_update_display_info *info;
};

rather than passing a pointer, count pair as separate parameters. That would also allow us to use ALLOC_GROW() rather than reallocating the array each time we append to it which is rather inefficient.

Thanks
Phillip
Show 449 quoted lines
> +static struct ref_update_display_info *ref_update_display_info_append(
> +					   struct ref_update_display_info **list,
> +					   size_t *count,
> +					   char success_code,
> +					   char fail_code,
> +					   const char *summary,
> +					   const char *success_detail,
> +					   const char *fail_detail,
> +					   const char *ref,
> +					   const char *remote,
> +					   const struct object_id *old_oid,
> +					   const struct object_id *new_oid)
> +{
> +	struct ref_update_display_info *info;
> +	size_t index = *count;
> +
> +	(*count)++;
> +	REALLOC_ARRAY(*list, *count);
> +
> +	info = &(*list)[index];
> +
> +	info->failed = false;
> +	info->success_code = success_code;
> +	info->fail_code = fail_code;
> +	info->summary = xstrdup(summary);
> +	info->success_detail = xstrdup_or_null(success_detail);
> +	info->fail_detail = xstrdup_or_null(fail_detail);
> +	info->remote = xstrdup(remote);
> +	info->ref = xstrdup(ref);
> +
> +	oidcpy(&info->old_oid, old_oid);
> +	oidcpy(&info->new_oid, new_oid);
> +
> +	return info;
> +}
> +
> +static void ref_update_display_info_set_failed(struct ref_update_display_info *info)
> +{
> +	info->failed = true;
> +}
> +
> +static void ref_update_display_info_free(struct ref_update_display_info *info)
> +{
> +	free((char *)info->summary);
> +	free((char *)info->success_detail);
> +	free((char *)info->fail_detail);
> +	free((char *)info->remote);
> +	free((char *)info->ref);
> +}
> +
> +static void ref_update_display_info_display(struct ref_update_display_info *info,
> +					    struct display_state *display_state,
> +					    int summary_width)
> +{
> +	display_ref_update(display_state,
> +			   info->failed ? info->fail_code : info->success_code,
> +			   info->summary,
> +			   info->failed ? info->fail_detail : info->success_detail,
> +			   info->remote, info->ref, &info->old_oid,
> +			   &info->new_oid, summary_width);
> +}
> +
>   static int update_local_ref(struct ref *ref,
>   			    struct ref_transaction *transaction,
> -			    struct display_state *display_state,
>   			    const struct ref *remote_ref,
> -			    int summary_width,
> -			    const struct fetch_config *config)
> +			    const struct fetch_config *config,
> +			    struct ref_update_display_info **display_list,
> +			    size_t *display_count)
>   {
>   	struct commit *current = NULL, *updated;
>   	int fast_forward = 0;
> @@ -877,41 +952,56 @@ static int update_local_ref(struct ref *ref,
>   
>   	if (oideq(&ref->old_oid, &ref->new_oid)) {
>   		if (verbosity > 0)
> -			display_ref_update(display_state, '=', _("[up to date]"), NULL,
> -					   remote_ref->name, ref->name,
> -					   &ref->old_oid, &ref->new_oid, summary_width);
> +			ref_update_display_info_append(display_list, display_count,
> +						       '=', '=', _("[up to date]"),
> +						       NULL, NULL, ref->name,
> +						       remote_ref->name, &ref->old_oid,
> +						       &ref->new_oid);
>   		return 0;
>   	}
>   
>   	if (!update_head_ok &&
>   	    !is_null_oid(&ref->old_oid) &&
>   	    branch_checked_out(ref->name)) {
> +		struct ref_update_display_info *info;
>   		/*
>   		 * If this is the head, and it's not okay to update
>   		 * the head, and the old value of the head isn't empty...
>   		 */
> -		display_ref_update(display_state, '!', _("[rejected]"),
> -				   _("can't fetch into checked-out branch"),
> -				   remote_ref->name, ref->name,
> -				   &ref->old_oid, &ref->new_oid, summary_width);
> +		info = ref_update_display_info_append(display_list, display_count,
> +						      '!', '!', _("[rejected]"),
> +						      NULL, _("can't fetch into checked-out branch"),
> +						      ref->name, remote_ref->name,
> +						      &ref->old_oid, &ref->new_oid);
> +		ref_update_display_info_set_failed(info);
>   		return 1;
>   	}
>   
>   	if (!is_null_oid(&ref->old_oid) &&
>   	    starts_with(ref->name, "refs/tags/")) {
> +		struct ref_update_display_info *info;
> +
>   		if (force || ref->force) {
>   			int r;
> +
>   			r = s_update_ref("updating tag", ref, transaction, 0);
> -			display_ref_update(display_state, r ? '!' : 't', _("[tag update]"),
> -					   r ? _("unable to update local ref") : NULL,
> -					   remote_ref->name, ref->name,
> -					   &ref->old_oid, &ref->new_oid, summary_width);
> +
> +			info = ref_update_display_info_append(display_list, display_count,
> +							      't', '!', _("[tag update]"), NULL,
> +							      _("unable to update local ref"),
> +							      ref->name, remote_ref->name,
> +							      &ref->old_oid, &ref->new_oid);
> +			if (r)
> +				ref_update_display_info_set_failed(info);
> +
>   			return r;
>   		} else {
> -			display_ref_update(display_state, '!', _("[rejected]"),
> -					   _("would clobber existing tag"),
> -					   remote_ref->name, ref->name,
> -					   &ref->old_oid, &ref->new_oid, summary_width);
> +			info = ref_update_display_info_append(display_list, display_count,
> +							      '!', '!', _("[rejected]"), NULL,
> +							      _("would clobber existing tag"),
> +							      ref->name, remote_ref->name,
> +							      &ref->old_oid, &ref->new_oid);
> +			ref_update_display_info_set_failed(info);
>   			return 1;
>   		}
>   	}
> @@ -921,6 +1011,7 @@ static int update_local_ref(struct ref *ref,
>   	updated = lookup_commit_reference_gently(the_repository,
>   						 &ref->new_oid, 1);
>   	if (!current || !updated) {
> +		struct ref_update_display_info *info;
>   		const char *msg;
>   		const char *what;
>   		int r;
> @@ -941,10 +1032,15 @@ static int update_local_ref(struct ref *ref,
>   		}
>   
>   		r = s_update_ref(msg, ref, transaction, 0);
> -		display_ref_update(display_state, r ? '!' : '*', what,
> -				   r ? _("unable to update local ref") : NULL,
> -				   remote_ref->name, ref->name,
> -				   &ref->old_oid, &ref->new_oid, summary_width);
> +
> +		info = ref_update_display_info_append(display_list, display_count,
> +						      '*', '!', what, NULL,
> +						      _("unable to update local ref"),
> +						      ref->name, remote_ref->name,
> +						      &ref->old_oid, &ref->new_oid);
> +		if (r)
> +			ref_update_display_info_set_failed(info);
> +
>   		return r;
>   	}
>   
> @@ -960,6 +1056,7 @@ static int update_local_ref(struct ref *ref,
>   	}
>   
>   	if (fast_forward) {
> +		struct ref_update_display_info *info;
>   		struct strbuf quickref = STRBUF_INIT;
>   		int r;
>   
> @@ -967,29 +1064,46 @@ static int update_local_ref(struct ref *ref,
>   		strbuf_addstr(&quickref, "..");
>   		strbuf_add_unique_abbrev(&quickref, &ref->new_oid, DEFAULT_ABBREV);
>   		r = s_update_ref("fast-forward", ref, transaction, 1);
> -		display_ref_update(display_state, r ? '!' : ' ', quickref.buf,
> -				   r ? _("unable to update local ref") : NULL,
> -				   remote_ref->name, ref->name,
> -				   &ref->old_oid, &ref->new_oid, summary_width);
> +
> +		info = ref_update_display_info_append(display_list, display_count,
> +						      ' ', '!', quickref.buf, NULL,
> +						      _("unable to update local ref"),
> +						      ref->name, remote_ref->name,
> +						      &ref->old_oid, &ref->new_oid);
> +		if (r)
> +			ref_update_display_info_set_failed(info);
> +
>   		strbuf_release(&quickref);
>   		return r;
>   	} else if (force || ref->force) {
> +		struct ref_update_display_info *info;
>   		struct strbuf quickref = STRBUF_INIT;
>   		int r;
> +
>   		strbuf_add_unique_abbrev(&quickref, &current->object.oid, DEFAULT_ABBREV);
>   		strbuf_addstr(&quickref, "...");
>   		strbuf_add_unique_abbrev(&quickref, &ref->new_oid, DEFAULT_ABBREV);
>   		r = s_update_ref("forced-update", ref, transaction, 1);
> -		display_ref_update(display_state, r ? '!' : '+', quickref.buf,
> -				   r ? _("unable to update local ref") : _("forced update"),
> -				   remote_ref->name, ref->name,
> -				   &ref->old_oid, &ref->new_oid, summary_width);
> +
> +		info = ref_update_display_info_append(display_list, display_count,
> +						      '+', '!', quickref.buf, _("forced update"),
> +						      _("unable to update local ref"),
> +						      ref->name, remote_ref->name,
> +						      &ref->old_oid, &ref->new_oid);
> +
> +		if (r)
> +			ref_update_display_info_set_failed(info);
> +
>   		strbuf_release(&quickref);
>   		return r;
>   	} else {
> -		display_ref_update(display_state, '!', _("[rejected]"), _("non-fast-forward"),
> -				   remote_ref->name, ref->name,
> -				   &ref->old_oid, &ref->new_oid, summary_width);
> +		struct ref_update_display_info *info;
> +		info = ref_update_display_info_append(display_list, display_count,
> +						      '!', '!', _("[rejected]"), NULL,
> +						      _("non-fast-forward"),
> +						      ref->name, remote_ref->name,
> +						      &ref->old_oid, &ref->new_oid);
> +		ref_update_display_info_set_failed(info);
>   		return 1;
>   	}
>   }
> @@ -1103,17 +1217,15 @@ static int store_updated_refs(struct display_state *display_state,
>   			      int connectivity_checked,
>   			      struct ref_transaction *transaction, struct ref *ref_map,
>   			      struct fetch_head *fetch_head,
> -			      const struct fetch_config *config)
> +			      const struct fetch_config *config,
> +			      struct ref_update_display_info **display_list,
> +			      size_t *display_count)
>   {
>   	int rc = 0;
>   	struct strbuf note = STRBUF_INIT;
>   	const char *what, *kind;
>   	struct ref *rm;
>   	int want_status;
> -	int summary_width = 0;
> -
> -	if (verbosity >= 0)
> -		summary_width = transport_summary_width(ref_map);
>   
>   	if (!connectivity_checked) {
>   		struct check_connected_options opt = CHECK_CONNECTED_INIT;
> @@ -1218,8 +1330,9 @@ static int store_updated_refs(struct display_state *display_state,
>   					  display_state->url_len);
>   
>   			if (ref) {
> -				rc |= update_local_ref(ref, transaction, display_state,
> -						       rm, summary_width, config);
> +				rc |= update_local_ref(ref, transaction, rm,
> +						       config, display_list,
> +						       display_count);
>   				free(ref);
>   			} else if (write_fetch_head || dry_run) {
>   				/*
> @@ -1227,12 +1340,11 @@ static int store_updated_refs(struct display_state *display_state,
>   				 * would be written to FETCH_HEAD, if --dry-run
>   				 * is set).
>   				 */
> -				display_ref_update(display_state, '*',
> -						   *kind ? kind : "branch", NULL,
> -						   rm->name,
> -						   "FETCH_HEAD",
> -						   &rm->new_oid, &rm->old_oid,
> -						   summary_width);
> +
> +				ref_update_display_info_append(display_list, display_count,
> +							       '*', '*', *kind ? kind : "branch",
> +							       NULL, NULL, "FETCH_HEAD", rm->name,
> +							       &rm->new_oid, &rm->old_oid);
>   			}
>   		}
>   	}
> @@ -1300,7 +1412,9 @@ static int fetch_and_consume_refs(struct display_state *display_state,
>   				  struct ref_transaction *transaction,
>   				  struct ref *ref_map,
>   				  struct fetch_head *fetch_head,
> -				  const struct fetch_config *config)
> +				  const struct fetch_config *config,
> +				  struct ref_update_display_info **display_list,
> +				  size_t *display_count)
>   {
>   	int connectivity_checked = 1;
>   	int ret;
> @@ -1322,7 +1436,8 @@ static int fetch_and_consume_refs(struct display_state *display_state,
>   
>   	trace2_region_enter("fetch", "consume_refs", the_repository);
>   	ret = store_updated_refs(display_state, connectivity_checked,
> -				 transaction, ref_map, fetch_head, config);
> +				 transaction, ref_map, fetch_head, config,
> +				 display_list, display_count);
>   	trace2_region_leave("fetch", "consume_refs", the_repository);
>   
>   out:
> @@ -1493,7 +1608,9 @@ static int backfill_tags(struct display_state *display_state,
>   			 struct ref_transaction *transaction,
>   			 struct ref *ref_map,
>   			 struct fetch_head *fetch_head,
> -			 const struct fetch_config *config)
> +			 const struct fetch_config *config,
> +			 struct ref_update_display_info **display_list,
> +			 size_t *display_count)
>   {
>   	int retcode, cannot_reuse;
>   
> @@ -1515,7 +1632,7 @@ static int backfill_tags(struct display_state *display_state,
>   	transport_set_option(transport, TRANS_OPT_DEPTH, "0");
>   	transport_set_option(transport, TRANS_OPT_DEEPEN_RELATIVE, NULL);
>   	retcode = fetch_and_consume_refs(display_state, transport, transaction, ref_map,
> -					 fetch_head, config);
> +					 fetch_head, config, display_list, display_count);
>   
>   	if (gsecondary) {
>   		transport_disconnect(gsecondary);
> @@ -1641,6 +1758,7 @@ struct ref_rejection_data {
>   	bool conflict_msg_shown;
>   	bool case_sensitive_msg_shown;
>   	const char *remote_name;
> +	struct strmap *rejected_refs;
>   };
>   
>   static void ref_transaction_rejection_handler(const char *refname,
> @@ -1681,6 +1799,7 @@ static void ref_transaction_rejection_handler(const char *refname,
>   			      refname, ref_transaction_error_msg(err));
>   	}
>   
> +	strmap_put(data->rejected_refs, refname, NULL);
>   	*data->retcode = 1;
>   }
>   
> @@ -1690,6 +1809,7 @@ static void ref_transaction_rejection_handler(const char *refname,
>    */
>   static int commit_ref_transaction(struct ref_transaction **transaction,
>   				  bool is_atomic, const char *remote_name,
> +				  struct strmap *rejected_refs,
>   				  struct strbuf *err)
>   {
>   	int retcode = ref_transaction_commit(*transaction, err);
> @@ -1701,6 +1821,7 @@ static int commit_ref_transaction(struct ref_transaction **transaction,
>   			.conflict_msg_shown = 0,
>   			.remote_name = remote_name,
>   			.retcode = &retcode,
> +			.rejected_refs = rejected_refs,
>   		};
>   
>   		ref_transaction_for_each_rejected_update(*transaction,
> @@ -1729,6 +1850,10 @@ static int do_fetch(struct transport *transport,
>   	struct fetch_head fetch_head = { 0 };
>   	struct strbuf err = STRBUF_INIT;
>   	int do_set_head = 0;
> +	struct ref_update_display_info *display_list = NULL;
> +	struct strmap rejected_refs = STRMAP_INIT;
> +	size_t display_count = 0;
> +	int summary_width = 0;
>   
>   	if (tags == TAGS_DEFAULT) {
>   		if (transport->remote->fetch_tags == 2)
> @@ -1853,7 +1978,7 @@ static int do_fetch(struct transport *transport,
>   	}
>   
>   	if (fetch_and_consume_refs(&display_state, transport, transaction, ref_map,
> -				   &fetch_head, config)) {
> +				   &fetch_head, config, &display_list, &display_count)) {
>   		retcode = 1;
>   		goto cleanup;
>   	}
> @@ -1876,7 +2001,7 @@ static int do_fetch(struct transport *transport,
>   			 * the transaction and don't commit anything.
>   			 */
>   			if (backfill_tags(&display_state, transport, transaction, tags_ref_map,
> -					  &fetch_head, config))
> +					  &fetch_head, config, &display_list, &display_count))
>   				retcode = 1;
>   		}
>   
> @@ -1886,8 +2011,12 @@ static int do_fetch(struct transport *transport,
>   	if (retcode)
>   		goto cleanup;
>   
> +	if (verbosity >= 0)
> +		summary_width = transport_summary_width(ref_map);
> +
>   	retcode = commit_ref_transaction(&transaction, atomic_fetch,
> -					 transport->remote->name, &err);
> +					 transport->remote->name,
> +					 &rejected_refs, &err);
>   	/*
>   	 * With '--atomic', bail out if the transaction fails. Without '--atomic',
>   	 * continue to fetch head and perform other post-fetch operations.
> @@ -1965,7 +2094,17 @@ static int do_fetch(struct transport *transport,
>   	 */
>   	if (retcode && !atomic_fetch && transaction)
>   		commit_ref_transaction(&transaction, false,
> -				       transport->remote->name, &err);
> +				       transport->remote->name,
> +				       &rejected_refs, &err);
> +
> +	for (size_t i = 0; i < display_count; i++) {
> +		struct ref_update_display_info *info = &display_list[i];
> +
> +		if (!info->failed && strmap_contains(&rejected_refs, info->ref))
> +			ref_update_display_info_set_failed(info);
> +		ref_update_display_info_display(info, &display_state, summary_width);
> +		ref_update_display_info_free(info);
> +	}
>   
>   	if (retcode) {
>   		if (err.len) {
> @@ -1980,6 +2119,9 @@ static int do_fetch(struct transport *transport,
>   
>   	if (transaction)
>   		ref_transaction_free(transaction);
> +
> +	free(display_list);
> +	strmap_clear(&rejected_refs, 0);
>   	display_state_release(&display_state);
>   	close_fetch_head(&fetch_head);
>   	strbuf_release(&err);
> diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
> index 45595991c8..29e2f17608 100755
> --- a/t/t5516-fetch-push.sh
> +++ b/t/t5516-fetch-push.sh
> @@ -1893,6 +1893,7 @@ test_expect_success 'pushing non-commit objects should report error' '
>   
>   		tagsha=$(git rev-parse test^{tag}) &&
>   		test_must_fail git push ../dest "$tagsha:refs/heads/branch" 2>err &&
> +		test_grep "! \[remote rejected\] $tagsha -> branch (invalid new value provided)" err &&
>   		test_grep "trying to write non-commit object $tagsha to branch ${SQ}refs/heads/branch${SQ}" err
>   	)
>   '
> 
Junio C Hamano· Jan 21, 2026, 18:43 UTC · re: Phillip Wood · lore

Re: [PATCH v3 6/6] fetch: delay user information post committing of transaction

Phillip Wood <phillip.wood123@gmail.com> writes:
Show 25 quoted lines
>> +struct ref_update_display_info {
>> +	bool failed;
>> +	char success_code;
>> +	char fail_code;
>> +	const char *summary;
>> +	const char *fail_detail;
>> +	const char *success_detail;
>> +	const char *ref;
>> +	const char *remote;
>> +	struct object_id old_oid;
>> +	struct object_id new_oid;
>> +};
>
> I was expecting that we'd pass around a struct like
>
> struct ref_update_display_info_array {
> 	size_t alloc, nr;
> 	ref_update_display_info *info;
> };
>
> rather than passing a pointer, count pair as separate parameters. That 
> would also allow us to use ALLOC_GROW() rather than reallocating the 
> array each time we append to it which is rather inefficient.
>
> Thanks
Indeed.  That sounds like a sensible way to keep track of them.
Show 452 quoted lines
>
> Phillip
>
>> +static struct ref_update_display_info *ref_update_display_info_append(
>> +					   struct ref_update_display_info **list,
>> +					   size_t *count,
>> +					   char success_code,
>> +					   char fail_code,
>> +					   const char *summary,
>> +					   const char *success_detail,
>> +					   const char *fail_detail,
>> +					   const char *ref,
>> +					   const char *remote,
>> +					   const struct object_id *old_oid,
>> +					   const struct object_id *new_oid)
>> +{
>> +	struct ref_update_display_info *info;
>> +	size_t index = *count;
>> +
>> +	(*count)++;
>> +	REALLOC_ARRAY(*list, *count);
>> +
>> +	info = &(*list)[index];
>> +
>> +	info->failed = false;
>> +	info->success_code = success_code;
>> +	info->fail_code = fail_code;
>> +	info->summary = xstrdup(summary);
>> +	info->success_detail = xstrdup_or_null(success_detail);
>> +	info->fail_detail = xstrdup_or_null(fail_detail);
>> +	info->remote = xstrdup(remote);
>> +	info->ref = xstrdup(ref);
>> +
>> +	oidcpy(&info->old_oid, old_oid);
>> +	oidcpy(&info->new_oid, new_oid);
>> +
>> +	return info;
>> +}
>> +
>> +static void ref_update_display_info_set_failed(struct ref_update_display_info *info)
>> +{
>> +	info->failed = true;
>> +}
>> +
>> +static void ref_update_display_info_free(struct ref_update_display_info *info)
>> +{
>> +	free((char *)info->summary);
>> +	free((char *)info->success_detail);
>> +	free((char *)info->fail_detail);
>> +	free((char *)info->remote);
>> +	free((char *)info->ref);
>> +}
>> +
>> +static void ref_update_display_info_display(struct ref_update_display_info *info,
>> +					    struct display_state *display_state,
>> +					    int summary_width)
>> +{
>> +	display_ref_update(display_state,
>> +			   info->failed ? info->fail_code : info->success_code,
>> +			   info->summary,
>> +			   info->failed ? info->fail_detail : info->success_detail,
>> +			   info->remote, info->ref, &info->old_oid,
>> +			   &info->new_oid, summary_width);
>> +}
>> +
>>   static int update_local_ref(struct ref *ref,
>>   			    struct ref_transaction *transaction,
>> -			    struct display_state *display_state,
>>   			    const struct ref *remote_ref,
>> -			    int summary_width,
>> -			    const struct fetch_config *config)
>> +			    const struct fetch_config *config,
>> +			    struct ref_update_display_info **display_list,
>> +			    size_t *display_count)
>>   {
>>   	struct commit *current = NULL, *updated;
>>   	int fast_forward = 0;
>> @@ -877,41 +952,56 @@ static int update_local_ref(struct ref *ref,
>>   
>>   	if (oideq(&ref->old_oid, &ref->new_oid)) {
>>   		if (verbosity > 0)
>> -			display_ref_update(display_state, '=', _("[up to date]"), NULL,
>> -					   remote_ref->name, ref->name,
>> -					   &ref->old_oid, &ref->new_oid, summary_width);
>> +			ref_update_display_info_append(display_list, display_count,
>> +						       '=', '=', _("[up to date]"),
>> +						       NULL, NULL, ref->name,
>> +						       remote_ref->name, &ref->old_oid,
>> +						       &ref->new_oid);
>>   		return 0;
>>   	}
>>   
>>   	if (!update_head_ok &&
>>   	    !is_null_oid(&ref->old_oid) &&
>>   	    branch_checked_out(ref->name)) {
>> +		struct ref_update_display_info *info;
>>   		/*
>>   		 * If this is the head, and it's not okay to update
>>   		 * the head, and the old value of the head isn't empty...
>>   		 */
>> -		display_ref_update(display_state, '!', _("[rejected]"),
>> -				   _("can't fetch into checked-out branch"),
>> -				   remote_ref->name, ref->name,
>> -				   &ref->old_oid, &ref->new_oid, summary_width);
>> +		info = ref_update_display_info_append(display_list, display_count,
>> +						      '!', '!', _("[rejected]"),
>> +						      NULL, _("can't fetch into checked-out branch"),
>> +						      ref->name, remote_ref->name,
>> +						      &ref->old_oid, &ref->new_oid);
>> +		ref_update_display_info_set_failed(info);
>>   		return 1;
>>   	}
>>   
>>   	if (!is_null_oid(&ref->old_oid) &&
>>   	    starts_with(ref->name, "refs/tags/")) {
>> +		struct ref_update_display_info *info;
>> +
>>   		if (force || ref->force) {
>>   			int r;
>> +
>>   			r = s_update_ref("updating tag", ref, transaction, 0);
>> -			display_ref_update(display_state, r ? '!' : 't', _("[tag update]"),
>> -					   r ? _("unable to update local ref") : NULL,
>> -					   remote_ref->name, ref->name,
>> -					   &ref->old_oid, &ref->new_oid, summary_width);
>> +
>> +			info = ref_update_display_info_append(display_list, display_count,
>> +							      't', '!', _("[tag update]"), NULL,
>> +							      _("unable to update local ref"),
>> +							      ref->name, remote_ref->name,
>> +							      &ref->old_oid, &ref->new_oid);
>> +			if (r)
>> +				ref_update_display_info_set_failed(info);
>> +
>>   			return r;
>>   		} else {
>> -			display_ref_update(display_state, '!', _("[rejected]"),
>> -					   _("would clobber existing tag"),
>> -					   remote_ref->name, ref->name,
>> -					   &ref->old_oid, &ref->new_oid, summary_width);
>> +			info = ref_update_display_info_append(display_list, display_count,
>> +							      '!', '!', _("[rejected]"), NULL,
>> +							      _("would clobber existing tag"),
>> +							      ref->name, remote_ref->name,
>> +							      &ref->old_oid, &ref->new_oid);
>> +			ref_update_display_info_set_failed(info);
>>   			return 1;
>>   		}
>>   	}
>> @@ -921,6 +1011,7 @@ static int update_local_ref(struct ref *ref,
>>   	updated = lookup_commit_reference_gently(the_repository,
>>   						 &ref->new_oid, 1);
>>   	if (!current || !updated) {
>> +		struct ref_update_display_info *info;
>>   		const char *msg;
>>   		const char *what;
>>   		int r;
>> @@ -941,10 +1032,15 @@ static int update_local_ref(struct ref *ref,
>>   		}
>>   
>>   		r = s_update_ref(msg, ref, transaction, 0);
>> -		display_ref_update(display_state, r ? '!' : '*', what,
>> -				   r ? _("unable to update local ref") : NULL,
>> -				   remote_ref->name, ref->name,
>> -				   &ref->old_oid, &ref->new_oid, summary_width);
>> +
>> +		info = ref_update_display_info_append(display_list, display_count,
>> +						      '*', '!', what, NULL,
>> +						      _("unable to update local ref"),
>> +						      ref->name, remote_ref->name,
>> +						      &ref->old_oid, &ref->new_oid);
>> +		if (r)
>> +			ref_update_display_info_set_failed(info);
>> +
>>   		return r;
>>   	}
>>   
>> @@ -960,6 +1056,7 @@ static int update_local_ref(struct ref *ref,
>>   	}
>>   
>>   	if (fast_forward) {
>> +		struct ref_update_display_info *info;
>>   		struct strbuf quickref = STRBUF_INIT;
>>   		int r;
>>   
>> @@ -967,29 +1064,46 @@ static int update_local_ref(struct ref *ref,
>>   		strbuf_addstr(&quickref, "..");
>>   		strbuf_add_unique_abbrev(&quickref, &ref->new_oid, DEFAULT_ABBREV);
>>   		r = s_update_ref("fast-forward", ref, transaction, 1);
>> -		display_ref_update(display_state, r ? '!' : ' ', quickref.buf,
>> -				   r ? _("unable to update local ref") : NULL,
>> -				   remote_ref->name, ref->name,
>> -				   &ref->old_oid, &ref->new_oid, summary_width);
>> +
>> +		info = ref_update_display_info_append(display_list, display_count,
>> +						      ' ', '!', quickref.buf, NULL,
>> +						      _("unable to update local ref"),
>> +						      ref->name, remote_ref->name,
>> +						      &ref->old_oid, &ref->new_oid);
>> +		if (r)
>> +			ref_update_display_info_set_failed(info);
>> +
>>   		strbuf_release(&quickref);
>>   		return r;
>>   	} else if (force || ref->force) {
>> +		struct ref_update_display_info *info;
>>   		struct strbuf quickref = STRBUF_INIT;
>>   		int r;
>> +
>>   		strbuf_add_unique_abbrev(&quickref, &current->object.oid, DEFAULT_ABBREV);
>>   		strbuf_addstr(&quickref, "...");
>>   		strbuf_add_unique_abbrev(&quickref, &ref->new_oid, DEFAULT_ABBREV);
>>   		r = s_update_ref("forced-update", ref, transaction, 1);
>> -		display_ref_update(display_state, r ? '!' : '+', quickref.buf,
>> -				   r ? _("unable to update local ref") : _("forced update"),
>> -				   remote_ref->name, ref->name,
>> -				   &ref->old_oid, &ref->new_oid, summary_width);
>> +
>> +		info = ref_update_display_info_append(display_list, display_count,
>> +						      '+', '!', quickref.buf, _("forced update"),
>> +						      _("unable to update local ref"),
>> +						      ref->name, remote_ref->name,
>> +						      &ref->old_oid, &ref->new_oid);
>> +
>> +		if (r)
>> +			ref_update_display_info_set_failed(info);
>> +
>>   		strbuf_release(&quickref);
>>   		return r;
>>   	} else {
>> -		display_ref_update(display_state, '!', _("[rejected]"), _("non-fast-forward"),
>> -				   remote_ref->name, ref->name,
>> -				   &ref->old_oid, &ref->new_oid, summary_width);
>> +		struct ref_update_display_info *info;
>> +		info = ref_update_display_info_append(display_list, display_count,
>> +						      '!', '!', _("[rejected]"), NULL,
>> +						      _("non-fast-forward"),
>> +						      ref->name, remote_ref->name,
>> +						      &ref->old_oid, &ref->new_oid);
>> +		ref_update_display_info_set_failed(info);
>>   		return 1;
>>   	}
>>   }
>> @@ -1103,17 +1217,15 @@ static int store_updated_refs(struct display_state *display_state,
>>   			      int connectivity_checked,
>>   			      struct ref_transaction *transaction, struct ref *ref_map,
>>   			      struct fetch_head *fetch_head,
>> -			      const struct fetch_config *config)
>> +			      const struct fetch_config *config,
>> +			      struct ref_update_display_info **display_list,
>> +			      size_t *display_count)
>>   {
>>   	int rc = 0;
>>   	struct strbuf note = STRBUF_INIT;
>>   	const char *what, *kind;
>>   	struct ref *rm;
>>   	int want_status;
>> -	int summary_width = 0;
>> -
>> -	if (verbosity >= 0)
>> -		summary_width = transport_summary_width(ref_map);
>>   
>>   	if (!connectivity_checked) {
>>   		struct check_connected_options opt = CHECK_CONNECTED_INIT;
>> @@ -1218,8 +1330,9 @@ static int store_updated_refs(struct display_state *display_state,
>>   					  display_state->url_len);
>>   
>>   			if (ref) {
>> -				rc |= update_local_ref(ref, transaction, display_state,
>> -						       rm, summary_width, config);
>> +				rc |= update_local_ref(ref, transaction, rm,
>> +						       config, display_list,
>> +						       display_count);
>>   				free(ref);
>>   			} else if (write_fetch_head || dry_run) {
>>   				/*
>> @@ -1227,12 +1340,11 @@ static int store_updated_refs(struct display_state *display_state,
>>   				 * would be written to FETCH_HEAD, if --dry-run
>>   				 * is set).
>>   				 */
>> -				display_ref_update(display_state, '*',
>> -						   *kind ? kind : "branch", NULL,
>> -						   rm->name,
>> -						   "FETCH_HEAD",
>> -						   &rm->new_oid, &rm->old_oid,
>> -						   summary_width);
>> +
>> +				ref_update_display_info_append(display_list, display_count,
>> +							       '*', '*', *kind ? kind : "branch",
>> +							       NULL, NULL, "FETCH_HEAD", rm->name,
>> +							       &rm->new_oid, &rm->old_oid);
>>   			}
>>   		}
>>   	}
>> @@ -1300,7 +1412,9 @@ static int fetch_and_consume_refs(struct display_state *display_state,
>>   				  struct ref_transaction *transaction,
>>   				  struct ref *ref_map,
>>   				  struct fetch_head *fetch_head,
>> -				  const struct fetch_config *config)
>> +				  const struct fetch_config *config,
>> +				  struct ref_update_display_info **display_list,
>> +				  size_t *display_count)
>>   {
>>   	int connectivity_checked = 1;
>>   	int ret;
>> @@ -1322,7 +1436,8 @@ static int fetch_and_consume_refs(struct display_state *display_state,
>>   
>>   	trace2_region_enter("fetch", "consume_refs", the_repository);
>>   	ret = store_updated_refs(display_state, connectivity_checked,
>> -				 transaction, ref_map, fetch_head, config);
>> +				 transaction, ref_map, fetch_head, config,
>> +				 display_list, display_count);
>>   	trace2_region_leave("fetch", "consume_refs", the_repository);
>>   
>>   out:
>> @@ -1493,7 +1608,9 @@ static int backfill_tags(struct display_state *display_state,
>>   			 struct ref_transaction *transaction,
>>   			 struct ref *ref_map,
>>   			 struct fetch_head *fetch_head,
>> -			 const struct fetch_config *config)
>> +			 const struct fetch_config *config,
>> +			 struct ref_update_display_info **display_list,
>> +			 size_t *display_count)
>>   {
>>   	int retcode, cannot_reuse;
>>   
>> @@ -1515,7 +1632,7 @@ static int backfill_tags(struct display_state *display_state,
>>   	transport_set_option(transport, TRANS_OPT_DEPTH, "0");
>>   	transport_set_option(transport, TRANS_OPT_DEEPEN_RELATIVE, NULL);
>>   	retcode = fetch_and_consume_refs(display_state, transport, transaction, ref_map,
>> -					 fetch_head, config);
>> +					 fetch_head, config, display_list, display_count);
>>   
>>   	if (gsecondary) {
>>   		transport_disconnect(gsecondary);
>> @@ -1641,6 +1758,7 @@ struct ref_rejection_data {
>>   	bool conflict_msg_shown;
>>   	bool case_sensitive_msg_shown;
>>   	const char *remote_name;
>> +	struct strmap *rejected_refs;
>>   };
>>   
>>   static void ref_transaction_rejection_handler(const char *refname,
>> @@ -1681,6 +1799,7 @@ static void ref_transaction_rejection_handler(const char *refname,
>>   			      refname, ref_transaction_error_msg(err));
>>   	}
>>   
>> +	strmap_put(data->rejected_refs, refname, NULL);
>>   	*data->retcode = 1;
>>   }
>>   
>> @@ -1690,6 +1809,7 @@ static void ref_transaction_rejection_handler(const char *refname,
>>    */
>>   static int commit_ref_transaction(struct ref_transaction **transaction,
>>   				  bool is_atomic, const char *remote_name,
>> +				  struct strmap *rejected_refs,
>>   				  struct strbuf *err)
>>   {
>>   	int retcode = ref_transaction_commit(*transaction, err);
>> @@ -1701,6 +1821,7 @@ static int commit_ref_transaction(struct ref_transaction **transaction,
>>   			.conflict_msg_shown = 0,
>>   			.remote_name = remote_name,
>>   			.retcode = &retcode,
>> +			.rejected_refs = rejected_refs,
>>   		};
>>   
>>   		ref_transaction_for_each_rejected_update(*transaction,
>> @@ -1729,6 +1850,10 @@ static int do_fetch(struct transport *transport,
>>   	struct fetch_head fetch_head = { 0 };
>>   	struct strbuf err = STRBUF_INIT;
>>   	int do_set_head = 0;
>> +	struct ref_update_display_info *display_list = NULL;
>> +	struct strmap rejected_refs = STRMAP_INIT;
>> +	size_t display_count = 0;
>> +	int summary_width = 0;
>>   
>>   	if (tags == TAGS_DEFAULT) {
>>   		if (transport->remote->fetch_tags == 2)
>> @@ -1853,7 +1978,7 @@ static int do_fetch(struct transport *transport,
>>   	}
>>   
>>   	if (fetch_and_consume_refs(&display_state, transport, transaction, ref_map,
>> -				   &fetch_head, config)) {
>> +				   &fetch_head, config, &display_list, &display_count)) {
>>   		retcode = 1;
>>   		goto cleanup;
>>   	}
>> @@ -1876,7 +2001,7 @@ static int do_fetch(struct transport *transport,
>>   			 * the transaction and don't commit anything.
>>   			 */
>>   			if (backfill_tags(&display_state, transport, transaction, tags_ref_map,
>> -					  &fetch_head, config))
>> +					  &fetch_head, config, &display_list, &display_count))
>>   				retcode = 1;
>>   		}
>>   
>> @@ -1886,8 +2011,12 @@ static int do_fetch(struct transport *transport,
>>   	if (retcode)
>>   		goto cleanup;
>>   
>> +	if (verbosity >= 0)
>> +		summary_width = transport_summary_width(ref_map);
>> +
>>   	retcode = commit_ref_transaction(&transaction, atomic_fetch,
>> -					 transport->remote->name, &err);
>> +					 transport->remote->name,
>> +					 &rejected_refs, &err);
>>   	/*
>>   	 * With '--atomic', bail out if the transaction fails. Without '--atomic',
>>   	 * continue to fetch head and perform other post-fetch operations.
>> @@ -1965,7 +2094,17 @@ static int do_fetch(struct transport *transport,
>>   	 */
>>   	if (retcode && !atomic_fetch && transaction)
>>   		commit_ref_transaction(&transaction, false,
>> -				       transport->remote->name, &err);
>> +				       transport->remote->name,
>> +				       &rejected_refs, &err);
>> +
>> +	for (size_t i = 0; i < display_count; i++) {
>> +		struct ref_update_display_info *info = &display_list[i];
>> +
>> +		if (!info->failed && strmap_contains(&rejected_refs, info->ref))
>> +			ref_update_display_info_set_failed(info);
>> +		ref_update_display_info_display(info, &display_state, summary_width);
>> +		ref_update_display_info_free(info);
>> +	}
>>   
>>   	if (retcode) {
>>   		if (err.len) {
>> @@ -1980,6 +2119,9 @@ static int do_fetch(struct transport *transport,
>>   
>>   	if (transaction)
>>   		ref_transaction_free(transaction);
>> +
>> +	free(display_list);
>> +	strmap_clear(&rejected_refs, 0);
>>   	display_state_release(&display_state);
>>   	close_fetch_head(&fetch_head);
>>   	strbuf_release(&err);
>> diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
>> index 45595991c8..29e2f17608 100755
>> --- a/t/t5516-fetch-push.sh
>> +++ b/t/t5516-fetch-push.sh
>> @@ -1893,6 +1893,7 @@ test_expect_success 'pushing non-commit objects should report error' '
>>   
>>   		tagsha=$(git rev-parse test^{tag}) &&
>>   		test_must_fail git push ../dest "$tagsha:refs/heads/branch" 2>err &&
>> +		test_grep "! \[remote rejected\] $tagsha -> branch (invalid new value provided)" err &&
>>   		test_grep "trying to write non-commit object $tagsha to branch ${SQ}refs/heads/branch${SQ}" err
>>   	)
>>   '
>> 
Karthik Nayak· Jan 22, 2026, 09:05 UTC · re: Phillip Wood · lore

Re: [PATCH v3 6/6] fetch: delay user information post committing of transaction

Phillip Wood <phillip.wood123@gmail.com> writes:
Show 32 quoted lines
> Hi Karthik
>
> On 20/01/2026 09:59, Karthik Nayak wrote:
>
>> +struct ref_update_display_info {
>> +	bool failed;
>> +	char success_code;
>> +	char fail_code;
>> +	const char *summary;
>> +	const char *fail_detail;
>> +	const char *success_detail;
>> +	const char *ref;
>> +	const char *remote;
>> +	struct object_id old_oid;
>> +	struct object_id new_oid;
>> +};
>
> I was expecting that we'd pass around a struct like
>
> struct ref_update_display_info_array {
> 	size_t alloc, nr;
> 	ref_update_display_info *info;
> };
>
> rather than passing a pointer, count pair as separate parameters. That
> would also allow us to use ALLOC_GROW() rather than reallocating the
> array each time we append to it which is rather inefficient.
>
> Thanks
>
> Phillip
>

That's fair, I was considering an array and didn't see the need, but using 'ALLOC_GROW()' does make it simpler, plus we'd totally remove the need for the double pointer. Will change. Thanks!

Junio C Hamano· Jan 21, 2026, 18:12 UTC · re: Karthik Nayak · lore

Re: [PATCH v3 0/6] refs: provide detailed error messages when using batched update

Karthik Nayak <karthik.188@gmail.com> writes:
Show 31 quoted lines
> The refs namespace uses an error buffer to capture details about failed
> reference updates. However when we added batched update support to
> reference transactions, these messages were never propagated, instead
> only an error code pertaining to the type of failure was propagated.
>
> Currently, there are three regions which utilize batched updates:
>
>   - git update-ref --batch-updates
>   - git fetch
>   - git receive-pack
>
> While 'git update-ref --batch-updates' was a newly introduced flag, both
> 'git fetch' and 'git receive-pack' were pre-existing. Before using
> batched updates, they provided more detailed error messages to the user,
> but this changed with the introduction of batched updates. This is a
> regression in their workings.
>
> This patch series fixes this, by passing the detailed error message and
> utilizing it whenever available. The regression was reported by Elijah
> Newren [1] and based on the patch submitted by Jeff King [2].
>
> [1]: https://lore.kernel.org/all/CABPp-BGL2tJR4dPidQuFcp-X0_VkVTknCY-0Zgo=jHVGv_P=wA@mail.gmail.com/
> [2]: https://lore.kernel.org/all/20251224081214.GA1879908@coredump.intra.peff.net/
>
> ---
> Changes in v3:
> - Drop the first commit.
> - For the last commit, where we delay 'git fetch' status information,
>   delay all information to the end. Also use a list to compliment the
>   existing strmap, this ensures that the order is maintained.
> - Link to v2: https://patch.msgid.link/20260116-633-regression-lost-diagnostic-message-when-pushing-non-commit-objects-to-refs-heads-v2-0-925a0e9c7f32@gmail.com
Thanks.  These look good.

← back to recent threads