From: Maciej Ciemborowicz <maciej.ciemborowicz@gmail.com>
To: git@vger.kernel.org
Cc: "Maciej Ciemborowicz" <maciej.ciemborowicz@gmail.com>,
"Junio C Hamano" <gitster@pobox.com>,
"Patrick Steinhardt" <ps@pks.im>,
"Phil Hord" <phil.hord@gmail.com>,
"Elijah Newren" <newren@gmail.com>,
"Ævar Arnfjörð Bjarmason" <avarab@gmail.com>,
"D . Ben Knoble" <ben.knoble@gmail.com>,
"Karthik Nayak" <karthik.188@gmail.com>
Subject: [PATCH v2 3/3] fetch, remote: retain old OIDs when pruning refs
Date: Sun, 20 Sep 2026 12:54:22 +0200 [thread overview]
Message-ID: <461c36ccdae09fb827a3c0efc7eed5aef072e09b.1789901584.git.maciej.ciemborowicz@gmail.com> (raw)
In-Reply-To: <cover.1789901584.git.maciej.ciemborowicz@gmail.com>
get_stale_heads() records the current value of each stale local ref in its
new_oid member. The pruning paths discard that value and request unconditional
deletion, so reference-transaction hooks receive a null old OID.
Carry the recorded values into the deletion transactions. Besides giving the
hooks useful values, this stops a stale scan from deleting a ref that another
process updated before the transaction acquired its locks. A concurrent
change now makes the prune fail and preserves the new value.
This reuses data collected while finding stale refs and therefore requires no
additional ref reads. Do not print deletion status when a non-atomic prune
fails its old-OID check.
Signed-off-by: Maciej Ciemborowicz <maciej.ciemborowicz@gmail.com>
---
builtin/fetch.c | 13 +++++--
builtin/remote.c | 36 ++++++++++++++---
t/t1416-ref-transaction-hooks.sh | 66 ++++++++++++++++++++++++++++++++
3 files changed, 106 insertions(+), 9 deletions(-)
diff --git a/builtin/fetch.c b/builtin/fetch.c
index d202147b2..da413ace0 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1452,14 +1452,18 @@ static int prune_refs(struct display_state *display_state,
struct ref *ref, *stale_refs = get_stale_heads(rs, ref_map);
struct strbuf err = STRBUF_INIT;
struct string_list refnames = STRING_LIST_INIT_NODUP;
+ struct oid_array old_oids = OID_ARRAY_INIT;
- for (ref = stale_refs; ref; ref = ref->next)
+ for (ref = stale_refs; ref; ref = ref->next) {
string_list_append(&refnames, ref->name);
+ oid_array_append(&old_oids, &ref->new_oid);
+ }
if (!dry_run) {
if (transaction) {
for (ref = stale_refs; ref; ref = ref->next) {
- result = ref_transaction_delete(transaction, ref->name, NULL,
+ result = ref_transaction_delete(transaction, ref->name,
+ &ref->new_oid,
NULL, 0, "fetch: prune", &err);
if (result)
goto cleanup;
@@ -1467,8 +1471,10 @@ static int prune_refs(struct display_state *display_state,
} else {
result = refs_delete_refs(get_main_ref_store(the_repository),
"fetch: prune", &refnames,
- NULL, 0);
+ &old_oids, 0);
}
+ if (result)
+ goto cleanup;
}
if (verbosity >= 0) {
@@ -1487,6 +1493,7 @@ static int prune_refs(struct display_state *display_state,
cleanup:
string_list_clear(&refnames, 0);
+ oid_array_clear(&old_oids);
strbuf_release(&err);
free_refs(stale_refs);
return result;
diff --git a/builtin/remote.c b/builtin/remote.c
index 13d3cc52d..b899bec55 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -17,6 +17,7 @@
#include "refs.h"
#include "refspec.h"
#include "odb.h"
+#include "oid-array.h"
#include "strvec.h"
#include "commit-reach.h"
#include "progress.h"
@@ -380,6 +381,11 @@ struct ref_states {
int queried;
};
+struct stale_ref {
+ struct object_id oid;
+ char name[FLEX_ARRAY];
+};
+
#define REF_STATES_INIT { \
.new_refs = STRING_LIST_INIT_DUP, \
.skipped = STRING_LIST_INIT_DUP, \
@@ -410,9 +416,13 @@ static int get_ref_states(const struct ref *remote_refs, struct ref_states *stat
}
stale_refs = get_stale_heads(&states->remote->fetch, fetch_map);
for (ref = stale_refs; ref; ref = ref->next) {
+ struct stale_ref *stale_ref;
struct string_list_item *item =
string_list_append(&states->stale, abbrev_branch(ref->name));
- item->util = xstrdup(ref->name);
+
+ FLEX_ALLOC_STR(stale_ref, name, ref->name);
+ oidcpy(&stale_ref->oid, &ref->new_oid);
+ item->util = stale_ref;
}
free_refs(stale_refs);
free_refs(fetch_map);
@@ -1627,6 +1637,7 @@ static int prune_remote(const char *remote, int dry_run)
int result = 0;
struct ref_states states = REF_STATES_INIT;
struct string_list refs_to_prune = STRING_LIST_INIT_NODUP;
+ struct oid_array old_oids = OID_ARRAY_INIT;
struct string_list_item *item;
get_remote_ref_states(remote, &states, GET_REF_STATES);
@@ -1639,17 +1650,28 @@ static int prune_remote(const char *remote, int dry_run)
printf_ln(_("Pruning %s"), remote);
printf_ln(_("URL: %s"), states.remote->url.v[0]);
- for_each_string_list_item(item, &states.stale)
- string_list_append(&refs_to_prune, item->util);
+ for_each_string_list_item(item, &states.stale) {
+ struct stale_ref *stale_ref = item->util;
+ struct string_list_item *to_prune;
+
+ to_prune = string_list_append(&refs_to_prune, stale_ref->name);
+ to_prune->util = &stale_ref->oid;
+ }
string_list_sort(&refs_to_prune);
+ for_each_string_list_item(item, &refs_to_prune)
+ oid_array_append(&old_oids, item->util);
- if (!dry_run)
+ if (!dry_run) {
result |= refs_delete_refs(get_main_ref_store(the_repository),
"remote: prune", &refs_to_prune,
- NULL, 0);
+ &old_oids, 0);
+ if (result)
+ goto cleanup;
+ }
for_each_string_list_item(item, &states.stale) {
- const char *refname = item->util;
+ struct stale_ref *stale_ref = item->util;
+ const char *refname = stale_ref->name;
if (dry_run)
printf_ln(_(" * [would prune] %s"),
@@ -1662,7 +1684,9 @@ static int prune_remote(const char *remote, int dry_run)
refs_warn_dangling_symrefs(get_main_ref_store(the_repository),
stdout, " ", dry_run, &refs_to_prune);
+cleanup:
string_list_clear(&refs_to_prune, 0);
+ oid_array_clear(&old_oids);
free_remote_ref_states(&states);
return result;
}
diff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh
index 01b5ba8c4..2b51b216b 100755
--- a/t/t1416-ref-transaction-hooks.sh
+++ b/t/t1416-ref-transaction-hooks.sh
@@ -58,6 +58,72 @@ test_expect_success 'branch deletion rejects a concurrent update' '
test_cmp_rev POST refs/heads/delete-race
'
+test_expect_success 'hook gets old values when pruning remote refs' '
+ test_create_repo empty.git --bare &&
+ test_create_repo prune &&
+ git -C prune remote add origin ../empty.git &&
+ test_commit -C prune one &&
+ one=$(git -C prune rev-parse HEAD) &&
+ test_commit -C prune two &&
+ two=$(git -C prune rev-parse HEAD) &&
+ git -C prune update-ref refs/remotes/origin/remote-prune-z "$one" &&
+ git -C prune update-ref refs/remotes/origin/remote-prune-a "$two" &&
+ git -C prune pack-refs --all &&
+ test_hook -C prune reference-transaction <<-\EOF &&
+ if test "$1" = committed
+ then
+ # Ignore backend-internal zero-to-zero records.
+ while read -r old new ref
+ do
+ case "$old" in
+ *[!0]*)
+ echo "$old $new $ref"
+ ;;
+ esac
+ done >>actual
+ fi
+ EOF
+ (
+ cd prune &&
+ git remote prune origin &&
+ git update-ref refs/remotes/origin/fetch-prune "$one" &&
+ git fetch --prune origin &&
+ git update-ref refs/remotes/origin/atomic-prune "$one" &&
+ git fetch --atomic --prune origin &&
+ cat >expect <<-EOF &&
+ $two $ZERO_OID refs/remotes/origin/remote-prune-a
+ $one $ZERO_OID refs/remotes/origin/remote-prune-z
+ $one $ZERO_OID refs/remotes/origin/fetch-prune
+ $one $ZERO_OID refs/remotes/origin/atomic-prune
+ EOF
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'remote prune rejects a concurrent update' '
+ test_when_finished "rm -rf race-empty.git race-prune" &&
+ test_create_repo race-empty.git --bare &&
+ test_create_repo race-prune &&
+ test_commit -C race-prune one &&
+ one=$(git -C race-prune rev-parse HEAD) &&
+ test_commit -C race-prune two &&
+ two=$(git -C race-prune rev-parse HEAD) &&
+ git -C race-prune remote add origin ../race-empty.git &&
+ git -C race-prune update-ref refs/remotes/origin/race "$one" &&
+ test_hook -C race-prune reference-transaction <<-\EOF &&
+ marker=$(git rev-parse --git-path prune-race-once)
+ if test "$1" = preparing && test ! -e "$marker"
+ then
+ >"$marker"
+ git update-ref refs/remotes/origin/race HEAD
+ fi
+ exit 0
+ EOF
+ test_must_fail git -C race-prune remote prune origin >out 2>err &&
+ test "$two" = "$(git -C race-prune rev-parse refs/remotes/origin/race)" &&
+ ! grep "\[pruned\]" out
+'
+
test_expect_success 'hook allows updating ref if successful' '
git reset --hard PRE &&
test_hook reference-transaction <<-\EOF &&
--
2.39.3 (Apple Git-146)
next prev parent reply other threads:[~2026-09-20 10:54 UTC|newest]
Thread overview: 52+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-19 13:34 [BUG] reference-transaction reports zero OIDs for branch and tag deletion Maciej Ciemborowicz
2026-09-19 14:59 ` D. Ben Knoble
2026-09-19 15:42 ` Maciej Ciemborowicz
2026-09-19 20:11 ` [PATCH 0/3] refs: report old OIDs for batched deletions Maciej Ciemborowicz
2026-09-19 20:11 ` [PATCH 1/3] refs: allow callers to supply old OIDs for batch deletion Maciej Ciemborowicz
2026-09-19 20:41 ` Karthik Nayak
2026-09-20 10:38 ` Maciej Ciemborowicz
2026-09-20 10:54 ` [PATCH v2 0/3] refs: report old OIDs for batched deletions Maciej Ciemborowicz
2026-09-20 10:54 ` [PATCH v2 1/3] refs: allow callers to supply old OIDs for batch deletion Maciej Ciemborowicz
2026-09-21 13:12 ` Karthik Nayak
2026-09-21 23:55 ` Junio C Hamano
2026-09-20 10:54 ` [PATCH v2 2/3] branch, tag: retain old OIDs in batched deletions Maciej Ciemborowicz
2026-09-21 13:19 ` Karthik Nayak
2026-09-20 10:54 ` Maciej Ciemborowicz [this message]
2026-09-21 13:56 ` [PATCH v2 3/3] fetch, remote: retain old OIDs when pruning refs Karthik Nayak
2026-09-21 13:57 ` [PATCH v2 0/3] refs: report old OIDs for batched deletions Karthik Nayak
2026-09-21 20:01 ` Maciej Ciemborowicz
2026-09-22 12:26 ` [PATCH v3 " Maciej Ciemborowicz
2026-09-22 12:26 ` [PATCH v3 1/3] refs: allow callers to supply old OIDs for batch deletion Maciej Ciemborowicz
2026-09-22 18:55 ` Junio C Hamano
2026-09-22 19:21 ` Maciej Ciemborowicz
2026-09-22 23:31 ` Junio C Hamano
2026-09-22 12:26 ` [PATCH v3 2/3] branch, tag: retain old OIDs in batched deletions Maciej Ciemborowicz
2026-09-22 12:26 ` [PATCH v3 3/3] fetch, remote: retain old OIDs when pruning refs Maciej Ciemborowicz
2026-09-22 19:16 ` Junio C Hamano
2026-09-22 22:29 ` [PATCH v4 0/3] refs: report old OIDs for batched deletions Maciej Ciemborowicz
2026-09-22 22:31 ` [PATCH v4 1/3] refs: allow callers to supply old OIDs for batch deletion Maciej Ciemborowicz
2026-09-22 22:31 ` [PATCH v4 2/3] branch, tag: retain old OIDs in batched deletions Maciej Ciemborowicz
2026-09-22 22:31 ` [PATCH v4 3/3] fetch, remote: retain old OIDs when pruning refs Maciej Ciemborowicz
2026-09-23 20:03 ` Junio C Hamano
2026-09-23 21:02 ` Maciej Ciemborowicz
2026-09-23 21:04 ` [PATCH v5 0/3] refs: report old OIDs for batched deletions Maciej Ciemborowicz
2026-09-23 21:04 ` [PATCH v5 1/3] refs: allow callers to supply old OIDs for batch deletion Maciej Ciemborowicz
2026-09-24 10:04 ` Karthik Nayak
2026-09-24 16:34 ` Junio C Hamano
2026-09-24 19:43 ` Maciej Ciemborowicz
2026-09-24 11:07 ` Patrick Steinhardt
2026-09-24 16:45 ` Junio C Hamano
2026-09-24 20:13 ` Maciej Ciemborowicz
2026-09-28 6:44 ` Patrick Steinhardt
2026-09-28 6:43 ` Patrick Steinhardt
2026-09-24 19:56 ` Maciej Ciemborowicz
2026-09-23 21:04 ` [PATCH v5 2/3] branch, tag: retain old OIDs in batched deletions Maciej Ciemborowicz
2026-09-24 11:08 ` Patrick Steinhardt
2026-09-23 21:04 ` [PATCH v5 3/3] fetch, remote: retain old OIDs when pruning refs Maciej Ciemborowicz
2026-09-23 21:55 ` [PATCH v5 0/3] refs: report old OIDs for batched deletions Junio C Hamano
2026-09-24 22:33 ` [PATCH v6 0/1] refs: report old values to transaction hooks Maciej Ciemborowicz
2026-09-24 22:33 ` [PATCH v6 1/1] " Maciej Ciemborowicz
2026-09-30 3:11 ` Maciej Ciemborowicz
2026-10-01 17:37 ` Maciej Ciemborowicz
2026-09-19 20:11 ` [PATCH 2/3] branch, tag: retain old OIDs in batched deletions Maciej Ciemborowicz
2026-09-19 20:11 ` [PATCH 3/3] fetch, remote: retain old OIDs when pruning refs Maciej Ciemborowicz
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=461c36ccdae09fb827a3c0efc7eed5aef072e09b.1789901584.git.maciej.ciemborowicz@gmail.com \
--to=maciej.ciemborowicz@gmail.com \
--cc=avarab@gmail.com \
--cc=ben.knoble@gmail.com \
--cc=git@vger.kernel.org \
--cc=gitster@pobox.com \
--cc=karthik.188@gmail.com \
--cc=newren@gmail.com \
--cc=phil.hord@gmail.com \
--cc=ps@pks.im \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox