refs: allow callers to supply old OIDs for batch deletion

refs_delete_refs() performs unconditional deletions, so callers cannot
preserve old values that they have already resolved. Consequently,
reference-transaction hooks see a null old OID.

Let callers provide an optional array of expected old OIDs in parallel with
the refname list. When the array is provided, delete the ref at position N
only if it still points at the OID at position N. A null OID requests an
unconditional deletion for refs whose old value cannot be resolved, such as
broken refs.

Use REF_TRANSACTION_ALLOW_FAILURE when old OIDs are supplied. This retains
the helper's best-effort behavior: an old-OID mismatch rejects that deletion
while independent deletions in the batch can still proceed.

Signed-off-by: Maciej Ciemborowicz <maciej.ciemborowicz@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Maciej Ciemborowicz 2026-09-22 14:26:07 +02:00 committed by Junio C Hamano
parent 3bc0341126
commit d79b73c2a9
8 changed files with 65 additions and 21 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

@ -351,7 +351,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, NULL, REF_NO_DEREF))
ret = 1;

for_each_string_list_item(item, &refs_to_delete) {

View File

@ -1486,7 +1486,7 @@ static int prune_refs(struct display_state *display_state,
} else {
result = refs_delete_refs(get_main_ref_store(the_repository),
"fetch: prune", &refnames,
0);
NULL, 0);
}
}


View File

@ -1073,7 +1073,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) {
@ -1645,7 +1645,8 @@ static int prune_remote(const char *remote, int dry_run)

if (!dry_run)
result |= refs_delete_refs(get_main_ref_store(the_repository),
"remote: prune", &refs_to_prune, 0);
"remote: prune", &refs_to_prune,
NULL, 0);

for_each_string_list_item(item, &states.stale) {
const char *refname = item->util;

View File

@ -122,7 +122,8 @@ static int delete_tags(const char **argv)
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))
if (refs_delete_refs(get_main_ref_store(the_repository), NULL,
&refs_to_delete, NULL, REF_NO_DEREF))
result = 1;

for_each_string_list_item(item, &refs_to_delete) {

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"
@ -3069,34 +3070,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);
@ -3112,9 +3139,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;
@ -623,13 +624,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;
}