Git development
 help / color / mirror / Atom feed
* [PATCH v2 2/5] builtin/refs: add "delete" subcommand
From: Patrick Steinhardt @ 2026-06-17 10:15 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano
In-Reply-To: <20260617-pks-refs-writing-subcommands-v2-0-07f3d18336f9@pks.im>

Reference-related functionality in Git is currently spread across many
different commands: git-update-ref(1), git-for-each-ref(1),
git-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it
hard for users to discover what functionality we have available to work
with references.

We have thus started to consolidate this functionality into git-refs(1),
which is a toolbox of everything related to references. Until now, the
command doesn't handle functionality of git-update-ref(1).

Fix this gap by introducing a new "delete" subcommand, which is the
equivalent of `git update-ref -d`.

Note that we're intentionally not using a generic "write" subcommand
with a "-d" flag. This is rather harder to discover, and subcommands
that are implmented as flags tend to be hard to reason about in the code
as we'd have to handle mutually-exclusive flags that stem from the other
subcommand-like modes.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-refs.adoc |  17 ++++++
 builtin/refs.c              |  51 +++++++++++++++++
 t/meson.build               |   1 +
 t/t1464-refs-delete.sh      | 130 ++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 199 insertions(+)

diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc
index fa33680cc7..2633934463 100644
--- a/Documentation/git-refs.adoc
+++ b/Documentation/git-refs.adoc
@@ -20,6 +20,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]
 		   [ --stdin | (<pattern>...)]
 git refs exists <ref>
 git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
+git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]
 
 DESCRIPTION
 -----------
@@ -51,6 +52,12 @@ optimize::
 	usage. This subcommand is an alias for linkgit:git-pack-refs[1] and
 	offers identical functionality.
 
+delete::
+	Delete the given reference. This subcommand mirrors `git update-ref -d`
+	(see linkgit:git-update-ref[1]). When `<old-value>` is given, the
+	reference is only deleted after verifying that it currently contains
+	`<old-value>`.
+
 OPTIONS
 -------
 
@@ -90,6 +97,16 @@ The following options are specific to 'git refs optimize':
 
 include::pack-refs-options.adoc[]
 
+The following options are specific to commands which write references:
+
+`--message=<reason>`::
+	Use the given <reason> string for the reflog entry associated with the
+	update. An empty message is rejected.
+
+`--no-deref`::
+	Operate on <ref> itself rather than the reference it points to via a
+	symbolic ref.
+
 KNOWN LIMITATIONS
 -----------------
 
diff --git a/builtin/refs.c b/builtin/refs.c
index f0faabf45a..edb7d61663 100644
--- a/builtin/refs.c
+++ b/builtin/refs.c
@@ -21,6 +21,9 @@
 #define REFS_OPTIMIZE_USAGE \
 	N_("git refs optimize " PACK_REFS_OPTS)
 
+#define REFS_DELETE_USAGE \
+	N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]")
+
 static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
 			    struct repository *repo)
 {
@@ -175,6 +178,52 @@ static int cmd_refs_optimize(int argc, const char **argv, const char *prefix,
 	return pack_refs_core(argc, argv, prefix, repo, refs_optimize_usage);
 }
 
+static int cmd_refs_delete(int argc, const char **argv, const char *prefix,
+			   struct repository *repo)
+{
+	static char const * const refs_delete_usage[] = {
+		REFS_DELETE_USAGE,
+		NULL
+	};
+	const char *message = NULL;
+	unsigned flags = 0;
+	struct option opts[] = {
+		OPT_STRING(0, "message", &message, N_("reason"),
+			   N_("reason of the update")),
+		OPT_BIT(0 ,"no-deref", &flags,
+			N_("update <refname> not the one it points to"),
+			REF_NO_DEREF),
+		OPT_END(),
+	};
+	struct object_id oldoid;
+	const char *refname;
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, opts, refs_delete_usage, 0);
+	if (argc < 1 || argc > 2)
+		usage(_("delete requires reference name and an optional old object ID"));
+
+	if (message && !*message)
+		die(_("refusing to perform update with empty message"));
+
+	repo_config(repo, git_default_config, NULL);
+
+	refname = argv[0];
+	if (argc == 2) {
+		if (repo_get_oid_with_flags(repo, argv[1], &oldoid, GET_OID_SKIP_AMBIGUITY_CHECK))
+			die(_("invalid old object ID: '%s'"), argv[1]);
+		if (is_null_oid(&oldoid))
+			die(_("cannot delete reference with null old object ID"));
+	}
+
+	ret = refs_delete_ref(get_main_ref_store(repo), message, refname,
+			      argc == 2 ? &oldoid : NULL, flags);
+
+	if (ret < 0)
+		ret = 1;
+	return ret;
+}
+
 int cmd_refs(int argc,
 	     const char **argv,
 	     const char *prefix,
@@ -186,6 +235,7 @@ int cmd_refs(int argc,
 		"git refs list " COMMON_USAGE_FOR_EACH_REF,
 		REFS_EXISTS_USAGE,
 		REFS_OPTIMIZE_USAGE,
+		REFS_DELETE_USAGE,
 		NULL,
 	};
 	parse_opt_subcommand_fn *fn = NULL;
@@ -195,6 +245,7 @@ int cmd_refs(int argc,
 		OPT_SUBCOMMAND("list", &fn, cmd_refs_list),
 		OPT_SUBCOMMAND("exists", &fn, cmd_refs_exists),
 		OPT_SUBCOMMAND("optimize", &fn, cmd_refs_optimize),
+		OPT_SUBCOMMAND("delete", &fn, cmd_refs_delete),
 		OPT_END(),
 	};
 
diff --git a/t/meson.build b/t/meson.build
index c5832fee05..1ccf08a3b5 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -223,6 +223,7 @@ integration_tests = [
   't1461-refs-list.sh',
   't1462-refs-exists.sh',
   't1463-refs-optimize.sh',
+  't1464-refs-delete.sh',
   't1500-rev-parse.sh',
   't1501-work-tree.sh',
   't1502-rev-parse-parseopt.sh',
diff --git a/t/t1464-refs-delete.sh b/t/t1464-refs-delete.sh
new file mode 100755
index 0000000000..efff7d0574
--- /dev/null
+++ b/t/t1464-refs-delete.sh
@@ -0,0 +1,130 @@
+#!/bin/sh
+
+test_description='git refs delete'
+
+. ./test-lib.sh
+
+setup_repo () {
+	git init "$1" &&
+	test_commit -C "$1" A &&
+	test_commit -C "$1" B
+}
+
+test_expect_success 'delete without oldvalue verification' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	A=$(git -C repo rev-parse A) &&
+	git -C repo update-ref refs/heads/foo $A &&
+	git -C repo refs delete refs/heads/foo &&
+	test_must_fail git -C repo show-ref --verify -q refs/heads/foo
+'
+
+test_expect_success 'delete with matching oldvalue' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git update-ref refs/heads/foo $A &&
+		git refs delete refs/heads/foo $A &&
+		test_must_fail git refs exists refs/heads/foo
+	)
+'
+
+test_expect_success 'delete with stale oldvalue fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		B=$(git rev-parse B) &&
+		git update-ref refs/heads/foo $A &&
+		test_must_fail git refs delete refs/heads/foo $B 2>err &&
+		test_grep " but expected " err &&
+		git refs exists refs/heads/foo
+	)
+'
+
+test_expect_success 'delete with null oldvalue fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git update-ref refs/heads/foo $A &&
+		test_must_fail git refs delete refs/heads/foo $ZERO_OID 2>err &&
+		test_grep "null old object ID" err &&
+		git refs exists refs/heads/foo
+	)
+'
+
+test_expect_success 'delete with invalid oldvalue fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git update-ref refs/heads/foo $A &&
+		test_must_fail git refs delete refs/heads/foo invalid-oid 2>err &&
+		test_grep "invalid old object ID" err &&
+		git refs exists refs/heads/foo
+	)
+'
+
+test_expect_success 'delete symref with --no-deref leaves target intact' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git update-ref refs/heads/foo $A &&
+		git symbolic-ref refs/heads/symref refs/heads/foo &&
+		git refs delete --no-deref refs/heads/symref &&
+		test_must_fail git refs exists refs/heads/symref &&
+		git refs exists refs/heads/foo
+	)
+'
+
+test_expect_success 'delete with message records reason in reflog' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git update-ref refs/heads/foo $A &&
+		git symbolic-ref HEAD refs/heads/foo &&
+		git refs delete --message=delete-reason refs/heads/foo &&
+		test_must_fail git refs exists refs/heads/foo &&
+		test-tool ref-store main for-each-reflog-ent HEAD >actual &&
+		test_grep "delete-reason$" actual
+	)
+'
+
+test_expect_success 'delete with empty message fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git update-ref refs/heads/foo $A &&
+		test_must_fail git refs delete --message= refs/heads/foo 2>err &&
+		test_grep "empty message" err &&
+		git refs exists refs/heads/foo
+	)
+'
+
+test_expect_success 'delete without arguments fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	test_must_fail git -C repo refs delete 2>err &&
+	test_grep "requires reference name" err
+'
+
+test_expect_success 'delete with too many arguments fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	test_must_fail git refs delete one two three 2>err &&
+	test_grep "requires reference name" err
+'
+
+test_done

-- 
2.55.0.rc0.786.g65d90a0328.dirty


^ permalink raw reply related

* [PATCH v2 1/5] builtin/refs: drop `the_repository`
From: Patrick Steinhardt @ 2026-06-17 10:15 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano
In-Reply-To: <20260617-pks-refs-writing-subcommands-v2-0-07f3d18336f9@pks.im>

We still have a couple of uses of `the_repository` in "builtin/refs.c".
All of those are trivial to convert though as the command always
requires a repository to exist.

Convert them to use the passed-in repository and drop
`USE_THE_REPOSITORY_VARIABLE`.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/refs.c | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)

diff --git a/builtin/refs.c b/builtin/refs.c
index e3125bc61b..f0faabf45a 100644
--- a/builtin/refs.c
+++ b/builtin/refs.c
@@ -1,4 +1,3 @@
-#define USE_THE_REPOSITORY_VARIABLE
 #include "builtin.h"
 #include "config.h"
 #include "fsck.h"
@@ -23,7 +22,7 @@
 	N_("git refs optimize " PACK_REFS_OPTS)
 
 static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
-			    struct repository *repo UNUSED)
+			    struct repository *repo)
 {
 	const char * const migrate_usage[] = {
 		REFS_MIGRATE_USAGE,
@@ -59,13 +58,13 @@ static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
 		goto out;
 	}
 
-	if (the_repository->ref_storage_format == format) {
+	if (repo->ref_storage_format == format) {
 		err = error(_("repository already uses '%s' format"),
 			    ref_storage_format_to_name(format));
 		goto out;
 	}
 
-	if (repo_migrate_ref_storage_format(the_repository, format, flags, &errbuf) < 0) {
+	if (repo_migrate_ref_storage_format(repo, format, flags, &errbuf) < 0) {
 		err = error("%s", errbuf.buf);
 		goto out;
 	}
@@ -99,8 +98,8 @@ static int cmd_refs_verify(int argc, const char **argv, const char *prefix,
 	if (argc)
 		usage(_("'git refs verify' takes no arguments"));
 
-	repo_config(the_repository, git_fsck_config, &fsck_refs_options);
-	prepare_repo_settings(the_repository);
+	repo_config(repo, git_fsck_config, &fsck_refs_options);
+	prepare_repo_settings(repo);
 
 	worktrees = get_worktrees_without_reading_head();
 	for (size_t i = 0; worktrees[i]; i++)
@@ -124,7 +123,7 @@ static int cmd_refs_list(int argc, const char **argv, const char *prefix,
 }
 
 static int cmd_refs_exists(int argc, const char **argv, const char *prefix,
-			   struct repository *repo UNUSED)
+			   struct repository *repo)
 {
 	struct strbuf unused_referent = STRBUF_INIT;
 	struct object_id unused_oid;
@@ -145,7 +144,7 @@ static int cmd_refs_exists(int argc, const char **argv, const char *prefix,
 		die(_("'git refs exists' requires a reference"));
 
 	ref = *argv++;
-	if (refs_read_raw_ref(get_main_ref_store(the_repository), ref,
+	if (refs_read_raw_ref(get_main_ref_store(repo), ref,
 			      &unused_oid, &unused_referent, &unused_type,
 			      &failure_errno)) {
 		if (failure_errno == ENOENT || failure_errno == EISDIR) {

-- 
2.55.0.rc0.786.g65d90a0328.dirty


^ permalink raw reply related

* [PATCH v2 0/5] builtin/refs: add ability to write references
From: Patrick Steinhardt @ 2026-06-17 10:15 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano
In-Reply-To: <20260616-pks-refs-writing-subcommands-v1-0-9f5219b6109d@pks.im>

Hi,

Reference-related functionality in Git is currently spread across many
different commands: git-update-ref(1), git-for-each-ref(1),
git-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it
hard for users to discover what functionality we have available to work
with references.

We have thus started to consolidate this functionality into git-refs(1),
which is a toolbox of everything related to references. Until now, the
command doesn't handle functionality of git-update-ref(1).

This patch series backfills most of the functionality by introducing
three new commands:

  - `git refs delete` to delete references. This is the equivalent of
    `git update-ref -d`.

  - `git refs update` to update references. This is the equivalent of
    `git update-ref <refname> <oldvalue> <newvalue>`.

  - `git refs rename` to rename a reference, including its reflog. This
    does not have an equivalent in git-update-ref(1), but is inspired by
    and supersedes [1].

Changes in v2:
  - Add a new "create" subcommand.
  - Consistently quote in error messages.
  - Consistently use `<old-value>` in the synopsis.
  - Don't return negative exit codes.
  - Improve documentation of "update" subcommand to mention that you can
    create and delete branches.
  - Add tests to verify that we can use "update" to do this, both in
    racy and raceless ways.
  - Add missing calls to `repo_config()`.
  - Drop useless `GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME` variable.
  - Link to v1: https://patch.msgid.link/20260616-pks-refs-writing-subcommands-v1-0-9f5219b6109d@pks.im

Thanks!

Patrick

[1]: <xmqqv7brz9ba.fsf@gitster.g>

---
Patrick Steinhardt (5):
      builtin/refs: drop `the_repository`
      builtin/refs: add "delete" subcommand
      builtin/refs: add "update" subcommand
      builtin/refs: add "create" subcommand
      builtin/refs: add "rename" subcommand

 Documentation/git-refs.adoc |  40 +++++++
 builtin/refs.c              | 222 ++++++++++++++++++++++++++++++++++--
 t/meson.build               |   4 +
 t/t1464-refs-delete.sh      | 130 +++++++++++++++++++++
 t/t1465-refs-update.sh      | 268 ++++++++++++++++++++++++++++++++++++++++++++
 t/t1466-refs-create.sh      | 151 +++++++++++++++++++++++++
 t/t1467-refs-rename.sh      | 131 ++++++++++++++++++++++
 7 files changed, 938 insertions(+), 8 deletions(-)

Range-diff versus v1:

1:  cfbc247e81 = 1:  6d0c5bd06f builtin/refs: drop `the_repository`
2:  f5f33e5c5b ! 2:  db55d87116 builtin/refs: add "delete" subcommand
    @@ Documentation/git-refs.adoc: git refs list [--count=<count>] [--shell|--perl|--p
      		   [ --stdin | (<pattern>...)]
      git refs exists <ref>
      git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
    -+git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]
    ++git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]
      
      DESCRIPTION
      -----------
    @@ Documentation/git-refs.adoc: optimize::
      
     +delete::
     +	Delete the given reference. This subcommand mirrors `git update-ref -d`
    -+	(see linkgit:git-update-ref[1]). When `<oldvalue>` is given, the
    ++	(see linkgit:git-update-ref[1]). When `<old-value>` is given, the
     +	reference is only deleted after verifying that it currently contains
    -+	`<oldvalue>`.
    ++	`<old-value>`.
     +
      OPTIONS
      -------
    @@ builtin/refs.c
      	N_("git refs optimize " PACK_REFS_OPTS)
      
     +#define REFS_DELETE_USAGE \
    -+	N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]")
    ++	N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]")
     +
      static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
      			    struct repository *repo)
    @@ builtin/refs.c: static int cmd_refs_optimize(int argc, const char **argv, const
     +	};
     +	struct object_id oldoid;
     +	const char *refname;
    ++	int ret;
     +
     +	argc = parse_options(argc, argv, prefix, opts, refs_delete_usage, 0);
     +	if (argc < 1 || argc > 2)
    @@ builtin/refs.c: static int cmd_refs_optimize(int argc, const char **argv, const
     +		if (repo_get_oid_with_flags(repo, argv[1], &oldoid, GET_OID_SKIP_AMBIGUITY_CHECK))
     +			die(_("invalid old object ID: '%s'"), argv[1]);
     +		if (is_null_oid(&oldoid))
    -+			die(_("cannot delete object with null old object ID"));
    ++			die(_("cannot delete reference with null old object ID"));
     +	}
     +
    -+	return refs_delete_ref(get_main_ref_store(repo), message, refname,
    -+			       argc == 2 ? &oldoid : NULL, flags);
    ++	ret = refs_delete_ref(get_main_ref_store(repo), message, refname,
    ++			      argc == 2 ? &oldoid : NULL, flags);
    ++
    ++	if (ret < 0)
    ++		ret = 1;
    ++	return ret;
     +}
     +
      int cmd_refs(int argc,
    @@ t/t1464-refs-delete.sh (new)
     +
     +test_description='git refs delete'
     +
    -+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
    -+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
    -+
     +. ./test-lib.sh
     +
     +setup_repo () {
3:  1fc1bed619 ! 3:  85f07a2cb0 builtin/refs: add "update" subcommand
    @@ Documentation/git-refs.adoc
     @@ Documentation/git-refs.adoc: git refs list [--count=<count>] [--shell|--perl|--python|--tcl]
      git refs exists <ref>
      git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
    - git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]
    + git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]
     +git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]
      
      DESCRIPTION
      -----------
     @@ Documentation/git-refs.adoc: delete::
      	reference is only deleted after verifying that it currently contains
    - 	`<oldvalue>`.
    + 	`<old-value>`.
      
     +update::
    -+	Update the given reference to point at `<new-value>`. This subcommand
    -+	mirrors `git update-ref` (see linkgit:git-update-ref[1]). When
    -+	`<old-value>` is given, the reference is only updated after verifying
    -+	that it currently contains `<old-value>`.
    ++	Update the given reference to point at `<new-value>`. If `<old-value>`
    ++	is given, the reference is only updated after verifying that it
    ++	currently contains `<old-value>`. As a special case, an all-zeroes
    ++	`<new-value>` deletes the branch, whereas an all-zeroes `<old-value>`
    ++	ensures that the branch does not yet exist.
     +
      OPTIONS
      -------
      
    +@@ Documentation/git-refs.adoc: include::pack-refs-options.adoc[]
    + 
    + The following options are specific to commands which write references:
    + 
    ++`--create-reflog`::
    ++	Create a reflog for the reference even if one would not ordinarily be
    ++	created.
    ++
    + `--message=<reason>`::
    + 	Use the given <reason> string for the reflog entry associated with the
    + 	update. An empty message is rejected.
     
      ## builtin/refs.c ##
     @@
      #define REFS_DELETE_USAGE \
    - 	N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]")
    + 	N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]")
      
     +#define REFS_UPDATE_USAGE \
     +	N_("git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]")
    @@ builtin/refs.c
      			    struct repository *repo)
      {
     @@ builtin/refs.c: static int cmd_refs_delete(int argc, const char **argv, const char *prefix,
    - 			       argc == 2 ? &oldoid : NULL, flags);
    + 	return ret;
      }
      
     +static int cmd_refs_update(int argc, const char **argv, const char *prefix,
    @@ builtin/refs.c: static int cmd_refs_delete(int argc, const char **argv, const ch
     +	};
     +	struct object_id newoid, oldoid;
     +	const char *refname;
    ++	int ret;
     +
     +	argc = parse_options(argc, argv, prefix, opts, refs_update_usage, 0);
     +	if (argc < 2 || argc > 3)
    @@ builtin/refs.c: static int cmd_refs_delete(int argc, const char **argv, const ch
     +	refname = argv[0];
     +	if (repo_get_oid_with_flags(repo, argv[1], &newoid,
     +				    GET_OID_SKIP_AMBIGUITY_CHECK))
    -+		die(_("invalid new object ID: %s"), argv[1]);
    ++		die(_("invalid new object ID: '%s'"), argv[1]);
     +	if (argc == 3 &&
     +	    repo_get_oid_with_flags(repo, argv[2], &oldoid,
     +				    GET_OID_SKIP_AMBIGUITY_CHECK))
    -+		die(_("invalid old object ID: %s"), argv[2]);
    ++		die(_("invalid old object ID: '%s'"), argv[2]);
     +
    -+	return refs_update_ref(get_main_ref_store(repo), message, refname,
    -+			       &newoid, argc == 3 ? &oldoid : NULL, flags,
    -+			       UPDATE_REFS_DIE_ON_ERR);
    ++	ret = refs_update_ref(get_main_ref_store(repo), message, refname,
    ++			      &newoid, argc == 3 ? &oldoid : NULL, flags,
    ++			      UPDATE_REFS_MSG_ON_ERR);
    ++
    ++	if (ret < 0)
    ++		ret = 1;
    ++	return ret;
     +}
     +
      int cmd_refs(int argc,
    @@ t/t1465-refs-update.sh (new)
     +	)
     +'
     +
    ++test_expect_success 'update can create a new branch with oldvalue' '
    ++	test_when_finished "rm -rf repo" &&
    ++	setup_repo repo &&
    ++	(
    ++		cd repo &&
    ++		A=$(git rev-parse A) &&
    ++		git refs update refs/heads/foo $A $ZERO_OID 2>err &&
    ++		test_ref_matches refs/heads/foo $A
    ++	)
    ++'
    ++
    ++test_expect_success 'update can create a new branch without oldvalue' '
    ++	test_when_finished "rm -rf repo" &&
    ++	setup_repo repo &&
    ++	(
    ++		cd repo &&
    ++		A=$(git rev-parse A) &&
    ++		git refs update refs/heads/foo $A 2>err &&
    ++		test_ref_matches refs/heads/foo $A
    ++	)
    ++'
    ++
    ++test_expect_success 'update refuses to create preexisting branch' '
    ++	test_when_finished "rm -rf repo" &&
    ++	setup_repo repo &&
    ++	(
    ++		cd repo &&
    ++		A=$(git rev-parse A) &&
    ++		B=$(git rev-parse B) &&
    ++		git refs update refs/heads/foo $A &&
    ++		test_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err &&
    ++		test_grep "reference already exists" err &&
    ++		test_ref_matches refs/heads/foo $A
    ++	)
    ++'
    ++
    ++test_expect_success 'update can delete a branch with oldvalue' '
    ++	test_when_finished "rm -rf repo" &&
    ++	setup_repo repo &&
    ++	(
    ++		cd repo &&
    ++		A=$(git rev-parse A) &&
    ++		git refs update refs/heads/foo $A 2>err &&
    ++		git refs update refs/heads/foo $ZERO_OID $A 2>err &&
    ++		test_must_fail git refs exists refs/heads/foo
    ++	)
    ++'
    ++
    ++test_expect_success 'update can delete a branch without oldvalue' '
    ++	test_when_finished "rm -rf repo" &&
    ++	setup_repo repo &&
    ++	(
    ++		cd repo &&
    ++		A=$(git rev-parse A) &&
    ++		git refs update refs/heads/foo $A 2>err &&
    ++		git refs update refs/heads/foo $ZERO_OID 2>err &&
    ++		test_must_fail git refs exists refs/heads/foo
    ++	)
    ++'
    ++
    ++test_expect_success 'update refuses to delete a branch with mismatching value' '
    ++	test_when_finished "rm -rf repo" &&
    ++	setup_repo repo &&
    ++	(
    ++		cd repo &&
    ++		A=$(git rev-parse A) &&
    ++		B=$(git rev-parse B) &&
    ++		git refs update refs/heads/foo $A 2>err &&
    ++		test_must_fail git refs update refs/heads/foo $ZERO_OID $B 2>err &&
    ++		test_grep " but expected " err &&
    ++		git refs exists refs/heads/foo
    ++	)
    ++'
    ++
    ++test_expect_success 'update refuses to create preexisting branch' '
    ++	test_when_finished "rm -rf repo" &&
    ++	setup_repo repo &&
    ++	(
    ++		cd repo &&
    ++		A=$(git rev-parse A) &&
    ++		B=$(git rev-parse B) &&
    ++		git refs update refs/heads/foo $A &&
    ++		test_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err &&
    ++		test_grep "reference already exists" err &&
    ++		test_ref_matches refs/heads/foo $A
    ++	)
    ++'
    ++
    ++
     +test_expect_success 'update with invalid new value fails' '
     +	test_when_finished "rm -rf repo" &&
     +	setup_repo repo &&
-:  ---------- > 4:  03036ef730 builtin/refs: add "create" subcommand
4:  aadedb14e1 ! 5:  65f0ee4f03 builtin/refs: add "rename" subcommand
    @@ Commit message
         Signed-off-by: Patrick Steinhardt <ps@pks.im>
     
      ## Documentation/git-refs.adoc ##
    -@@ Documentation/git-refs.adoc: git refs exists <ref>
    - git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
    - git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]
    +@@ Documentation/git-refs.adoc: git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude
    + git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>
    + git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]
      git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]
    -+git refs rename [--message=<reason>] <oldref> <newref>
    ++git refs rename [--message=<reason>] <old-ref> <new-ref>
      
      DESCRIPTION
      -----------
     @@ Documentation/git-refs.adoc: update::
    - 	`<old-value>` is given, the reference is only updated after verifying
    - 	that it currently contains `<old-value>`.
    + 	`<new-value>` deletes the branch, whereas an all-zeroes `<old-value>`
    + 	ensures that the branch does not yet exist.
      
     +rename::
     +	Rename the reference `<oldref>` to `<newref>`. The old reference must
    @@ Documentation/git-refs.adoc: update::
      OPTIONS
      -------
      
    -@@ Documentation/git-refs.adoc: include::pack-refs-options.adoc[]
    - 
    - The following options are specific to commands which write references:
    - 
    -+`--create-reflog`::
    -+	Create a reflog for the reference even if one would not ordinarily be
    -+	created.
    -+
    - `--message=<reason>`::
    - 	Use the given <reason> string for the reflog entry associated with the
    - 	update. An empty message is rejected.
     
      ## builtin/refs.c ##
     @@
    @@ builtin/refs.c
      	N_("git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]")
      
     +#define REFS_RENAME_USAGE \
    -+	N_("git refs rename [--message=<reason>] <oldref> <newref>")
    ++	N_("git refs rename [--message=<reason>] <old-ref> <new-ref>")
     +
      static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
      			    struct repository *repo)
      {
     @@ builtin/refs.c: static int cmd_refs_update(int argc, const char **argv, const char *prefix,
    - 			       UPDATE_REFS_DIE_ON_ERR);
    + 	return ret;
      }
      
     +static int cmd_refs_rename(int argc, const char **argv, const char *prefix,
    @@ builtin/refs.c: static int cmd_refs_update(int argc, const char **argv, const ch
     +		OPT_END(),
     +	};
     +	const char *oldref, *newref;
    ++	int ret;
     +
     +	argc = parse_options(argc, argv, prefix, opts, refs_rename_usage, 0);
     +	if (argc != 2)
    @@ builtin/refs.c: static int cmd_refs_update(int argc, const char **argv, const ch
     +	if (message && !*message)
     +		die(_("refusing to perform update with empty message"));
     +
    ++	repo_config(repo, git_default_config, NULL);
    ++
     +	oldref = argv[0];
     +	newref = argv[1];
     +
     +	if (check_refname_format(oldref, 0))
    -+		die(_("invalid ref format: %s"), oldref);
    ++		die(_("invalid ref format: '%s'"), oldref);
     +	if (check_refname_format(newref, 0))
    -+		die(_("invalid ref format: %s"), newref);
    ++		die(_("invalid ref format: '%s'"), newref);
     +
     +	if (!refs_ref_exists(get_main_ref_store(repo), oldref))
     +		die(_("reference does not exist: '%s'"), oldref);
     +	if (refs_ref_exists(get_main_ref_store(repo), newref))
     +		die(_("reference already exists: '%s'"), newref);
     +
    -+	return refs_rename_ref(get_main_ref_store(repo), oldref, newref, message);
    ++	ret = refs_rename_ref(get_main_ref_store(repo), oldref, newref, message);
    ++
    ++	if (ret < 0)
    ++		ret = 1;
    ++	return ret;
     +}
     +
      int cmd_refs(int argc,
      	     const char **argv,
      	     const char *prefix,
     @@ builtin/refs.c: int cmd_refs(int argc,
    - 		REFS_OPTIMIZE_USAGE,
    + 		REFS_CREATE_USAGE,
      		REFS_DELETE_USAGE,
      		REFS_UPDATE_USAGE,
     +		REFS_RENAME_USAGE,
    @@ builtin/refs.c: int cmd_refs(int argc,
      	};
      	parse_opt_subcommand_fn *fn = NULL;
     @@ builtin/refs.c: int cmd_refs(int argc,
    - 		OPT_SUBCOMMAND("optimize", &fn, cmd_refs_optimize),
    + 		OPT_SUBCOMMAND("create", &fn, cmd_refs_create),
      		OPT_SUBCOMMAND("delete", &fn, cmd_refs_delete),
      		OPT_SUBCOMMAND("update", &fn, cmd_refs_update),
     +		OPT_SUBCOMMAND("rename", &fn, cmd_refs_rename),
    @@ builtin/refs.c: int cmd_refs(int argc,
     
      ## t/meson.build ##
     @@ t/meson.build: integration_tests = [
    -   't1463-refs-optimize.sh',
        't1464-refs-delete.sh',
        't1465-refs-update.sh',
    -+  't1466-refs-rename.sh',
    +   't1466-refs-create.sh',
    ++  't1467-refs-rename.sh',
        't1500-rev-parse.sh',
        't1501-work-tree.sh',
        't1502-rev-parse-parseopt.sh',
     
    - ## t/t1466-refs-rename.sh (new) ##
    + ## t/t1467-refs-rename.sh (new) ##
     @@
     +#!/bin/sh
     +

---
base-commit: 700432b2ba22603a0bcb71475c9c333d17c9b0d1
change-id: 20260616-pks-refs-writing-subcommands-7a77be5bda9b


^ permalink raw reply

* [PATCH] osxkeychain: fix build with Rust
From: Johannes Schindelin via GitGitGadget @ 2026-06-17 10:11 UTC (permalink / raw)
  To: git; +Cc: Johannes Schindelin, Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

Without NO_RUST defined, the varint encoder/decoder lives in the
RUST_LIB, which needs to be linked. Symptom:

cc [... -o contrib/credential/osxkeychain/git-credential-osxkeychain [...]
Undefined symbols for architecture x86_64:
  "_decode_varint", referenced from:
      _read_untracked_extension in libgit.a[x86_64][63](dir.o)
      _read_untracked_extension in libgit.a[x86_64][63](dir.o)
      _read_one_dir in libgit.a[x86_64][63](dir.o)
      _read_one_dir in libgit.a[x86_64][63](dir.o)
      _load_cache_entry_block in libgit.a[x86_64][174](read-cache.o)
  "_encode_varint", referenced from:
      _write_untracked_extension in libgit.a[x86_64][63](dir.o)
      _write_untracked_extension in libgit.a[x86_64][63](dir.o)
      _write_untracked_extension in libgit.a[x86_64][63](dir.o)
      _write_one_dir in libgit.a[x86_64][63](dir.o)
      _write_one_dir in libgit.a[x86_64][63](dir.o)
      _do_write_index in libgit.a[x86_64][174](read-cache.o)
ld: symbol(s) not found for architecture x86_64

While it is curious why these functions are needed at all (osxkeychain
does not read or write the index), the compile error is a real problem.

Instead of trying to play games to add `GITLIBS` while filtering out
`common-main.o`, replace the `$(LIB_FILE) $(EXTLIBS)` construct with the
much shorter `$(LIBS)` construct that _already_ filters out
`common-main.o` and adds the Rust library when needed.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
    osxkeychain: fix build with Rust
    
    I ran into this when trying to build Microsoft Git v2.55.0-rc0. This
    seems to be similar in spirit to
    https://lore.kernel.org/git/pull.2288.git.git.1778001976709.gitgitgadget@gmail.com/
    but the latter seems not to have gained traction. This build failure is
    a hard regression in v2.55.0, though.

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2154%2Fdscho%2Fosxkeychain-vs-rust-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2154/dscho/osxkeychain-vs-rust-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2154

 Makefile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Makefile b/Makefile
index 0976a69b4c..1cec251f43 100644
--- a/Makefile
+++ b/Makefile
@@ -4074,7 +4074,7 @@ contrib/libgit-sys/libgitpub.a: $(LIBGIT_HIDDEN_EXPORT)
 
 contrib/credential/osxkeychain/git-credential-osxkeychain: contrib/credential/osxkeychain/git-credential-osxkeychain.o $(LIB_FILE) GIT-LDFLAGS
 	$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \
-		$(filter %.o,$^) $(LIB_FILE) $(EXTLIBS) -framework Security -framework CoreFoundation
+		$(filter %.o,$^) $(LIBS) -framework Security -framework CoreFoundation
 
 contrib/credential/osxkeychain/git-credential-osxkeychain.o: contrib/credential/osxkeychain/git-credential-osxkeychain.c GIT-CFLAGS
 	$(QUIET_LINK)$(CC) -o $@ -c $(dep_args) $(compdb_args) $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) $<

base-commit: 0fae78c9d55efe705877ea537fe42c59164ccd94
-- 
gitgitgadget

^ permalink raw reply related

* Re: [PATCH v15 0/7] branch: delete-merged
From: Phillip Wood @ 2026-06-17 10:01 UTC (permalink / raw)
  To: Harald Nordgren via GitGitGadget, git
  Cc: Kristoffer Haugsbakk, Johannes Sixt, Phillip Wood,
	Harald Nordgren
In-Reply-To: <pull.2285.v15.git.git.1781542042.gitgitgadget@gmail.com>

Hi Harald

Our SubmittingPatches documentation recommends waiting for the 
discussion to settle before sending a new version. When you know someone 
is going send more comments on a series it is a good idea to wait for 
them before sending a new version to avoid too much churn on the list 
which makes it hard for people to keep up. I'm not going to read this 
version in detail because I know another version will be needed but I 
did spot a couple of things in the summary below.

On 15/06/2026 17:47, Harald Nordgren via GitGitGadget wrote:
>   * Renamed --prune-merged to --delete-merged throughout. Not necessarily
>     final, but something to advance the discussion.
>   * --delete-merged now silently skips not-yet-merged branches instead of
>     warning.

Good

>   * --forked now accepts a bare remote name (e.g. origin) for the branch
>     origin/HEAD points at using DWIM.

The range-diff below does not show any changes to the implementation, 
only the Documentation and tests

>   * Initialized the delete_branches() flag locals where declared. Only force
>     stays deferred.

Not changing force sounds like a bad idea. The whole point of unpacking 
the flags at the start of the function is to avoid accidental 
regressions. Unpacking the flags into separate variables means the rest 
of the function does not need to know that the function arguments have 
changed.

Thanks

Phillip

>   * delete_branches()/check_branch_commit() doc and code cleanups: redundant
>     branch NULL checks dropped, ref_array candidates = { 0 }, a BUG() for the
>     unreachable non-branch ref, and reworked --delete-merged doc wording.
>   * Broadened the --forked tests (local commits for realism, remote add -f,
>     --forked <pattern> <branch> coverage), renamed the misleading trunk
>     fixture, and replaced the misnamed detached branch with git checkout
>     --detach.
> 
> Harald Nordgren (7):
>    branch: add --forked filter for --list mode
>    branch: convert delete_branches() to a flags argument
>    branch: let delete_branches skip unmerged branches on bulk refusal
>    branch: prepare delete_branches for a bulk caller
>    branch: add --delete-merged <branch>
>    branch: add branch.<name>.deleteMerged opt-out
>    branch: add --dry-run for --delete-merged
> 
>   Documentation/config/branch.adoc |   7 +
>   Documentation/git-branch.adoc    |  43 +++-
>   builtin/branch.c                 | 184 ++++++++++++---
>   ref-filter.c                     |  70 ++++++
>   ref-filter.h                     |  10 +
>   t/t3200-branch.sh                | 387 +++++++++++++++++++++++++++++++
>   6 files changed, 673 insertions(+), 28 deletions(-)
> 
> 
> base-commit: ea97ad8d017de0c9037451a78008a0fd60abea0c
> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2285%2FHaraldNordgren%2Ffetch-prune-local-branches-v15
> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2285/HaraldNordgren/fetch-prune-local-branches-v15
> Pull-Request: https://github.com/git/git/pull/2285
> 
> Range-diff vs v14:
> 
>   1:  7383872f4b ! 1:  da741b5ea7 branch: add --forked filter for --list mode
>       @@ Commit message
>        
>            Add a --forked option to "git branch" list mode that lists only
>            branches whose configured upstream matches <branch>. The argument
>       -    can be a ref (e.g. "origin/main", "master") or a shell glob
>       +    can be a ref (e.g. "origin/main", "master"), a remote name like
>       +    "origin" for the branch its origin/HEAD points at, or a shell glob
>            (e.g. "origin/*"), and may be repeated to widen the filter.
>        
>            It is an ordinary list filter, so it combines with the others:
>       @@ Commit message
>            lists branches forked from origin that are already merged into
>            origin/main, and --no-merged inverts the question.
>        
>       -    This is the building block for --prune-merged, which deletes the
>       +    This is the building block for --delete-merged, which deletes the
>            listed branches once they have landed on their upstream.
>        
>            Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
>       @@ Documentation/git-branch.adoc: superproject's "origin/main", but tracks the subm
>        +`--forked <branch>`::
>        +	Only list branches whose configured upstream matches
>        +	_<branch>_. The argument can be a ref (e.g. `origin/main`,
>       -+	`master`) or a shell-style glob (e.g. `'origin/*'`). The
>       -+	option can be repeated to widen the filter. Implies `--list`.
>       ++	`master`), a remote name like `origin` for the branch its
>       ++	`origin/HEAD` points at, or a shell-style glob (e.g.
>       ++	`'origin/*'`). The option can be repeated to widen the
>       ++	filter. Implies `--list`.
>        +
>         `--points-at <object>`::
>         	Only list branches of _<object>_.
>       @@ t/t3200-branch.sh: test_expect_success 'errors if given a bad branch name' '
>        +	git -C forked-other branch foreign other-base &&
>        +
>        +	git clone forked-upstream forked &&
>       -+	git -C forked remote add other ../forked-other &&
>       -+	git -C forked fetch other &&
>       ++	git -C forked remote add -f other ../forked-other &&
>       ++	git -C forked remote set-head origin one &&
>        +	git -C forked branch local-base &&
>        +	git -C forked branch --track local-one origin/one &&
>        +	git -C forked branch --track local-two origin/two &&
>        +	git -C forked branch --track local-foreign other/foreign &&
>       -+	git -C forked branch detached &&
>       -+	git -C forked branch --track local-trunk local-base
>       ++	git -C forked branch --track local-onbase local-base &&
>       ++
>       ++	git -C forked checkout local-one &&
>       ++	test_commit -C forked --no-tag local-one-work local-one.t &&
>       ++	git -C forked checkout local-foreign &&
>       ++	test_commit -C forked --no-tag local-foreign-work local-foreign.t &&
>       ++	git -C forked checkout --detach
>        +'
>        +
>        +test_expect_success '--forked <upstream-tracking-branch> filters by upstream' '
>       @@ t/t3200-branch.sh: test_expect_success 'errors if given a bad branch name' '
>        +
>        +test_expect_success '--forked <local-branch> matches branches with local upstream' '
>        +	git -C forked branch --forked local-base --format="%(refname:short)" >actual &&
>       -+	echo local-trunk >expect &&
>       ++	echo local-onbase >expect &&
>        +	test_cmp expect actual
>        +'
>        +
>       @@ t/t3200-branch.sh: test_expect_success 'errors if given a bad branch name' '
>        +	git -C forked branch --forked local-base --forked "other/*" --format="%(refname:short)" >actual &&
>        +	cat >expect <<-\EOF &&
>        +	local-foreign
>       -+	local-trunk
>       ++	local-onbase
>        +	EOF
>        +	test_cmp expect actual
>        +'
>       @@ t/t3200-branch.sh: test_expect_success 'errors if given a bad branch name' '
>        +'
>        +
>        +test_expect_success '--forked composes with --no-merged' '
>       -+	test_when_finished "git -C forked checkout detached" &&
>       ++	test_when_finished "git -C forked checkout --detach" &&
>        +	git -C forked checkout local-one &&
>        +	test_commit -C forked local-only &&
>        +	git -C forked branch --forked "origin/*" --no-merged origin/one \
>       @@ t/t3200-branch.sh: test_expect_success 'errors if given a bad branch name' '
>        +	test_must_fail git -C forked branch --forked 2>err &&
>        +	test_grep "requires a value" err
>        +'
>       ++
>       ++test_expect_success '--forked <remote> uses the branch <remote>/HEAD points at' '
>       ++	git -C forked branch --forked origin --format="%(refname:short)" >actual &&
>       ++	echo local-one >expect &&
>       ++	test_cmp expect actual
>       ++'
>       ++
>       ++test_expect_success '--forked narrows a <pattern> argument' '
>       ++	git -C forked branch --forked "origin/*" "local-*" \
>       ++		--format="%(refname:short)" >actual &&
>       ++	cat >expect <<-\EOF &&
>       ++	local-one
>       ++	local-two
>       ++	EOF
>       ++	test_cmp expect actual
>       ++'
>        +
>         test_done
>   2:  7ef9502e01 ! 2:  91c35f10cc branch: let delete_branches warn instead of error on bulk refusal
>       @@ Metadata
>        Author: Harald Nordgren <haraldnordgren@gmail.com>
>        
>         ## Commit message ##
>       -    branch: let delete_branches warn instead of error on bulk refusal
>       +    branch: convert delete_branches() to a flags argument
>        
>       -    Add a warn-only mode to delete_branches() and check_branch_commit()
>       -    so a bulk caller can report branches that are not fully merged as a
>       -    short warning and carry on, rather than erroring with the longer
>       -    "use 'git branch -D'" advice that the plain "git branch -d" path
>       -    emits. Existing callers are unaffected.
>       +    delete_branches() and check_branch_commit() take a pair of int
>       +    booleans (force and quiet) that the next commits would grow further.
>       +    Replace them with a single "unsigned int flags" argument and an
>       +    enum, splitting the bits back into named bool locals so the body
>       +    keeps reading the same named values.
>       +
>       +    No change in behavior.
>        
>            Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
>        
>       @@ builtin/branch.c: static int branch_merged(int kind, const char *name,
>        +enum delete_branch_flags {
>        +	DELETE_BRANCH_FORCE = (1 << 0),
>        +	DELETE_BRANCH_QUIET = (1 << 1),
>       -+	DELETE_BRANCH_WARN_ONLY = (1 << 2),
>        +};
>        +
>         static int check_branch_commit(const char *branchname, const char *refname,
>       @@ builtin/branch.c: static int branch_merged(int kind, const char *name,
>        -			       int kinds, int force)
>        +			       int kinds, unsigned int flags)
>         {
>       -+	int force = flags & DELETE_BRANCH_FORCE;
>       ++	bool force = flags & DELETE_BRANCH_FORCE;
>         	struct commit *rev = lookup_commit_reference(the_repository, oid);
>         	if (!force && !rev) {
>         		error(_("couldn't look up commit object for '%s'"), refname);
>       - 		return -1;
>       - 	}
>       - 	if (!force && !branch_merged(kinds, branchname, rev, head_rev)) {
>       --		error(_("the branch '%s' is not fully merged"), branchname);
>       --		advise_if_enabled(ADVICE_FORCE_DELETE_BRANCH,
>       --				  _("If you are sure you want to delete it, "
>       --				  "run 'git branch -D %s'"), branchname);
>       -+		if (flags & DELETE_BRANCH_WARN_ONLY) {
>       -+			warning(_("the branch '%s' is not fully merged"),
>       -+				branchname);
>       -+		} else {
>       -+			error(_("the branch '%s' is not fully merged"),
>       -+			      branchname);
>       -+			advise_if_enabled(ADVICE_FORCE_DELETE_BRANCH,
>       -+					  _("If you are sure you want to delete it, "
>       -+					  "run 'git branch -D %s'"), branchname);
>       -+		}
>       - 		return -1;
>       - 	}
>       - 	return 0;
>        @@ builtin/branch.c: static void delete_branch_config(const char *branchname)
>         	strbuf_release(&buf);
>         }
>       @@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int fo
>         	int i;
>         	int ret = 0;
>         	int remote_branch = 0;
>       -+	int force, quiet;
>       ++	bool force;
>       ++	bool quiet = flags & DELETE_BRANCH_QUIET;
>         	struct strbuf bname = STRBUF_INIT;
>         	enum interpret_branch_kind allowed_interpret;
>         	struct string_list refs_to_delete = STRING_LIST_INIT_DUP;
>       @@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int fo
>         	branch_name_pos = strcspn(fmt, "%");
>         
>        +	force = flags & DELETE_BRANCH_FORCE;
>       -+	quiet = flags & DELETE_BRANCH_QUIET;
>        +
>         	if (!force)
>         		head_rev = lookup_commit_reference(the_repository, &head_oid);
>       @@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int fo
>        +		if (!(ref_flags & (REF_ISSYMREF|REF_ISBROKEN)) &&
>         		    check_branch_commit(bname.buf, name, &oid, head_rev, kinds,
>        -					force)) {
>       --			ret = 1;
>        +					flags)) {
>       -+			if (!(flags & DELETE_BRANCH_WARN_ONLY))
>       -+				ret = 1;
>       + 			ret = 1;
>         			goto next;
>         		}
>         
>   -:  ---------- > 3:  e101dd2886 branch: let delete_branches skip unmerged branches on bulk refusal
>   3:  259113e304 ! 4:  6c3534901a branch: prepare delete_branches for a bulk caller
>       @@ Commit message
>            branch: prepare delete_branches for a bulk caller
>        
>            Teach delete_branches() two new modes for the upcoming
>       -    --prune-merged: one that asks only whether a branch is merged into
>       +    --delete-merged: one that asks only whether a branch is merged into
>            its upstream, without falling back to HEAD when there is no
>            upstream, and one that rehearses the deletions without removing any
>            ref. Existing callers keep their current behavior.
>       @@ builtin/branch.c: static int branch_merged(int kind, const char *name,
>        @@ builtin/branch.c: enum delete_branch_flags {
>         	DELETE_BRANCH_FORCE = (1 << 0),
>         	DELETE_BRANCH_QUIET = (1 << 1),
>       - 	DELETE_BRANCH_WARN_ONLY = (1 << 2),
>       + 	DELETE_BRANCH_SKIP_UNMERGED = (1 << 2),
>        +	DELETE_BRANCH_NO_HEAD_FALLBACK = (1 << 3),
>        +	DELETE_BRANCH_DRY_RUN = (1 << 4),
>         };
>         
>         static int check_branch_commit(const char *branchname, const char *refname,
>        @@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int kinds,
>       - 	int i;
>       - 	int ret = 0;
>       - 	int remote_branch = 0;
>       --	int force, quiet;
>       -+	int force, quiet, dry_run, no_head_fallback;
>       + 	bool force;
>       + 	bool quiet = flags & DELETE_BRANCH_QUIET;
>       + 	bool skip_unmerged = flags & DELETE_BRANCH_SKIP_UNMERGED;
>       ++	bool dry_run = flags & DELETE_BRANCH_DRY_RUN;
>       ++	bool no_head_fallback = flags & DELETE_BRANCH_NO_HEAD_FALLBACK;
>         	struct strbuf bname = STRBUF_INIT;
>         	enum interpret_branch_kind allowed_interpret;
>         	struct string_list refs_to_delete = STRING_LIST_INIT_DUP;
>        @@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int kinds,
>         
>         	force = flags & DELETE_BRANCH_FORCE;
>       - 	quiet = flags & DELETE_BRANCH_QUIET;
>       -+	dry_run = flags & DELETE_BRANCH_DRY_RUN;
>       -+	no_head_fallback = flags & DELETE_BRANCH_NO_HEAD_FALLBACK;
>         
>        -	if (!force)
>        +	if (!force && !no_head_fallback)
>   4:  9924373da0 ! 5:  5899013b8f branch: add --prune-merged <branch>
>       @@ Metadata
>        Author: Harald Nordgren <haraldnordgren@gmail.com>
>        
>         ## Commit message ##
>       -    branch: add --prune-merged <branch>
>       +    branch: add --delete-merged <branch>
>        
>       -            git branch --prune-merged <branch>...
>       +            git branch --delete-merged <branch>...
>        
>            deletes the local branches that "--forked <branch>" would list,
>            keeping only those whose tip is reachable from their configured
>       -    upstream: the work has already landed on the upstream they track,
>       +    upstream. The work has already landed on the upstream they track,
>            so the local copy is no longer needed.
>        
>       -    Reachability is read from local refs; nothing is fetched. Run
>       -    "git fetch" first if you want fresh upstream refs.
>       +    Three kinds of branches are not deleted:
>        
>       -    Three kinds of branches are spared:
>       -
>       -      * any branch checked out in any worktree;
>       -      * any branch whose upstream no longer resolves locally, since a
>       -        missing upstream is not by itself a sign of integration;
>       +      * any branch checked out in any worktree
>       +      * any branch whose upstream remote-tracking branch no longer
>       +        exists, since a missing upstream is not by itself a sign of
>       +        integration
>              * any branch whose push destination equals its upstream
>                (<branch>@{push} is the same as <branch>@{upstream}), such as
>                a local "main" that tracks and pushes to "origin/main". Right
>       -        after a pull it just looks "fully merged", so it is left
>       -        alone. Only branches that push somewhere other than their
>       -        upstream, typically topics in a fork workflow, are candidates.
>       +        after a pull it just looks "fully merged", so it is kept. Only
>       +        branches that push somewhere other than their upstream,
>       +        typically topics in a fork workflow, are candidates.
>        
>       -    Branches that are not yet merged into their upstream are reported
>       -    as a short warning and skipped, so one unmerged topic does not
>       -    abort the whole sweep.
>       +    A branch whose work is not yet merged into its upstream is silently
>       +    skipped, so one unmerged topic does not abort the whole sweep.
>        
>            Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
>        
>       @@ Documentation/git-branch.adoc: git branch (-m|-M) [<old-branch>] <new-branch>
>         git branch (-c|-C) [<old-branch>] <new-branch>
>         git branch (-d|-D) [-r] <branch-name>...
>         git branch --edit-description [<branch-name>]
>       -+git branch --prune-merged <branch>...
>       ++git branch --delete-merged <branch>...
>         
>         DESCRIPTION
>         -----------
>       @@ Documentation/git-branch.adoc: This option is only applicable in non-verbose mod
>         	Print the name of the current branch. In detached `HEAD` state,
>         	nothing is printed.
>         
>       -+`--prune-merged <branch>...`::
>       ++`--delete-merged <branch>...`::
>        +	Delete the local branches that `--forked` would list for the
>        +	given _<branch>_ arguments, but only those whose tip is
>        +	reachable from their configured upstream. In other words, the
>        +	work on the branch has already landed on the upstream it
>        +	tracks, so the local copy is no longer needed. Several
>        +	_<branch>_ patterns may be given, e.g. `git branch
>       -+	--prune-merged origin/main 'feature*'`.
>       ++	--delete-merged origin/main 'feature*'`.
>        ++
>       -+Reachability is checked against whatever the upstream refs say
>       -+locally; nothing is fetched. Run `git fetch` first if you want
>       -+the upstream refs refreshed.
>       ++A branch is not deleted when:
>        ++
>       -+A branch is left alone if any of the following holds:
>       -+its upstream no longer resolves locally; it is checked out in any
>       -+worktree; or its push destination (`<branch>@{push}`) equals its
>       -+upstream (`<branch>@{upstream}`), so it cannot be distinguished
>       -+from a freshly pulled trunk that just looks "fully merged".
>       ++--
>       ++* its upstream remote-tracking branch no longer exists,
>       ++* it is checked out in any worktree, or
>       ++* its push destination (`<branch>@{push}`) equals its upstream
>       ++  (`<branch>@{upstream}`), so it cannot be distinguished from a
>       ++  branch that just looks "fully merged" right after a pull.
>       ++--
>        ++
>       -+Branches refused by the "fully merged" safety check are listed as
>       -+warnings and skipped; pass them to `git branch -D` explicitly if
>       -+you want them gone.
>       ++A branch whose work has not yet been merged into its upstream is
>       ++silently skipped. Delete it with `git branch -D` if you want to
>       ++remove it anyway.
>        +
>         `-v`::
>         `-vv`::
>       @@ builtin/branch.c: static const char * const builtin_branch_usage[] = {
>         	N_("git branch [<options>] (-c | -C) [<old-branch>] <new-branch>"),
>         	N_("git branch [<options>] [-r | -a] [--points-at]"),
>         	N_("git branch [<options>] [-r | -a] [--format]"),
>       -+	N_("git branch [<options>] --prune-merged <branch>..."),
>       ++	N_("git branch [<options>] --delete-merged <branch>..."),
>         	NULL
>         };
>         
>       @@ builtin/branch.c: static int parse_opt_forked(const struct option *opt, const ch
>         	return 0;
>         }
>         
>       -+static int prune_merged_branches(int argc, const char **argv,
>       ++static int delete_merged_branches(int argc, const char **argv,
>        +				 int quiet)
>        +{
>        +	struct ref_store *refs = get_main_ref_store(the_repository);
>        +	struct ref_filter filter = REF_FILTER_INIT;
>       -+	struct ref_array candidates;
>       ++	struct ref_array candidates = { 0 };
>        +	struct strvec deletable = STRVEC_INIT;
>        +	int i, ret = 0;
>        +
>        +	if (!argc)
>       -+		die(_("--prune-merged requires at least one <branch>"));
>       ++		die(_("--delete-merged requires at least one <branch>"));
>        +
>        +	for (i = 0; i < argc; i++)
>        +		if (ref_filter_forked_add(&filter, argv[i]) < 0)
>        +			die(_("'%s' is not a valid branch or pattern"), argv[i]);
>        +
>        +	filter.kind = FILTER_REFS_BRANCHES;
>       -+	memset(&candidates, 0, sizeof(candidates));
>        +	filter_refs(&candidates, &filter, filter.kind);
>        +
>        +	for (i = 0; i < candidates.nr; i++) {
>       @@ builtin/branch.c: static int parse_opt_forked(const struct option *opt, const ch
>        +		const char *upstream, *push;
>        +
>        +		if (!skip_prefix(full_name, "refs/heads/", &short_name))
>       -+			continue;
>       ++			BUG("filter returned non-branch ref '%s'", full_name);
>        +		if (branch_checked_out(full_name))
>        +			continue;
>        +
>        +		branch = branch_get(short_name);
>       -+		upstream = branch ? branch_get_upstream(branch, NULL) : NULL;
>       ++		upstream = branch_get_upstream(branch, NULL);
>        +		if (!upstream || !refs_ref_exists(refs, upstream))
>        +			continue;
>       -+		push = branch ? branch_get_push(branch, NULL) : NULL;
>       ++		push = branch_get_push(branch, NULL);
>        +		if (!push || !strcmp(push, upstream))
>        +			continue;
>        +
>       @@ builtin/branch.c: static int parse_opt_forked(const struct option *opt, const ch
>        +	if (deletable.nr)
>        +		ret = delete_branches(deletable.nr, deletable.v,
>        +				      FILTER_REFS_BRANCHES,
>       -+				      DELETE_BRANCH_WARN_ONLY |
>       ++				      DELETE_BRANCH_SKIP_UNMERGED |
>        +				      DELETE_BRANCH_NO_HEAD_FALLBACK |
>        +				      (quiet ? DELETE_BRANCH_QUIET : 0));
>        +
>       @@ builtin/branch.c: int cmd_branch(int argc,
>         	/* possible actions */
>         	int delete = 0, rename = 0, copy = 0, list = 0,
>         	    unset_upstream = 0, show_current = 0, edit_description = 0;
>       -+	int prune_merged = 0;
>       ++	int delete_merged = 0;
>         	const char *new_upstream = NULL;
>         	int noncreate_actions = 0;
>         	/* possible options */
>       @@ builtin/branch.c: int cmd_branch(int argc,
>         		OPT_BOOL(0, "create-reflog", &reflog, N_("create the branch's reflog")),
>         		OPT_BOOL(0, "edit-description", &edit_description,
>         			 N_("edit the description for the branch")),
>       -+		OPT_BOOL(0, "prune-merged", &prune_merged,
>       -+			N_("delete local branches whose upstream matches <branch> and is merged")),
>       ++		OPT_BOOL(0, "delete-merged", &delete_merged,
>       ++			N_("delete local branches whose upstream matches <branch> and are merged")),
>         		OPT__FORCE(&force, N_("force creation, move/rename, deletion"), PARSE_OPT_NOCOMPLETE),
>         		OPT_MERGED(&filter, N_("print only branches that are merged")),
>         		OPT_NO_MERGED(&filter, N_("print only branches that are not merged")),
>       @@ builtin/branch.c: int cmd_branch(int argc,
>         
>         	if (!delete && !rename && !copy && !edit_description && !new_upstream &&
>        -	    !show_current && !unset_upstream && argc == 0)
>       -+	    !show_current && !unset_upstream && !prune_merged &&
>       ++	    !show_current && !unset_upstream && !delete_merged &&
>        +	    argc == 0)
>         		list = 1;
>         
>       @@ builtin/branch.c: int cmd_branch(int argc,
>         	noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream +
>         			    !!show_current + !!list + !!edit_description +
>        -			    !!unset_upstream;
>       -+			    !!unset_upstream + !!prune_merged;
>       ++			    !!unset_upstream + !!delete_merged;
>         	if (noncreate_actions > 1)
>         		usage_with_options(builtin_branch_usage, options);
>         
>       @@ builtin/branch.c: int cmd_branch(int argc,
>         				      (delete > 1 ? DELETE_BRANCH_FORCE : 0) |
>         				      (quiet ? DELETE_BRANCH_QUIET : 0));
>         		goto out;
>       -+	} else if (prune_merged) {
>       -+		ret = prune_merged_branches(argc, argv, quiet);
>       ++	} else if (delete_merged) {
>       ++		ret = delete_merged_branches(argc, argv, quiet);
>        +		goto out;
>         	} else if (show_current) {
>         		print_current_branch_name();
>         		ret = 0;
>        
>         ## t/t3200-branch.sh ##
>       -@@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
>       - 	test_grep "requires a value" err
>       +@@ t/t3200-branch.sh: test_expect_success '--forked narrows a <pattern> argument' '
>       + 	test_cmp expect actual
>         '
>         
>       -+test_expect_success '--prune-merged: setup' '
>       ++test_expect_success '--delete-merged: setup' '
>        +	test_create_repo pm-upstream &&
>        +	test_commit -C pm-upstream base &&
>        +	git -C pm-upstream checkout -b next &&
>       @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
>        +	test_create_repo pm-fork
>        +'
>        +
>       -+test_expect_success '--prune-merged deletes branches integrated into upstream' '
>       ++test_expect_success '--delete-merged deletes branches integrated into upstream' '
>        +	test_when_finished "rm -rf pm-merged" &&
>        +	git clone pm-upstream pm-merged &&
>        +	git -C pm-merged remote add fork ../pm-fork &&
>       @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
>        +	git -C pm-merged branch two two-commit &&
>        +	git -C pm-merged branch --set-upstream-to=origin/next two &&
>        +
>       -+	git -C pm-merged branch --prune-merged "origin/*" &&
>       ++	git -C pm-merged branch --delete-merged "origin/*" &&
>        +
>        +	test_must_fail git -C pm-merged rev-parse --verify refs/heads/one &&
>        +	test_must_fail git -C pm-merged rev-parse --verify refs/heads/two
>        +'
>        +
>       -+test_expect_success '--prune-merged accepts a literal upstream' '
>       ++test_expect_success '--delete-merged accepts a literal upstream' '
>        +	test_when_finished "rm -rf pm-literal" &&
>        +	git clone pm-upstream pm-literal &&
>        +	git -C pm-literal remote add fork ../pm-fork &&
>       @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
>        +	git -C pm-literal branch one one-commit &&
>        +	git -C pm-literal branch --set-upstream-to=origin/next one &&
>        +
>       -+	git -C pm-literal branch --prune-merged origin/next &&
>       ++	git -C pm-literal branch --delete-merged origin/next &&
>        +
>        +	test_must_fail git -C pm-literal rev-parse --verify refs/heads/one
>        +'
>        +
>       -+test_expect_success '--prune-merged unions multiple <branch> arguments' '
>       ++test_expect_success '--delete-merged unions multiple <branch> arguments' '
>        +	test_when_finished "rm -rf pm-union" &&
>        +	git clone pm-upstream pm-union &&
>        +	git -C pm-union remote add fork ../pm-fork &&
>       @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
>        +	git -C pm-union branch --set-upstream-to=origin/main two &&
>        +	git -C pm-union checkout --detach &&
>        +
>       -+	git -C pm-union branch --prune-merged origin/next origin/main &&
>       ++	git -C pm-union branch --delete-merged origin/next origin/main &&
>        +
>        +	test_must_fail git -C pm-union rev-parse --verify refs/heads/one &&
>        +	test_must_fail git -C pm-union rev-parse --verify refs/heads/two
>        +'
>        +
>       -+test_expect_success '--prune-merged accepts a local upstream' '
>       ++test_expect_success '--delete-merged accepts a local upstream' '
>        +	test_when_finished "rm -rf pm-local" &&
>        +	git clone pm-upstream pm-local &&
>        +	git -C pm-local remote add fork ../pm-fork &&
>        +	test_config -C pm-local remote.pushDefault fork &&
>        +	test_config -C pm-local push.default current &&
>       -+	git -C pm-local checkout -b trunk &&
>       ++	git -C pm-local checkout -b mainline &&
>        +	git -C pm-local branch one one-commit &&
>       -+	git -C pm-local branch --set-upstream-to=trunk one &&
>       ++	git -C pm-local branch --set-upstream-to=mainline one &&
>        +	git -C pm-local merge --ff-only one-commit &&
>        +
>       -+	git -C pm-local branch --prune-merged trunk &&
>       ++	git -C pm-local branch --delete-merged mainline &&
>        +
>        +	test_must_fail git -C pm-local rev-parse --verify refs/heads/one
>        +'
>        +
>       -+test_expect_success '--prune-merged warns instead of erroring on un-integrated commits' '
>       ++test_expect_success '--delete-merged silently skips un-integrated commits' '
>        +	test_when_finished "rm -rf pm-unmerged" &&
>        +	git clone pm-upstream pm-unmerged &&
>        +	git -C pm-unmerged remote add fork ../pm-fork &&
>       @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
>        +	test_commit -C pm-unmerged local-only &&
>        +	git -C pm-unmerged checkout - &&
>        +
>       -+	git -C pm-unmerged branch --prune-merged "origin/*" 2>err &&
>       -+	test_grep "not fully merged" err &&
>       -+	test_grep ! "If you are sure you want to delete it" err &&
>       ++	git -C pm-unmerged branch --delete-merged "origin/*" 2>err &&
>       ++	test_grep ! "not fully merged" err &&
>        +	git -C pm-unmerged rev-parse --verify refs/heads/wip
>        +'
>        +
>       -+test_expect_success '--prune-merged is silent about not-merged-to-HEAD' '
>       ++test_expect_success '--delete-merged is silent about not-merged-to-HEAD' '
>        +	test_when_finished "rm -rf pm-nohead" &&
>        +	git clone pm-upstream pm-nohead &&
>        +	git -C pm-nohead remote add fork ../pm-fork &&
>       @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
>        +	git -C pm-nohead branch topic one-commit &&
>        +	git -C pm-nohead branch --set-upstream-to=origin/next topic &&
>        +
>       -+	git -C pm-nohead branch --prune-merged "origin/*" 2>err &&
>       ++	git -C pm-nohead branch --delete-merged "origin/*" 2>err &&
>        +
>        +	test_grep ! "not yet merged to HEAD" err &&
>        +	test_must_fail git -C pm-nohead rev-parse --verify refs/heads/topic
>        +'
>        +
>       -+test_expect_success '--prune-merged skips branches whose upstream is gone' '
>       ++test_expect_success '--delete-merged skips branches whose upstream is gone' '
>        +	test_when_finished "rm -rf pm-upstream-gone" &&
>        +	git clone pm-upstream pm-upstream-gone &&
>        +	git -C pm-upstream-gone remote add fork ../pm-fork &&
>       @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
>        +	git -C pm-upstream-gone branch --set-upstream-to=origin/next one &&
>        +
>        +	git -C pm-upstream-gone update-ref -d refs/remotes/origin/next &&
>       -+	git -C pm-upstream-gone branch --prune-merged "origin/*" &&
>       ++	git -C pm-upstream-gone branch --delete-merged "origin/*" &&
>        +
>        +	git -C pm-upstream-gone rev-parse --verify refs/heads/one
>        +'
>        +
>       -+test_expect_success '--prune-merged never deletes the checked-out branch' '
>       ++test_expect_success '--delete-merged never deletes the checked-out branch' '
>        +	test_when_finished "rm -rf pm-head" &&
>        +	git clone pm-upstream pm-head &&
>        +	git -C pm-head remote add fork ../pm-fork &&
>       @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
>        +	git -C pm-head checkout -b one one-commit &&
>        +	git -C pm-head branch --set-upstream-to=origin/next one &&
>        +
>       -+	git -C pm-head branch --prune-merged "origin/*" &&
>       ++	git -C pm-head branch --delete-merged "origin/*" &&
>        +
>        +	git -C pm-head rev-parse --verify refs/heads/one
>        +'
>        +
>       -+test_expect_success '--prune-merged spares branches that push back to their upstream' '
>       ++test_expect_success '--delete-merged spares branches that push back to their upstream' '
>        +	test_when_finished "rm -rf pm-push-eq" &&
>        +	git clone pm-upstream pm-push-eq &&
>        +	git -C pm-push-eq checkout --detach &&
>        +
>       -+	git -C pm-push-eq branch --prune-merged "origin/*" &&
>       ++	git -C pm-push-eq branch --delete-merged "origin/*" &&
>        +
>        +	git -C pm-push-eq rev-parse --verify refs/heads/main
>        +'
>        +
>       -+test_expect_success '--prune-merged spares a per-branch pushRemote==upstream remote' '
>       ++test_expect_success '--delete-merged spares a per-branch pushRemote==upstream remote' '
>        +	test_when_finished "rm -rf pm-push-branch" &&
>        +	git clone pm-upstream pm-push-branch &&
>        +	git -C pm-push-branch remote add fork ../pm-fork &&
>       @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
>        +	test_config -C pm-push-branch branch.main.pushRemote origin &&
>        +	git -C pm-push-branch checkout --detach &&
>        +
>       -+	git -C pm-push-branch branch --prune-merged "origin/*" &&
>       ++	git -C pm-push-branch branch --delete-merged "origin/*" &&
>        +
>        +	git -C pm-push-branch rev-parse --verify refs/heads/main
>        +'
>        +
>       -+test_expect_success '--prune-merged prunes when @{push} differs from @{upstream}' '
>       ++test_expect_success '--delete-merged prunes when @{push} differs from @{upstream}' '
>        +	test_when_finished "rm -rf pm-push-diff" &&
>        +	git clone pm-upstream pm-push-diff &&
>        +	git -C pm-push-diff remote add fork ../pm-fork &&
>       @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
>        +	git -C pm-push-diff branch --set-upstream-to=origin/next topic &&
>        +	git -C pm-push-diff checkout --detach &&
>        +
>       -+	git -C pm-push-diff branch --prune-merged "origin/*" &&
>       ++	git -C pm-push-diff branch --delete-merged "origin/*" &&
>        +
>        +	test_must_fail git -C pm-push-diff rev-parse --verify refs/heads/topic
>        +'
>        +
>       -+test_expect_success '--prune-merged requires at least one <branch>' '
>       -+	test_must_fail git -C forked branch --prune-merged 2>err &&
>       ++test_expect_success '--delete-merged requires at least one <branch>' '
>       ++	test_must_fail git -C forked branch --delete-merged 2>err &&
>        +	test_grep "requires at least one <branch>" err
>        +'
>        +
>       -+test_expect_success '--prune-merged takes positional <branch> arguments' '
>       ++test_expect_success '--delete-merged takes positional <branch> arguments' '
>        +	test_when_finished "rm -rf pm-positional" &&
>        +	git clone pm-upstream pm-positional &&
>        +	git -C pm-positional remote add fork ../pm-fork &&
>       @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
>        +	git -C pm-positional branch --set-upstream-to=origin/main two &&
>        +	git -C pm-positional checkout --detach &&
>        +
>       -+	git -C pm-positional branch --prune-merged origin/next origin/main &&
>       ++	git -C pm-positional branch --delete-merged origin/next origin/main &&
>        +
>        +	test_must_fail git -C pm-positional rev-parse --verify refs/heads/one &&
>        +	test_must_fail git -C pm-positional rev-parse --verify refs/heads/two
>   5:  d691d5051b ! 6:  72aaca0666 branch: add branch.<name>.pruneMerged opt-out
>       @@ Metadata
>        Author: Harald Nordgren <haraldnordgren@gmail.com>
>        
>         ## Commit message ##
>       -    branch: add branch.<name>.pruneMerged opt-out
>       +    branch: add branch.<name>.deleteMerged opt-out
>        
>       -    Setting branch.<name>.pruneMerged=false exempts that branch from
>       -    "git branch --prune-merged", which is useful for a topic you want
>       +    Setting branch.<name>.deleteMerged=false exempts that branch from
>       +    "git branch --delete-merged", which is useful for a topic you want
>            to keep developing after an early round of it has been merged
>            upstream. Unless --quiet is given, each skip is reported so the
>            user knows why their topic was kept.
>       @@ Documentation/config/branch.adoc: for details).
>         	automatically added to the `format-patch` cover letter or
>         	`request-pull` summary.
>        +
>       -+`branch.<name>.pruneMerged`::
>       ++`branch.<name>.deleteMerged`::
>        +	If set to `false`, branch _<name>_ is exempt from
>       -+	`git branch --prune-merged`.  Useful for a topic branch you
>       ++	`git branch --delete-merged`.  Useful for a topic branch you
>        +	intend to develop further after an initial round has been
>        +	merged upstream.  Defaults to true.  Explicit deletion via
>        +	`git branch -d` is unaffected.
>        
>         ## Documentation/git-branch.adoc ##
>       -@@ Documentation/git-branch.adoc: the upstream refs refreshed.
>       +@@ Documentation/git-branch.adoc: A branch is not deleted when:
>         +
>       - A branch is left alone if any of the following holds:
>       - its upstream no longer resolves locally; it is checked out in any
>       --worktree; or its push destination (`<branch>@{push}`) equals its
>       -+worktree; its push destination (`<branch>@{push}`) equals its
>       - upstream (`<branch>@{upstream}`), so it cannot be distinguished
>       --from a freshly pulled trunk that just looks "fully merged".
>       -+from a freshly pulled trunk that just looks "fully merged"; or
>       -+`branch.<name>.pruneMerged` is set to `false`.
>       + --
>       + * its upstream remote-tracking branch no longer exists,
>       +-* it is checked out in any worktree, or
>       ++* it is checked out in any worktree,
>       + * its push destination (`<branch>@{push}`) equals its upstream
>       +   (`<branch>@{upstream}`), so it cannot be distinguished from a
>       +-  branch that just looks "fully merged" right after a pull.
>       ++  branch that just looks "fully merged" right after a pull, or
>       ++* `branch.<name>.deleteMerged` is set to `false`.
>       + --
>         +
>       - Branches refused by the "fully merged" safety check are listed as
>       - warnings and skipped; pass them to `git branch -D` explicitly if
>       + A branch whose work has not yet been merged into its upstream is
>        
>         ## builtin/branch.c ##
>       -@@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv,
>       +@@ builtin/branch.c: static int delete_merged_branches(int argc, const char **argv,
>         		const char *short_name;
>         		struct branch *branch;
>         		const char *upstream, *push;
>       @@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv,
>        +		int opt_out;
>         
>         		if (!skip_prefix(full_name, "refs/heads/", &short_name))
>       - 			continue;
>       -@@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv,
>       + 			BUG("filter returned non-branch ref '%s'", full_name);
>       +@@ builtin/branch.c: static int delete_merged_branches(int argc, const char **argv,
>         		if (!push || !strcmp(push, upstream))
>         			continue;
>         
>       -+		strbuf_addf(&key, "branch.%s.prunemerged", short_name);
>       ++		strbuf_addf(&key, "branch.%s.deletemerged", short_name);
>        +		if (!repo_config_get_bool(the_repository, key.buf, &opt_out) &&
>        +		    !opt_out) {
>        +			if (!quiet)
>        +				fprintf(stderr,
>       -+					_("Skipping '%s' (branch.%s.pruneMerged is false)\n"),
>       ++					_("Skipping '%s' (branch.%s.deleteMerged is false)\n"),
>        +					short_name, short_name);
>        +			strbuf_release(&key);
>        +			continue;
>       @@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv,
>         
>        
>         ## t/t3200-branch.sh ##
>       -@@ t/t3200-branch.sh: test_expect_success '--prune-merged takes positional <branch> arguments' '
>       +@@ t/t3200-branch.sh: test_expect_success '--delete-merged takes positional <branch> arguments' '
>         	test_must_fail git -C pm-positional rev-parse --verify refs/heads/two
>         '
>         
>       -+test_expect_success '--prune-merged honours branch.<name>.pruneMerged=false' '
>       ++test_expect_success '--delete-merged honours branch.<name>.deleteMerged=false' '
>        +	test_when_finished "rm -rf pm-optout" &&
>        +	git clone pm-upstream pm-optout &&
>        +	git -C pm-optout remote add fork ../pm-fork &&
>       @@ t/t3200-branch.sh: test_expect_success '--prune-merged takes positional <branch>
>        +	git -C pm-optout branch --set-upstream-to=origin/next one &&
>        +	git -C pm-optout branch two two-commit &&
>        +	git -C pm-optout branch --set-upstream-to=origin/next two &&
>       -+	test_config -C pm-optout branch.one.pruneMerged false &&
>       ++	test_config -C pm-optout branch.one.deleteMerged false &&
>        +
>       -+	git -C pm-optout branch --prune-merged "origin/*" 2>err &&
>       ++	git -C pm-optout branch --delete-merged "origin/*" 2>err &&
>        +
>        +	git -C pm-optout rev-parse --verify refs/heads/one &&
>        +	test_must_fail git -C pm-optout rev-parse --verify refs/heads/two &&
>        +	test_grep "Skipping .one." err
>        +'
>        +
>       -+test_expect_success 'branch -d still deletes a pruneMerged=false branch' '
>       ++test_expect_success 'branch -d still deletes a deleteMerged=false branch' '
>        +	test_when_finished "rm -rf pm-optout-d" &&
>        +	git clone pm-upstream pm-optout-d &&
>        +	git -C pm-optout-d branch one one-commit &&
>        +	git -C pm-optout-d branch --set-upstream-to=origin/next one &&
>       -+	test_config -C pm-optout-d branch.one.pruneMerged false &&
>       ++	test_config -C pm-optout-d branch.one.deleteMerged false &&
>        +
>        +	git -C pm-optout-d branch -d one &&
>        +	test_must_fail git -C pm-optout-d rev-parse --verify refs/heads/one
>   6:  ede8c61729 ! 7:  7b2b01b988 branch: add --dry-run for --prune-merged
>       @@ Metadata
>        Author: Harald Nordgren <haraldnordgren@gmail.com>
>        
>         ## Commit message ##
>       -    branch: add --dry-run for --prune-merged
>       +    branch: add --dry-run for --delete-merged
>        
>       -    With --dry-run, --prune-merged prints the local branches it would
>       +    With --dry-run, --delete-merged prints the local branches it would
>            delete, one "Would delete branch <name>" line each, and exits
>            without touching any ref. The same filtering applies, so the output
>            is exactly the set that the real run would delete.
>        
>       -    --dry-run is only meaningful together with --prune-merged and is
>       +    --dry-run is only meaningful together with --delete-merged and is
>            rejected otherwise.
>        
>            Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
>       @@ Documentation/git-branch.adoc: git branch (-m|-M) [<old-branch>] <new-branch>
>         git branch (-c|-C) [<old-branch>] <new-branch>
>         git branch (-d|-D) [-r] <branch-name>...
>         git branch --edit-description [<branch-name>]
>       --git branch --prune-merged <branch>...
>       -+git branch [--dry-run] --prune-merged <branch>...
>       +-git branch --delete-merged <branch>...
>       ++git branch [--dry-run] --delete-merged <branch>...
>         
>         DESCRIPTION
>         -----------
>       -@@ Documentation/git-branch.adoc: Branches refused by the "fully merged" safety check are listed as
>       - warnings and skipped; pass them to `git branch -D` explicitly if
>       - you want them gone.
>       +@@ Documentation/git-branch.adoc: A branch whose work has not yet been merged into its upstream is
>       + silently skipped. Delete it with `git branch -D` if you want to
>       + remove it anyway.
>         
>        +`--dry-run`::
>       -+	With `--prune-merged`, print which branches would be
>       ++	With `--delete-merged`, print which branches would be
>        +	deleted and exit without touching any ref.  Useful for
>        +	sanity-checking a wide pattern like `'origin/*'` before
>        +	committing to the deletion.
>       @@ builtin/branch.c
>        @@ builtin/branch.c: static int parse_opt_forked(const struct option *opt, const char *arg, int unset
>         }
>         
>       - static int prune_merged_branches(int argc, const char **argv,
>       + static int delete_merged_branches(int argc, const char **argv,
>        -				 int quiet)
>        +				 int quiet, int dry_run)
>         {
>         	struct ref_store *refs = get_main_ref_store(the_repository);
>         	struct ref_filter filter = REF_FILTER_INIT;
>       -@@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv,
>       +@@ builtin/branch.c: static int delete_merged_branches(int argc, const char **argv,
>         				      FILTER_REFS_BRANCHES,
>       - 				      DELETE_BRANCH_WARN_ONLY |
>       + 				      DELETE_BRANCH_SKIP_UNMERGED |
>         				      DELETE_BRANCH_NO_HEAD_FALLBACK |
>        -				      (quiet ? DELETE_BRANCH_QUIET : 0));
>        +				      (quiet ? DELETE_BRANCH_QUIET : 0) |
>       @@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv,
>        @@ builtin/branch.c: int cmd_branch(int argc,
>         	int delete = 0, rename = 0, copy = 0, list = 0,
>         	    unset_upstream = 0, show_current = 0, edit_description = 0;
>       - 	int prune_merged = 0;
>       + 	int delete_merged = 0;
>        +	int dry_run = 0;
>         	const char *new_upstream = NULL;
>         	int noncreate_actions = 0;
>         	/* possible options */
>        @@ builtin/branch.c: int cmd_branch(int argc,
>         			 N_("edit the description for the branch")),
>       - 		OPT_BOOL(0, "prune-merged", &prune_merged,
>       - 			N_("delete local branches whose upstream matches <branch> and is merged")),
>       + 		OPT_BOOL(0, "delete-merged", &delete_merged,
>       + 			N_("delete local branches whose upstream matches <branch> and are merged")),
>        +		OPT_BOOL(0, "dry-run", &dry_run,
>       -+			N_("with --prune-merged, only print which branches would be deleted")),
>       ++			N_("with --delete-merged, only print which branches would be deleted")),
>         		OPT__FORCE(&force, N_("force creation, move/rename, deletion"), PARSE_OPT_NOCOMPLETE),
>         		OPT_MERGED(&filter, N_("print only branches that are merged")),
>         		OPT_NO_MERGED(&filter, N_("print only branches that are not merged")),
>       @@ builtin/branch.c: int cmd_branch(int argc,
>         	if (noncreate_actions > 1)
>         		usage_with_options(builtin_branch_usage, options);
>         
>       -+	if (dry_run && !prune_merged)
>       -+		die(_("--dry-run requires --prune-merged"));
>       ++	if (dry_run && !delete_merged)
>       ++		die(_("--dry-run requires --delete-merged"));
>        +
>         	if (recurse_submodules_explicit) {
>         		if (!submodule_propagate_branches)
>       @@ builtin/branch.c: int cmd_branch(int argc,
>        @@ builtin/branch.c: int cmd_branch(int argc,
>         				      (quiet ? DELETE_BRANCH_QUIET : 0));
>         		goto out;
>       - 	} else if (prune_merged) {
>       --		ret = prune_merged_branches(argc, argv, quiet);
>       -+		ret = prune_merged_branches(argc, argv, quiet, dry_run);
>       + 	} else if (delete_merged) {
>       +-		ret = delete_merged_branches(argc, argv, quiet);
>       ++		ret = delete_merged_branches(argc, argv, quiet, dry_run);
>         		goto out;
>         	} else if (show_current) {
>         		print_current_branch_name();
>        
>         ## t/t3200-branch.sh ##
>       -@@ t/t3200-branch.sh: test_expect_success 'branch -d still deletes a pruneMerged=false branch' '
>       +@@ t/t3200-branch.sh: test_expect_success 'branch -d still deletes a deleteMerged=false branch' '
>         	test_must_fail git -C pm-optout-d rev-parse --verify refs/heads/one
>         '
>         
>       -+test_expect_success '--prune-merged --dry-run lists but does not delete' '
>       ++test_expect_success '--delete-merged --dry-run lists but does not delete' '
>        +	test_when_finished "rm -rf pm-dry" &&
>        +	git clone pm-upstream pm-dry &&
>        +	git -C pm-dry remote add fork ../pm-fork &&
>       @@ t/t3200-branch.sh: test_expect_success 'branch -d still deletes a pruneMerged=fa
>        +	git -C pm-dry branch two two-commit &&
>        +	git -C pm-dry branch --set-upstream-to=origin/next two &&
>        +
>       -+	git -C pm-dry branch --dry-run --prune-merged "origin/*" >actual &&
>       ++	git -C pm-dry branch --dry-run --delete-merged "origin/*" >actual &&
>        +	test_grep "Would delete branch one " actual &&
>        +	test_grep "Would delete branch two " actual &&
>        +
>       @@ t/t3200-branch.sh: test_expect_success 'branch -d still deletes a pruneMerged=fa
>        +	git -C pm-dry rev-parse --verify refs/heads/two
>        +'
>        +
>       -+test_expect_success '--prune-merged --dry-run only lists branches the live run would delete' '
>       ++test_expect_success '--delete-merged --dry-run only lists branches the live run would delete' '
>        +	test_when_finished "rm -rf pm-dry-mixed" &&
>        +	git clone pm-upstream pm-dry-mixed &&
>        +	git -C pm-dry-mixed remote add fork ../pm-fork &&
>       @@ t/t3200-branch.sh: test_expect_success 'branch -d still deletes a pruneMerged=fa
>        +	git -C pm-dry-mixed branch merged one-commit &&
>        +	git -C pm-dry-mixed branch --set-upstream-to=origin/next merged &&
>        +
>       -+	git -C pm-dry-mixed branch --dry-run --prune-merged "origin/*" >out &&
>       ++	git -C pm-dry-mixed branch --dry-run --delete-merged "origin/*" >out &&
>        +	test_grep "Would delete branch merged" out &&
>        +	test_grep ! "Would delete branch wip" out &&
>        +	git -C pm-dry-mixed rev-parse --verify refs/heads/wip &&
>        +	git -C pm-dry-mixed rev-parse --verify refs/heads/merged
>        +'
>        +
>       -+test_expect_success '--dry-run without --prune-merged is rejected' '
>       ++test_expect_success '--dry-run without --delete-merged is rejected' '
>        +	test_must_fail git -C forked branch --dry-run 2>err &&
>       -+	test_grep "requires --prune-merged" err
>       ++	test_grep "requires --delete-merged" err
>        +'
>        +
>         test_done
> 


^ permalink raw reply

* Re: [PATCH] rebase: mention --abort alongside --continue
From: Phillip Wood @ 2026-06-17  9:52 UTC (permalink / raw)
  To: Junio C Hamano, Phillip Wood
  Cc: Harald Nordgren via GitGitGadget, git, Harald Nordgren
In-Reply-To: <xmqqpl1q2xw5.fsf@gitster.g>

On 16/06/2026 18:33, Junio C Hamano wrote:
> Phillip Wood <phillip.wood123@gmail.com> writes:
> 
>> Hi Harald
>>
>> On 15/06/2026 20:19, Harald Nordgren via GitGitGadget wrote:
>>> From: Harald Nordgren <haraldnordgren@gmail.com>
>>>
>>> The warning shown when an "exec" step fails and the "git status"
>>> advice while splitting or editing a commit pointed users at "git
>>> rebase --continue" but not "--abort". Mention it in both, matching
>>> the conflict case.
>>
>> I'm not sure that the "failed exec" and "conflicts" cases are equivalent
>> though. If you have some nasty conflict that you don't want to resolve
>> then aborting and trying another approach such is incrementally rebasing
>> is the only option. If an exec command fails then it likely means that a
>> test has failed or some something similar which is minor inconvenience
>> which needs fixing before continuing - it seems very unlikely that the
>> user would want to abort the rebase.
> 
> It is very true that users who know what they are doing and got into
> such conflicts are opted to go into such a situation tnat it is
> unlikely that they would appreciate a choice to abort.

That's not quite what I was trying to say which was that aborting in the 
case of conflicts is more likely than in the case of a failed exec.

> But given that for any system, everybody starts as a newbie, it may
> be assuring to always give "here is a way out" option when they get
> in a nasty confusing situation.  Discouraging the way to use the
> tool that can lead to confusing situation by guiding them with BCP
> workflows would help, but they always get into pitfall.
> 
> The patch adds new message into the existing message to suggest how
> to move forward, but as a training wheel option, it may not be a bad
> thing to offer "--abort" as an extra hint, separate from the
> existing warning() message.

So if I've understood we'd print a message explaining what's happened 
and how to continue followed by a hint about aborting. The message would 
depend on what problem caused the rebase to stop, but the hint would be 
the same in each case. That sounds fine to me.

Thanks

Phillip


^ permalink raw reply

* Re: [PATCH v2 0/2] rebase: add --squash to fold a range into its first commit
From: Phillip Wood @ 2026-06-17  9:48 UTC (permalink / raw)
  To: Harald Nordgren, Phillip Wood
  Cc: Harald Nordgren via GitGitGadget, git, D. Ben Knoble,
	Patrick Steinhardt, Junio C Hamano
In-Reply-To: <CAHwyqnV_pt1fEhUGPyGtXrJAwhjpQHOyX9juHRv_88T2md554Q@mail.gmail.com>

On 17/06/2026 10:11, Harald Nordgren wrote:
> On Tue, Jun 16, 2026 at 12:10 PM Phillip Wood <phillip.wood123@gmail.com> wrote:
>> On 15/06/2026 09:37, Harald Nordgren via GitGitGadget wrote:
>>> Rename to rebase --squash.
>>
>> Please include the original cover letter as well so people who have not
>> read the previous version know what the series is about.
> 
> So you mean this one, should that be included in each version, and
> append each subsequent one:
> 
> ```
> Adds `git rebase --autosquash --fixup [<upstream>]` to fold a range of
> commits into its oldest one, reusing that commit's message.

Yes, see 
https://lore.kernel.org/20260615-b4-pks-history-drop-v6-0-2e329e536d78@pks.im 
for an example and the "Cover Letter" section of 
Documentation/SubmittingPatches

Thanks

Phillip

> 
> Related idea: https://github.com/gitgitgadget/git/issues/1135
> ```
> 
> Or make each message a full cover letter instead of just a diff?
> 
> 
> 
> 
> Harald
> 


^ permalink raw reply

* Re: [PATCH 0/2] rebase: add --fixup to fold a range into its oldest commit
From: Harald Nordgren @ 2026-06-17  9:30 UTC (permalink / raw)
  To: Patrick Steinhardt
  Cc: D. Ben Knoble, Junio C Hamano, Harald Nordgren via GitGitGadget,
	git
In-Reply-To: <ajEKf-jCIDVPQCeO@pks.im>

> Yes, it does fit into git-history(1), and I do indeed already have plans
> to implement such a command going forward. I wouldn't mind at all though
> if somebody else beat me to it, I want to implement at least one more
> command before I get to this.

What I like about 'git rebase --squash' is that when upstream is set
up, it understands the commit range automatically, whereas history
feels more removed from the current upstream. Maybe I'm wrong about
that.


Harald

^ permalink raw reply

* Re: [PATCH v2 0/2] rebase: add --squash to fold a range into its first commit
From: Harald Nordgren @ 2026-06-17  9:11 UTC (permalink / raw)
  To: Phillip Wood
  Cc: Harald Nordgren via GitGitGadget, git, D. Ben Knoble,
	Patrick Steinhardt, Junio C Hamano
In-Reply-To: <d55b6600-50f3-4e81-a6bf-d270cd7abd2d@gmail.com>

On Tue, Jun 16, 2026 at 12:10 PM Phillip Wood <phillip.wood123@gmail.com> wrote:
>
> Hi Harald
>
> On 15/06/2026 09:37, Harald Nordgren via GitGitGadget wrote:
> > Rename to rebase --squash.
>
> Please include the original cover letter as well so people who have not
> read the previous version know what the series is about.

So you mean this one, should that be included in each version, and
append each subsequent one:

```
Adds `git rebase --autosquash --fixup [<upstream>]` to fold a range of
commits into its oldest one, reusing that commit's message.

Related idea: https://github.com/gitgitgadget/git/issues/1135
```

Or make each message a full cover letter instead of just a diff?




Harald

^ permalink raw reply

* Re: [PATCH] rebase: mention --abort alongside --continue
From: Harald Nordgren @ 2026-06-17  8:56 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Phillip Wood, Harald Nordgren via GitGitGadget, git
In-Reply-To: <xmqqpl1q2xw5.fsf@gitster.g>

For the record, I have gotten into ill-fated rebases, and then it
would have been nice to have that message there. Sometimes you realize
after starting that you chose the wrong upstream, etc.


Harald

^ permalink raw reply

* Re: [PATCH 2/6] SubmittingPatches: discuss non-ident trailers
From: Patrick Steinhardt @ 2026-06-17  7:32 UTC (permalink / raw)
  To: Kristoffer Haugsbakk; +Cc: git
In-Reply-To: <43cd8c48-c933-4eef-a6c0-970c38885967@app.fastmail.com>

On Tue, Jun 16, 2026 at 10:02:46PM +0200, Kristoffer Haugsbakk wrote:
> On Fri, Jun 12, 2026, at 13:35, Patrick Steinhardt wrote:
> > On Thu, Jun 11, 2026 at 12:22:45AM +0200,
> > kristofferhaugsbakk@fastmail.com wrote:
> >> diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches
> >> index 0b12badf86d..51c308a89a8 100644
> >> --- a/Documentation/SubmittingPatches
> >> +++ b/Documentation/SubmittingPatches
> >> @@ -474,7 +474,10 @@ These are the common trailers in use:
> >>
> >>  While you can also create your own trailer if the situation warrants it, we
> >>  encourage you to instead use one of the common trailers in this project
> >> -highlighted above.
> >> +highlighted above. A trailer that credits someone might be more likely
> >> +to be accepted since these are the most common ones. But another kind of
> >> +trailer might be relevant, for example to link to an issue tracker
> >> +belonging to a downstream project that is affected by a bug in Git.
> >
> > Hm, I wonder whether this is a bit too vague to really be helpful for a
> > newcomer. Instead of alluding to such trailers, wouldn't it be
> > preferable if we added those as actual examples to the list of known
> > trailers and then tell folks that they can invent their own ones if
> > there is a good reason to do so?
> 
> Honestly there are so few non-ident trailers that I don’t think they can
> be listed as common trailers:
> 
> 1. The Git project doesn’t need them (e.g. no bug tracker)
> 2. They seem mostly for use by other projects (bug trackers again)
> 
> With this list:
> 
>     git log --format='%(trailers:only,keyonly)' | sort | uniq
> 
> If you filter out the ident-looking ones:
> 
>     grep -v --extended-regexp -- '-[Bb]y$'
> 
> There are few left. And some can be discarded:
> 
> • Change-Id
> • Message-ID
> • Fixes (pointing to a commit)
> 
> So to address your point:
> 
> 1. Maybe this is so niche that it is not worth mentioning; or
> 2. Maybe give a concrete example like `Closes: <bug link>`?

Well, we don't use "Closes:" trailers, either. So I'd rather side with
your (1) and just not mention them at all.

Patrick

^ permalink raw reply

* Re: [PATCH 4/4] builtin/refs: add "rename" subcommand
From: Patrick Steinhardt @ 2026-06-17  7:28 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git
In-Reply-To: <xmqqo6ha4jw1.fsf@gitster.g>

On Tue, Jun 16, 2026 at 07:53:02AM -0700, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
[snip]
> > +	if (!refs_ref_exists(get_main_ref_store(repo), oldref))
> > +		die(_("reference does not exist: '%s'"), oldref);
> > +	if (refs_ref_exists(get_main_ref_store(repo), newref))
> > +		die(_("reference already exists: '%s'"), newref);
> > +
> > +	return refs_rename_ref(get_main_ref_store(repo), oldref, newref, message);
> > +}
> 
> I suspect that my version shared the same issue, but doesn't
> refs_rename_ref() return -1 for failure, which we may want to turn
> to positive 1 before returning?
> 
> This is a tangent but git.c:handle_builtin() that calls
> git.c:run_builtin() may want to do the "negative return? flip the
> polarity" conversion to make this worry go away.  I dunno what such
> a change would break, though.

Fair. The other subcommands also suffer from the same problem, so I'll
update all of them to return 1 explicitly.

> If we rename a ref that does not have a reflog, would it leave the
> ref under the new name without reflog, or would we get a reflog with
> a single entry that marks the fact the old ref was renamed into the
> new ref?  Should that be controlled via --create-reflog option?

It would leave it without a reflog. In theory I agree that it might make
sense to introduce a "--create-reflog" option, but that would require
some new plumbing in `refs_rename_ref()`. So I'd say that we can add it
at a later point as needed.

Patrick

^ permalink raw reply

* Re: [PATCH 3/4] builtin/refs: add "update" subcommand
From: Patrick Steinhardt @ 2026-06-17  7:28 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git
In-Reply-To: <xmqqse6m4jw4.fsf@gitster.g>

On Tue, Jun 16, 2026 at 07:52:59AM -0700, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> 
> >  git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]
> > +git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]
> 
> "<old-value> vs <new-value>" is good, we should update "delete" to
> use "<old-value>" to match.

Good catch.

> > @@ -58,6 +59,12 @@ delete::
> >  	reference is only deleted after verifying that it currently contains
> >  	`<oldvalue>`.
> >  
> > +update::
> > +	Update the given reference to point at `<new-value>`. This subcommand
> > +	mirrors `git update-ref` (see linkgit:git-update-ref[1]). When
> > +	`<old-value>` is given, the reference is only updated after verifying
> > +	that it currently contains `<old-value>`.
> 
> As to the lack of "create", among the two potential changes, I have
> a slight preference for adding "create" and failing "update" that
> does not refer to an existing ref.  If we go that route, the
> "--create-reflog" option should move to "create", as "update" will
> never be used to create a new ref.

I agree that we should add "create", but I also think that we should
keep the special syntax of:

  - "git refs update $NULL_OID [$OLD_OID]" to delete a branch.

  - "git refs update $NEW_OID $NULL_OID" to create a branch.

While the other commands are more ergonomic, I don't see a strong reason
to restrict the "update" subcommand. Also, this ensures that it's in
line with git-update-ref(1), which also allows for these use cases.

> > +	if (repo_get_oid_with_flags(repo, argv[1], &newoid,
> > +				    GET_OID_SKIP_AMBIGUITY_CHECK))
> > +		die(_("invalid new object ID: %s"), argv[1]);
> > +	if (argc == 3 &&
> > +	    repo_get_oid_with_flags(repo, argv[2], &oldoid,
> > +				    GET_OID_SKIP_AMBIGUITY_CHECK))
> > +		die(_("invalid old object ID: %s"), argv[2]);
> 
> On the "delete" side, these messages quote the object name, i.e.,
> 
> 			die(_("invalid old object ID: '%s'"), argv[1]);
> 
> We should be consistent.

Yup, fixed now.

Patrick

^ permalink raw reply

* Re: [PATCH 3/4] builtin/refs: add "update" subcommand
From: Patrick Steinhardt @ 2026-06-17  7:28 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git
In-Reply-To: <xmqqeci6bupk.fsf@gitster.g>

On Tue, Jun 16, 2026 at 04:17:27AM -0700, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> 
> > Add a new "update" subcommand which mirrors `git update-ref <refname>
> > <oldoid> <newoid>`. This follows the same reasoning as the preceding
> > commit.
> >
> > Signed-off-by: Patrick Steinhardt <ps@pks.im>
> > ---
> >  Documentation/git-refs.adoc |   7 ++
> >  builtin/refs.c              |  50 +++++++++++++
> >  t/meson.build               |   1 +
> >  t/t1465-refs-update.sh      | 179 ++++++++++++++++++++++++++++++++++++++++++++
> >  4 files changed, 237 insertions(+)
> 
> I do not offhand know (and I am still away by 2 hours from the time
> I wake up and start functioning) if update-ref shares the same
> issue, but with "delete, update, rename" combo, lack of "create"
> feels a bit annoying.  Wouldn't we want to offer an option to users
> who want to ensure that the refs they create are truly new and they
> are not overwriting a ref somebody has created?  Either (1) drop
> "delete" and take a special value (e.g. "") as <newvalue> to signal
> deletion and make the same special value used as <oldvalue> signals
> creation, or (2) add "create" and insist that "update" takes only an
> existing ref, would make the annoyance go away, I guess.

In theory "update" can handle both updating existing references,
deleting them and creating them race-free by providing NUL object IDs
for either old or new value. But I agree that this is cumbersome, and
adding another "create" subcommand as an easy-to-understand shortcut
feels sensible.

> > +test_expect_success 'update creates a new reference' '
> > +	test_when_finished "rm -rf repo" &&
> > +	setup_repo repo &&
> > +	(
> > +		cd repo &&
> > +		A=$(git rev-parse A) &&
> > +		git refs update refs/heads/foo $A &&
> > +		test_ref_matches refs/heads/foo "$A"
> > +	)
> > +'
> 
> Here we cannot test (and I strongly suspect that "git refs update"
> and "git update-ref" lack ability to do so) a case where a creation
> is attempted on an existing ref and fails.

We can:

    $ git update-ref $NEW_OID $NULL_OID
    $ git refs update $NEW_OID $NULL_OID

This will verify that the reference doesn't exist before actually
writing it. Will add a test.

Patrick

^ permalink raw reply

* Re: [PATCH v2 04/17] odb/source-packed: store pointer to "files" instead of generic source
From: Patrick Steinhardt @ 2026-06-17  7:26 UTC (permalink / raw)
  To: Justin Tobler; +Cc: git, Karthik Nayak
In-Reply-To: <ajG69JZHx_u2mt7q@denethor>

On Tue, Jun 16, 2026 at 04:14:02PM -0500, Justin Tobler wrote:
> On 26/06/09 10:50AM, Patrick Steinhardt wrote:
[snip]
> > Detangling this mess is somewhat intricate though, and is made even more
> > intricate because it's not always clear which kind of source one is
> > holding at a specific point in time -- either the parent "files" source,
> > or the child "packed" source.
> > 
> > Make this relationship more explicit by storing a pointer to the "files"
> > source instead of storing a pointer to a generic `struct odb_source`.
> > This will help make subsequent steps a bit clearer.
> > 
> > Note that this is a temporary step, only. At the end of this series
> > we will have dropped the parent pointer completely.
> 
> Ok, so IIUC the eventual goal is to get rid of the pointer entirely, but
> for now we are just making its concrete type explicit without having to
> downcast. It's not immediately obvious to me how this step gets us
> closer to that goal, but that may become more obvious in the next
> patches. :)

The reason this helps is that during the intermediate steps, it's at
times very hard to see which source you're holding if we have both the
generic `base` and the generic `parent` pointer. That makes it quite
easy to confuse those two. So by turning the latter into the `files`
source, some of the intermediate steps become easier to reason about.

Patrick

^ permalink raw reply

* Re: [PATCH v2 17/17] odb/source-packed: drop pointer to "files" parent source
From: Patrick Steinhardt @ 2026-06-17  7:26 UTC (permalink / raw)
  To: Justin Tobler; +Cc: git, Karthik Nayak
In-Reply-To: <ajHRkrsvNtcBMFx2@denethor>

On Tue, Jun 16, 2026 at 05:51:43PM -0500, Justin Tobler wrote:
> On 26/06/09 10:51AM, Patrick Steinhardt wrote:
> > @@ -626,7 +625,7 @@ static void prepare_pack(const char *full_name, size_t full_name_len,
> >  		report_garbage(PACKDIR_FILE_GARBAGE, full_name);
> >  }
> >  
> > -static void prepare_packed_git_one(struct odb_source *source)
> > +static void prepare_packed_git_one(struct odb_source_packed *source)
> 
> At first I was a bit confused to see this change here, but IIUC
> previously this function was passed the "base" source of the "files"
> ODB. Now that we have a proper "packed" source we can use that directly
> instead.

Yes, exactly.

Thanks for your review!

Patrick

^ permalink raw reply

* Re: [PATCH v2 16/17] midx: refactor interfaces to work on "packed" source
From: Patrick Steinhardt @ 2026-06-17  7:26 UTC (permalink / raw)
  To: Justin Tobler; +Cc: git, Karthik Nayak
In-Reply-To: <ajHKmbimHJnFV2IE@denethor>

On Tue, Jun 16, 2026 at 05:37:53PM -0500, Justin Tobler wrote:
> On 26/06/09 10:51AM, Patrick Steinhardt wrote:
> > diff --git a/builtin/multi-pack-index.c b/builtin/multi-pack-index.c
> > index 00ffb36394..6e73c85cde 100644
> > --- a/builtin/multi-pack-index.c
> > +++ b/builtin/multi-pack-index.c
> > @@ -85,12 +86,12 @@ static int parse_object_dir(const struct option *opt, const char *arg,
> >  	return 0;
> >  }
> >  
> > -static struct odb_source *handle_object_dir_option(struct repository *repo)
> > +static struct odb_source_files *handle_object_dir_option(struct repository *repo)
> >  {
> >  	struct odb_source *source = odb_find_source(repo->objects, opts.object_dir);
> >  	if (!source)
> >  		source = odb_add_to_alternates_memory(repo->objects, opts.object_dir);
> > -	return source;
> > +	return odb_source_files_downcast(source);
> 
> Now that we are passing around concrete ODB sources to many of these
> interfaces, callers may have to handle downcasting explicitly. Since now
> the downcasting may happen a bit ealier than previously in some cases, I
> wondered if this would alter any error flows. I don't think this would
> be the case though because ultimately we only have the "files" source
> currently anyways.

Well, we'd be calling `BUG()` all the same. So as we don't have
conditional logic anywhere (yet?) that checks whether or not we use the
"files" backend all error flows are expected to ultimately be the same.
It's only that we may `BUG()` a bit earlier, but I guess the user is
going to be somewhat indifferent about this.

Patrick

^ permalink raw reply

* Re: [PATCH v2 11/17] odb/source-packed: wire up `for_each_object()` callback
From: Patrick Steinhardt @ 2026-06-17  7:26 UTC (permalink / raw)
  To: Justin Tobler; +Cc: git, Karthik Nayak
In-Reply-To: <ajHIjrQJvWtbCrZp@denethor>

On Tue, Jun 16, 2026 at 05:10:28PM -0500, Justin Tobler wrote:
> On 26/06/09 10:51AM, Patrick Steinhardt wrote:
> > diff --git a/packfile.c b/packfile.c
> > index 42c84397eb..b8d6054c16 100644
> > --- a/packfile.c
> > +++ b/packfile.c
> > @@ -1362,8 +1362,8 @@ static void add_delta_base_cache(struct packed_git *p, off_t base_offset,
> >  	hashmap_add(&delta_base_cache, &ent->ent);
> >  }
> >  
> > -static int packed_object_info_with_index_pos(struct packed_git *p, off_t obj_offset,
> > -					     uint32_t *maybe_index_pos, struct object_info *oi)
> > +int packed_object_info_with_index_pos(struct packed_git *p, off_t obj_offset,
> > +				      uint32_t *maybe_index_pos, struct object_info *oi)
> 
> Looks like we are also exposing `packed_object_info_with_index_pos()`
> now. Not sure yet if this is also intended to be temporary like
> `find_pack_entry()` in a previous patch though though.

No, it's not. This function is also required in case you just have a
single packfile to look up objects in there. So there are use cases
outside of the "packed" source where we need it going forward.

Patrick

^ permalink raw reply

* Re: [PATCH v2 07/17] odb/source-packed: wire up `reprepare()` callback
From: Patrick Steinhardt @ 2026-06-17  7:26 UTC (permalink / raw)
  To: Justin Tobler; +Cc: git, Karthik Nayak
In-Reply-To: <ajHDm_pG3Ckca4mp@denethor>

On Tue, Jun 16, 2026 at 04:53:38PM -0500, Justin Tobler wrote:
> On 26/06/09 10:51AM, Patrick Steinhardt wrote:
> > Move the logic to prepare and reprepare the "packed" source into
> > "odb/source-packed.c" and wire it up as the `reprepare()` callback.
> > 
> > Note that "preparing" a source is not yet generic. Eventually, it would
> > probably make sense to turn the existing `reprepare()` callback into a
> > `prepare()` callback with an optional flag to force re-preparing. But
> > this step will be handled in a separate patch series.
> 
> I do find the prepare vs reprepare semantics a bit confusing. The
> mentioned change above would be nice to see in the future. :)

It is confusing, agreed. I have already written the patch series that
converts this.

Patrick

^ permalink raw reply

* Re: [PATCH v2 05/17] odb/source-packed: start converting to a proper `struct odb_source`
From: Patrick Steinhardt @ 2026-06-17  7:26 UTC (permalink / raw)
  To: Justin Tobler; +Cc: git, Karthik Nayak
In-Reply-To: <ajG-rP7cRjZa3zJE@denethor>

On Tue, Jun 16, 2026 at 04:36:59PM -0500, Justin Tobler wrote:
> On 26/06/09 10:50AM, Patrick Steinhardt wrote:
> >  struct odb_source_packed *odb_source_packed_new(struct odb_source_files *parent)
> >  {
> > -	struct odb_source_packed *store;
> > -	CALLOC_ARRAY(store, 1);
> > -	store->files = parent;
> > -	strmap_init(&store->packs_by_path);
> > -	return store;
> > +	struct odb_source_packed *packed;
> > +
> > +	CALLOC_ARRAY(packed, 1);
> > +	odb_source_init(&packed->base, parent->base.odb, ODB_SOURCE_PACKED,
> > +			parent->base.path, parent->base.local);
> > +	packed->files = parent;
> > +	strmap_init(&packed->packs_by_path);
> > +
> > +	packed->base.free = odb_source_packed_free;
> > +
> > +	if (!is_absolute_path(parent->base.path))
> > +		chdir_notify_register(NULL, odb_source_packed_reparent, packed);
> 
> Out of curiousity, did the packfile store previously not have to worry
> about changing directories?

No, it didn't, and at this step here it still doesn't. This is mostly
because we still use the path of the "files" source, and that source
handles the reparenting for us. But it will be needed in a subsequent
step once we get rid of the pointer to the owning "files" source.

Patrick

^ permalink raw reply

* [PATCH v3 17/17] odb/source-packed: drop pointer to "files" parent source
From: Patrick Steinhardt @ 2026-06-17  6:40 UTC (permalink / raw)
  To: git; +Cc: Karthik Nayak, Justin Tobler
In-Reply-To: <20260617-pks-odb-source-packed-v3-0-b5c7583cd795@pks.im>

Over the last commits we have turned the packfile store into a proper
object database source that can be used as a standalone backend. As
such, it is no longer necessary to have it coupled to the "files" parent
source.

Remove the pointer to the owning "files" source so that the "packed"
source can be used as a standalone entity.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 odb/source-files.c  |  2 +-
 odb/source-packed.c | 27 +++++++++++++--------------
 odb/source-packed.h |  7 ++++---
 packfile.c          |  2 +-
 4 files changed, 19 insertions(+), 19 deletions(-)

diff --git a/odb/source-files.c b/odb/source-files.c
index fa2e18e71b..3bc6419dd7 100644
--- a/odb/source-files.c
+++ b/odb/source-files.c
@@ -269,7 +269,7 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb,
 	CALLOC_ARRAY(files, 1);
 	odb_source_init(&files->base, odb, ODB_SOURCE_FILES, path, local);
 	files->loose = odb_source_loose_new(odb, path, local);
-	files->packed = odb_source_packed_new(files);
+	files->packed = odb_source_packed_new(odb, path, local);
 
 	files->base.free = odb_source_files_free;
 	files->base.close = odb_source_files_close;
diff --git a/odb/source-packed.c b/odb/source-packed.c
index d513b3efc3..42c28fba0e 100644
--- a/odb/source-packed.c
+++ b/odb/source-packed.c
@@ -585,7 +585,7 @@ static void report_pack_garbage(struct string_list *list)
 }
 
 struct prepare_pack_data {
-	struct odb_source *source;
+	struct odb_source_packed *source;
 	struct string_list *garbage;
 };
 
@@ -593,15 +593,14 @@ static void prepare_pack(const char *full_name, size_t full_name_len,
 			 const char *file_name, void *_data)
 {
 	struct prepare_pack_data *data = (struct prepare_pack_data *)_data;
-	struct odb_source_files *files = odb_source_files_downcast(data->source);
 	size_t base_len = full_name_len;
 
 	if (strip_suffix_mem(full_name, &base_len, ".idx") &&
-	    !(files->packed->midx &&
-	      midx_contains_pack(files->packed->midx, file_name))) {
+	    !(data->source->midx &&
+	      midx_contains_pack(data->source->midx, file_name))) {
 		char *trimmed_path = xstrndup(full_name, full_name_len);
-		packfile_store_load_pack(files->packed,
-					 trimmed_path, data->source->local);
+		packfile_store_load_pack(data->source,
+					 trimmed_path, data->source->base.local);
 		free(trimmed_path);
 	}
 
@@ -626,7 +625,7 @@ static void prepare_pack(const char *full_name, size_t full_name_len,
 		report_garbage(PACKDIR_FILE_GARBAGE, full_name);
 }
 
-static void prepare_packed_git_one(struct odb_source *source)
+static void prepare_packed_git_one(struct odb_source_packed *source)
 {
 	struct string_list garbage = STRING_LIST_INIT_DUP;
 	struct prepare_pack_data data = {
@@ -634,7 +633,7 @@ static void prepare_packed_git_one(struct odb_source *source)
 		.garbage = &garbage,
 	};
 
-	for_each_file_in_pack_dir(source->path, prepare_pack, &data);
+	for_each_file_in_pack_dir(source->base.path, prepare_pack, &data);
 
 	report_pack_garbage(data.garbage);
 	string_list_clear(data.garbage, 0);
@@ -675,7 +674,7 @@ void odb_source_packed_prepare(struct odb_source_packed *source)
 		return;
 
 	prepare_multi_pack_index_one(source);
-	prepare_packed_git_one(&source->files->base);
+	prepare_packed_git_one(source);
 
 	sort_packs(&source->packs.head, sort_pack);
 	for (struct packfile_list_entry *e = source->packs.head; e; e = e->next)
@@ -733,14 +732,14 @@ static void odb_source_packed_free(struct odb_source *source)
 	free(packed);
 }
 
-struct odb_source_packed *odb_source_packed_new(struct odb_source_files *parent)
+struct odb_source_packed *odb_source_packed_new(struct object_database *odb,
+						const char *path,
+						bool local)
 {
 	struct odb_source_packed *packed;
 
 	CALLOC_ARRAY(packed, 1);
-	odb_source_init(&packed->base, parent->base.odb, ODB_SOURCE_PACKED,
-			parent->base.path, parent->base.local);
-	packed->files = parent;
+	odb_source_init(&packed->base, odb, ODB_SOURCE_PACKED, path, local);
 	strmap_init(&packed->packs_by_path);
 
 	packed->base.free = odb_source_packed_free;
@@ -758,7 +757,7 @@ struct odb_source_packed *odb_source_packed_new(struct odb_source_files *parent)
 	packed->base.read_alternates = odb_source_packed_read_alternates;
 	packed->base.write_alternate = odb_source_packed_write_alternate;
 
-	if (!is_absolute_path(parent->base.path))
+	if (!is_absolute_path(path))
 		chdir_notify_register(NULL, odb_source_packed_reparent, packed);
 
 	return packed;
diff --git a/odb/source-packed.h b/odb/source-packed.h
index 9d4796261a..88994098c1 100644
--- a/odb/source-packed.h
+++ b/odb/source-packed.h
@@ -10,7 +10,6 @@
  */
 struct odb_source_packed {
 	struct odb_source base;
-	struct odb_source_files *files;
 
 	/*
 	 * The list of packfiles in the order in which they have been most
@@ -66,9 +65,11 @@ struct odb_source_packed {
 
 /*
  * Allocate and initialize a new empty packfile store for the given object
- * database source.
+ * database.
  */
-struct odb_source_packed *odb_source_packed_new(struct odb_source_files *parent);
+struct odb_source_packed *odb_source_packed_new(struct object_database *odb,
+						const char *path,
+						bool local);
 
 /*
  * Cast the given object database source to the packed backend. This will cause
diff --git a/packfile.c b/packfile.c
index a577275d4f..59cee7925d 100644
--- a/packfile.c
+++ b/packfile.c
@@ -801,7 +801,7 @@ struct packed_git *packfile_store_load_pack(struct odb_source_packed *store,
 
 	p = strmap_get(&store->packs_by_path, key.buf);
 	if (!p) {
-		p = add_packed_git(store->files->base.odb->repo, idx_path,
+		p = add_packed_git(store->base.odb->repo, idx_path,
 				   strlen(idx_path), local);
 		if (p)
 			packfile_store_add_pack(store, p);

-- 
2.55.0.rc0.786.g65d90a0328.dirty


^ permalink raw reply related

* [PATCH v3 16/17] midx: refactor interfaces to work on "packed" source
From: Patrick Steinhardt @ 2026-06-17  6:39 UTC (permalink / raw)
  To: git; +Cc: Karthik Nayak, Justin Tobler
In-Reply-To: <20260617-pks-odb-source-packed-v3-0-b5c7583cd795@pks.im>

Our interfaces used to interact with MIDXs all work on top of the
generic `struct odb_source`. This doesn't make much sense though: a MIDX
is strictly tied to the "packed" source, so passing in a generic source
gives the false sense that it may also work with a different type of
source.

Fix this conceptual weirdness and instead require the caller to pass in
a "packed" source explicitly. This also makes the next commit easier to
implement, where we drop the pointer to the "files" source in the
"packed" source.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/multi-pack-index.c |  29 +++++------
 builtin/pack-objects.c     |   3 +-
 builtin/repack.c           |   8 ++-
 midx-write.c               |  34 ++++++-------
 midx.c                     | 118 ++++++++++++++++++++++-----------------------
 midx.h                     |  30 ++++++------
 odb/source-packed.c        |  12 ++---
 pack-bitmap.c              |   8 +--
 pack-revindex.c            |   6 +--
 repack-geometry.c          |   3 +-
 repack-midx.c              |   9 ++--
 repack.c                   |   6 +--
 t/helper/test-read-midx.c  |   7 ++-
 13 files changed, 144 insertions(+), 129 deletions(-)

diff --git a/builtin/multi-pack-index.c b/builtin/multi-pack-index.c
index 00ffb36394..6e73c85cde 100644
--- a/builtin/multi-pack-index.c
+++ b/builtin/multi-pack-index.c
@@ -10,6 +10,7 @@
 #include "trace2.h"
 #include "odb.h"
 #include "odb/source.h"
+#include "odb/source-files.h"
 #include "replace-object.h"
 #include "repository.h"
 
@@ -85,12 +86,12 @@ static int parse_object_dir(const struct option *opt, const char *arg,
 	return 0;
 }
 
-static struct odb_source *handle_object_dir_option(struct repository *repo)
+static struct odb_source_files *handle_object_dir_option(struct repository *repo)
 {
 	struct odb_source *source = odb_find_source(repo->objects, opts.object_dir);
 	if (!source)
 		source = odb_add_to_alternates_memory(repo->objects, opts.object_dir);
-	return source;
+	return odb_source_files_downcast(source);
 }
 
 static struct option common_opts[] = {
@@ -167,7 +168,7 @@ static int cmd_multi_pack_index_write(int argc, const char **argv,
 			     N_("refs snapshot for selecting bitmap commits")),
 		OPT_END(),
 	};
-	struct odb_source *source;
+	struct odb_source_files *source;
 	int ret;
 
 	opts.flags |= MIDX_WRITE_BITMAP_HASH_CACHE;
@@ -211,7 +212,7 @@ static int cmd_multi_pack_index_write(int argc, const char **argv,
 
 		read_packs_from_stdin(&packs);
 
-		ret = write_midx_file_only(source, &packs,
+		ret = write_midx_file_only(source->packed, &packs,
 					   opts.preferred_pack,
 					   opts.refs_snapshot,
 					   opts.incremental_base, opts.flags);
@@ -223,7 +224,7 @@ static int cmd_multi_pack_index_write(int argc, const char **argv,
 
 	}
 
-	ret = write_midx_file(source, opts.preferred_pack,
+	ret = write_midx_file(source->packed, opts.preferred_pack,
 			      opts.refs_snapshot, opts.flags);
 
 	free(opts.refs_snapshot);
@@ -237,7 +238,7 @@ static int cmd_multi_pack_index_compact(int argc, const char **argv,
 	struct multi_pack_index *m, *cur;
 	struct multi_pack_index *from_midx = NULL;
 	struct multi_pack_index *to_midx = NULL;
-	struct odb_source *source;
+	struct odb_source_files *source;
 	int ret;
 
 	struct option *options;
@@ -282,7 +283,7 @@ static int cmd_multi_pack_index_compact(int argc, const char **argv,
 
 	FREE_AND_NULL(options);
 
-	m = get_multi_pack_index(source);
+	m = get_multi_pack_index(source->packed);
 
 	for (cur = m; cur && !(from_midx && to_midx); cur = cur->base_midx) {
 		const char *midx_csum = midx_get_checksum_hex(cur);
@@ -305,7 +306,7 @@ static int cmd_multi_pack_index_compact(int argc, const char **argv,
 			die(_("MIDX %s must be an ancestor of %s"), argv[0], argv[1]);
 	}
 
-	ret = write_midx_file_compact(source, from_midx, to_midx,
+	ret = write_midx_file_compact(source->packed, from_midx, to_midx,
 				      opts.incremental_base, opts.flags);
 
 	return ret;
@@ -319,7 +320,7 @@ static int cmd_multi_pack_index_verify(int argc, const char **argv,
 	static struct option builtin_multi_pack_index_verify_options[] = {
 		OPT_END(),
 	};
-	struct odb_source *source;
+	struct odb_source_files *source;
 
 	options = add_common_options(builtin_multi_pack_index_verify_options);
 
@@ -337,7 +338,7 @@ static int cmd_multi_pack_index_verify(int argc, const char **argv,
 
 	FREE_AND_NULL(options);
 
-	return verify_midx_file(source, opts.flags);
+	return verify_midx_file(source->packed, opts.flags);
 }
 
 static int cmd_multi_pack_index_expire(int argc, const char **argv,
@@ -348,7 +349,7 @@ static int cmd_multi_pack_index_expire(int argc, const char **argv,
 	static struct option builtin_multi_pack_index_expire_options[] = {
 		OPT_END(),
 	};
-	struct odb_source *source;
+	struct odb_source_files *source;
 
 	options = add_common_options(builtin_multi_pack_index_expire_options);
 
@@ -366,7 +367,7 @@ static int cmd_multi_pack_index_expire(int argc, const char **argv,
 
 	FREE_AND_NULL(options);
 
-	return expire_midx_packs(source, opts.flags);
+	return expire_midx_packs(source->packed, opts.flags);
 }
 
 static int cmd_multi_pack_index_repack(int argc, const char **argv,
@@ -379,7 +380,7 @@ static int cmd_multi_pack_index_repack(int argc, const char **argv,
 		  N_("during repack, collect pack-files of smaller size into a batch that is larger than this size")),
 		OPT_END(),
 	};
-	struct odb_source *source;
+	struct odb_source_files *source;
 
 	options = add_common_options(builtin_multi_pack_index_repack_options);
 
@@ -398,7 +399,7 @@ static int cmd_multi_pack_index_repack(int argc, const char **argv,
 
 	FREE_AND_NULL(options);
 
-	return midx_repack(source, (size_t)opts.batch_size, opts.flags);
+	return midx_repack(source->packed, (size_t)opts.batch_size, opts.flags);
 }
 
 int cmd_multi_pack_index(int argc,
diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c
index 5e94805478..424c92cc29 100644
--- a/builtin/pack-objects.c
+++ b/builtin/pack-objects.c
@@ -1775,7 +1775,8 @@ static int want_object_in_pack_mtime(const struct object_id *oid,
 	odb_prepare_alternates(the_repository->objects);
 
 	for (source = the_repository->objects->sources; source; source = source->next) {
-		struct multi_pack_index *m = get_multi_pack_index(source);
+		struct odb_source_files *files = odb_source_files_downcast(source);
+		struct multi_pack_index *m = get_multi_pack_index(files->packed);
 		struct pack_entry e;
 
 		if (m && fill_midx_entry(m, oid, &e)) {
diff --git a/builtin/repack.c b/builtin/repack.c
index 1524a9c13a..47966a686b 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -458,6 +458,8 @@ int cmd_repack(int argc,
 	}
 
 	if (!names.nr) {
+		struct odb_source_files *files = odb_source_files_downcast(existing.source);
+
 		if (!po_args.quiet)
 			printf_ln(_("Nothing new to pack."));
 		/*
@@ -473,7 +475,7 @@ int cmd_repack(int argc,
 		 * midx_has_unknown_packs() will make the decision for
 		 * us.
 		 */
-		if (!get_multi_pack_index(existing.source))
+		if (!get_multi_pack_index(files->packed))
 			midx_must_contain_cruft = 1;
 	}
 
@@ -626,10 +628,12 @@ int cmd_repack(int argc,
 		update_server_info(repo, 0);
 
 	if (git_env_bool(GIT_TEST_MULTI_PACK_INDEX, 0)) {
+		struct odb_source_files *files = odb_source_files_downcast(existing.source);
 		unsigned flags = 0;
+
 		if (git_env_bool(GIT_TEST_MULTI_PACK_INDEX_WRITE_INCREMENTAL, 0))
 			flags |= MIDX_WRITE_INCREMENTAL;
-		write_midx_file(existing.source, NULL, NULL, flags);
+		write_midx_file(files->packed, NULL, NULL, flags);
 	}
 
 cleanup:
diff --git a/midx-write.c b/midx-write.c
index 561e9eedc0..7cafc49fb8 100644
--- a/midx-write.c
+++ b/midx-write.c
@@ -25,9 +25,9 @@
 #define NO_PREFERRED_PACK (~((uint32_t)0))
 
 extern int midx_checksum_valid(struct multi_pack_index *m);
-extern void clear_midx_files_ext(struct odb_source *source, const char *ext,
+extern void clear_midx_files_ext(struct odb_source_packed *source, const char *ext,
 				 const char *keep_hash);
-extern void clear_incremental_midx_files_ext(struct odb_source *source,
+extern void clear_incremental_midx_files_ext(struct odb_source_packed *source,
 					     const char *ext,
 					     const struct strvec *keep_hashes);
 extern int cmp_idx_or_pack_name(const char *idx_or_pack_name,
@@ -119,7 +119,7 @@ struct write_midx_context {
 	struct string_list *to_include;
 
 	struct repository *repo;
-	struct odb_source *source;
+	struct odb_source_packed *source;
 };
 
 static uint32_t midx_pack_perm(struct write_midx_context *ctx,
@@ -1107,7 +1107,7 @@ static int link_midx_to_chain(struct multi_pack_index *m)
 	return ret;
 }
 
-static void clear_midx_files(struct odb_source *source,
+static void clear_midx_files(struct odb_source_packed *source,
 			     const struct strvec *hashes, unsigned incremental)
 {
 	/*
@@ -1237,7 +1237,7 @@ static int midx_hashcmp(const struct multi_pack_index *a,
 }
 
 struct write_midx_opts {
-	struct odb_source *source; /* non-optional */
+	struct odb_source_packed *source; /* non-optional */
 
 	struct string_list *packs_to_include;
 	struct string_list *packs_to_drop;
@@ -1253,7 +1253,7 @@ struct write_midx_opts {
 
 static int write_midx_internal(struct write_midx_opts *opts)
 {
-	struct repository *r = opts->source->odb->repo;
+	struct repository *r = opts->source->base.odb->repo;
 	struct strbuf midx_name = STRBUF_INIT;
 	unsigned char midx_hash[GIT_MAX_RAWSZ];
 	uint32_t start_pack;
@@ -1301,7 +1301,7 @@ static int write_midx_internal(struct write_midx_opts *opts)
 	if (ctx.incremental)
 		strbuf_addf(&midx_name,
 			    "%s/pack/multi-pack-index.d/tmp_midx_XXXXXX",
-			    opts->source->path);
+			    opts->source->base.path);
 	else
 		get_midx_filename(opts->source, &midx_name);
 	if (safe_create_leading_directories(r, midx_name.buf))
@@ -1396,7 +1396,7 @@ static int write_midx_internal(struct write_midx_opts *opts)
 		fill_packs_from_midx_range(&ctx, bitmap_order);
 	} else {
 		ctx.to_include = opts->packs_to_include;
-		for_each_file_in_pack_dir(opts->source->path, add_pack_to_midx, &ctx);
+		for_each_file_in_pack_dir(opts->source->base.path, add_pack_to_midx, &ctx);
 	}
 	stop_progress(&ctx.progress);
 
@@ -1847,7 +1847,7 @@ static int write_midx_internal(struct write_midx_opts *opts)
 	return result;
 }
 
-int write_midx_file(struct odb_source *source,
+int write_midx_file(struct odb_source_packed *source,
 		    const char *preferred_pack_name,
 		    const char *refs_snapshot,
 		    unsigned flags)
@@ -1862,7 +1862,7 @@ int write_midx_file(struct odb_source *source,
 	return write_midx_internal(&opts);
 }
 
-int write_midx_file_only(struct odb_source *source,
+int write_midx_file_only(struct odb_source_packed *source,
 			 struct string_list *packs_to_include,
 			 const char *preferred_pack_name,
 			 const char *refs_snapshot,
@@ -1881,7 +1881,7 @@ int write_midx_file_only(struct odb_source *source,
 	return write_midx_internal(&opts);
 }
 
-int write_midx_file_compact(struct odb_source *source,
+int write_midx_file_compact(struct odb_source_packed *source,
 			    struct multi_pack_index *from,
 			    struct multi_pack_index *to,
 			    const char *incremental_base,
@@ -1898,7 +1898,7 @@ int write_midx_file_compact(struct odb_source *source,
 	return write_midx_internal(&opts);
 }
 
-int expire_midx_packs(struct odb_source *source, unsigned flags)
+int expire_midx_packs(struct odb_source_packed *source, unsigned flags)
 {
 	uint32_t i, *count, result = 0;
 	struct string_list packs_to_drop = STRING_LIST_INIT_DUP;
@@ -1915,7 +1915,7 @@ int expire_midx_packs(struct odb_source *source, unsigned flags)
 
 	if (flags & MIDX_PROGRESS)
 		progress = start_delayed_progress(
-					  source->odb->repo,
+					  source->base.odb->repo,
 					  _("Counting referenced objects"),
 					  m->num_objects);
 	for (i = 0; i < m->num_objects; i++) {
@@ -1927,7 +1927,7 @@ int expire_midx_packs(struct odb_source *source, unsigned flags)
 
 	if (flags & MIDX_PROGRESS)
 		progress = start_delayed_progress(
-					  source->odb->repo,
+					  source->base.odb->repo,
 					  _("Finding and deleting unreferenced packfiles"),
 					  m->num_packs);
 	for (i = 0; i < m->num_packs; i++) {
@@ -2085,9 +2085,9 @@ static void fill_included_packs_batch(struct repository *r,
 	free(pack_info);
 }
 
-int midx_repack(struct odb_source *source, size_t batch_size, unsigned flags)
+int midx_repack(struct odb_source_packed *source, size_t batch_size, unsigned flags)
 {
-	struct repository *r = source->odb->repo;
+	struct repository *r = source->base.odb->repo;
 	int result = 0;
 	uint32_t i, packs_to_repack = 0;
 	unsigned char *include_pack;
@@ -2131,7 +2131,7 @@ int midx_repack(struct odb_source *source, size_t batch_size, unsigned flags)
 
 	strvec_push(&cmd.args, "pack-objects");
 
-	strvec_pushf(&cmd.args, "%s/pack/pack", source->path);
+	strvec_pushf(&cmd.args, "%s/pack/pack", source->base.path);
 
 	if (delta_base_offset)
 		strvec_push(&cmd.args, "--delta-base-offset");
diff --git a/midx.c b/midx.c
index 00bbd137b2..cc6b94f9dd 100644
--- a/midx.c
+++ b/midx.c
@@ -17,9 +17,9 @@
 #define MIDX_PACK_ERROR ((void *)(intptr_t)-1)
 
 int midx_checksum_valid(struct multi_pack_index *m);
-void clear_midx_files_ext(struct odb_source *source, const char *ext,
+void clear_midx_files_ext(struct odb_source_packed *source, const char *ext,
 			  const char *keep_hash);
-void clear_incremental_midx_files_ext(struct odb_source *source, const char *ext,
+void clear_incremental_midx_files_ext(struct odb_source_packed *source, const char *ext,
 				      const struct strvec *keep_hashes);
 int cmp_idx_or_pack_name(const char *idx_or_pack_name,
 			 const char *idx_name);
@@ -27,25 +27,25 @@ int cmp_idx_or_pack_name(const char *idx_or_pack_name,
 const char *midx_get_checksum_hex(const struct multi_pack_index *m)
 {
 	return hash_to_hex_algop(midx_get_checksum_hash(m),
-				 m->source->odb->repo->hash_algo);
+				 m->source->base.odb->repo->hash_algo);
 }
 
 const unsigned char *midx_get_checksum_hash(const struct multi_pack_index *m)
 {
-	return m->data + m->data_len - m->source->odb->repo->hash_algo->rawsz;
+	return m->data + m->data_len - m->source->base.odb->repo->hash_algo->rawsz;
 }
 
-void get_midx_filename(struct odb_source *source, struct strbuf *out)
+void get_midx_filename(struct odb_source_packed *source, struct strbuf *out)
 {
 	get_midx_filename_ext(source, out, NULL, NULL);
 }
 
-void get_midx_filename_ext(struct odb_source *source, struct strbuf *out,
+void get_midx_filename_ext(struct odb_source_packed *source, struct strbuf *out,
 			   const unsigned char *hash, const char *ext)
 {
-	strbuf_addf(out, "%s/pack/multi-pack-index", source->path);
+	strbuf_addf(out, "%s/pack/multi-pack-index", source->base.path);
 	if (ext)
-		strbuf_addf(out, "-%s.%s", hash_to_hex_algop(hash, source->odb->repo->hash_algo), ext);
+		strbuf_addf(out, "-%s.%s", hash_to_hex_algop(hash, source->base.odb->repo->hash_algo), ext);
 }
 
 static int midx_read_oid_fanout(const unsigned char *chunk_start,
@@ -99,17 +99,16 @@ static int midx_read_object_offsets(const unsigned char *chunk_start,
 	return 0;
 }
 
-struct multi_pack_index *get_multi_pack_index(struct odb_source *source)
+struct multi_pack_index *get_multi_pack_index(struct odb_source_packed *source)
 {
-	struct odb_source_files *files = odb_source_files_downcast(source);
-	odb_source_packed_prepare(files->packed);
-	return files->packed->midx;
+	odb_source_packed_prepare(source);
+	return source->midx;
 }
 
-static struct multi_pack_index *load_multi_pack_index_one(struct odb_source *source,
+static struct multi_pack_index *load_multi_pack_index_one(struct odb_source_packed *source,
 							  const char *midx_name)
 {
-	struct repository *r = source->odb->repo;
+	struct repository *r = source->base.odb->repo;
 	struct multi_pack_index *m = NULL;
 	int fd;
 	struct stat st;
@@ -234,23 +233,23 @@ static struct multi_pack_index *load_multi_pack_index_one(struct odb_source *sou
 	return NULL;
 }
 
-void get_midx_chain_dirname(struct odb_source *source, struct strbuf *buf)
+void get_midx_chain_dirname(struct odb_source_packed *source, struct strbuf *buf)
 {
-	strbuf_addf(buf, "%s/pack/multi-pack-index.d", source->path);
+	strbuf_addf(buf, "%s/pack/multi-pack-index.d", source->base.path);
 }
 
-void get_midx_chain_filename(struct odb_source *source, struct strbuf *buf)
+void get_midx_chain_filename(struct odb_source_packed *source, struct strbuf *buf)
 {
 	get_midx_chain_dirname(source, buf);
 	strbuf_addstr(buf, "/multi-pack-index-chain");
 }
 
-void get_split_midx_filename_ext(struct odb_source *source, struct strbuf *buf,
+void get_split_midx_filename_ext(struct odb_source_packed *source, struct strbuf *buf,
 				 const unsigned char *hash, const char *ext)
 {
 	get_midx_chain_dirname(source, buf);
 	strbuf_addf(buf, "/multi-pack-index-%s.%s",
-		    hash_to_hex_algop(hash, source->odb->repo->hash_algo), ext);
+		    hash_to_hex_algop(hash, source->base.odb->repo->hash_algo), ext);
 }
 
 static int open_multi_pack_index_chain(const struct git_hash_algo *hash_algo,
@@ -306,11 +305,11 @@ static int add_midx_to_chain(struct multi_pack_index *midx,
 	return 1;
 }
 
-static struct multi_pack_index *load_midx_chain_fd_st(struct odb_source *source,
+static struct multi_pack_index *load_midx_chain_fd_st(struct odb_source_packed *source,
 						      int fd, struct stat *st,
 						      int *incomplete_chain)
 {
-	const struct git_hash_algo *hash_algo = source->odb->repo->hash_algo;
+	const struct git_hash_algo *hash_algo = source->base.odb->repo->hash_algo;
 	struct multi_pack_index *midx_chain = NULL;
 	struct strbuf buf = STRBUF_INIT;
 	int valid = 1;
@@ -362,7 +361,7 @@ static struct multi_pack_index *load_midx_chain_fd_st(struct odb_source *source,
 	return midx_chain;
 }
 
-static struct multi_pack_index *load_multi_pack_index_chain(struct odb_source *source)
+static struct multi_pack_index *load_multi_pack_index_chain(struct odb_source_packed *source)
 {
 	struct strbuf chain_file = STRBUF_INIT;
 	struct stat st;
@@ -370,7 +369,8 @@ static struct multi_pack_index *load_multi_pack_index_chain(struct odb_source *s
 	struct multi_pack_index *m = NULL;
 
 	get_midx_chain_filename(source, &chain_file);
-	if (open_multi_pack_index_chain(source->odb->repo->hash_algo, chain_file.buf, &fd, &st)) {
+	if (open_multi_pack_index_chain(source->base.odb->repo->hash_algo,
+					chain_file.buf, &fd, &st)) {
 		int incomplete;
 		/* ownership of fd is taken over by load function */
 		m = load_midx_chain_fd_st(source, fd, &st, &incomplete);
@@ -380,7 +380,7 @@ static struct multi_pack_index *load_multi_pack_index_chain(struct odb_source *s
 	return m;
 }
 
-struct multi_pack_index *load_multi_pack_index(struct odb_source *source)
+struct multi_pack_index *load_multi_pack_index(struct odb_source_packed *source)
 {
 	struct strbuf midx_name = STRBUF_INIT;
 	struct multi_pack_index *m;
@@ -456,7 +456,7 @@ static uint32_t midx_for_pack(struct multi_pack_index **_m,
 int prepare_midx_pack(struct multi_pack_index *m,
 		      uint32_t pack_int_id)
 {
-	struct odb_source_files *files = odb_source_files_downcast(m->source);
+	struct odb_source_packed *packed = m->source;
 	struct strbuf pack_name = STRBUF_INIT;
 	struct packed_git *p;
 
@@ -467,10 +467,10 @@ int prepare_midx_pack(struct multi_pack_index *m,
 	if (m->packs[pack_int_id])
 		return 0;
 
-	strbuf_addf(&pack_name, "%s/pack/%s", files->base.path,
+	strbuf_addf(&pack_name, "%s/pack/%s", packed->base.path,
 		    m->pack_names[pack_int_id]);
-	p = packfile_store_load_pack(files->packed,
-				     pack_name.buf, files->base.local);
+	p = packfile_store_load_pack(packed,
+				     pack_name.buf, packed->base.local);
 	strbuf_release(&pack_name);
 
 	if (!p) {
@@ -523,7 +523,7 @@ int bsearch_one_midx(const struct object_id *oid, struct multi_pack_index *m,
 {
 	int ret = bsearch_hash(oid->hash, m->chunk_oid_fanout,
 			       m->chunk_oid_lookup,
-			       m->source->odb->repo->hash_algo->rawsz,
+			       m->source->base.odb->repo->hash_algo->rawsz,
 			       result);
 	if (result)
 		*result += m->num_objects_in_base;
@@ -554,7 +554,7 @@ struct object_id *nth_midxed_object_oid(struct object_id *oid,
 	n = midx_for_object(&m, n);
 
 	oidread(oid, m->chunk_oid_lookup + st_mult(m->hash_len, n),
-		m->source->odb->repo->hash_algo);
+		m->source->base.odb->repo->hash_algo);
 	return oid;
 }
 
@@ -734,26 +734,25 @@ int midx_preferred_pack(struct multi_pack_index *m, uint32_t *pack_int_id)
 	return 0;
 }
 
-int prepare_multi_pack_index_one(struct odb_source *source)
+int prepare_multi_pack_index_one(struct odb_source_packed *source)
 {
-	struct odb_source_files *files = odb_source_files_downcast(source);
-	struct repository *r = source->odb->repo;
+	struct repository *r = source->base.odb->repo;
 
 	prepare_repo_settings(r);
 	if (!r->settings.core_multi_pack_index)
 		return 0;
 
-	if (files->packed->midx)
+	if (source->midx)
 		return 1;
 
-	files->packed->midx = load_multi_pack_index(source);
+	source->midx = load_multi_pack_index(source);
 
-	return !!files->packed->midx;
+	return !!source->midx;
 }
 
 int midx_checksum_valid(struct multi_pack_index *m)
 {
-	return hashfile_checksum_valid(m->source->odb->repo->hash_algo,
+	return hashfile_checksum_valid(m->source->base.odb->repo->hash_algo,
 				       m->data, m->data_len);
 }
 
@@ -776,7 +775,7 @@ static void clear_midx_file_ext(const char *full_path, size_t full_path_len UNUS
 		die_errno(_("failed to remove %s"), full_path);
 }
 
-void clear_midx_files_ext(struct odb_source *source, const char *ext,
+void clear_midx_files_ext(struct odb_source_packed *source, const char *ext,
 			  const char *keep_hash)
 {
 	struct clear_midx_data data = {
@@ -793,12 +792,12 @@ void clear_midx_files_ext(struct odb_source *source, const char *ext,
 		strbuf_release(&buf);
 	}
 
-	for_each_file_in_pack_dir(source->path, clear_midx_file_ext, &data);
+	for_each_file_in_pack_dir(source->base.path, clear_midx_file_ext, &data);
 
 	strset_clear(&data.keep);
 }
 
-void clear_incremental_midx_files_ext(struct odb_source *source, const char *ext,
+void clear_incremental_midx_files_ext(struct odb_source_packed *source, const char *ext,
 				      const struct strvec *keep_hashes)
 {
 	struct clear_midx_data data = {
@@ -817,7 +816,7 @@ void clear_incremental_midx_files_ext(struct odb_source *source, const char *ext
 		}
 	}
 
-	for_each_file_in_pack_subdir(source->path, "multi-pack-index.d",
+	for_each_file_in_pack_subdir(source->base.path, "multi-pack-index.d",
 				     clear_midx_file_ext, &data);
 
 	strbuf_release(&buf);
@@ -826,26 +825,28 @@ void clear_incremental_midx_files_ext(struct odb_source *source, const char *ext
 
 void clear_midx_file(struct repository *r)
 {
+	struct odb_source_files *files;
 	struct strbuf midx = STRBUF_INIT;
 
-	get_midx_filename(r->objects->sources, &midx);
-
 	if (r->objects) {
 		struct odb_source *source;
 
 		for (source = r->objects->sources; source; source = source->next) {
-			struct odb_source_files *files = odb_source_files_downcast(source);
+			files = odb_source_files_downcast(source);
 			if (files->packed->midx)
 				close_midx(files->packed->midx);
 			files->packed->midx = NULL;
 		}
 	}
 
+	files = odb_source_files_downcast(r->objects->sources);
+	get_midx_filename(files->packed, &midx);
+
 	if (remove_path(midx.buf))
 		die(_("failed to clear multi-pack-index at %s"), midx.buf);
 
-	clear_midx_files_ext(r->objects->sources, MIDX_EXT_BITMAP, NULL);
-	clear_midx_files_ext(r->objects->sources, MIDX_EXT_REV, NULL);
+	clear_midx_files_ext(files->packed, MIDX_EXT_BITMAP, NULL);
+	clear_midx_files_ext(files->packed, MIDX_EXT_REV, NULL);
 
 	strbuf_release(&midx);
 }
@@ -853,28 +854,27 @@ void clear_midx_file(struct repository *r)
 void clear_incremental_midx_files(struct repository *r,
 				  const struct strvec *keep_hashes)
 {
-	struct odb_source *source = r->objects->sources;
+	struct odb_source_files *files;
+	struct odb_source *source;
 	struct strbuf chain = STRBUF_INIT;
 
-	get_midx_chain_filename(source, &chain);
-
-	for (; source; source = source->next) {
-		struct odb_source_files *files = odb_source_files_downcast(source);
+	for (source = r->objects->sources; source; source = source->next) {
+		files = odb_source_files_downcast(source);
 		if (files->packed->midx)
 			close_midx(files->packed->midx);
 		files->packed->midx = NULL;
 	}
 
+	files = odb_source_files_downcast(r->objects->sources);
+	get_midx_chain_filename(files->packed, &chain);
+
 	if (!keep_hashes && remove_path(chain.buf))
 		die(_("failed to clear multi-pack-index chain at %s"),
 		    chain.buf);
 
-	clear_incremental_midx_files_ext(r->objects->sources, MIDX_EXT_BITMAP,
-					 keep_hashes);
-	clear_incremental_midx_files_ext(r->objects->sources, MIDX_EXT_REV,
-					 keep_hashes);
-	clear_incremental_midx_files_ext(r->objects->sources, MIDX_EXT_MIDX,
-					 keep_hashes);
+	clear_incremental_midx_files_ext(files->packed, MIDX_EXT_BITMAP, keep_hashes);
+	clear_incremental_midx_files_ext(files->packed, MIDX_EXT_REV, keep_hashes);
+	clear_incremental_midx_files_ext(files->packed, MIDX_EXT_MIDX, keep_hashes);
 
 	strbuf_release(&chain);
 }
@@ -918,9 +918,9 @@ static int compare_pair_pos_vs_id(const void *_a, const void *_b)
 			display_progress(progress, _n); \
 	} while (0)
 
-int verify_midx_file(struct odb_source *source, unsigned flags)
+int verify_midx_file(struct odb_source_packed *source, unsigned flags)
 {
-	struct repository *r = source->odb->repo;
+	struct repository *r = source->base.odb->repo;
 	struct pair_pos_vs_id *pairs = NULL;
 	uint32_t i;
 	struct progress *progress = NULL;
diff --git a/midx.h b/midx.h
index 63853a03a4..939c18e588 100644
--- a/midx.h
+++ b/midx.h
@@ -37,7 +37,7 @@ struct strvec;
 	"GIT_TEST_MULTI_PACK_INDEX_WRITE_INCREMENTAL"
 
 struct multi_pack_index {
-	struct odb_source *source;
+	struct odb_source_packed *source;
 
 	const unsigned char *data;
 	size_t data_len;
@@ -92,16 +92,16 @@ struct multi_pack_index {
 
 const char *midx_get_checksum_hex(const struct multi_pack_index *m) /* static buffer */;
 const unsigned char *midx_get_checksum_hash(const struct multi_pack_index *m);
-void get_midx_filename(struct odb_source *source, struct strbuf *out);
-void get_midx_filename_ext(struct odb_source *source, struct strbuf *out,
+void get_midx_filename(struct odb_source_packed *source, struct strbuf *out);
+void get_midx_filename_ext(struct odb_source_packed *source, struct strbuf *out,
 			   const unsigned char *hash, const char *ext);
-void get_midx_chain_dirname(struct odb_source *source, struct strbuf *out);
-void get_midx_chain_filename(struct odb_source *source, struct strbuf *out);
-void get_split_midx_filename_ext(struct odb_source *source, struct strbuf *buf,
+void get_midx_chain_dirname(struct odb_source_packed *source, struct strbuf *out);
+void get_midx_chain_filename(struct odb_source_packed *source, struct strbuf *out);
+void get_split_midx_filename_ext(struct odb_source_packed *source, struct strbuf *buf,
 				 const unsigned char *hash, const char *ext);
 
-struct multi_pack_index *get_multi_pack_index(struct odb_source *source);
-struct multi_pack_index *load_multi_pack_index(struct odb_source *source);
+struct multi_pack_index *get_multi_pack_index(struct odb_source_packed *source);
+struct multi_pack_index *load_multi_pack_index(struct odb_source_packed *source);
 int prepare_midx_pack(struct multi_pack_index *m, uint32_t pack_int_id);
 struct packed_git *nth_midxed_pack(struct multi_pack_index *m,
 				   uint32_t pack_int_id);
@@ -123,22 +123,22 @@ int midx_contains_pack(struct multi_pack_index *m,
 int midx_layer_contains_pack(struct multi_pack_index *m,
 			     const char *idx_or_pack_name);
 int midx_preferred_pack(struct multi_pack_index *m, uint32_t *pack_int_id);
-int prepare_multi_pack_index_one(struct odb_source *source);
+int prepare_multi_pack_index_one(struct odb_source_packed *source);
 
 /*
  * Variant of write_midx_file which writes a MIDX containing only the packs
  * specified in packs_to_include.
  */
-int write_midx_file(struct odb_source *source,
+int write_midx_file(struct odb_source_packed *source,
 		    const char *preferred_pack_name, const char *refs_snapshot,
 		    unsigned flags);
-int write_midx_file_only(struct odb_source *source,
+int write_midx_file_only(struct odb_source_packed *source,
 			 struct string_list *packs_to_include,
 			 const char *preferred_pack_name,
 			 const char *refs_snapshot,
 			 const char *incremental_base,
 			 unsigned flags);
-int write_midx_file_compact(struct odb_source *source,
+int write_midx_file_compact(struct odb_source_packed *source,
 			    struct multi_pack_index *from,
 			    struct multi_pack_index *to,
 			    const char *incremental_base,
@@ -146,9 +146,9 @@ int write_midx_file_compact(struct odb_source *source,
 void clear_midx_file(struct repository *r);
 void clear_incremental_midx_files(struct repository *r,
 				  const struct strvec *keep_hashes);
-int verify_midx_file(struct odb_source *source, unsigned flags);
-int expire_midx_packs(struct odb_source *source, unsigned flags);
-int midx_repack(struct odb_source *source, size_t batch_size, unsigned flags);
+int verify_midx_file(struct odb_source_packed *source, unsigned flags);
+int expire_midx_packs(struct odb_source_packed *source, unsigned flags);
+int midx_repack(struct odb_source_packed *source, size_t batch_size, unsigned flags);
 
 void close_midx(struct multi_pack_index *m);
 
diff --git a/odb/source-packed.c b/odb/source-packed.c
index 08a2de9bc5..d513b3efc3 100644
--- a/odb/source-packed.c
+++ b/odb/source-packed.c
@@ -136,8 +136,8 @@ static int for_each_prefixed_object_in_midx(
 
 	for (; m; m = m->base_midx) {
 		uint32_t num, i, first = 0;
-		int len = opts->prefix_hex_len > m->source->odb->repo->hash_algo->hexsz ?
-			m->source->odb->repo->hash_algo->hexsz : opts->prefix_hex_len;
+		int len = opts->prefix_hex_len > m->source->base.odb->repo->hash_algo->hexsz ?
+			m->source->base.odb->repo->hash_algo->hexsz : opts->prefix_hex_len;
 
 		if (!m->num_objects)
 			continue;
@@ -249,7 +249,7 @@ static int odb_source_packed_for_each_prefixed_object(
 
 	store->skip_mru_updates = true;
 
-	m = get_multi_pack_index(&store->files->base);
+	m = get_multi_pack_index(store);
 	if (m) {
 		ret = for_each_prefixed_object_in_midx(store, m, opts, data);
 		if (ret)
@@ -348,7 +348,7 @@ static int odb_source_packed_count_objects(struct odb_source *source,
 	unsigned long count = 0;
 	int ret;
 
-	m = get_multi_pack_index(&packed->files->base);
+	m = get_multi_pack_index(packed);
 	if (m)
 		count += m->num_objects + m->num_objects_in_base;
 
@@ -465,7 +465,7 @@ static int odb_source_packed_find_abbrev_len(struct odb_source *source,
 	struct packfile_list_entry *e;
 	struct multi_pack_index *m;
 
-	m = get_multi_pack_index(&packed->files->base);
+	m = get_multi_pack_index(packed);
 	if (m)
 		find_abbrev_len_for_midx(m, oid, min_len, &min_len);
 
@@ -674,7 +674,7 @@ void odb_source_packed_prepare(struct odb_source_packed *source)
 	if (source->initialized)
 		return;
 
-	prepare_multi_pack_index_one(&source->files->base);
+	prepare_multi_pack_index_one(source);
 	prepare_packed_git_one(&source->files->base);
 
 	sort_packs(&source->packs.head, sort_pack);
diff --git a/pack-bitmap.c b/pack-bitmap.c
index f9af8a96bd..6bfcbc8ce6 100644
--- a/pack-bitmap.c
+++ b/pack-bitmap.c
@@ -238,7 +238,7 @@ static uint32_t bitmap_name_hash(struct bitmap_index *index, uint32_t pos)
 static struct repository *bitmap_repo(struct bitmap_index *bitmap_git)
 {
 	if (bitmap_is_midx(bitmap_git))
-		return bitmap_git->midx->source->odb->repo;
+		return bitmap_git->midx->source->base.odb->repo;
 	return bitmap_git->pack->repo;
 }
 
@@ -711,7 +711,8 @@ static int open_midx_bitmap(struct repository *r,
 
 	odb_prepare_alternates(r->objects);
 	for (source = r->objects->sources; source; source = source->next) {
-		struct multi_pack_index *midx = get_multi_pack_index(source);
+		struct odb_source_files *files = odb_source_files_downcast(source);
+		struct multi_pack_index *midx = get_multi_pack_index(files->packed);
 		if (midx && !open_midx_bitmap_1(bitmap_git, midx))
 			ret = 0;
 	}
@@ -3399,7 +3400,8 @@ int verify_bitmap_files(struct repository *r)
 
 	odb_prepare_alternates(r->objects);
 	for (source = r->objects->sources; source; source = source->next) {
-		struct multi_pack_index *m = get_multi_pack_index(source);
+		struct odb_source_files *files = odb_source_files_downcast(source);
+		struct multi_pack_index *m = get_multi_pack_index(files->packed);
 		char *midx_bitmap_name;
 
 		if (!m)
diff --git a/pack-revindex.c b/pack-revindex.c
index 1b67863606..62387ae632 100644
--- a/pack-revindex.c
+++ b/pack-revindex.c
@@ -383,13 +383,13 @@ int load_midx_revindex(struct multi_pack_index *m)
 		 * not want to accidentally call munmap() in the middle of the
 		 * MIDX.
 		 */
-		trace2_data_string("load_midx_revindex", m->source->odb->repo,
+		trace2_data_string("load_midx_revindex", m->source->base.odb->repo,
 				   "source", "midx");
 		m->revindex_data = (const uint32_t *)m->chunk_revindex;
 		return 0;
 	}
 
-	trace2_data_string("load_midx_revindex", m->source->odb->repo,
+	trace2_data_string("load_midx_revindex", m->source->base.odb->repo,
 			   "source", "rev");
 
 	if (m->has_chain)
@@ -401,7 +401,7 @@ int load_midx_revindex(struct multi_pack_index *m)
 				      midx_get_checksum_hash(m),
 				      MIDX_EXT_REV);
 
-	ret = load_revindex_from_disk(m->source->odb->repo->hash_algo,
+	ret = load_revindex_from_disk(m->source->base.odb->repo->hash_algo,
 				      revindex_name.buf,
 				      m->num_objects,
 				      &m->revindex_map,
diff --git a/repack-geometry.c b/repack-geometry.c
index 2064683dcf..15b3412950 100644
--- a/repack-geometry.c
+++ b/repack-geometry.c
@@ -32,7 +32,8 @@ void pack_geometry_init(struct pack_geometry *geometry,
 {
 	struct packed_git *p;
 	struct strbuf buf = STRBUF_INIT;
-	struct multi_pack_index *m = get_multi_pack_index(existing->source);
+	struct odb_source_files *files = odb_source_files_downcast(existing->source);
+	struct multi_pack_index *m = get_multi_pack_index(files->packed);
 
 	repo_for_each_pack(existing->repo, p) {
 		if (geometry->midx_layer_threshold_set && m &&
diff --git a/repack-midx.c b/repack-midx.c
index b6b1de7180..7c7c3620e5 100644
--- a/repack-midx.c
+++ b/repack-midx.c
@@ -557,13 +557,14 @@ static void repack_make_midx_append_plan(struct repack_write_midx_opts *opts,
 					 struct midx_compaction_step **steps_p,
 					 size_t *steps_nr_p)
 {
+	struct odb_source_files *files = odb_source_files_downcast(opts->existing->source);
 	struct multi_pack_index *m;
 	struct midx_compaction_step *steps = NULL;
 	struct midx_compaction_step *step;
 	size_t steps_nr = 0, steps_alloc = 0;
 
 	odb_reprepare(opts->existing->repo->objects);
-	m = get_multi_pack_index(opts->existing->source);
+	m = get_multi_pack_index(files->packed);
 
 	if (opts->names->nr) {
 		struct strbuf buf = STRBUF_INIT;
@@ -606,6 +607,7 @@ static int repack_make_midx_compaction_plan(struct repack_write_midx_opts *opts,
 					    struct midx_compaction_step **steps_p,
 					    size_t *steps_nr_p)
 {
+	struct odb_source_files *files = odb_source_files_downcast(opts->existing->source);
 	struct multi_pack_index *m;
 	struct midx_compaction_step *steps = NULL;
 	struct midx_compaction_step step = { 0 };
@@ -618,7 +620,7 @@ static int repack_make_midx_compaction_plan(struct repack_write_midx_opts *opts,
 			    opts->existing->repo);
 
 	odb_reprepare(opts->existing->repo->objects);
-	m = get_multi_pack_index(opts->existing->source);
+	m = get_multi_pack_index(files->packed);
 
 	for (i = 0; m && i < m->num_packs + m->num_packs_in_base; i++) {
 		if (prepare_midx_pack(m, i)) {
@@ -938,6 +940,7 @@ static int repack_make_midx_compaction_plan(struct repack_write_midx_opts *opts,
 
 static int write_midx_incremental(struct repack_write_midx_opts *opts)
 {
+	struct odb_source_files *files = odb_source_files_downcast(opts->existing->source);
 	struct midx_compaction_step *steps = NULL;
 	struct strbuf lock_name = STRBUF_INIT;
 	struct lock_file lf;
@@ -946,7 +949,7 @@ static int write_midx_incremental(struct repack_write_midx_opts *opts)
 	size_t i;
 	int ret = 0;
 
-	get_midx_chain_filename(opts->existing->source, &lock_name);
+	get_midx_chain_filename(files->packed, &lock_name);
 	if (safe_create_leading_directories(opts->existing->repo,
 					    lock_name.buf))
 		die_errno(_("unable to create leading directories of %s"),
diff --git a/repack.c b/repack.c
index 571dabb665..d2aa58e134 100644
--- a/repack.c
+++ b/repack.c
@@ -59,10 +59,10 @@ void repack_remove_redundant_pack(struct repository *repo, const char *dir_name,
 				  bool wrote_incremental_midx)
 {
 	struct strbuf buf = STRBUF_INIT;
-	struct odb_source *source = repo->objects->sources;
-	struct multi_pack_index *m = get_multi_pack_index(source);
+	struct odb_source_files *files = odb_source_files_downcast(repo->objects->sources);
+	struct multi_pack_index *m = get_multi_pack_index(files->packed);
 	strbuf_addf(&buf, "%s.pack", base_name);
-	if (m && source->local && midx_contains_pack(m, buf.buf)) {
+	if (m && files->base.local && midx_contains_pack(m, buf.buf)) {
 		clear_midx_file(repo);
 		if (!wrote_incremental_midx)
 			clear_incremental_midx_files(repo, NULL);
diff --git a/t/helper/test-read-midx.c b/t/helper/test-read-midx.c
index 790000fb26..fb16ec0176 100644
--- a/t/helper/test-read-midx.c
+++ b/t/helper/test-read-midx.c
@@ -13,13 +13,16 @@
 
 static struct multi_pack_index *setup_midx(const char *object_dir)
 {
+	struct odb_source_files *files;
 	struct odb_source *source;
 	setup_git_directory(the_repository);
 	source = odb_find_source(the_repository->objects, object_dir);
 	if (!source)
 		source = odb_add_to_alternates_memory(the_repository->objects,
 						      object_dir);
-	return load_multi_pack_index(source);
+	files = odb_source_files_downcast(source);
+
+	return load_multi_pack_index(files->packed);
 }
 
 static int read_midx_file(const char *object_dir, const char *checksum,
@@ -70,7 +73,7 @@ static int read_midx_file(const char *object_dir, const char *checksum,
 	for (i = 0; i < m->num_packs; i++)
 		printf("%s\n", m->pack_names[i]);
 
-	printf("object-dir: %s\n", m->source->path);
+	printf("object-dir: %s\n", m->source->base.path);
 
 	if (show_objects) {
 		struct object_id oid;

-- 
2.55.0.rc0.786.g65d90a0328.dirty


^ permalink raw reply related

* [PATCH v3 15/17] odb/source-packed: stub out remaining functions
From: Patrick Steinhardt @ 2026-06-17  6:39 UTC (permalink / raw)
  To: git; +Cc: Karthik Nayak, Justin Tobler
In-Reply-To: <20260617-pks-odb-source-packed-v3-0-b5c7583cd795@pks.im>

Stub out remaining functions that we either don't need or that are
basically no-ops.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 odb/source-packed.c | 42 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 42 insertions(+)

diff --git a/odb/source-packed.c b/odb/source-packed.c
index e40b52e445..08a2de9bc5 100644
--- a/odb/source-packed.c
+++ b/odb/source-packed.c
@@ -501,6 +501,43 @@ static int odb_source_packed_freshen_object(struct odb_source *source,
 	return 1;
 }
 
+static int odb_source_packed_write_object(struct odb_source *source UNUSED,
+					  const void *buf UNUSED,
+					  unsigned long len UNUSED,
+					  enum object_type type UNUSED,
+					  struct object_id *oid UNUSED,
+					  struct object_id *compat_oid UNUSED,
+					  unsigned flags UNUSED)
+{
+	return error("packed backend cannot write objects");
+}
+
+static int odb_source_packed_write_object_stream(struct odb_source *source UNUSED,
+						 struct odb_write_stream *stream UNUSED,
+						 size_t len UNUSED,
+						 struct object_id *oid UNUSED)
+{
+	return error("packed backend cannot write object streams");
+}
+
+static int odb_source_packed_begin_transaction(struct odb_source *source UNUSED,
+					       struct odb_transaction **out UNUSED)
+{
+	return error("packed backend cannot begin transactions");
+}
+
+static int odb_source_packed_read_alternates(struct odb_source *source UNUSED,
+					     struct strvec *out UNUSED)
+{
+	return 0;
+}
+
+static int odb_source_packed_write_alternate(struct odb_source *source UNUSED,
+					     const char *alternate UNUSED)
+{
+	return error("packed backend cannot write alternates");
+}
+
 void (*report_garbage)(unsigned seen_bits, const char *path);
 
 static void report_helper(const struct string_list *list,
@@ -715,6 +752,11 @@ struct odb_source_packed *odb_source_packed_new(struct odb_source_files *parent)
 	packed->base.count_objects = odb_source_packed_count_objects;
 	packed->base.find_abbrev_len = odb_source_packed_find_abbrev_len;
 	packed->base.freshen_object = odb_source_packed_freshen_object;
+	packed->base.write_object = odb_source_packed_write_object;
+	packed->base.write_object_stream = odb_source_packed_write_object_stream;
+	packed->base.begin_transaction = odb_source_packed_begin_transaction;
+	packed->base.read_alternates = odb_source_packed_read_alternates;
+	packed->base.write_alternate = odb_source_packed_write_alternate;
 
 	if (!is_absolute_path(parent->base.path))
 		chdir_notify_register(NULL, odb_source_packed_reparent, packed);

-- 
2.55.0.rc0.786.g65d90a0328.dirty


^ permalink raw reply related

* [PATCH v3 14/17] odb/source-packed: wire up `freshen_object()` callback
From: Patrick Steinhardt @ 2026-06-17  6:39 UTC (permalink / raw)
  To: git; +Cc: Karthik Nayak, Justin Tobler
In-Reply-To: <20260617-pks-odb-source-packed-v3-0-b5c7583cd795@pks.im>

Move `packfile_store_freshen_object()` and from "packfile.c" into
"odb/source-packed.c" and wire it up as the `freshen_object()` callback
of the "packed" source.

Note that this removes the last external caller of `find_pack_entry()`
from "packfile.c", which means that we can now make this function
static.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 odb/source-files.c  |  2 +-
 odb/source-packed.c | 26 +++++++++++++++++++++++---
 odb/source-packed.h |  6 ------
 packfile.c          | 16 ----------------
 packfile.h          |  3 ---
 5 files changed, 24 insertions(+), 29 deletions(-)

diff --git a/odb/source-files.c b/odb/source-files.c
index 8ad782dc7b..fa2e18e71b 100644
--- a/odb/source-files.c
+++ b/odb/source-files.c
@@ -152,7 +152,7 @@ static int odb_source_files_freshen_object(struct odb_source *source,
 					   const struct object_id *oid)
 {
 	struct odb_source_files *files = odb_source_files_downcast(source);
-	if (packfile_store_freshen_object(files->packed, oid) ||
+	if (odb_source_freshen_object(&files->packed->base, oid) ||
 	    odb_source_freshen_object(&files->loose->base, oid))
 		return 1;
 	return 0;
diff --git a/odb/source-packed.c b/odb/source-packed.c
index b801b62023..e40b52e445 100644
--- a/odb/source-packed.c
+++ b/odb/source-packed.c
@@ -9,9 +9,9 @@
 #include "odb/streaming.h"
 #include "packfile.h"
 
-int find_pack_entry(struct odb_source_packed *store,
-		    const struct object_id *oid,
-		    struct pack_entry *e)
+static int find_pack_entry(struct odb_source_packed *store,
+			   const struct object_id *oid,
+			   struct pack_entry *e)
 {
 	struct packfile_list_entry *l;
 
@@ -482,6 +482,25 @@ static int odb_source_packed_find_abbrev_len(struct odb_source *source,
 	return 0;
 }
 
+static int odb_source_packed_freshen_object(struct odb_source *source,
+					    const struct object_id *oid)
+{
+	struct odb_source_packed *packed = odb_source_packed_downcast(source);
+	struct pack_entry e;
+
+	if (!find_pack_entry(packed, oid, &e))
+		return 0;
+	if (e.p->is_cruft)
+		return 0;
+	if (e.p->freshened)
+		return 1;
+	if (utime(e.p->pack_name, NULL))
+		return 0;
+	e.p->freshened = 1;
+
+	return 1;
+}
+
 void (*report_garbage)(unsigned seen_bits, const char *path);
 
 static void report_helper(const struct string_list *list,
@@ -695,6 +714,7 @@ struct odb_source_packed *odb_source_packed_new(struct odb_source_files *parent)
 	packed->base.for_each_object = odb_source_packed_for_each_object;
 	packed->base.count_objects = odb_source_packed_count_objects;
 	packed->base.find_abbrev_len = odb_source_packed_find_abbrev_len;
+	packed->base.freshen_object = odb_source_packed_freshen_object;
 
 	if (!is_absolute_path(parent->base.path))
 		chdir_notify_register(NULL, odb_source_packed_reparent, packed);
diff --git a/odb/source-packed.h b/odb/source-packed.h
index f430ee0b94..9d4796261a 100644
--- a/odb/source-packed.h
+++ b/odb/source-packed.h
@@ -90,10 +90,4 @@ static inline struct odb_source_packed *odb_source_packed_downcast(struct odb_so
  */
 void odb_source_packed_prepare(struct odb_source_packed *source);
 
-struct pack_entry;
-
-int find_pack_entry(struct odb_source_packed *store,
-		    const struct object_id *oid,
-		    struct pack_entry *e);
-
 #endif
diff --git a/packfile.c b/packfile.c
index 7f84094e53..a577275d4f 100644
--- a/packfile.c
+++ b/packfile.c
@@ -1892,22 +1892,6 @@ int packfile_fill_entry(struct packed_git *p,
 	return 1;
 }
 
-int packfile_store_freshen_object(struct odb_source_packed *store,
-				  const struct object_id *oid)
-{
-	struct pack_entry e;
-	if (!find_pack_entry(store, oid, &e))
-		return 0;
-	if (e.p->is_cruft)
-		return 0;
-	if (e.p->freshened)
-		return 1;
-	if (utime(e.p->pack_name, NULL))
-		return 0;
-	e.p->freshened = 1;
-	return 1;
-}
-
 static void maybe_invalidate_kept_pack_cache(struct odb_source_packed *store,
 					     unsigned flags)
 {
diff --git a/packfile.h b/packfile.h
index 79324e4010..71a71017ee 100644
--- a/packfile.h
+++ b/packfile.h
@@ -132,9 +132,6 @@ static inline void repo_for_each_pack_data_next(struct repo_for_each_pack_data *
 struct packed_git *packfile_store_load_pack(struct odb_source_packed *store,
 					    const char *idx_path, int local);
 
-int packfile_store_freshen_object(struct odb_source_packed *store,
-				  const struct object_id *oid);
-
 enum kept_pack_type {
 	KEPT_PACK_ON_DISK = (1 << 0),
 	KEPT_PACK_IN_CORE = (1 << 1),

-- 
2.55.0.rc0.786.g65d90a0328.dirty


^ permalink raw reply related

* [PATCH v3 13/17] odb/source-packed: wire up `find_abbrev_len()` callback
From: Patrick Steinhardt @ 2026-06-17  6:39 UTC (permalink / raw)
  To: git; +Cc: Karthik Nayak, Justin Tobler
In-Reply-To: <20260617-pks-odb-source-packed-v3-0-b5c7583cd795@pks.im>

Move `packfile_store_find_abbrev_len()` and its associated helpers from
"packfile.c" into "odb/source-packed.c" and wire it up as the
`find_abbrev_len()` callback of the "packed" source.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 odb/source-files.c  |   2 +-
 odb/source-packed.c | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 packfile.c          | 111 ---------------------------------------------------
 packfile.h          |   5 ---
 4 files changed, 114 insertions(+), 117 deletions(-)

diff --git a/odb/source-files.c b/odb/source-files.c
index 274923e0ba..8ad782dc7b 100644
--- a/odb/source-files.c
+++ b/odb/source-files.c
@@ -133,7 +133,7 @@ static int odb_source_files_find_abbrev_len(struct odb_source *source,
 	unsigned len = min_len;
 	int ret;
 
-	ret = packfile_store_find_abbrev_len(files->packed, oid, len, &len);
+	ret = odb_source_find_abbrev_len(&files->packed->base, oid, len, &len);
 	if (ret < 0)
 		goto out;
 
diff --git a/odb/source-packed.c b/odb/source-packed.c
index 070a4e3958..b801b62023 100644
--- a/odb/source-packed.c
+++ b/odb/source-packed.c
@@ -370,6 +370,118 @@ static int odb_source_packed_count_objects(struct odb_source *source,
 	return ret;
 }
 
+static int extend_abbrev_len(const struct object_id *a,
+			     const struct object_id *b,
+			     unsigned *out)
+{
+	unsigned len = oid_common_prefix_hexlen(a, b);
+	if (len != hash_algos[a->algo].hexsz && len >= *out)
+		*out = len + 1;
+	return 0;
+}
+
+static void find_abbrev_len_for_midx(struct multi_pack_index *m,
+				     const struct object_id *oid,
+				     unsigned min_len,
+				     unsigned *out)
+{
+	unsigned len = min_len;
+
+	for (; m; m = m->base_midx) {
+		int match = 0;
+		uint32_t num, first = 0;
+		struct object_id found_oid;
+
+		if (!m->num_objects)
+			continue;
+
+		num = m->num_objects + m->num_objects_in_base;
+		match = bsearch_one_midx(oid, m, &first);
+
+		/*
+		 * first is now the position in the packfile where we
+		 * would insert the object ID if it does not exist (or the
+		 * position of the object ID if it does exist). Hence, we
+		 * consider a maximum of two objects nearby for the
+		 * abbreviation length.
+		 */
+
+		if (!match) {
+			if (nth_midxed_object_oid(&found_oid, m, first))
+				extend_abbrev_len(&found_oid, oid, &len);
+		} else if (first < num - 1) {
+			if (nth_midxed_object_oid(&found_oid, m, first + 1))
+				extend_abbrev_len(&found_oid, oid, &len);
+		}
+		if (first > 0) {
+			if (nth_midxed_object_oid(&found_oid, m, first - 1))
+				extend_abbrev_len(&found_oid, oid, &len);
+		}
+	}
+
+	*out = len;
+}
+
+static void find_abbrev_len_for_pack(struct packed_git *p,
+				     const struct object_id *oid,
+				     unsigned min_len,
+				     unsigned *out)
+{
+	int match;
+	uint32_t num, first = 0;
+	struct object_id found_oid;
+	unsigned len = min_len;
+
+	num = p->num_objects;
+	match = bsearch_pack(oid, p, &first);
+
+	/*
+	 * first is now the position in the packfile where we would insert
+	 * the object ID if it does not exist (or the position of mad->hash if
+	 * it does exist). Hence, we consider a maximum of two objects
+	 * nearby for the abbreviation length.
+	 */
+	if (!match) {
+		if (!nth_packed_object_id(&found_oid, p, first))
+			extend_abbrev_len(&found_oid, oid, &len);
+	} else if (first < num - 1) {
+		if (!nth_packed_object_id(&found_oid, p, first + 1))
+			extend_abbrev_len(&found_oid, oid, &len);
+	}
+	if (first > 0) {
+		if (!nth_packed_object_id(&found_oid, p, first - 1))
+			extend_abbrev_len(&found_oid, oid, &len);
+	}
+
+	*out = len;
+}
+
+static int odb_source_packed_find_abbrev_len(struct odb_source *source,
+					     const struct object_id *oid,
+					     unsigned min_len,
+					     unsigned *out)
+{
+	struct odb_source_packed *packed = odb_source_packed_downcast(source);
+	struct packfile_list_entry *e;
+	struct multi_pack_index *m;
+
+	m = get_multi_pack_index(&packed->files->base);
+	if (m)
+		find_abbrev_len_for_midx(m, oid, min_len, &min_len);
+
+	for (e = packfile_store_get_packs(packed); e; e = e->next) {
+		if (e->pack->multi_pack_index)
+			continue;
+		if (open_pack_index(e->pack) || !e->pack->num_objects)
+			continue;
+
+		find_abbrev_len_for_pack(e->pack, oid, min_len, &min_len);
+	}
+
+	*out = min_len;
+	return 0;
+}
+
 void (*report_garbage)(unsigned seen_bits, const char *path);
 
 static void report_helper(const struct string_list *list,
@@ -582,6 +694,7 @@ struct odb_source_packed *odb_source_packed_new(struct odb_source_files *parent)
 	packed->base.read_object_stream = odb_source_packed_read_object_stream;
 	packed->base.for_each_object = odb_source_packed_for_each_object;
 	packed->base.count_objects = odb_source_packed_count_objects;
+	packed->base.find_abbrev_len = odb_source_packed_find_abbrev_len;
 
 	if (!is_absolute_path(parent->base.path))
 		chdir_notify_register(NULL, odb_source_packed_reparent, packed);
diff --git a/packfile.c b/packfile.c
index 2da6bbe2b5..7f84094e53 100644
--- a/packfile.c
+++ b/packfile.c
@@ -2037,117 +2037,6 @@ int for_each_object_in_pack(struct packed_git *p,
 	return r;
 }
 
-static int extend_abbrev_len(const struct object_id *a,
-			     const struct object_id *b,
-			     unsigned *out)
-{
-	unsigned len = oid_common_prefix_hexlen(a, b);
-	if (len != hash_algos[a->algo].hexsz && len >= *out)
-		*out = len + 1;
-	return 0;
-}
-
-static void find_abbrev_len_for_midx(struct multi_pack_index *m,
-				     const struct object_id *oid,
-				     unsigned min_len,
-				     unsigned *out)
-{
-	unsigned len = min_len;
-
-	for (; m; m = m->base_midx) {
-		int match = 0;
-		uint32_t num, first = 0;
-		struct object_id found_oid;
-
-		if (!m->num_objects)
-			continue;
-
-		num = m->num_objects + m->num_objects_in_base;
-		match = bsearch_one_midx(oid, m, &first);
-
-		/*
-		 * first is now the position in the packfile where we
-		 * would insert the object ID if it does not exist (or the
-		 * position of the object ID if it does exist). Hence, we
-		 * consider a maximum of two objects nearby for the
-		 * abbreviation length.
-		 */
-
-		if (!match) {
-			if (nth_midxed_object_oid(&found_oid, m, first))
-				extend_abbrev_len(&found_oid, oid, &len);
-		} else if (first < num - 1) {
-			if (nth_midxed_object_oid(&found_oid, m, first + 1))
-				extend_abbrev_len(&found_oid, oid, &len);
-		}
-		if (first > 0) {
-			if (nth_midxed_object_oid(&found_oid, m, first - 1))
-				extend_abbrev_len(&found_oid, oid, &len);
-		}
-	}
-
-	*out = len;
-}
-
-static void find_abbrev_len_for_pack(struct packed_git *p,
-				     const struct object_id *oid,
-				     unsigned min_len,
-				     unsigned *out)
-{
-	int match;
-	uint32_t num, first = 0;
-	struct object_id found_oid;
-	unsigned len = min_len;
-
-	num = p->num_objects;
-	match = bsearch_pack(oid, p, &first);
-
-	/*
-	 * first is now the position in the packfile where we would insert
-	 * the object ID if it does not exist (or the position of mad->hash if
-	 * it does exist). Hence, we consider a maximum of two objects
-	 * nearby for the abbreviation length.
-	 */
-	if (!match) {
-		if (!nth_packed_object_id(&found_oid, p, first))
-			extend_abbrev_len(&found_oid, oid, &len);
-	} else if (first < num - 1) {
-		if (!nth_packed_object_id(&found_oid, p, first + 1))
-			extend_abbrev_len(&found_oid, oid, &len);
-	}
-	if (first > 0) {
-		if (!nth_packed_object_id(&found_oid, p, first - 1))
-			extend_abbrev_len(&found_oid, oid, &len);
-	}
-
-	*out = len;
-}
-
-int packfile_store_find_abbrev_len(struct odb_source_packed *store,
-				   const struct object_id *oid,
-				   unsigned min_len,
-				   unsigned *out)
-{
-	struct packfile_list_entry *e;
-	struct multi_pack_index *m;
-
-	m = get_multi_pack_index(&store->files->base);
-	if (m)
-		find_abbrev_len_for_midx(m, oid, min_len, &min_len);
-
-	for (e = packfile_store_get_packs(store); e; e = e->next) {
-		if (e->pack->multi_pack_index)
-			continue;
-		if (open_pack_index(e->pack) || !e->pack->num_objects)
-			continue;
-
-		find_abbrev_len_for_pack(e->pack, oid, min_len, &min_len);
-	}
-
-	*out = min_len;
-	return 0;
-}
-
 struct add_promisor_object_data {
 	struct repository *repo;
 	struct oidset *set;
diff --git a/packfile.h b/packfile.h
index 0613fd3c63..79324e4010 100644
--- a/packfile.h
+++ b/packfile.h
@@ -217,11 +217,6 @@ int for_each_object_in_pack(struct packed_git *p,
 			    each_packed_object_fn, void *data,
 			    enum odb_for_each_object_flags flags);
 
-int packfile_store_find_abbrev_len(struct odb_source_packed *store,
-				   const struct object_id *oid,
-				   unsigned min_len,
-				   unsigned *out);
-
 /* A hook to report invalid files in pack directory */
 #define PACKDIR_FILE_PACK 1
 #define PACKDIR_FILE_IDX 2

-- 
2.55.0.rc0.786.g65d90a0328.dirty


^ permalink raw reply related


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox