fetch-prune: optimize dangling-ref reporting

When pruning during `git fetch` we check each pruned ref against the ref_store one at a time to decide whether to report it as dangling. This causes every local ref to be scanned for each ref being pruned. If there are N refs in the repo and M refs being pruned, this code is O(M*N). However, `git remote prune` uses a very similar function that is only O(N*log(M)). Remove the wasteful ref scanning for each pruned ref and use the faster version already available in refs_warn_dangling_symrefs. Change the message to include the original refname since the message is no longer printed immediately after the line that did just print the refname. In a repo with 126,000 refs, where I was pruning 28,000 refs, this code made about 3.6 billion calls to strcmp and consumed 410 seconds of CPU. (Invariably in that time, my remote would timeout and the fetch would fail anyway.) After this change, the same operation completes in under a second. Signed-off-by: Phil Hord <phil.hord@gmail.com> Reviewed-by: Jacob Keller <jacob.e.keller@intel.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Phil Hord committed Jul 1, 2025 at 18:12 UTC cc7dc407fe4c153195e5ce38d0109f3c2c35ceaf
3 files changed +13 -13
builtin/fetch.c
+10 -10
@@ -1384,9 +1384,13 @@ static int prune_refs(struct display_state *display_state,
1384 int result = 0;
1385 struct ref *ref, *stale_refs = get_stale_heads(rs, ref_map);
1386 struct strbuf err = STRBUF_INIT;
1387 + struct string_list refnames = STRING_LIST_INIT_NODUP;
1388 const char *dangling_msg = dry_run
1388 - ? _(" (%s will become dangling)")
1389 - : _(" (%s has become dangling)");
1389 + ? _(" %s will become dangling after %s is deleted")
1390 + : _(" %s has become dangling after %s was deleted");
1391 +
1392 + for (ref = stale_refs; ref; ref = ref->next)
1393 + string_list_append(&refnames, ref->name);
1394
1395 if (!dry_run) {
1396 if (transaction) {
@@ -1397,15 +1401,9 @@ static int prune_refs(struct display_state *display_state,
1401 goto cleanup;
1402 }
1403 } else {
1400 - struct string_list refnames = STRING_LIST_INIT_NODUP;
1401 -
1402 - for (ref = stale_refs; ref; ref = ref->next)
1403 - string_list_append(&refnames, ref->name);
1404 -
1404 result = refs_delete_refs(get_main_ref_store(the_repository),
1405 "fetch: prune", &refnames,
1406 0);
1408 - string_list_clear(&refnames, 0);
1407 }
1408 }
1409
@@ -1417,12 +1415,14 @@ static int prune_refs(struct display_state *display_state,
1415 _("(none)"), ref->name,
1416 &ref->new_oid, &ref->old_oid,
1417 summary_width);
1420 - refs_warn_dangling_symref(get_main_ref_store(the_repository),
1421 - stderr, dangling_msg, ref->name);
1418 }
1419 + string_list_sort(&refnames);
1420 + refs_warn_dangling_symrefs(get_main_ref_store(the_repository),
1421 + stderr, dangling_msg, &refnames);
1422 }
1423
1424 cleanup:
1425 + string_list_clear(&refnames, 0);
1426 strbuf_release(&err);
1427 free_refs(stale_refs);
1428 return result;
builtin/remote.c
+2 -2
@@ -1516,8 +1516,8 @@ static int prune_remote(const char *remote, int dry_run)
1516 struct string_list refs_to_prune = STRING_LIST_INIT_NODUP;
1517 struct string_list_item *item;
1518 const char *dangling_msg = dry_run
1519 - ? _(" %s will become dangling!")
1520 - : _(" %s has become dangling!");
1519 + ? _(" %s will become dangling after %s is deleted!")
1520 + : _(" %s has become dangling after %s was deleted!");
1521
1522 get_remote_ref_states(remote, &states, GET_REF_STATES);
1523
refs.c
+1 -1
@@ -461,7 +461,7 @@ static int warn_if_dangling_symref(const char *refname, const char *referent UNU
461 return 0;
462 }
463
464 - fprintf(d->fp, d->msg_fmt, refname);
464 + fprintf(d->fp, d->msg_fmt, refname, resolves_to);
465 fputc('\n', d->fp);
466 return 0;
467 }