Merge branch 'mc/refs-batched-deletions-old-oids' into seen

* mc/refs-batched-deletions-old-oids:
  fetch, remote: retain old OIDs when pruning refs
  branch, tag: retain old OIDs in batched deletions
  refs: allow callers to supply old OIDs for batch deletion
Junio C Hamano 2026-09-22 13:33:30 -07:00
commit d710ef9b26
9 changed files with 251 additions and 39 deletions

View File

@ -1206,7 +1206,7 @@ int bisect_clean_state(void)
string_list_append(&refs_for_removal, "BISECT_EXPECTED_REV");
result = refs_delete_refs(get_main_ref_store(the_repository),
"bisect: remove", &refs_for_removal,
REF_NO_DEREF);
NULL, REF_NO_DEREF);
string_list_clear(&refs_for_removal, 0);
unlink_or_warn(git_path_bisect_ancestors_ok());
unlink_or_warn(git_path_bisect_log());

View File

@ -16,6 +16,7 @@
#include "commit.h"
#include "gettext.h"
#include "object-name.h"
#include "oid-array.h"
#include "remote.h"
#include "parse-options.h"
#include "branch.h"
@ -248,6 +249,7 @@ static int delete_branches(int argc, const char **argv, int kinds,
struct strbuf bname = STRBUF_INIT;
enum interpret_branch_kind allowed_interpret;
struct string_list refs_to_delete = STRING_LIST_INIT_DUP;
struct oid_array old_oids = OID_ARRAY_INIT;
struct string_list_item *item;
int branch_name_pos;
const char *fmt_remotes = "refs/remotes/%s";
@ -342,6 +344,7 @@ static int delete_branches(int argc, const char **argv, int kinds,
}

item = string_list_append(&refs_to_delete, name);
oid_array_append(&old_oids, &oid);
item->util = xstrdup((ref_flags & REF_ISBROKEN) ? "broken"
: (ref_flags & REF_ISSYMREF) ? target
: repo_find_unique_abbrev(the_repository, &oid, DEFAULT_ABBREV));
@ -351,7 +354,8 @@ static int delete_branches(int argc, const char **argv, int kinds,
}

if (!(flags & DELETE_BRANCH_DRY_RUN) &&
refs_delete_refs(get_main_ref_store(the_repository), NULL, &refs_to_delete, REF_NO_DEREF))
refs_delete_refs(get_main_ref_store(the_repository), NULL,
&refs_to_delete, &old_oids, REF_NO_DEREF))
ret = 1;

for_each_string_list_item(item, &refs_to_delete) {
@ -376,6 +380,7 @@ static int delete_branches(int argc, const char **argv, int kinds,
free(describe_ref);
}
string_list_clear(&refs_to_delete, 0);
oid_array_clear(&old_oids);

free(name);
strbuf_release(&bname);

View File

@ -1506,28 +1506,36 @@ 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;

for (ref = stale_refs; ref; ref = ref->next)
string_list_append(&refnames, ref->name);
struct oid_array old_oids = OID_ARRAY_INIT;

if (!dry_run) {
if (transaction) {
for (ref = stale_refs; ref; ref = ref->next) {
result = ref_transaction_delete(transaction, ref->name, NULL,
NULL, 0, "fetch: prune", &err);
result = ref_transaction_delete(transaction, ref->name,
&ref->new_oid, NULL, 0,
"fetch: prune", &err);
if (result)
goto cleanup;
}
} else {
for (ref = stale_refs; ref; ref = ref->next) {
string_list_append(&refnames, ref->name);
oid_array_append(&old_oids, &ref->new_oid);
}
result = refs_delete_refs(get_main_ref_store(the_repository),
"fetch: prune", &refnames,
0);
&old_oids, 0);
}
if (result)
goto cleanup;
}

if (verbosity >= 0) {
int summary_width = transport_summary_width(stale_refs);

if (!refnames.nr)
for (ref = stale_refs; ref; ref = ref->next)
string_list_append(&refnames, ref->name);
for (ref = stale_refs; ref; ref = ref->next) {
display_ref_update(display_state, '-', _("[deleted]"), NULL,
_("(none)"), ref->name,
@ -1541,6 +1549,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;

View File

@ -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);
@ -1073,7 +1083,7 @@ static int rm(int argc, const char **argv, const char *prefix,
if (!result)
result = refs_delete_refs(get_main_ref_store(the_repository),
"remote: remove", &branches,
REF_NO_DEREF);
NULL, REF_NO_DEREF);
string_list_clear(&branches, 0);

if (skipped.nr) {
@ -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,16 +1650,24 @@ 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);
string_list_sort(&refs_to_prune);
for_each_string_list_item(item, &states.stale) {
struct stale_ref *stale_ref = item->util;

if (!dry_run)
string_list_append(&refs_to_prune, stale_ref->name);
oid_array_append(&old_oids, &stale_ref->oid);
}

if (!dry_run) {
result |= refs_delete_refs(get_main_ref_store(the_repository),
"remote: prune", &refs_to_prune, 0);
"remote: prune", &refs_to_prune,
&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"),
@ -1661,7 +1680,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;
}

View File

@ -105,27 +105,38 @@ static int for_each_tag_name(const char **argv, each_tag_name_fn fn,
return had_error;
}

struct tags_to_delete {
struct string_list refs;
struct oid_array old_oids;
};

static int collect_tags(const char *name UNUSED, const char *ref,
const struct object_id *oid, void *cb_data)
{
struct string_list *ref_list = cb_data;
struct tags_to_delete *data = cb_data;
struct string_list_item *item;

string_list_append(ref_list, ref);
ref_list->items[ref_list->nr - 1].util = oiddup(oid);
item = string_list_append(&data->refs, ref);
item->util = oiddup(oid);
oid_array_append(&data->old_oids, oid);
return 0;
}

static int delete_tags(const char **argv)
{
int result;
struct string_list refs_to_delete = STRING_LIST_INIT_DUP;
struct tags_to_delete data = {
.refs = STRING_LIST_INIT_DUP,
.old_oids = OID_ARRAY_INIT,
};
struct string_list_item *item;

result = for_each_tag_name(argv, collect_tags, (void *)&refs_to_delete);
if (refs_delete_refs(get_main_ref_store(the_repository), NULL, &refs_to_delete, REF_NO_DEREF))
result = for_each_tag_name(argv, collect_tags, &data);
if (refs_delete_refs(get_main_ref_store(the_repository), NULL,
&data.refs, &data.old_oids, REF_NO_DEREF))
result = 1;

for_each_string_list_item(item, &refs_to_delete) {
for_each_string_list_item(item, &data.refs) {
const char *name = item->string;
struct object_id *oid = item->util;
if (!refs_ref_exists(get_main_ref_store(the_repository), name))
@ -135,7 +146,8 @@ static int delete_tags(const char **argv)

free(oid);
}
string_list_clear(&refs_to_delete, 0);
string_list_clear(&data.refs, 0);
oid_array_clear(&data.old_oids);
return result;
}


56
refs.c
View File

@ -16,6 +16,7 @@
#include "refs/refs-internal.h"
#include "hook.h"
#include "object-name.h"
#include "oid-array.h"
#include "odb.h"
#include "object.h"
#include "path.h"
@ -3092,34 +3093,60 @@ void ref_transaction_for_each_rejected_update(struct ref_transaction *transactio
}
}

int refs_delete_refs(struct ref_store *refs, const char *logmsg,
struct string_list *refnames, unsigned int flags)
struct delete_refs_rejection_data {
int failures;
};

static void delete_refs_rejection_handler(const char *refname,
const struct object_id *old_oid UNUSED,
const struct object_id *new_oid UNUSED,
const char *old_target UNUSED,
const char *new_target UNUSED,
enum ref_transaction_error err,
const char *details,
void *cb_data)
{
struct delete_refs_rejection_data *data = cb_data;

warning(_("could not delete reference %s: %s"), refname,
details ? details : ref_transaction_error_msg(err));
data->failures = 1;
}

int refs_delete_refs(struct ref_store *refs, const char *logmsg,
struct string_list *refnames,
const struct oid_array *old_oids,
unsigned int flags)
{
struct delete_refs_rejection_data rejection_data = { 0 };
struct ref_transaction *transaction;
struct strbuf err = STRBUF_INIT;
struct string_list_item *item;
size_t i;
int ret = 0, failures = 0;
char *msg;

if (!refnames->nr)
return 0;
if (old_oids && old_oids->nr != refnames->nr)
BUG("refname and old OID counts do not match");

msg = normalize_reflog_message(logmsg);

/*
* Since we don't check the references' old_oids, the
* individual updates can't fail, so we can pack all of the
* updates into a single transaction.
*/
transaction = ref_store_transaction_begin(refs, 0, &err);
transaction = ref_store_transaction_begin(refs,
old_oids ? REF_TRANSACTION_ALLOW_FAILURE : 0, &err);
if (!transaction) {
ret = error("%s", err.buf);
goto out;
}

for_each_string_list_item(item, refnames) {
for (i = 0; i < refnames->nr; i++) {
struct string_list_item *item = &refnames->items[i];
const struct object_id *old_oid = old_oids ? &old_oids->oid[i] : NULL;

if (old_oid && is_null_oid(old_oid))
old_oid = NULL;
ret = ref_transaction_delete(transaction, item->string,
NULL, NULL, flags, msg, &err);
old_oid, NULL, flags, msg, &err);
if (ret) {
warning(_("could not delete reference %s: %s"),
item->string, err.buf);
@ -3135,9 +3162,14 @@ int refs_delete_refs(struct ref_store *refs, const char *logmsg,
refnames->items[0].string, err.buf);
else
error(_("could not delete references: %s"), err.buf);
}
} else if (old_oids)
ref_transaction_for_each_rejected_update(transaction,
delete_refs_rejection_handler,
&rejection_data);

out:
if (rejection_data.failures)
failures = 1;
if (!ret && failures)
ret = -1;
ref_transaction_free(transaction);

13
refs.h
View File

@ -9,6 +9,7 @@
struct fsck_options;
struct object_id;
struct ref_store;
struct oid_array;
struct strbuf;
struct string_list;
struct string_list_item;
@ -627,13 +628,21 @@ int refs_delete_ref(struct ref_store *refs, const char *msg,
unsigned int flags);

/*
* Delete the specified references. If there are any problems, emit
* Delete the specified references. If old_oids is non-NULL, it must contain
* an entry for each refname, in the same order. Each non-null OID is used to
* verify the current value of the corresponding reference before deleting
* it. A null OID requests an unconditional deletion, which allows callers to
* include broken refs whose old value cannot be resolved.
*
* If there are any problems, emit
* errors but attempt to keep going (i.e., the deletes are not done in
* an all-or-nothing transaction). msg and flags are passed through to
* ref_transaction_delete().
*/
int refs_delete_refs(struct ref_store *refs, const char *msg,
struct string_list *refnames, unsigned int flags);
struct string_list *refnames,
const struct oid_array *old_oids,
unsigned int flags);

/** Delete a reflog */
int refs_delete_reflog(struct ref_store *refs, const char *refname);

View File

@ -132,7 +132,7 @@ static int cmd_delete_refs(struct ref_store *refs, const char **argv)
while (*argv)
string_list_append(&refnames, *argv++);

result = refs_delete_refs(refs, msg, &refnames, flags);
result = refs_delete_refs(refs, msg, &refnames, NULL, flags);
string_list_clear(&refnames, 0);
return result;
}

View File

@ -14,6 +14,130 @@ test_expect_success setup '
POST_OID=$(git rev-parse POST)
'

test_expect_success 'hook gets old values for batched branch/tag deletion' '
test_when_finished "rm -f actual" &&
git branch to-delete PRE &&
git tag delete-tag POST &&
git pack-refs --all &&
test_hook 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
cat >expect <<-EOF &&
$PRE_OID $ZERO_OID refs/heads/to-delete
$POST_OID $ZERO_OID refs/tags/delete-tag
EOF
git branch -D to-delete &&
git tag -d delete-tag &&
test_cmp expect actual
'

test_expect_success 'branch deletion rejects a concurrent update' '
git branch delete-race PRE &&
test_hook reference-transaction <<-\EOF &&
marker=$(git rev-parse --git-path delete-race-once)
if test "$1" = preparing && test ! -e "$marker"
then
>"$marker"
git update-ref refs/heads/delete-race POST
fi
exit 0
EOF
test_must_fail git branch -D delete-race 2>err &&
test_grep "is at $POST_OID but expected $PRE_OID" err &&
test_cmp_rev POST refs/heads/delete-race
'

test_expect_success 'hook gets old values when pruning remote refs' '
test_when_finished "rm -rf empty.git prune" &&
git init --bare empty.git &&
git init prune &&
(
cd prune &&
git remote add origin ../empty.git &&
git commit --allow-empty -m one &&
one=$(git rev-parse HEAD) &&
git commit --allow-empty -m two &&
two=$(git rev-parse HEAD) &&
git update-ref refs/remotes/origin/remote-prune-z "$one" &&
git update-ref refs/remotes/origin/remote-prune-a "$two"
) &&
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 &&
one=$(git rev-parse HEAD^) &&
two=$(git rev-parse HEAD) &&
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" &&
git init --bare race-empty.git &&
git init race-prune &&
(
cd race-prune &&
git commit --allow-empty -m one &&
one=$(git rev-parse HEAD) &&
git commit --allow-empty -m two &&
two=$(git rev-parse HEAD) &&
git remote add origin ../race-empty.git &&
git update-ref refs/remotes/origin/race "$one" &&
git update-ref refs/remotes/origin/other "$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
(
cd race-prune &&
two=$(git rev-parse HEAD) &&
test_must_fail git remote prune origin >out 2>err &&
test_cmp_rev "$two" refs/remotes/origin/race &&
test_must_fail git rev-parse --verify refs/remotes/origin/other &&
test_grep ! "\[pruned\]" out
)
'

test_expect_success 'hook allows updating ref if successful' '
git reset --hard PRE &&
test_hook reference-transaction <<-\EOF &&