Skip to content

Commit 23fc8e4

Browse files
KarthikNayakgitster
authored andcommitted
refs: implement batch reference update support
Git supports making reference updates with or without transactions. Updates with transactions are generally better optimized. But transactions are all or nothing. This means, if a user wants to batch updates to take advantage of the optimizations without the hard requirement that all updates must succeed, there is no way currently to do so. Particularly with the reftable backend where batching multiple reference updates is more efficient than performing them sequentially. Introduce batched update support with a new flag, 'REF_TRANSACTION_ALLOW_FAILURE'. Batched updates while different from transactions, use the transaction infrastructure under the hood. When enabled, this flag allows individual reference updates that would typically cause the entire transaction to fail due to non-system-related errors to be marked as rejected while permitting other updates to proceed. System errors referred by 'REF_TRANSACTION_ERROR_GENERIC' continue to result in the entire transaction failing. This approach enhances flexibility while preserving transactional integrity where necessary. The implementation introduces several key components: - Add 'rejection_err' field to struct `ref_update` to track failed updates with failure reason. - Add a new struct `ref_transaction_rejections` and a field within `ref_transaction` to this struct to allow quick iteration over rejected updates. - Modify reference backends (files, packed, reftable) to handle partial transactions by using `ref_transaction_set_rejected()` instead of failing the entire transaction when `REF_TRANSACTION_ALLOW_FAILURE` is set. - Add `ref_transaction_for_each_rejected_update()` to let callers examine which updates were rejected and why. This foundational change enables batched update support throughout the reference subsystem. A following commit will expose this capability to users by adding a `--batch-updates` flag to 'git-update-ref(1)', providing both a user-facing feature and a testable implementation. Signed-off-by: Karthik Nayak <[email protected]> Acked-by: Patrick Steinhardt <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 76e760b commit 23fc8e4

File tree

6 files changed

+156
-4
lines changed

6 files changed

+156
-4
lines changed

refs.c

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,6 +1176,10 @@ struct ref_transaction *ref_store_transaction_begin(struct ref_store *refs,
11761176
tr->ref_store = refs;
11771177
tr->flags = flags;
11781178
string_list_init_dup(&tr->refnames);
1179+
1180+
if (flags & REF_TRANSACTION_ALLOW_FAILURE)
1181+
CALLOC_ARRAY(tr->rejections, 1);
1182+
11791183
return tr;
11801184
}
11811185

@@ -1206,11 +1210,45 @@ void ref_transaction_free(struct ref_transaction *transaction)
12061210
free((char *)transaction->updates[i]->old_target);
12071211
free(transaction->updates[i]);
12081212
}
1213+
1214+
if (transaction->rejections)
1215+
free(transaction->rejections->update_indices);
1216+
free(transaction->rejections);
1217+
12091218
string_list_clear(&transaction->refnames, 0);
12101219
free(transaction->updates);
12111220
free(transaction);
12121221
}
12131222

1223+
int ref_transaction_maybe_set_rejected(struct ref_transaction *transaction,
1224+
size_t update_idx,
1225+
enum ref_transaction_error err)
1226+
{
1227+
if (update_idx >= transaction->nr)
1228+
BUG("trying to set rejection on invalid update index");
1229+
1230+
if (!(transaction->flags & REF_TRANSACTION_ALLOW_FAILURE))
1231+
return 0;
1232+
1233+
if (!transaction->rejections)
1234+
BUG("transaction not inititalized with failure support");
1235+
1236+
/*
1237+
* Don't accept generic errors, since these errors are not user
1238+
* input related.
1239+
*/
1240+
if (err == REF_TRANSACTION_ERROR_GENERIC)
1241+
return 0;
1242+
1243+
transaction->updates[update_idx]->rejection_err = err;
1244+
ALLOC_GROW(transaction->rejections->update_indices,
1245+
transaction->rejections->nr + 1,
1246+
transaction->rejections->alloc);
1247+
transaction->rejections->update_indices[transaction->rejections->nr++] = update_idx;
1248+
1249+
return 1;
1250+
}
1251+
12141252
struct ref_update *ref_transaction_add_update(
12151253
struct ref_transaction *transaction,
12161254
const char *refname, unsigned int flags,
@@ -1236,6 +1274,7 @@ struct ref_update *ref_transaction_add_update(
12361274
transaction->updates[transaction->nr++] = update;
12371275

12381276
update->flags = flags;
1277+
update->rejection_err = 0;
12391278

12401279
update->new_target = xstrdup_or_null(new_target);
12411280
update->old_target = xstrdup_or_null(old_target);
@@ -2728,6 +2767,28 @@ void ref_transaction_for_each_queued_update(struct ref_transaction *transaction,
27282767
}
27292768
}
27302769

2770+
void ref_transaction_for_each_rejected_update(struct ref_transaction *transaction,
2771+
ref_transaction_for_each_rejected_update_fn cb,
2772+
void *cb_data)
2773+
{
2774+
if (!transaction->rejections)
2775+
return;
2776+
2777+
for (size_t i = 0; i < transaction->rejections->nr; i++) {
2778+
size_t update_index = transaction->rejections->update_indices[i];
2779+
struct ref_update *update = transaction->updates[update_index];
2780+
2781+
if (!update->rejection_err)
2782+
continue;
2783+
2784+
cb(update->refname,
2785+
(update->flags & REF_HAVE_OLD) ? &update->old_oid : NULL,
2786+
(update->flags & REF_HAVE_NEW) ? &update->new_oid : NULL,
2787+
update->old_target, update->new_target,
2788+
update->rejection_err, cb_data);
2789+
}
2790+
}
2791+
27312792
int refs_delete_refs(struct ref_store *refs, const char *logmsg,
27322793
struct string_list *refnames, unsigned int flags)
27332794
{

refs.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,13 @@ enum ref_transaction_flag {
667667
* either be absent or null_oid.
668668
*/
669669
REF_TRANSACTION_FLAG_INITIAL = (1 << 0),
670+
671+
/*
672+
* The transaction mechanism by default fails all updates if any conflict
673+
* is detected. This flag allows transactions to partially apply updates
674+
* while rejecting updates which do not match the expected state.
675+
*/
676+
REF_TRANSACTION_ALLOW_FAILURE = (1 << 1),
670677
};
671678

672679
/*
@@ -897,6 +904,21 @@ void ref_transaction_for_each_queued_update(struct ref_transaction *transaction,
897904
ref_transaction_for_each_queued_update_fn cb,
898905
void *cb_data);
899906

907+
/*
908+
* Execute the given callback function for each of the reference updates which
909+
* have been rejected in the given transaction.
910+
*/
911+
typedef void ref_transaction_for_each_rejected_update_fn(const char *refname,
912+
const struct object_id *old_oid,
913+
const struct object_id *new_oid,
914+
const char *old_target,
915+
const char *new_target,
916+
enum ref_transaction_error err,
917+
void *cb_data);
918+
void ref_transaction_for_each_rejected_update(struct ref_transaction *transaction,
919+
ref_transaction_for_each_rejected_update_fn cb,
920+
void *cb_data);
921+
900922
/*
901923
* Free `*transaction` and all associated data.
902924
*/

refs/files-backend.c

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2852,8 +2852,15 @@ static int files_transaction_prepare(struct ref_store *ref_store,
28522852
ret = lock_ref_for_update(refs, update, transaction,
28532853
head_ref, &refnames_to_check,
28542854
err);
2855-
if (ret)
2855+
if (ret) {
2856+
if (ref_transaction_maybe_set_rejected(transaction, i, ret)) {
2857+
strbuf_reset(err);
2858+
ret = 0;
2859+
2860+
continue;
2861+
}
28562862
goto cleanup;
2863+
}
28572864

28582865
if (update->flags & REF_DELETING &&
28592866
!(update->flags & REF_LOG_ONLY) &&
@@ -3151,6 +3158,9 @@ static int files_transaction_finish(struct ref_store *ref_store,
31513158
struct ref_update *update = transaction->updates[i];
31523159
struct ref_lock *lock = update->backend_data;
31533160

3161+
if (update->rejection_err)
3162+
continue;
3163+
31543164
if (update->flags & REF_NEEDS_COMMIT ||
31553165
update->flags & REF_LOG_ONLY) {
31563166
if (parse_and_write_reflog(refs, update, lock, err)) {

refs/packed-backend.c

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,10 +1327,11 @@ static int packed_ref_store_remove_on_disk(struct ref_store *ref_store,
13271327
* remain locked when it is done.
13281328
*/
13291329
static enum ref_transaction_error write_with_updates(struct packed_ref_store *refs,
1330-
struct string_list *updates,
1330+
struct ref_transaction *transaction,
13311331
struct strbuf *err)
13321332
{
13331333
enum ref_transaction_error ret = REF_TRANSACTION_ERROR_GENERIC;
1334+
struct string_list *updates = &transaction->refnames;
13341335
struct ref_iterator *iter = NULL;
13351336
size_t i;
13361337
int ok;
@@ -1411,6 +1412,13 @@ static enum ref_transaction_error write_with_updates(struct packed_ref_store *re
14111412
"reference already exists",
14121413
update->refname);
14131414
ret = REF_TRANSACTION_ERROR_CREATE_EXISTS;
1415+
1416+
if (ref_transaction_maybe_set_rejected(transaction, i, ret)) {
1417+
strbuf_reset(err);
1418+
ret = 0;
1419+
continue;
1420+
}
1421+
14141422
goto error;
14151423
} else if (!oideq(&update->old_oid, iter->oid)) {
14161424
strbuf_addf(err, "cannot update ref '%s': "
@@ -1419,6 +1427,13 @@ static enum ref_transaction_error write_with_updates(struct packed_ref_store *re
14191427
oid_to_hex(iter->oid),
14201428
oid_to_hex(&update->old_oid));
14211429
ret = REF_TRANSACTION_ERROR_INCORRECT_OLD_VALUE;
1430+
1431+
if (ref_transaction_maybe_set_rejected(transaction, i, ret)) {
1432+
strbuf_reset(err);
1433+
ret = 0;
1434+
continue;
1435+
}
1436+
14221437
goto error;
14231438
}
14241439
}
@@ -1456,6 +1471,13 @@ static enum ref_transaction_error write_with_updates(struct packed_ref_store *re
14561471
update->refname,
14571472
oid_to_hex(&update->old_oid));
14581473
ret = REF_TRANSACTION_ERROR_NONEXISTENT_REF;
1474+
1475+
if (ref_transaction_maybe_set_rejected(transaction, i, ret)) {
1476+
strbuf_reset(err);
1477+
ret = 0;
1478+
continue;
1479+
}
1480+
14591481
goto error;
14601482
}
14611483
}
@@ -1521,6 +1543,7 @@ static enum ref_transaction_error write_with_updates(struct packed_ref_store *re
15211543
write_error:
15221544
strbuf_addf(err, "error writing to %s: %s",
15231545
get_tempfile_path(refs->tempfile), strerror(errno));
1546+
ret = REF_TRANSACTION_ERROR_GENERIC;
15241547

15251548
error:
15261549
ref_iterator_free(iter);
@@ -1679,7 +1702,7 @@ static int packed_transaction_prepare(struct ref_store *ref_store,
16791702
data->own_lock = 1;
16801703
}
16811704

1682-
ret = write_with_updates(refs, &transaction->refnames, err);
1705+
ret = write_with_updates(refs, transaction, err);
16831706
if (ret)
16841707
goto failure;
16851708

refs/refs-internal.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,12 @@ struct ref_update {
123123
*/
124124
uint64_t index;
125125

126+
/*
127+
* Used in batched reference updates to mark if a given update
128+
* was rejected.
129+
*/
130+
enum ref_transaction_error rejection_err;
131+
126132
/*
127133
* If this ref_update was split off of a symref update via
128134
* split_symref_update(), then this member points at that
@@ -142,6 +148,13 @@ int refs_read_raw_ref(struct ref_store *ref_store, const char *refname,
142148
struct object_id *oid, struct strbuf *referent,
143149
unsigned int *type, int *failure_errno);
144150

151+
/*
152+
* Mark a given update as rejected with a given reason.
153+
*/
154+
int ref_transaction_maybe_set_rejected(struct ref_transaction *transaction,
155+
size_t update_idx,
156+
enum ref_transaction_error err);
157+
145158
/*
146159
* Add a ref_update with the specified properties to transaction, and
147160
* return a pointer to the new object. This function does not verify
@@ -183,6 +196,18 @@ enum ref_transaction_state {
183196
REF_TRANSACTION_CLOSED = 2
184197
};
185198

199+
/*
200+
* Data structure to hold indices of updates which were rejected, for batched
201+
* reference updates. While the updates themselves hold the rejection error,
202+
* this structure allows a transaction to iterate only over the rejected
203+
* updates.
204+
*/
205+
struct ref_transaction_rejections {
206+
size_t *update_indices;
207+
size_t alloc;
208+
size_t nr;
209+
};
210+
186211
/*
187212
* Data structure for holding a reference transaction, which can
188213
* consist of checks and updates to multiple references, carried out
@@ -195,6 +220,7 @@ struct ref_transaction {
195220
size_t alloc;
196221
size_t nr;
197222
enum ref_transaction_state state;
223+
struct ref_transaction_rejections *rejections;
198224
void *backend_data;
199225
unsigned int flags;
200226
uint64_t max_index;

refs/reftable-backend.c

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1371,8 +1371,15 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
13711371
transaction->updates[i],
13721372
&refnames_to_check, head_type,
13731373
&head_referent, &referent, err);
1374-
if (ret)
1374+
if (ret) {
1375+
if (ref_transaction_maybe_set_rejected(transaction, i, ret)) {
1376+
strbuf_reset(err);
1377+
ret = 0;
1378+
1379+
continue;
1380+
}
13751381
goto done;
1382+
}
13761383
}
13771384

13781385
ret = refs_verify_refnames_available(ref_store, &refnames_to_check,
@@ -1454,6 +1461,9 @@ static int write_transaction_table(struct reftable_writer *writer, void *cb_data
14541461
struct reftable_transaction_update *tx_update = &arg->updates[i];
14551462
struct ref_update *u = tx_update->update;
14561463

1464+
if (u->rejection_err)
1465+
continue;
1466+
14571467
/*
14581468
* Write a reflog entry when updating a ref to point to
14591469
* something new in either of the following cases:

0 commit comments

Comments
 (0)