diff --git a/refs.c b/refs.c
index b420a120102b3793168598b885bba68e4f5f5f03..75dbd84acbc41658d4b8b6b5e7763c04e78d0061 100644
--- a/refs.c
+++ b/refs.c
@@ -1204,6 +1204,7 @@ void ref_transaction_free(struct ref_transaction *transaction)
 		free(transaction->updates[i]->committer_info);
 		free((char *)transaction->updates[i]->new_target);
 		free((char *)transaction->updates[i]->old_target);
+		strbuf_release(&transaction->updates[i]->rejection_err);
 		free(transaction->updates[i]);
 	}
 	string_list_clear(&transaction->refnames, 0);
@@ -1211,6 +1212,14 @@ void ref_transaction_free(struct ref_transaction *transaction)
 	free(transaction);
 }
 
+void ref_transaction_add_rejection(struct ref_transaction *transaction,
+				   size_t update_idx, struct strbuf *err)
+{
+	struct ref_update *update = transaction->updates[update_idx];
+	update->rejected = 1;
+	strbuf_addbuf(&update->rejection_err, err);
+}
+
 struct ref_update *ref_transaction_add_update(
 		struct ref_transaction *transaction,
 		const char *refname, unsigned int flags,
@@ -1237,6 +1246,8 @@ struct ref_update *ref_transaction_add_update(
 
 	update->flags = flags;
 
+	strbuf_init(&update->rejection_err, 0);
+
 	update->new_target = xstrdup_or_null(new_target);
 	update->old_target = xstrdup_or_null(old_target);
 	if ((flags & REF_HAVE_NEW) && new_oid)
@@ -2676,6 +2687,27 @@ void ref_transaction_for_each_queued_update(struct ref_transaction *transaction,
 	}
 }
 
+void ref_transaction_for_each_rejected_update(struct ref_transaction *transaction,
+					      ref_transaction_for_each_rejected_update_fn cb,
+					      void *cb_data)
+{
+	if (!(transaction->flags & REF_TRANSACTION_ALLOW_PARTIAL))
+		return;
+
+	for (size_t i = 0; i < transaction->nr; i++) {
+		struct ref_update *update = transaction->updates[i];
+
+		if (!update->rejected)
+			continue;
+
+		cb(update->refname,
+		   (update->flags & REF_HAVE_OLD) ? &update->old_oid : NULL,
+		   (update->flags & REF_HAVE_NEW) ? &update->new_oid : NULL,
+		   update->old_target, update->new_target,
+		   &update->rejection_err, cb_data);
+	}
+}
+
 int refs_delete_refs(struct ref_store *refs, const char *logmsg,
 		     struct string_list *refnames, unsigned int flags)
 {
diff --git a/refs.h b/refs.h
index a0cdd99250e8286b55808b697b0a94afac5d8319..a0f15fdea024527fcfdb478f78cbf6fd6568a25b 100644
--- a/refs.h
+++ b/refs.h
@@ -638,6 +638,13 @@ enum ref_transaction_flag {
 	 * either be absent or null_oid.
 	 */
 	REF_TRANSACTION_FLAG_INITIAL = (1 << 0),
+
+	/*
+	 * The transaction mechanism by default fails all updates if any conflict
+	 * is detected. This flag allows transactions to partially apply updates
+	 * while rejecting updates which do not match the expected state.
+	 */
+	REF_TRANSACTION_ALLOW_PARTIAL = (1 << 1),
 };
 
 /*
@@ -889,6 +896,21 @@ void ref_transaction_for_each_queued_update(struct ref_transaction *transaction,
 					    ref_transaction_for_each_queued_update_fn cb,
 					    void *cb_data);
 
+/*
+ * Execute the given callback function for each of the reference updates which
+ * have been rejected in the given transaction.
+ */
+typedef void ref_transaction_for_each_rejected_update_fn(const char *refname,
+							 const struct object_id *old_oid,
+							 const struct object_id *new_oid,
+							 const char *old_target,
+							 const char *new_target,
+							 const struct strbuf *reason,
+							 void *cb_data);
+void ref_transaction_for_each_rejected_update(struct ref_transaction *transaction,
+					      ref_transaction_for_each_rejected_update_fn cb,
+					      void *cb_data);
+
 /*
  * Free `*transaction` and all associated data.
  */
diff --git a/refs/files-backend.c b/refs/files-backend.c
index 9fc5454678340dd7c72539bfa0f15ee7eb24b1ff..99ec29164fbd30635125cc2325aab3d300cf906c 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -2852,8 +2852,18 @@ static int files_transaction_prepare(struct ref_store *ref_store,
 
 		ret = lock_ref_for_update(refs, update, transaction,
 					  head_ref, err);
-		if (ret)
+		if (ret) {
+			if (transaction->flags & REF_TRANSACTION_ALLOW_PARTIAL) {
+				ref_transaction_add_rejection(transaction, i, err);
+
+				strbuf_setlen(err, 0);
+				ret = 0;
+
+				continue;
+			}
 			goto cleanup;
+		}
+
 
 		if (update->flags & REF_DELETING &&
 		    !(update->flags & REF_LOG_ONLY) &&
diff --git a/refs/packed-backend.c b/refs/packed-backend.c
index 6e7acb077e81435715a1ca3cc928550147c8c56a..cb9b6f0a620eaa59941f6fbc653600304f2bae8c 100644
--- a/refs/packed-backend.c
+++ b/refs/packed-backend.c
@@ -1313,9 +1313,10 @@ static int packed_ref_store_remove_on_disk(struct ref_store *ref_store,
  * remain locked when it is done.
  */
 static int write_with_updates(struct packed_ref_store *refs,
-			      struct string_list *updates,
+			      struct ref_transaction *transaction,
 			      struct strbuf *err)
 {
+	struct string_list *updates = &transaction->refnames;
 	struct ref_iterator *iter = NULL;
 	size_t i;
 	int ok;
@@ -1393,6 +1394,13 @@ static int write_with_updates(struct packed_ref_store *refs,
 					strbuf_addf(err, "cannot update ref '%s': "
 						    "reference already exists",
 						    update->refname);
+
+					if (transaction->flags & REF_TRANSACTION_ALLOW_PARTIAL) {
+						ref_transaction_add_rejection(transaction, i, err);
+						strbuf_setlen(err, 0);
+						continue;
+					}
+
 					goto error;
 				} else if (!oideq(&update->old_oid, iter->oid)) {
 					strbuf_addf(err, "cannot update ref '%s': "
@@ -1400,6 +1408,13 @@ static int write_with_updates(struct packed_ref_store *refs,
 						    update->refname,
 						    oid_to_hex(iter->oid),
 						    oid_to_hex(&update->old_oid));
+
+					if (transaction->flags & REF_TRANSACTION_ALLOW_PARTIAL) {
+						ref_transaction_add_rejection(transaction, i, err);
+						strbuf_setlen(err, 0);
+						continue;
+					}
+
 					goto error;
 				}
 			}
@@ -1434,6 +1449,13 @@ static int write_with_updates(struct packed_ref_store *refs,
 					    "reference is missing but expected %s",
 					    update->refname,
 					    oid_to_hex(&update->old_oid));
+
+				if (transaction->flags & REF_TRANSACTION_ALLOW_PARTIAL) {
+					ref_transaction_add_rejection(transaction, i, err);
+					strbuf_setlen(err, 0);
+					continue;
+				}
+
 				goto error;
 			}
 		}
@@ -1657,7 +1679,7 @@ static int packed_transaction_prepare(struct ref_store *ref_store,
 		data->own_lock = 1;
 	}
 
-	if (write_with_updates(refs, &transaction->refnames, err))
+	if (write_with_updates(refs, transaction, err))
 		goto failure;
 
 	transaction->state = REF_TRANSACTION_PREPARED;
diff --git a/refs/refs-internal.h b/refs/refs-internal.h
index 434362b6099a35f92906a04ddd65365140147572..6b8f5b2bd83baa22480083e1002daba9300f1b70 100644
--- a/refs/refs-internal.h
+++ b/refs/refs-internal.h
@@ -3,6 +3,7 @@
 
 #include "refs.h"
 #include "iterator.h"
+#include "strbuf.h"
 #include "string-list.h"
 
 struct fsck_options;
@@ -123,6 +124,13 @@ struct ref_update {
 	 */
 	unsigned int index;
 
+	/*
+	 * Used in partial transactions to mark a given update as rejected,
+	 * with rejection reason.
+	 */
+	unsigned int rejected;
+	struct strbuf rejection_err;
+
 	/*
 	 * If this ref_update was split off of a symref update via
 	 * split_symref_update(), then this member points at that
@@ -142,6 +150,13 @@ int refs_read_raw_ref(struct ref_store *ref_store, const char *refname,
 		      struct object_id *oid, struct strbuf *referent,
 		      unsigned int *type, int *failure_errno);
 
+/*
+ * Mark a given update as rejected with a given reason. To be used in conjuction
+ * with the `REF_TRANSACTION_ALLOW_PARTIAL` flag to allow partial transactions.
+ */
+void ref_transaction_add_rejection(struct ref_transaction *transaction,
+				   size_t update_idx, struct strbuf *err);
+
 /*
  * Add a ref_update with the specified properties to transaction, and
  * return a pointer to the new object. This function does not verify
diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
index 5533acfaf9027765d5a270abfce96225e42cc823..a2d86d1c5098b30bd212fc12a3708d2c0a60c677 100644
--- a/refs/reftable-backend.c
+++ b/refs/reftable-backend.c
@@ -1364,8 +1364,18 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 					    transaction, be,
 					    transaction->updates[i], head_type,
 					    &head_referent, &referent, err);
-		if (ret)
+
+		if (ret) {
+			if (transaction->flags & REF_TRANSACTION_ALLOW_PARTIAL) {
+				ref_transaction_add_rejection(transaction, i, err);
+
+				strbuf_setlen(err, 0);
+				ret = 0;
+
+				continue;
+			}
 			goto done;
+		}
 	}
 
 	transaction->backend_data = tx_data;
