Subject: [PATCH v3 0/9] refs: move some of the generic logic out of the backends

From: Karthik Nayak <[email-masked]>
Date: Mon, 27 Apr 2026 12:42:01 +0200

This series came together while I was working on other reference related
code and realized that some of the individual logic implemented with the
reference backends can be moved to the generic layer.

Moving code to the generic layer, simplifies the responsibility of
individual backends and avoids deviation in logic between the backends.

The biggest changes are related to moving out usage of `parse_object()`
and `peel_object()` from reference transactions. The former is used to
validate that the OID provided points to a commit object. The latter is
an optimization technique where the packed/reftable backend store the
peeled OID whenever available, so reading such references provides the
peeled OID without having to call the ODB.

Moving object parsing to the generic layout involves moving it out of
the prepare stage of the transaction and into `ref_transaction_update()`
where every added update is checked. As such, this also involves
modifying update-ref(1) and receive-pack(1) to follow this paradigm.

---
Changes in v3:
- Remove an unwanted change which creeped up during a rebase.
- Add information in the commit message around how the order of errors
  in git-update-ref(1) will change while maintaining functionality.
- Change up the order of an `if..else` to make it clearer.
- Other small typos and fixes.
- Link to v2: https://patch.msgid.link/[email-masked]

Changes in v2:
- Split the second commit into two: one introducing
  `ref_store_init_options` and the second to use it for reflog config.
- Use opts as the variable name consistently.
- A bunch of grammar fixes.
- Link to v1: https://patch.msgid.link/[email-masked]

---
 builtin/receive-pack.c  |  22 ++++---
 builtin/update-ref.c    | 151 +++++++++++++++++++++++++++++++-----------------
 refs.c                  |  60 ++++++++++++++-----
 refs.h                  |  16 ++---
 refs/files-backend.c    |  58 +++++++------------
 refs/packed-backend.c   |  10 ++--
 refs/packed-backend.h   |   3 +-
 refs/refs-internal.h    |  35 +++++++++--
 refs/reftable-backend.c |  40 +++----------
 9 files changed, 231 insertions(+), 164 deletions(-)

Karthik Nayak (9):
      refs: remove unused typedef 'ref_transaction_commit_fn'
      refs: introduce `ref_store_init_options`
      refs: extract out reflog config to generic layer
      refs: return `ref_transaction_error` from `ref_transaction_update()`
      update-ref: move `print_rejected_refs()` up
      update-ref: handle rejections while adding updates
      refs: move object parsing to the generic layer
      refs: add peeled object ID to the `ref_update` struct
      refs: use peeled tag values in reference backends

Range-diff versus v2:

 1:  b8ab8a6c8b =  1:  704a218bce refs: remove unused typedef 'ref_transaction_commit_fn'
 2:  dd419614e0 !  2:  f3e0caa8e4 refs: introduce `ref_store_init_options`
    @@ refs/files-backend.c: static struct ref_store *files_ref_store_init(struct repos
      	refs->gitcommondir = strbuf_detach(&ref_common_dir, NULL);
      	refs->packed_ref_store =
     -		packed_ref_store_init(repo, NULL, refs->gitcommondir, flags);
    -+		packed_ref_store_init(repo, payload, refs->gitcommondir, opts);
    ++		packed_ref_store_init(repo, NULL, refs->gitcommondir, opts);
     +	refs->store_flags = opts->access_flags;
      	refs->log_all_ref_updates = repo_settings_get_log_all_ref_updates(repo);
     +
 3:  337e5c8c5b !  3:  48ebcc2438 refs: extract out reflog config to generic layer
    @@ refs.c: static struct ref_store *ref_store_init(struct repository *repo,
      ## refs/files-backend.c ##
     @@ refs/files-backend.c: static struct ref_store *files_ref_store_init(struct repository *repo,
      	refs->packed_ref_store =
    - 		packed_ref_store_init(repo, payload, refs->gitcommondir, opts);
    + 		packed_ref_store_init(repo, NULL, refs->gitcommondir, opts);
      	refs->store_flags = opts->access_flags;
     -	refs->log_all_ref_updates = repo_settings_get_log_all_ref_updates(repo);
     +	refs->log_all_ref_updates = opts->log_all_ref_updates;
 4:  cdb4aad11e =  4:  d4c120ab28 refs: return `ref_transaction_error` from `ref_transaction_update()`
 5:  454549240a =  5:  6e3b16258c update-ref: move `print_rejected_refs()` up
 6:  1f224fb868 !  6:  505c5b8edb update-ref: handle rejections while adding updates
    @@ Commit message
         rejected, and also checking for rejections and only dying for generic
         failures.
     
    +    Errors encountered during updates will be shown to the user immediately
    +    unlike other errors encountered only when the transaction is
    +    prepared/committed. As the verification of object IDs and peeled tag
    +    objects will move into `ref_transaction_update()` in the following
    +    commit, this means that those errors will be shown to the user before
    +    other errors, this changes the order of errors, but the functionality
    +    remains the same.
    +
         Signed-off-by: Karthik Nayak <[email-masked]>
     
      ## builtin/update-ref.c ##
    @@ builtin/update-ref.c: static void parse_cmd_update(struct ref_transaction *trans
     +					update_flags | create_reflog_flag,
     +					msg, &err);
     +
    -+	if (tx_err && tx_err != REF_TRANSACTION_ERROR_GENERIC &&
    -+	    opts->allow_update_failures) {
    ++	/*
    ++	 * Generic errors are non-recoverable, so we cannot skip the update
    ++	 * or mark it as rejected.
    ++	 */
    ++	if (tx_err == REF_TRANSACTION_ERROR_GENERIC)
    + 		die("%s", err.buf);
    + 
    ++	if (tx_err && opts->allow_update_failures)
     +		print_rejected_refs(refname, have_old ? &old_oid : NULL,
     +				    &new_oid, NULL, NULL, tx_err, err.buf,
     +				    NULL);
    -+	} else if (tx_err) {
    - 		die("%s", err.buf);
    -+	}
    - 
    ++
      	update_flags = default_flags;
      	free(refname);
    -@@ builtin/update-ref.c: static void parse_cmd_update(struct ref_transaction *transaction,
    + 	strbuf_release(&err);
      }
      
      static void parse_cmd_symref_update(struct ref_transaction *transaction,
    @@ builtin/update-ref.c: static void parse_cmd_symref_update(struct ref_transaction
     +					update_flags | create_reflog_flag,
     +					msg, &err);
     +
    -+	if (tx_err && tx_err != REF_TRANSACTION_ERROR_GENERIC &&
    -+	    opts->allow_update_failures) {
    ++	/*
    ++	 * Generic errors are non-recoverable, so we cannot skip the update
    ++	 * or mark it as rejected.
    ++	 */
    ++	if (tx_err == REF_TRANSACTION_ERROR_GENERIC)
    + 		die("%s", err.buf);
    + 
    ++	if (tx_err && opts->allow_update_failures)
     +		print_rejected_refs(refname, have_old_oid ? &old_oid : NULL,
     +				    NULL, have_old_oid ? NULL : old_target,
     +				    new_target, tx_err, err.buf, NULL);
    -+	} else if (tx_err) {
    - 		die("%s", err.buf);
    -+	}
    - 
    ++
      	update_flags = default_flags;
      	free(refname);
    + 	free(old_arg);
     @@ builtin/update-ref.c: static void parse_cmd_symref_update(struct ref_transaction *transaction,
      }
      
 7:  a52b019afa !  7:  ad1aae6b35 refs: move object parsing to the generic layer
    @@ builtin/receive-pack.c: static const char *update(struct command *cmd, struct sh
     -	}
     -	else {
     +	} else {
    -+		enum ref_transaction_error err_type;
    ++		enum ref_transaction_error tx_err;
      		struct strbuf err = STRBUF_INIT;
      		if (shallow_update && si->shallow_ref[cmd->index] &&
      		    update_shallow_ref(cmd, si)) {
    @@ builtin/receive-pack.c: static const char *update(struct command *cmd, struct sh
     -					   NULL, NULL,
     -					   0, "push",
     -					   &err)) {
    -+		err_type = ref_transaction_update(transaction,
    ++		tx_err = ref_transaction_update(transaction,
     +						  namespaced_name,
     +						  new_oid, old_oid,
     +						  NULL, NULL,
     +						  0, "push",
     +						  &err);
    -+		if (err_type) {
    ++		if (tx_err) {
      			rp_error("%s", err.buf);
     -			ret = "failed to update ref";
    -+			if (err_type == REF_TRANSACTION_ERROR_GENERIC)
    ++			if (tx_err == REF_TRANSACTION_ERROR_GENERIC)
     +				ret = "failed to update ref";
     +			else
    -+				ret = ref_transaction_error_msg(err_type);
    ++				ret = ref_transaction_error_msg(tx_err);
      		} else {
      			ret = NULL; /* good */
      		}
 8:  aa5fd09831 !  8:  5cb7ee9853 refs: add peeled object ID to the `ref_update` struct
    @@ Metadata
      ## Commit message ##
         refs: add peeled object ID to the `ref_update` struct
     
    -    Certain reference backend {packed, reftable}, have the ability to also
    +    Certain reference backends {packed, reftable}, have the ability to also
         store the peeled object ID for a reference pointing to a tag object.
         This has the added benefit that during retrieval of such references, we
         also obtain the peeled object ID without having to use the ODB.
 9:  96146b5083 =  9:  ba34e10548 refs: use peeled tag values in reference backends

base-commit: f65aba1e87db64413b6d1ed5ae5a45b5a84a0997
change-id: 20260417-refs-move-to-generic-layer-f7525c5e8764

Thanks
- Karthik