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

From: Phillip Wood <[email-masked]>
Date: Wed, 21 Jan 2026 16:21:20 +0000

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

> +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
>   	)
>   '
>