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

From: Karthik Nayak <[email-masked]>
Date: Mon, 04 May 2026 19:44:04 +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 v4:
- Fix a bug in the error handling code, move it to a function and also
  add missing call sites. Add tests to ensure we catch this.
- Link to v3: https://patch.msgid.link/[email-masked]

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    | 182 +++++++++++++++++++++++++++++++-----------------
 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 +++--------
 t/t1400-update-ref.sh   |  14 ++++
 10 files changed, 266 insertions(+), 174 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 v3:

 1:  b89e273583 =  1:  cf5fa53f34 refs: remove unused typedef 'ref_transaction_commit_fn'
 2:  8f7f8dadc8 =  2:  afd10fd7a3 refs: introduce `ref_store_init_options`
 3:  9cdc35f5b6 =  3:  4eb6950cdc refs: extract out reflog config to generic layer
 4:  36c0c86a31 =  4:  dd8177a243 refs: return `ref_transaction_error` from `ref_transaction_update()`
 5:  1b7eca9353 =  5:  eb86178ae5 update-ref: move `print_rejected_refs()` up
 6:  aef1529054 !  6:  464c6371a4 update-ref: handle rejections while adding updates
    @@ builtin/update-ref.c: static unsigned int default_flags;
      /*
       * Parse one whitespace- or NUL-terminated, possibly C-quoted argument
       * and append the result to arg.  Return a pointer to the terminator.
    +@@ builtin/update-ref.c: static void print_rejected_refs(const char *refname,
    + 	strbuf_release(&sb);
    + }
    + 
    ++/*
    ++ * Handle transaction errors. If we're using batches updates, we want to only
    ++ * die for generic errors and print the remaining to the user.
    ++ */
    ++static void handle_ref_transaction_error(const char *refname,
    ++					 struct object_id *new_oid,
    ++					 struct object_id *old_oid,
    ++					 const char *new_target,
    ++					 const char *old_target,
    ++					 enum ref_transaction_error tx_err,
    ++					 struct strbuf *err,
    ++					 struct command_options *opts)
    ++{
    ++	if (!tx_err)
    ++		return;
    ++
    ++	if (tx_err != REF_TRANSACTION_ERROR_GENERIC && opts->allow_update_failures) {
    ++		print_rejected_refs(refname, old_oid, new_oid, old_target,
    ++				    new_target, tx_err, err->buf, NULL);
    ++		return;
    ++	}
    ++
    ++	die("%s", err->buf);
    ++}
    ++
    + /*
    +  * The following five parse_cmd_*() functions parse the corresponding
    +  * command.  In each case, next points at the character following the
     @@ builtin/update-ref.c: static void print_rejected_refs(const char *refname,
       */
      
    @@ builtin/update-ref.c: static void parse_cmd_update(struct ref_transaction *trans
     -				   NULL, NULL,
     -				   update_flags | create_reflog_flag,
     -				   msg, &err))
    +-		die("%s", err.buf);
     +	tx_err = ref_transaction_update(transaction, refname,
     +					&new_oid, have_old ? &old_oid : NULL,
     +					NULL, NULL,
     +					update_flags | create_reflog_flag,
     +					msg, &err);
    ++	handle_ref_transaction_error(refname, &new_oid, have_old ? &old_oid : NULL,
    ++				     NULL, NULL, tx_err, &err, opts);
     +
    -+	/*
    -+	 * 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);
    -+
      	update_flags = default_flags;
      	free(refname);
    - 	strbuf_release(&err);
    +@@ builtin/update-ref.c: static void parse_cmd_update(struct ref_transaction *transaction,
      }
      
      static void parse_cmd_symref_update(struct ref_transaction *transaction,
    @@ builtin/update-ref.c: static void parse_cmd_symref_update(struct ref_transaction
     -				   have_old_oid ? NULL : old_target,
     -				   update_flags | create_reflog_flag,
     -				   msg, &err))
    +-		die("%s", err.buf);
     +	tx_err = ref_transaction_update(transaction, refname, NULL,
     +					have_old_oid ? &old_oid : NULL,
     +					new_target,
     +					have_old_oid ? NULL : old_target,
     +					update_flags | create_reflog_flag,
     +					msg, &err);
    -+
    -+	/*
    -+	 * 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);
    ++	handle_ref_transaction_error(refname, NULL, have_old_oid ? &old_oid : NULL,
    ++				     new_target, have_old_oid ? NULL : old_target,
    ++				     tx_err, &err, opts);
      
    -+	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);
    -+
      	update_flags = default_flags;
      	free(refname);
    - 	free(old_arg);
     @@ builtin/update-ref.c: static void parse_cmd_symref_update(struct ref_transaction *transaction,
      }
      
      static void parse_cmd_create(struct ref_transaction *transaction,
     -			     const char *next, const char *end)
     +			     const char *next, const char *end,
    -+			     struct command_options *opts UNUSED)
    ++			     struct command_options *opts)
      {
      	struct strbuf err = STRBUF_INIT;
      	char *refname;
    + 	struct object_id new_oid;
    ++	enum ref_transaction_error tx_err;
    + 
    + 	refname = parse_refname(&next);
    + 	if (!refname)
     @@ builtin/update-ref.c: static void parse_cmd_create(struct ref_transaction *transaction,
    + 	if (*next != line_termination)
    + 		die("create %s: extra input: %s", refname, next);
    + 
    +-	if (ref_transaction_create(transaction, refname, &new_oid, NULL,
    +-				   update_flags | create_reflog_flag,
    +-				   msg, &err))
    +-		die("%s", err.buf);
    ++	tx_err = ref_transaction_create(transaction, refname, &new_oid, NULL,
    ++					update_flags | create_reflog_flag,
    ++					msg, &err);
    ++	handle_ref_transaction_error(refname, &new_oid, NULL, NULL, NULL, tx_err,
    ++				     &err, opts);
    + 
    + 	update_flags = default_flags;
    + 	free(refname);
      	strbuf_release(&err);
      }
      
    @@ builtin/update-ref.c: static void parse_cmd_create(struct ref_transaction *trans
      static void parse_cmd_symref_create(struct ref_transaction *transaction,
     -				    const char *next, const char *end UNUSED)
     +				    const char *next, const char *end UNUSED,
    -+				    struct command_options *opts UNUSED)
    ++				    struct command_options *opts)
      {
      	struct strbuf err = STRBUF_INIT;
      	char *refname, *new_target;
    ++	enum ref_transaction_error tx_err;
    + 
    + 	refname = parse_refname(&next);
    + 	if (!refname)
    +@@ builtin/update-ref.c: static void parse_cmd_symref_create(struct ref_transaction *transaction,
    + 	if (*next != line_termination)
    + 		die("symref-create %s: extra input: %s", refname, next);
    + 
    +-	if (ref_transaction_create(transaction, refname, NULL, new_target,
    +-				   update_flags | create_reflog_flag,
    +-				   msg, &err))
    +-		die("%s", err.buf);
    ++	tx_err = ref_transaction_create(transaction, refname, NULL, new_target,
    ++					update_flags | create_reflog_flag,
    ++					msg, &err);
    ++	handle_ref_transaction_error(refname, NULL, NULL, new_target, NULL,
    ++				     tx_err, &err, opts);
    + 
    + 	update_flags = default_flags;
    + 	free(refname);
     @@ builtin/update-ref.c: static void parse_cmd_symref_create(struct ref_transaction *transaction,
      }
      
 7:  4b225f4f4d !  7:  f971226d89 refs: move object parsing to the generic layer
    @@ refs/reftable-backend.c: static enum ref_transaction_error prepare_single_update
      	/*
      	 * When we update the reference that HEAD points to we enqueue
      	 * a second log-only update for HEAD so that its reflog is
    +
    + ## t/t1400-update-ref.sh ##
    +@@ t/t1400-update-ref.sh: test_expect_success 'stdin -z create ref fails with empty new value' '
    + 	test_must_fail git rev-parse --verify -q $c
    + '
    + 
    ++test_expect_success 'stdin -z create ref fails with non commit object' '
    ++	printf $F "create $c" "$(test_oid 001)" >stdin &&
    ++	test_must_fail git update-ref -z --stdin <stdin 2>err &&
    ++	grep "fatal: trying to write ref ${SQ}$c${SQ} with nonexistent object" err &&
    ++	test_must_fail git rev-parse --verify -q $c
    ++'
    ++
    ++test_expect_success 'stdin -z update ref fails with non commit object' '
    ++	printf $F "update $b" "$(test_oid 001)" "" >stdin &&
    ++	test_must_fail git update-ref -z --stdin <stdin 2>err &&
    ++	grep "fatal: trying to write ref ${SQ}$b${SQ} with nonexistent object" err &&
    ++	test_must_fail git rev-parse --verify -q $c
    ++'
    ++
    + test_expect_success 'stdin -z update ref works with right old value' '
    + 	printf $F "update $b" "$m~1" "$m" >stdin &&
    + 	git update-ref -z --stdin <stdin &&
 8:  944af6a454 =  8:  d271167077 refs: add peeled object ID to the `ref_update` struct
 9:  47653fdde9 =  9:  ed88b9e2ce refs: use peeled tag values in reference backends

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

Thanks
- Karthik