Git development
 help / color / mirror / Atom feed
* [PATCH 3/4] builtin/refs: add "update" subcommand
From: Patrick Steinhardt @ 2026-06-16  8:44 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano
In-Reply-To: <20260616-pks-refs-writing-subcommands-v1-0-9f5219b6109d@pks.im>

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(+)

diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc
index c03e8e6ac3..0a887cf5e5 100644
--- a/Documentation/git-refs.adoc
+++ b/Documentation/git-refs.adoc
@@ -21,6 +21,7 @@ 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 update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]
 
 DESCRIPTION
 -----------
@@ -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>`.
+
 OPTIONS
 -------
 
diff --git a/builtin/refs.c b/builtin/refs.c
index 69eb528522..3238ddf3f0 100644
--- a/builtin/refs.c
+++ b/builtin/refs.c
@@ -24,6 +24,9 @@
 #define REFS_DELETE_USAGE \
 	N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]")
 
+#define REFS_UPDATE_USAGE \
+	N_("git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]")
+
 static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
 			    struct repository *repo)
 {
@@ -219,6 +222,51 @@ static int cmd_refs_delete(int argc, const char **argv, const char *prefix,
 			       argc == 2 ? &oldoid : NULL, flags);
 }
 
+static int cmd_refs_update(int argc, const char **argv, const char *prefix,
+			   struct repository *repo)
+{
+	static char const * const refs_update_usage[] = {
+		REFS_UPDATE_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_BIT(0, "create-reflog", &flags, N_("create a reflog"),
+			REF_FORCE_CREATE_REFLOG),
+		OPT_END(),
+	};
+	struct object_id newoid, oldoid;
+	const char *refname;
+
+	argc = parse_options(argc, argv, prefix, opts, refs_update_usage, 0);
+	if (argc < 2 || argc > 3)
+		usage(_("update requires reference name, new value and an optional old value"));
+
+	if (message && !*message)
+		die(_("refusing to perform update with empty message"));
+
+	repo_config(repo, git_default_config, NULL);
+
+	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]);
+	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]);
+
+	return refs_update_ref(get_main_ref_store(repo), message, refname,
+			       &newoid, argc == 3 ? &oldoid : NULL, flags,
+			       UPDATE_REFS_DIE_ON_ERR);
+}
+
 int cmd_refs(int argc,
 	     const char **argv,
 	     const char *prefix,
@@ -231,6 +279,7 @@ int cmd_refs(int argc,
 		REFS_EXISTS_USAGE,
 		REFS_OPTIMIZE_USAGE,
 		REFS_DELETE_USAGE,
+		REFS_UPDATE_USAGE,
 		NULL,
 	};
 	parse_opt_subcommand_fn *fn = NULL;
@@ -241,6 +290,7 @@ int cmd_refs(int argc,
 		OPT_SUBCOMMAND("exists", &fn, cmd_refs_exists),
 		OPT_SUBCOMMAND("optimize", &fn, cmd_refs_optimize),
 		OPT_SUBCOMMAND("delete", &fn, cmd_refs_delete),
+		OPT_SUBCOMMAND("update", &fn, cmd_refs_update),
 		OPT_END(),
 	};
 
diff --git a/t/meson.build b/t/meson.build
index 1ccf08a3b5..2063962dab 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -224,6 +224,7 @@ integration_tests = [
   't1462-refs-exists.sh',
   't1463-refs-optimize.sh',
   't1464-refs-delete.sh',
+  't1465-refs-update.sh',
   't1500-rev-parse.sh',
   't1501-work-tree.sh',
   't1502-rev-parse-parseopt.sh',
diff --git a/t/t1465-refs-update.sh b/t/t1465-refs-update.sh
new file mode 100755
index 0000000000..e7582a6195
--- /dev/null
+++ b/t/t1465-refs-update.sh
@@ -0,0 +1,179 @@
+#!/bin/sh
+
+test_description='git refs update'
+
+. ./test-lib.sh
+
+setup_repo () {
+	git init "$1" &&
+	test_commit -C "$1" A &&
+	test_commit -C "$1" B
+}
+
+test_ref_matches () {
+	git rev-parse "$1" >expect &&
+	echo "$2" >actual &&
+	test_cmp expect actual
+}
+
+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"
+	)
+'
+
+test_expect_success 'update an existing reference without oldvalue' '
+	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 &&
+		git refs update refs/heads/foo $B &&
+		test_ref_matches refs/heads/foo $B
+	)
+'
+
+test_expect_success 'update with matching oldvalue' '
+	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 &&
+		git refs update refs/heads/foo $B $A &&
+		test_ref_matches refs/heads/foo $B
+	)
+'
+
+test_expect_success 'update 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 refs update refs/heads/foo $A &&
+		test_must_fail git refs update refs/heads/foo $B $B 2>err &&
+		test_grep " but expected " 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 &&
+	(
+		cd repo &&
+		test_must_fail git refs update refs/heads/foo invalid-oid 2>err &&
+		test_grep "invalid new object ID" err &&
+		test_must_fail git refs exists refs/heads/foo
+	)
+'
+
+test_expect_success 'update with invalid old value fails' '
+	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 invalid-oid 2>err &&
+		test_grep "invalid old object ID" err &&
+		test_ref_matches refs/heads/foo $A
+	)
+'
+
+test_expect_success 'update --no-deref rewrites the symref itself' '
+	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 &&
+		git symbolic-ref refs/heads/symref refs/heads/foo &&
+		git refs update --no-deref refs/heads/symref $B &&
+		test_must_fail git symbolic-ref refs/heads/symref &&
+		test_ref_matches refs/heads/symref $B &&
+		test_ref_matches refs/heads/foo $A
+	)
+'
+
+test_expect_success 'update does not create a reflog by default' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git refs update refs/foo $A &&
+		test_must_fail git reflog exists refs/foo
+	)
+'
+
+test_expect_success 'update creates a reflog with --create-reflog' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git refs update --create-reflog refs/foo $A &&
+		git reflog exists refs/foo
+	)
+'
+
+test_expect_success 'update with message records reason in reflog' '
+	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 &&
+		git refs update --message=update-reason refs/heads/foo $B &&
+		git reflog show refs/heads/foo >actual &&
+		test_grep "update-reason$" actual
+	)
+'
+
+test_expect_success 'update with empty message fails' '
+	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 --message= refs/heads/foo $B 2>err &&
+		test_grep "empty message" err
+	)
+'
+
+test_expect_success 'update with too few arguments fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	test_must_fail git -C repo refs update refs/heads/foo 2>err &&
+	test_grep "requires reference name, new value" err
+'
+
+test_expect_success 'update with too many arguments fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		B=$(git rev-parse B) &&
+		test_must_fail git refs update refs/heads/foo $A $B extra 2>err &&
+		test_grep "requires reference name, new value" err
+	)
+'
+
+test_done

-- 
2.55.0.rc0.786.g65d90a0328.dirty


^ permalink raw reply related

* [PATCH 2/4] builtin/refs: add "delete" subcommand
From: Patrick Steinhardt @ 2026-06-16  8:44 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano
In-Reply-To: <20260616-pks-refs-writing-subcommands-v1-0-9f5219b6109d@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              |  46 +++++++++++++++
 t/meson.build               |   1 +
 t/t1464-refs-delete.sh      | 133 ++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 197 insertions(+)

diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc
index fa33680cc7..c03e8e6ac3 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> [<oldvalue>]
 
 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 `<oldvalue>` is given, the
+	reference is only deleted after verifying that it currently contains
+	`<oldvalue>`.
+
 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..69eb528522 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> [<oldvalue>]")
+
 static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
 			    struct repository *repo)
 {
@@ -175,6 +178,47 @@ 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;
+
+	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 object with null old object ID"));
+	}
+
+	return refs_delete_ref(get_main_ref_store(repo), message, refname,
+			       argc == 2 ? &oldoid : NULL, flags);
+}
+
 int cmd_refs(int argc,
 	     const char **argv,
 	     const char *prefix,
@@ -186,6 +230,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 +240,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..4a36d3866b
--- /dev/null
+++ b/t/t1464-refs-delete.sh
@@ -0,0 +1,133 @@
+#!/bin/sh
+
+test_description='git refs delete'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./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 1/4] builtin/refs: drop `the_repository`
From: Patrick Steinhardt @ 2026-06-16  8:44 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano
In-Reply-To: <20260616-pks-refs-writing-subcommands-v1-0-9f5219b6109d@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 0/4] builtin/refs: add ability to write references
From: Patrick Steinhardt @ 2026-06-16  8:44 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano

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].

Thanks!

Patrick

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

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

 Documentation/git-refs.adoc |  34 +++++++++
 builtin/refs.c              | 153 +++++++++++++++++++++++++++++++++++--
 t/meson.build               |   3 +
 t/t1464-refs-delete.sh      | 133 ++++++++++++++++++++++++++++++++
 t/t1465-refs-update.sh      | 179 ++++++++++++++++++++++++++++++++++++++++++++
 t/t1466-refs-rename.sh      | 131 ++++++++++++++++++++++++++++++++
 6 files changed, 625 insertions(+), 8 deletions(-)


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


^ permalink raw reply

* Re: [PATCH v2 3/3] replay: offer an option to linearize the commit topology
From: Toon Claes @ 2026-06-16  8:38 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git, Johannes Schindelin
In-Reply-To: <xmqqjys6wcpo.fsf@gitster.g>

Junio C Hamano <gitster@pobox.com> writes:

> In the review response during the previous iteration, I commented
> that (1) the original excluded only merges, but (2) your version
> excluded both merges and the root commits the same way.  Your
> response was:
>
>     The way it was written in v1 was maybe a bit too smart and hard to
>     follow. I agree with your suggestion and will adopt this (with some
>     tweaks) in the next version.
>
> which I took as saying "it may be confusing, but it correctly
> expresses what we want to do", meaning "yes, roots and merges should
> be handled the same way".  But the above no longer treats roots the
> same way as merges.  I think that is intended, but just wanted to
> double check.

Great callout. I was running the "replay down to root" test with v1 vs
v2, but as you pointed out, the test I wrote doesn't actually replay
down to root. Now I've fixed the test and reran the test against both
versions and verified what you're saying.

So to answer your question, yes this change is intentional and shout out
to you for requesting to add this test (properly) so we actually catch
this. 

>> +test_expect_success 'replay to rebase merge commit with --linearize down to root commit' '
>> +	git replay --ref-action=print --linearize --onto main A..topic-with-merge >result &&
>
> As with other test pieces, this "git replay" command line is overly
> long and hides the important bit which is that the range being
> replayed is *not* actually down to the root, which is A (it excludes
> A).  Intended?

No, not intended. And while at it, I'll split up the command on two
lines. 

-- 
Cheers,
Toon

^ permalink raw reply

* Re: [PATCH] rebase: mention --abort alongside --continue
From: Phillip Wood @ 2026-06-16  8:36 UTC (permalink / raw)
  To: Harald Nordgren via GitGitGadget, git; +Cc: Harald Nordgren
In-Reply-To: <pull.2330.git.git.1781551170529.gitgitgadget@gmail.com>

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.

Thanks

Phillip

> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
> ---
>      rebase: mention --abort when an exec step fails
>      
>      Mention git rebase --abort both in the warning shown when an exec step
>      fails and in the git status advice while splitting or editing a commit,
>      since rebase pointed users at --continue there without saying how to
>      bail out, unlike every comparable command.
> 
> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2330%2FHaraldNordgren%2Frebase-exec-abort-hint-v1
> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2330/HaraldNordgren/rebase-exec-abort-hint-v1
> Pull-Request: https://github.com/git/git/pull/2330
> 
>   sequencer.c            |  8 ++++++--
>   t/t7512-status-help.sh | 17 +++++++++++++++++
>   wt-status.c            |  7 ++++++-
>   3 files changed, 29 insertions(+), 3 deletions(-)
> 
> diff --git a/sequencer.c b/sequencer.c
> index 57855b0066..c46e5b95bc 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -3884,7 +3884,9 @@ static int do_exec(struct repository *r, const char *command_line, int quiet)
>   			  "You can fix the problem, and then run\n"
>   			  "\n"
>   			  "  git rebase --continue\n"
> -			  "\n"),
> +			  "\n"
> +			  "To abort and get back to the state before \"git rebase\", run "
> +			  "\"git rebase --abort\".\n"),
>   			command_line,
>   			dirty ? _("and made changes to the index and/or the "
>   				"working tree.\n") : "");
> @@ -3897,7 +3899,9 @@ static int do_exec(struct repository *r, const char *command_line, int quiet)
>   			  "Commit or stash your changes, and then run\n"
>   			  "\n"
>   			  "  git rebase --continue\n"
> -			  "\n"), command_line);
> +			  "\n"
> +			  "To abort and get back to the state before \"git rebase\", run "
> +			  "\"git rebase --abort\".\n"), command_line);
>   		status = 1;
>   	}
>   
> diff --git a/t/t7512-status-help.sh b/t/t7512-status-help.sh
> index 08e82f7914..ca7ef66ae3 100755
> --- a/t/t7512-status-help.sh
> +++ b/t/t7512-status-help.sh
> @@ -206,6 +206,7 @@ No commands remaining.
>   You are currently editing a commit while rebasing branch '\''rebase_i_edit'\'' on '\''$ONTO'\''.
>     (use "git commit --amend" to amend the current commit)
>     (use "git rebase --continue" once you are satisfied with your changes)
> +  (use "git rebase --abort" to check out the original branch)
>   
>   nothing to commit (use -u to show untracked files)
>   EOF
> @@ -240,6 +241,7 @@ Next command to do (1 remaining command):
>     (use "git rebase --edit-todo" to view and edit)
>   You are currently splitting a commit while rebasing branch '\''split_commit'\'' on '\''$ONTO'\''.
>     (Once your working directory is clean, run "git rebase --continue")
> +  (use "git rebase --abort" to check out the original branch)
>   
>   Changes not staged for commit:
>     (use "git add <file>..." to update what will be committed)
> @@ -278,6 +280,7 @@ No commands remaining.
>   You are currently editing a commit while rebasing branch '\''amend_last'\'' on '\''$ONTO'\''.
>     (use "git commit --amend" to amend the current commit)
>     (use "git rebase --continue" once you are satisfied with your changes)
> +  (use "git rebase --abort" to check out the original branch)
>   
>   nothing to commit (use -u to show untracked files)
>   EOF
> @@ -317,6 +320,7 @@ Next command to do (1 remaining command):
>   You are currently editing a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
>     (use "git commit --amend" to amend the current commit)
>     (use "git rebase --continue" once you are satisfied with your changes)
> +  (use "git rebase --abort" to check out the original branch)
>   
>   nothing to commit (use -u to show untracked files)
>   EOF
> @@ -347,6 +351,7 @@ Next command to do (1 remaining command):
>     (use "git rebase --edit-todo" to view and edit)
>   You are currently splitting a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
>     (Once your working directory is clean, run "git rebase --continue")
> +  (use "git rebase --abort" to check out the original branch)
>   
>   Changes not staged for commit:
>     (use "git add <file>..." to update what will be committed)
> @@ -383,6 +388,7 @@ Next command to do (1 remaining command):
>   You are currently editing a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
>     (use "git commit --amend" to amend the current commit)
>     (use "git rebase --continue" once you are satisfied with your changes)
> +  (use "git rebase --abort" to check out the original branch)
>   
>   nothing to commit (use -u to show untracked files)
>   EOF
> @@ -414,6 +420,7 @@ Next command to do (1 remaining command):
>   You are currently editing a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
>     (use "git commit --amend" to amend the current commit)
>     (use "git rebase --continue" once you are satisfied with your changes)
> +  (use "git rebase --abort" to check out the original branch)
>   
>   nothing to commit (use -u to show untracked files)
>   EOF
> @@ -445,6 +452,7 @@ Next command to do (1 remaining command):
>     (use "git rebase --edit-todo" to view and edit)
>   You are currently splitting a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
>     (Once your working directory is clean, run "git rebase --continue")
> +  (use "git rebase --abort" to check out the original branch)
>   
>   Changes not staged for commit:
>     (use "git add <file>..." to update what will be committed)
> @@ -482,6 +490,7 @@ Next command to do (1 remaining command):
>   You are currently editing a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
>     (use "git commit --amend" to amend the current commit)
>     (use "git rebase --continue" once you are satisfied with your changes)
> +  (use "git rebase --abort" to check out the original branch)
>   
>   nothing to commit (use -u to show untracked files)
>   EOF
> @@ -515,6 +524,7 @@ Next command to do (1 remaining command):
>   You are currently editing a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
>     (use "git commit --amend" to amend the current commit)
>     (use "git rebase --continue" once you are satisfied with your changes)
> +  (use "git rebase --abort" to check out the original branch)
>   
>   nothing to commit (use -u to show untracked files)
>   EOF
> @@ -548,6 +558,7 @@ Next command to do (1 remaining command):
>     (use "git rebase --edit-todo" to view and edit)
>   You are currently splitting a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
>     (Once your working directory is clean, run "git rebase --continue")
> +  (use "git rebase --abort" to check out the original branch)
>   
>   Changes not staged for commit:
>     (use "git add <file>..." to update what will be committed)
> @@ -587,6 +598,7 @@ Next command to do (1 remaining command):
>   You are currently editing a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
>     (use "git commit --amend" to amend the current commit)
>     (use "git rebase --continue" once you are satisfied with your changes)
> +  (use "git rebase --abort" to check out the original branch)
>   
>   nothing to commit (use -u to show untracked files)
>   EOF
> @@ -718,6 +730,7 @@ No commands remaining.
>   You are currently editing a commit while rebasing branch '\''bisect'\'' on '\''$ONTO'\''.
>     (use "git commit --amend" to amend the current commit)
>     (use "git rebase --continue" once you are satisfied with your changes)
> +  (use "git rebase --abort" to check out the original branch)
>   
>   You are currently bisecting, started from branch '\''bisect_while_rebasing'\''.
>     (use "git bisect reset" to get back to the original branch)
> @@ -987,6 +1000,7 @@ No commands remaining.
>   You are currently editing a commit while rebasing branch '\''several_commits'\'' on '\''$ONTO'\''.
>     (use "git commit --amend" to amend the current commit)
>     (use "git rebase --continue" once you are satisfied with your changes)
> +  (use "git rebase --abort" to check out the original branch)
>   
>   nothing to commit (use -u to show untracked files)
>   EOF
> @@ -1015,6 +1029,7 @@ Next commands to do (2 remaining commands):
>   You are currently editing a commit while rebasing branch '\''several_commits'\'' on '\''$ONTO'\''.
>     (use "git commit --amend" to amend the current commit)
>     (use "git rebase --continue" once you are satisfied with your changes)
> +  (use "git rebase --abort" to check out the original branch)
>   
>   nothing to commit (use -u to show untracked files)
>   EOF
> @@ -1044,6 +1059,7 @@ Next commands to do (2 remaining commands):
>   You are currently editing a commit while rebasing branch '\''several_commits'\'' on '\''$ONTO'\''.
>     (use "git commit --amend" to amend the current commit)
>     (use "git rebase --continue" once you are satisfied with your changes)
> +  (use "git rebase --abort" to check out the original branch)
>   
>   nothing to commit (use -u to show untracked files)
>   EOF
> @@ -1064,6 +1080,7 @@ Next command to do (1 remaining command):
>   You are currently editing a commit while rebasing branch '\''several_commits'\'' on '\''$ONTO'\''.
>     (use "git commit --amend" to amend the current commit)
>     (use "git rebase --continue" once you are satisfied with your changes)
> +  (use "git rebase --abort" to check out the original branch)
>   
>   nothing to commit (use -u to show untracked files)
>   EOF
> diff --git a/wt-status.c b/wt-status.c
> index b17372390c..94fd14a058 100644
> --- a/wt-status.c
> +++ b/wt-status.c
> @@ -1527,9 +1527,12 @@ static void show_rebase_in_progress(struct wt_status *s,
>   		else
>   			status_printf_ln(s, color,
>   					 _("You are currently splitting a commit during a rebase."));
> -		if (s->hints)
> +		if (s->hints) {
>   			status_printf_ln(s, color,
>   				_("  (Once your working directory is clean, run \"git rebase --continue\")"));
> +			status_printf_ln(s, color,
> +				_("  (use \"git rebase --abort\" to check out the original branch)"));
> +		}
>   	} else {
>   		if (s->state.branch)
>   			status_printf_ln(s, color,
> @@ -1544,6 +1547,8 @@ static void show_rebase_in_progress(struct wt_status *s,
>   				_("  (use \"git commit --amend\" to amend the current commit)"));
>   			status_printf_ln(s, color,
>   				_("  (use \"git rebase --continue\" once you are satisfied with your changes)"));
> +			status_printf_ln(s, color,
> +				_("  (use \"git rebase --abort\" to check out the original branch)"));
>   		}
>   	}
>   	wt_longstatus_print_trailer(s);
> 
> base-commit: ea97ad8d017de0c9037451a78008a0fd60abea0c


^ permalink raw reply

* Re: [PATCH 0/2] rebase: add --fixup to fold a range into its oldest commit
From: Patrick Steinhardt @ 2026-06-16  8:34 UTC (permalink / raw)
  To: D. Ben Knoble
  Cc: Harald Nordgren, Junio C Hamano, Harald Nordgren via GitGitGadget,
	git
In-Reply-To: <CALnO6CBgHz5d5BT5gCyqyhw_HpV733msWOnrxmu-TJ0QGHE9tA@mail.gmail.com>

On Mon, Jun 15, 2026 at 11:17:21AM -0400, D. Ben Knoble wrote:
> On Mon, Jun 15, 2026 at 4:22 AM Harald Nordgren
> <haraldnordgren@gmail.com> wrote:
> > > > Adds git rebase --autosquash --fixup [<upstream>] to fold a range of commits
> > > > into its oldest one, reusing that commit's message.
> [snip]
> > > I also wonder if we can do something like this without adding any
> > > new option or command.  E.g., if you have four patch series, where
> > > the initial implementation HEAD~3 is followed by "oops it was still
> > > wrong" fix-up HEAD~2, HEAD~1 and HEAD, then
> > >
> > >     git reset --soft HEAD~3 && git commit --amend --no-edit
> > >
> > > is what the user wants to do, no?
> >
> > I don't think it's enough. First of all the user has to know the N for
> > HEAD~N, and then 'git reset --soft HEAD~N && git commit --amend
> > --no-edit' is still quite ugly.
> 
> Well, there are a few ways to get this more easily than counting; for example,
> 
> - git rev-list @{u}.. | tail -n1
> - the lovely ":/<pattern>" or "@^{/<pattern>}" revision notations
> - etc.
> 
> ---
> 
> Stepping back a moment and assuming that the important thing you want
> is the "squash" (and not necessarily the "rebase" moving commits onto
> a new base), I wonder about
> 
>      git history squash <range>
> 
> which would squash all commits in the (now arbitrary!) range into the
> first. That makes it somewhat more versatile at selecting commits, I
> think, at the cost that re-basing is somewhat harder. That is, you
> could then do
> 
>     git history squash @~3..
> 
> and things like
> 
>     git history squash @~5..@~2
> 
> As a future extension, I think we could support merge commits: merges
> could be replayed as a merge into the final squash instead (creating
> an octopus merge if there are multiple merges to replay), though I'm
> hand-waving what we should do for conflicts. (We _do_ know what the
> final tree should look like—the same as the final commit in the
> range—so maybe we can actually avoid all conflicts?)
> 
> Anyway, I've cc'd Patrick for his opinion about whether this fits in
> "git-history".

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.

Patrick

^ permalink raw reply

* Re: [PATCH v3] update-ref: add --rename option
From: Patrick Steinhardt @ 2026-06-16  7:41 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git
In-Reply-To: <xmqqv7bjbusb.fsf@gitster.g>

On Mon, Jun 15, 2026 at 10:03:32AM -0700, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> > On Fri, Jun 12, 2026 at 08:41:48AM -0700, Junio C Hamano wrote:
> >> Patrick Steinhardt <ps@pks.im> writes:
> >> 
> >> > A slight tangent: this is part of why I really don't like commands that
> >> > determine their mode via flags: you now have to worry about every
> >> > combination of flags and whether they even make sense. With subcommands
> >> > we at least only have to worry about the set of flags that directly
> >> > apply to that given subcommand.
> >> >
> >> > Makes me wonder whether I should have a look at extending git-refs(1)
> >> > further:
> >> >
> >> >     git refs delete <ref> [<oldvalue>]
> >> >     git refs update <ref> <newvalue> [<oldvalue>]
> >> >     git refs rename <ref> <oldname> <newname>
> >> >
> >> > I always wanted to do this eventually so that we have one top-level
> >> > command that knows how to do "everything refs".
> >> 
> >> That may indeed be a better direction to go, but isn't update-ref
> >> the "everything refs" command already?
> >
> > Well, it doesn't handle reading references, which is something that
> > git-refs(1) already knows to do.
> 
> OK, fair enough.  "git refs" should become superset of "git
> show-ref" and "git update-ref", I guess.
> 
> So do you want to take the topic over and add it to "git refs"?

Sure, I can do that.

Patrick

^ permalink raw reply

* Re: [PATCH v2 3/3] replay: offer an option to linearize the commit topology
From: Toon Claes @ 2026-06-16  7:09 UTC (permalink / raw)
  To: Elijah Newren; +Cc: git, Johannes Schindelin
In-Reply-To: <CABPp-BGRi2obnqRGEY9pSMyvRbNGs8AdVUpZmr0C6vZSgHb=cg@mail.gmail.com>

Elijah Newren <newren@gmail.com> writes:

> Hi,
>
> On Wed, Jun 10, 2026 at 7:51 AM Toon Claes <toon@iotcl.com> wrote:
>>
>> From: Johannes Schindelin <Johannes.Schindelin@gmx.de>
>>
>> One of the stated goals of git-replay(1) is to allow implementing the
>> git-rebase(1) functionality on the server side.
>>
>> The default mode of git-rebase(1) is to act as if `--no-rebase-merges`
>> was given. This mode drops merge commits instead of replaying them, and
>> linearizes the commit history into a sequence of the
>> regular (single-parent) commits.
>>
>> Add option `--linearize` to git-replay(1) to do the same.
>
> I think this version is nicer overall than the one from my
> replay-upstream branch; sorry for repeatedly getting distracted from
> that, but this does look nice.

Dscho gets most of the credit here. And don't worry about being
distracted, we know how things go around here. I appreciate the review!

>
> A few small comments:
>
>> Co-authored-by: Toon Claes <toon@iotcl.com>
>> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
>> Signed-off-by: Toon Claes <toon@iotcl.com>
>> ---
>>  Documentation/git-replay.adoc |  5 +++++
>>  builtin/replay.c              |  4 ++++
>>  replay.c                      | 30 +++++++++++++++++++++++-------
>>  replay.h                      |  5 +++++
>>  t/t3650-replay-basics.sh      | 26 ++++++++++++++++++++++++++
>>  5 files changed, 63 insertions(+), 7 deletions(-)
>>
>> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
>> index a32f72aead..41c96c7061 100644
>> --- a/Documentation/git-replay.adoc
>> +++ b/Documentation/git-replay.adoc
>> @@ -88,6 +88,11 @@ incompatible with `--contained` (which is a modifier for `--onto` only).
>>  +
>>  The default mode can be configured via the `replay.refAction` configuration variable.
>>
>> +--linearize::
>> +       In this mode, `git replay` imitates `git rebase --no-rebase-merges`,
>> +       i.e. it cherry-picks only non-merge commits, each one on top of the
>> +       previous one.
>
> The SYNOPSIS block at the top of the file is missing this new flag.
>
> The replay_usage[] variable in cmd_replay is also missing this new flag.
>
>>  <revision-range>::
>>         Range of commits to replay; see "Specifying Ranges" in
>>         linkgit:git-rev-parse[1]. In `--advance=<branch>` or
>> diff --git a/builtin/replay.c b/builtin/replay.c
>> index 39e3a86f6c..fedfe46dc6 100644
>> --- a/builtin/replay.c
>> +++ b/builtin/replay.c
>> @@ -111,6 +111,8 @@ int cmd_replay(int argc,
>>                              N_("mode"),
>>                              N_("control ref update behavior (update|print)"),
>>                              PARSE_OPT_NONEG),
>> +               OPT_BOOL(0, "linearize", &opts.linearize,
>> +                        N_("ignore merge commits instead of replaying them")),
>
> "ignore" feels a bit ambiguous to me.  Can we use "drop" instead,
> matching your commit message?

Agreed, I don't like it too. "drop" sounds better.

>>                 OPT_END()
>>         };
>>
>> @@ -132,6 +134,8 @@ int cmd_replay(int argc,
>>                                   opts.contained, "--contained");
>>         die_for_incompatible_opt2(!!opts.ref, "--ref",
>>                                   !!opts.contained, "--contained");
>> +       die_for_incompatible_opt2(!!opts.revert, "--revert",
>> +                                 opts.linearize, "--linearize");
>
> Sensible; should the docs mention this incompatibility?  (I'm not sure
> myself; just throwing it out as food for thought.)

Let's add it.

>>
>>         /* Parse ref action mode from command line or config */
>>         ref_mode = get_ref_action_mode(repo, ref_action);
>> diff --git a/replay.c b/replay.c
>> index 7921d7dba3..81033fb889 100644
>> --- a/replay.c
>> +++ b/replay.c
>> @@ -277,12 +277,16 @@ static struct commit *pick_regular_commit(struct repository *repo,
>>                                           struct commit *onto,
>>                                           struct merge_options *merge_opt,
>>                                           struct merge_result *result,
>> +                                         struct commit *replayed_base,
>>                                           bool reverse,
>>                                           enum replay_empty_commit_action empty)
>>  {
>> -       struct commit *base, *replayed_base;
>> +       struct commit *base;
>>         struct tree *pickme_tree, *base_tree, *replayed_base_tree;
>>
>> +       if (replayed_base && reverse)
>> +               BUG("Linearizing commits is not supported when replaying in reverse");
>> +
>
> This is dead code given the die_for_incompatible_opt2 check above,
> right?  Just extra defense in depth?

We also have another defense-in-depth for --onto/--advance/--revert:

    BUG("expected one of onto_name, *advance_name, or *revert_name");

I don't mind having it for --linearize too.

>>         if (pickme->parents) {
>>                 base = pickme->parents->item;
>>                 base_tree = repo_get_commit_tree(repo, base);
>> @@ -291,7 +295,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
>>                 base_tree = lookup_tree(repo, repo->hash_algo->empty_tree);
>>         }
>>
>> -       replayed_base = get_mapped_commit(replayed_commits, base, onto);
>> +       if (!replayed_base)
>> +               replayed_base = get_mapped_commit(replayed_commits, base, onto);
>>         replayed_base_tree = repo_get_commit_tree(repo, replayed_base);
>>         pickme_tree = repo_get_commit_tree(repo, pickme);
>>
>> @@ -430,12 +435,23 @@ int replay_revisions(struct rev_info *revs,
>>         while ((commit = get_revision(revs))) {
>>                 const struct name_decoration *decoration;
>>
>> -               if (commit->parents && commit->parents->next)
>> -                       die(_("replaying merge commits is not supported yet!"));
>> +               if (commit->parents && commit->parents->next) {
>> +                       if (!opts->linearize)
>> +                               die(_("replaying merge commits is not supported yet!"));
>> +                       /*
>> +                        * When linearizing, a merge commit itself is not picked,
>> +                        * but refs that point to it might need updating.
>> +                        */
>
> Is it worth pointing out that last_commit is intentionally not updated
> by this code path?  That is implied by your comment, but it takes a
> bit of reasoning to get there, and I think it might help future
> readers to just explicitly state it.

Ah yes, I didn't realize that, but you make a good point. I'll rephrase
the comment a bit.

>> +               } else {
>> +                       struct commit *to_pick = reverse ? last_commit : onto;
>> +                       last_commit =
>> +                               pick_regular_commit(revs->repo, commit,
>> +                                                   replayed_commits, to_pick,
>> +                                                   &merge_opt, &result,
>> +                                                   opts->linearize ? last_commit : NULL,
>> +                                                   reverse, opts->empty);
>> +               }
>>
>> -               last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
>> -                                                 reverse ? last_commit : onto,
>> -                                                 &merge_opt, &result, reverse, opts->empty);
>>                 if (!last_commit)
>>                         break;
>>
>> diff --git a/replay.h b/replay.h
>> index 1851a07705..07e6fdcca3 100644
>> --- a/replay.h
>> +++ b/replay.h
>> @@ -62,6 +62,11 @@ struct replay_revisions_options {
>>          * Defaults to REPLAY_EMPTY_COMMIT_DROP.
>>          */
>>         enum replay_empty_commit_action empty;
>> +
>> +       /*
>> +        * Whether to linearize the commits (i.e. drop merge commits).
>> +        */
>> +       int linearize;
>>  };
>>
>>  /* This struct is used as an out-parameter by `replay_revisions()`. */
>> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
>> index 3353bc4a4d..64e0731188 100755
>> --- a/t/t3650-replay-basics.sh
>> +++ b/t/t3650-replay-basics.sh
>> @@ -565,4 +565,30 @@ test_expect_success '--onto with --ref rejects multiple revision ranges' '
>>         test_grep "cannot be used with multiple revision ranges" err
>>  '
>>
>> +test_expect_success 'replay merge commit fails' '
>> +       echo "fatal: replaying merge commits is not supported yet!" >expect &&
>> +       test_must_fail git replay --ref-action=print --onto main I..P 2>actual &&
>> +       test_cmp expect actual
>> +'
>> +
>> +test_expect_success 'replay to rebase merge commit with --linearize' '
>> +       git replay --ref-action=print --linearize --onto main I..topic-with-merge >result &&
>> +
>> +       test_line_count = 1 result &&
>> +
>> +       git log --format=%s $(cut -f 3 -d " " result) >actual &&
>> +       test_write_lines O N J M L B A >expect &&
>> +       test_cmp expect actual
>> +'
>> +
>> +test_expect_success 'replay to rebase merge commit with --linearize down to root commit' '
>> +       git replay --ref-action=print --linearize --onto main A..topic-with-merge >result &&
>
> You'd need to drop "A.." to have it go down to the root commit, as
> Junio mentioned elsewhere.

Yes, thanks for double confirmation.

>> +
>> +       test_line_count = 1 result &&
>> +
>> +       git log --format=%s $(cut -f 3 -d " " result) >actual &&
>> +       test_write_lines O N J I M L B A >expect &&
>> +       test_cmp expect actual
>> +'
>> +
>>  test_done
>
> Should there also be a testcase combining --linearize and --advance?

Sure.

> Should there be a test with the incompatibility of --revert &
> --linearize?  I think we have a few other tests for incompatible
> options.

I was already about to add that.

> One additional testing idea, borrowed from an older variant of
> this patch I had sitting in a local branch (dscho's original
> linearize patch, adapted): in addition to checking specific commit
> subjects, it's worth verifying that the linearized chain produces
> the *same patches* as the original.  Something along the lines of:
>
>         test_expect_success '--linearize preserves patches' '
>                 test_when_finished "git update-ref -d refs/heads/merge_I_L" &&
>                 test_tick &&
>                 git checkout -b merge_I_L I &&
>                 git merge --no-edit L &&
>
>                 git replay --linearize --onto A B..merge_I_L &&
>
>                 # range-diff ignores merges, so the original
>                 # {I, L, merge} reduces to {I, L} on the LHS,
>                 # and the replayed chain on the RHS should match.
>                 git range-diff B..merge_I_L@{1} B..merge_I_L >out &&
>                 ! test_grep -v "=" out &&
>
>                 git log --oneline A..merge_I_L >out &&
>                 test_line_count = 2 out
>         '
>
> The range-diff check is nice because it asserts patch equivalence
> rather than tying the test to a particular replay ordering, which
> makes the test less brittle if the rev-walk order ever changes.
> Feel free to take, adapt, or ignore.

Interesting idea and I like it. Lemme add it.

> Anyway, thanks for working on this; looking good.

Thanks!

-- 
Cheers,
Toon

^ permalink raw reply

* Re: [PATCH v2 0/7] setup: drop global state
From: Toon Claes @ 2026-06-16  6:42 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: Justin Tobler, git
In-Reply-To: <aivsoM9Qwv0m_P1c@pks.im>

Patrick Steinhardt <ps@pks.im> writes:

> There's only a single change to a commit message I've queued, so I'll
> for now not send another iteration.

Yeah, not worth a reroll.

-- 
Cheers,
Toon

^ permalink raw reply

* Re: [PATCH v4 0/3] Documentation: recommend the use of b4
From: Toon Claes @ 2026-06-16  6:40 UTC (permalink / raw)
  To: Patrick Steinhardt, git
  Cc: Junio C Hamano, Tuomas Ahola, Weijie Yuan, Ramsay Jones,
	SZEDER Gábor, Kristoffer Haugsbakk, Karthik Nayak
In-Reply-To: <20260615-pks-b4-v4-0-22cfca8f19c5@pks.im>

Patrick Steinhardt <ps@pks.im> writes:

> Hi,
>
> this small patch series wires up b4 in Git and recommends the use
> thereof via "MyFirstContribution", as discussed in [1].
>
> Changes in v4:
>   - Improve a commit message.
>   - Link to v3: https://patch.msgid.link/20260608-pks-b4-v3-0-f5e497d10c56@pks.im
>
> Changes in v3:
>   - I wasn't really able to judge consensus one way or the other
>     regarding the deep vs shallow nesting of cover letters, so I still
>     have the change to shallow nesting of cover letters part of this
>     series. If we continue to be split on this one (or if we favor the
>     current status quo) I'm happy to drop the first patch and adapt the
>     last patch to use deep nesting of cover letters instead.
>   - Hopefully fix some confusion by saying "shallow/deep threading of
>     cover letters".
>   - Fix some more instances where we recommend deep threading of cover
>     letters.
>   - Link to v2: https://patch.msgid.link/20260603-pks-b4-v2-0-a8aea0aa2c23@pks.im
>
> Changes in v2:
>   - Reorder commits so that the b4 docs are added first.
>   - Add a section that highlights how to configure b4, and that points
>     out that the per-project defaults can be overridden via Git
>     configuration.
>   - Add a patch to MyFirstContribution that recommends shallow
>     threading. I mostly intend this to be a discussion starter so that
>     the `.b4-config` file matches our preferred threading style.
>   - Fix a typo.
>   - Link to v1: https://patch.msgid.link/20260602-pks-b4-v1-0-a7ae5a49e9cf@pks.im
>
> Thanks!
>
> Patrick
>
> [1]: <xmqqik81xpqx.fsf@gitster.g>
>
> ---
> Patrick Steinhardt (3):
>       MyFirstContribution: recommend shallow threading of cover letters
>       MyFirstContribution: recommend the use of b4
>       b4: introduce configuration for the Git project
>
>  .b4-config                             |   6 ++
>  .b4-cover-template                     |  11 ++++
>  Documentation/MyFirstContribution.adoc | 100 ++++++++++++++++++++++++++++++---
>  Documentation/SubmittingPatches        |   6 +-
>  4 files changed, 114 insertions(+), 9 deletions(-)
>
> Range-diff versus v3:
>
> 1:  1aec56f76c = 1:  b6b488e6a8 MyFirstContribution: recommend shallow threading of cover letters
> 2:  f2036769bd = 2:  1a68b993d2 MyFirstContribution: recommend the use of b4
> 3:  fb522c7d90 ! 3:  5bc8fba96a b4: introduce configuration for the Git project
>     @@ Metadata
>       ## Commit message ##
>          b4: introduce configuration for the Git project
>      
>     -    We're about to extend our documentation to recommend b4 for sending
>     -    patch series to the mailing list. Prepare for this by introducing a b4
>     -    configuration so that the tool knows to honor our preferences. For now,
>     -    this configuration does two things:
>     +    In the preceding commit we have extended our documentation to recommend
>     +    b4 for sending patch series to the mailing list. Introduce configuration
>     +    so that it knows to honor preferences of the Git project by default. For
>     +    now, this configuration does two things:
>      
>            - It configures "send-same-thread = shallow", which tells b4 to always
>              send subsequent versions of the same patch series as a reply to the

No further comments based on the range-diff.

-- 
Cheers,
Toon

^ permalink raw reply

* Re: [PATCH] builtin/history: unuse the commit buffer after use
From: Patrick Steinhardt @ 2026-06-16  5:45 UTC (permalink / raw)
  To: Jeff King; +Cc: Kaartic Sivaraam, Git mailing list
In-Reply-To: <20260615172946.GD91269@coredump.intra.peff.net>

On Mon, Jun 15, 2026 at 01:29:46PM -0400, Jeff King wrote:
> On Mon, Jun 15, 2026 at 11:48:10AM +0200, Patrick Steinhardt wrote:
> 
> > On Sun, Jun 14, 2026 at 02:15:40PM +0000, Kaartic Sivaraam wrote:
> > > While running `git history reword` using a Git built with `SANITIZE` flag set
> > > to `address,leak`, we could observe the following leak being reported:
> > 
> > Huh, curious. That seems to hint that we're missing test coverage for
> > this specific scenario, as our test suite doesn't detect this leak.
> 
> I think it will only leak when the commit object has an "encoding"
> header. See below.
> 
> > > As part of rewording a commit in `git history`, we get the commit message
> > > buffer in the `commit_tree_ext` function. This in turn obtains the buffer
> > > from `repo_logmsg_reencode`. Given how `commit_tree_ext` is invoking the
> > > function with the last two parameters as NULL, we are clearly not expecting
> > > a reencode to happen. In this case, the buffer that we receive from
> > > `repo_logmsg_reencode` ends up always being obtained from a call to
> > > `repo_get_commit_buffer`.
> > > 
> > > This buffer is expected to be released with an accompanying call to
> > > `repo_unuse_commit_buffer` which takes care of freeing it. This call
> > > is missing in the `commit_tree_ext` flow thus resulting in the leak.
> > 
> > So this doesn't really read specific at all, and I would have expected
> > us to hit this leak. Puzzling.
> 
> The first paragraph is accurate here. We'd generally just get a pointer
> to the buffer cached in the slab, because no re-encoding occurs. And in
> that case you _don't_ need to call unuse_commit_buffer(), because you
> have a read-only copy, and the slab cache will hold it forever[1].
> Calling the unuse function will be a noop.
> 
> But when we _do_ re-encode, then you get a new buffer which must be
> freed. And that is when you have to call the unuse function. And the
> reason it is "unuse" and not just "free" is that you don't necessarily
> know which you have, but that function figures it out (and frees it only
> if necessary).
> 
> So what the patch is doing is correct, but the explanation is a little
> confused. We see the leak only when re-encoding, so we'd probably want a
> test case that triggers that. Which I assume implies rewriting a commit
> that was previously generated with an encoding header.
> 
> Now back to that [1] note. Even if we didn't re-encode, we'll still hold
> onto that buffer forever. It's not a "leak" in the traditional sense
> because it's still referenced in the commit slab cache. But if you are
> going to walk over a million commits (like git-log does), you probably
> don't want to hold a million commit messages in memory at once.
> 
> For that you'd want to call free_commit_buffer() when you know you're
> totally done with it (again, like git-log does after it finishes showing
> the commit). That might be the case here in commit_tree_ext(), or it
> might happen later (I'm not familiar with the git-history code).
> 
> But note that you need to do _both_ the unuse and free calls. If we did
> re-encode, the former is needed to free the newly allocated buffer. The
> latter only drops the original buffer in the cache.

Thanks for the explanation!

Patrick

^ permalink raw reply

* [GSoC Patch v5 4/4] repo: add path.gitdir with absolute and relative suffix formatting
From: K Jayatheerth @ 2026-06-16  4:49 UTC (permalink / raw)
  To: git
  Cc: jltobler, lucasseikioshiro, gitster, phillip.wood, sandals,
	kumarayushjha123, a3205153416, K Jayatheerth
In-Reply-To: <20260616044953.184806-1-jayatheerthkulkarni2005@gmail.com>

Scripts need a stable way to locate the git directory without
parsing rev-parse output or relying on its flag-driven path format
selection. There is no way to retrieve this path from git repo info
today.

Introduce path.gitdir.absolute and path.gitdir.relative keys,
consistent with the path.commondir keys added in the previous patch.
Reuse the test_repo_info_path helper introduced there to validate
both variants.

Mentored-by: Justin Tobler <jltobler@gmail.com>
Mentored-by: Lucas Seiki Oshiro <lucasseikioshiro@gmail.com>
Signed-off-by: K Jayatheerth <jayatheerthkulkarni2005@gmail.com>
---
 Documentation/git-repo.adoc |  6 ++++++
 builtin/repo.c              | 24 ++++++++++++++++++++++++
 t/t1900-repo-info.sh        |  6 ++++++
 3 files changed, 36 insertions(+)

diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index 890c34051d..ed7d80c690 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -113,6 +113,12 @@ values that they return:
 	The path to the Git repository's common directory relative to
 	the current working directory.
 
+`path.gitdir.absolute`::
+	The canonical absolute path to the Git repository directory (the `.git` directory).
+
+`path.gitdir.relative`::
+	The path to the Git repository directory relative to the current working directory.
+
 `references.format`::
 	The reference storage format. The valid values are:
 +
diff --git a/builtin/repo.c b/builtin/repo.c
index c4cc3bf3fc..9a312d127a 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -99,6 +99,28 @@ static int get_path_commondir_relative(struct repository *repo, struct strbuf *b
 	return 0;
 }
 
+static int get_path_gitdir_absolute(struct repository *repo, struct strbuf *buf)
+{
+	const char *git_dir = repo_get_git_dir(repo);
+
+	if (!git_dir)
+		return error(_("unable to get git directory"));
+
+	append_formatted_path(buf, git_dir, startup_info->prefix, PATH_FORMAT_CANONICAL);
+	return 0;
+}
+
+static int get_path_gitdir_relative(struct repository *repo, struct strbuf *buf)
+{
+	const char *git_dir = repo_get_git_dir(repo);
+
+	if (!git_dir)
+		return error(_("unable to get git directory"));
+
+	append_formatted_path(buf, git_dir, startup_info->prefix, PATH_FORMAT_RELATIVE);
+	return 0;
+}
+
 static int get_references_format(struct repository *repo, struct strbuf *buf)
 {
 	strbuf_addstr(buf,
@@ -113,6 +135,8 @@ static const struct repo_info_field repo_info_field[] = {
 	{ "object.format", get_object_format },
 	{ "path.commondir.absolute", get_path_commondir_absolute },
 	{ "path.commondir.relative", get_path_commondir_relative },
+	{ "path.gitdir.absolute", get_path_gitdir_absolute },
+	{ "path.gitdir.relative", get_path_gitdir_relative },
 	{ "references.format", get_references_format },
 };
 
diff --git a/t/t1900-repo-info.sh b/t/t1900-repo-info.sh
index 09158d29f9..ae8c22c817 100755
--- a/t/t1900-repo-info.sh
+++ b/t/t1900-repo-info.sh
@@ -207,4 +207,10 @@ test_repo_info_path 'commondir with only GIT_DIR' 'commondir' \
 	'.git' \
 	'GIT_DIR="../.git" && export GIT_DIR'
 
+test_repo_info_path 'gitdir standard' 'gitdir' '.git'
+
+test_repo_info_path 'gitdir with explicit GIT_DIR' 'gitdir' \
+	'.git' \
+	'GIT_DIR="../.git" && export GIT_DIR'
+
 test_done
-- 
2.54.0


^ permalink raw reply related

* [GSoC Patch v5 3/4] repo: add path.commondir with absolute and relative suffix formatting
From: K Jayatheerth @ 2026-06-16  4:49 UTC (permalink / raw)
  To: git
  Cc: jltobler, lucasseikioshiro, gitster, phillip.wood, sandals,
	kumarayushjha123, a3205153416, K Jayatheerth
In-Reply-To: <20260616044953.184806-1-jayatheerthkulkarni2005@gmail.com>

Scripts working with worktree setups need a reliable way to discover
the common directory, which diverges from the git directory when
multiple worktrees are in use. There is no way to retrieve this path
from git repo info today.

Introduce path.commondir.absolute and path.commondir.relative keys.
Exposing explicit format variants rather than a single key with a
default avoids ambiguity for scripts that require predictable output.

Mentored-by: Justin Tobler <jltobler@gmail.com>
Mentored-by: Lucas Seiki Oshiro <lucasseikioshiro@gmail.com>
Signed-off-by: K Jayatheerth <jayatheerthkulkarni2005@gmail.com>
---
 Documentation/git-repo.adoc |  9 +++++++
 builtin/repo.c              | 26 +++++++++++++++++++
 t/t1900-repo-info.sh        | 52 +++++++++++++++++++++++++++++++++++++
 3 files changed, 87 insertions(+)

diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index 42262c1983..890c34051d 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -104,6 +104,15 @@ values that they return:
 `object.format`::
 	The object format (hash algorithm) used in the repository.
 
+`path.commondir.absolute`::
+	The canonical absolute path to the Git repository's common
+	directory (the shared `.git` directory containing objects,
+	refs, and global configuration).
+
+`path.commondir.relative`::
+	The path to the Git repository's common directory relative to
+	the current working directory.
+
 `references.format`::
 	The reference storage format. The valid values are:
 +
diff --git a/builtin/repo.c b/builtin/repo.c
index 71a5c1c29c..c4cc3bf3fc 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -7,12 +7,14 @@
 #include "hex.h"
 #include "odb.h"
 #include "parse-options.h"
+#include "path.h"
 #include "path-walk.h"
 #include "progress.h"
 #include "quote.h"
 #include "ref-filter.h"
 #include "refs.h"
 #include "revision.h"
+#include "setup.h"
 #include "strbuf.h"
 #include "string-list.h"
 #include "shallow.h"
@@ -75,6 +77,28 @@ static int get_object_format(struct repository *repo, struct strbuf *buf)
 	return 0;
 }
 
+static int get_path_commondir_absolute(struct repository *repo, struct strbuf *buf)
+{
+	const char *common_dir = repo_get_common_dir(repo);
+
+	if (!common_dir)
+		return error(_("unable to get common directory"));
+
+	append_formatted_path(buf, common_dir, startup_info->prefix, PATH_FORMAT_CANONICAL);
+	return 0;
+}
+
+static int get_path_commondir_relative(struct repository *repo, struct strbuf *buf)
+{
+	const char *common_dir = repo_get_common_dir(repo);
+
+	if (!common_dir)
+		return error(_("unable to get common directory"));
+
+	append_formatted_path(buf, common_dir, startup_info->prefix, PATH_FORMAT_RELATIVE);
+	return 0;
+}
+
 static int get_references_format(struct repository *repo, struct strbuf *buf)
 {
 	strbuf_addstr(buf,
@@ -87,6 +111,8 @@ static const struct repo_info_field repo_info_field[] = {
 	{ "layout.bare", get_layout_bare },
 	{ "layout.shallow", get_layout_shallow },
 	{ "object.format", get_object_format },
+	{ "path.commondir.absolute", get_path_commondir_absolute },
+	{ "path.commondir.relative", get_path_commondir_relative },
 	{ "references.format", get_references_format },
 };
 
diff --git a/t/t1900-repo-info.sh b/t/t1900-repo-info.sh
index 39bb77dda0..09158d29f9 100755
--- a/t/t1900-repo-info.sh
+++ b/t/t1900-repo-info.sh
@@ -155,4 +155,56 @@ test_expect_success 'git repo info -h shows only repo info usage' '
 	test_grep ! "git repo structure" actual
 '
 
+# Helper function to test path keys in both absolute and relative formats.
+# $1: label for the test
+# $2: field_name (e.g., commondir)
+# $3: expected_dir (the directory name, e.g., .git or custom-common)
+# $4: init_command (extra setup like exporting env vars)
+test_repo_info_path () {
+	label=$1
+	field_name=$2
+	expected_dir=$3
+	init_command=$4
+
+	test_expect_success "absolute: $label" '
+		test_when_finished "rm -rf repo" &&
+		git init repo &&
+		(
+			mkdir -p repo/sub &&
+			cd repo/sub &&
+			ROOT="$(test-tool path-utils real_path ..)" && export ROOT &&
+			eval "$init_command" &&
+			echo "path.$field_name.absolute=$ROOT/$expected_dir" >expect &&
+			git repo info "path.$field_name.absolute" >actual &&
+			test_cmp expect actual
+		)
+	'
+
+	test_expect_success "relative: $label" '
+		test_when_finished "rm -rf repo" &&
+		git init repo &&
+		(
+			mkdir -p repo/sub &&
+			cd repo/sub &&
+			ROOT="$(test-tool path-utils real_path ..)" && export ROOT &&
+			eval "$init_command" &&
+			echo "path.$field_name.relative=../$expected_dir" >expect &&
+			git repo info "path.$field_name.relative" >actual &&
+			test_cmp expect actual
+		)
+	'
+}
+
+test_repo_info_path 'commondir standard' 'commondir' '.git'
+
+test_repo_info_path 'commondir with GIT_COMMON_DIR and GIT_DIR' 'commondir' \
+	'custom-common' \
+	'GIT_COMMON_DIR="$ROOT/custom-common" && export GIT_COMMON_DIR &&
+	 GIT_DIR="../.git" && export GIT_DIR &&
+	 git init --bare "$ROOT/custom-common"'
+
+test_repo_info_path 'commondir with only GIT_DIR' 'commondir' \
+	'.git' \
+	'GIT_DIR="../.git" && export GIT_DIR'
+
 test_done
-- 
2.54.0


^ permalink raw reply related

* [GSoC Patch v5 2/4] rev-parse: use append_formatted_path() for path formatting
From: K Jayatheerth @ 2026-06-16  4:49 UTC (permalink / raw)
  To: git
  Cc: jltobler, lucasseikioshiro, gitster, phillip.wood, sandals,
	kumarayushjha123, a3205153416, K Jayatheerth
In-Reply-To: <20260616044953.184806-1-jayatheerthkulkarni2005@gmail.com>

Now that path formatting logic lives in a shared helper, keeping a
duplicate implementation in rev-parse is unnecessary and risks the
two diverging over time.

Replace the local format_type and default_type enums and the
hand-rolled formatting logic with a call to append_formatted_path().
Introduce PATH_FORMAT_DEFAULT as the initial value of arg_path_format
so that per-path fallback behavior is resolved in print_path() rather
than leaked into the shared helper.

Mentored-by: Justin Tobler <jltobler@gmail.com>
Mentored-by: Lucas Seiki Oshiro <lucasseikioshiro@gmail.com>
Signed-off-by: K Jayatheerth <jayatheerthkulkarni2005@gmail.com>
---
 builtin/rev-parse.c | 104 ++++++++++----------------------------------
 1 file changed, 24 insertions(+), 80 deletions(-)

diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c
index 218b5f34d6..1fdcb946a7 100644
--- a/builtin/rev-parse.c
+++ b/builtin/rev-parse.c
@@ -632,73 +632,17 @@ static void handle_ref_opt(const char *pattern, const char *prefix)
 	clear_ref_exclusions(&ref_excludes);
 }
 
-enum format_type {
-	/* We would like a relative path. */
-	FORMAT_RELATIVE,
-	/* We would like a canonical absolute path. */
-	FORMAT_CANONICAL,
-	/* We would like the default behavior. */
-	FORMAT_DEFAULT,
-};
-
-enum default_type {
-	/* Our default is a relative path. */
-	DEFAULT_RELATIVE,
-	/* Our default is a relative path if there's a shared root. */
-	DEFAULT_RELATIVE_IF_SHARED,
-	/* Our default is a canonical absolute path. */
-	DEFAULT_CANONICAL,
-	/* Our default is not to modify the item. */
-	DEFAULT_UNMODIFIED,
-};
-
-static void print_path(const char *path, const char *prefix, enum format_type format, enum default_type def)
+static void print_path(const char *path, const char *prefix,
+		       enum path_format arg_path_format, enum path_format def_format)
 {
-	char *cwd = NULL;
-	/*
-	 * We don't ever produce a relative path if prefix is NULL, so set the
-	 * prefix to the current directory so that we can produce a relative
-	 * path whenever possible.  If we're using RELATIVE_IF_SHARED mode, then
-	 * we want an absolute path unless the two share a common prefix, so don't
-	 * set it in that case, since doing so causes a relative path to always
-	 * be produced if possible.
-	 */
-	if (!prefix && (format != FORMAT_DEFAULT || def != DEFAULT_RELATIVE_IF_SHARED))
-		prefix = cwd = xgetcwd();
-	if (format == FORMAT_DEFAULT && def == DEFAULT_UNMODIFIED) {
-		puts(path);
-	} else if (format == FORMAT_RELATIVE ||
-		  (format == FORMAT_DEFAULT && def == DEFAULT_RELATIVE)) {
-		/*
-		 * In order for relative_path to work as expected, we need to
-		 * make sure that both paths are absolute paths.  If we don't,
-		 * we can end up with an unexpected absolute path that the user
-		 * didn't want.
-		 */
-		struct strbuf buf = STRBUF_INIT, realbuf = STRBUF_INIT, prefixbuf = STRBUF_INIT;
-		if (!is_absolute_path(path)) {
-			strbuf_realpath_forgiving(&realbuf, path,  1);
-			path = realbuf.buf;
-		}
-		if (!is_absolute_path(prefix)) {
-			strbuf_realpath_forgiving(&prefixbuf, prefix, 1);
-			prefix = prefixbuf.buf;
-		}
-		puts(relative_path(path, prefix, &buf));
-		strbuf_release(&buf);
-		strbuf_release(&realbuf);
-		strbuf_release(&prefixbuf);
-	} else if (format == FORMAT_DEFAULT && def == DEFAULT_RELATIVE_IF_SHARED) {
-		struct strbuf buf = STRBUF_INIT;
-		puts(relative_path(path, prefix, &buf));
-		strbuf_release(&buf);
-	} else {
-		struct strbuf buf = STRBUF_INIT;
-		strbuf_realpath_forgiving(&buf, path, 1);
-		puts(buf.buf);
-		strbuf_release(&buf);
-	}
-	free(cwd);
+	struct strbuf sb = STRBUF_INIT;
+	/* If the user didn't explicitly specify a format, fallback to the path-specific default. */
+	enum path_format fmt = (arg_path_format != PATH_FORMAT_DEFAULT) ? arg_path_format : def_format;
+
+	append_formatted_path(&sb, path, prefix, fmt);
+	puts(sb.buf);
+
+	strbuf_release(&sb);
 }
 
 int cmd_rev_parse(int argc,
@@ -717,7 +661,7 @@ int cmd_rev_parse(int argc,
 	const char *name = NULL;
 	struct strbuf buf = STRBUF_INIT;
 	int seen_end_of_options = 0;
-	enum format_type format = FORMAT_DEFAULT;
+	enum path_format arg_path_format = PATH_FORMAT_DEFAULT;
 
 	show_usage_if_asked(argc, argv, builtin_rev_parse_usage);
 
@@ -797,8 +741,8 @@ int cmd_rev_parse(int argc,
 					die(_("--git-path requires an argument"));
 				print_path(repo_git_path_replace(the_repository, &buf,
 								 "%s", argv[i + 1]), prefix,
-						format,
-						DEFAULT_RELATIVE_IF_SHARED);
+						arg_path_format,
+						PATH_FORMAT_RELATIVE_IF_SHARED);
 				i++;
 				continue;
 			}
@@ -820,9 +764,9 @@ int cmd_rev_parse(int argc,
 				if (!arg)
 					die(_("--path-format requires an argument"));
 				if (!strcmp(arg, "absolute")) {
-					format = FORMAT_CANONICAL;
+					arg_path_format = PATH_FORMAT_CANONICAL;
 				} else if (!strcmp(arg, "relative")) {
-					format = FORMAT_RELATIVE;
+					arg_path_format = PATH_FORMAT_RELATIVE;
 				} else {
 					die(_("unknown argument to --path-format: %s"), arg);
 				}
@@ -985,7 +929,7 @@ int cmd_rev_parse(int argc,
 			if (!strcmp(arg, "--show-toplevel")) {
 				const char *work_tree = repo_get_work_tree(the_repository);
 				if (work_tree)
-					print_path(work_tree, prefix, format, DEFAULT_UNMODIFIED);
+					print_path(work_tree, prefix, arg_path_format, PATH_FORMAT_UNMODIFIED);
 				else
 					die(_("this operation must be run in a work tree"));
 				continue;
@@ -993,7 +937,7 @@ int cmd_rev_parse(int argc,
 			if (!strcmp(arg, "--show-superproject-working-tree")) {
 				struct strbuf superproject = STRBUF_INIT;
 				if (get_superproject_working_tree(&superproject))
-					print_path(superproject.buf, prefix, format, DEFAULT_UNMODIFIED);
+					print_path(superproject.buf, prefix, arg_path_format, PATH_FORMAT_UNMODIFIED);
 				strbuf_release(&superproject);
 				continue;
 			}
@@ -1028,18 +972,18 @@ int cmd_rev_parse(int argc,
 				const char *gitdir = getenv(GIT_DIR_ENVIRONMENT);
 				char *cwd;
 				int len;
-				enum format_type wanted = format;
+				enum path_format wanted = arg_path_format;
 				if (arg[2] == 'g') {	/* --git-dir */
 					if (gitdir) {
-						print_path(gitdir, prefix, format, DEFAULT_UNMODIFIED);
+						print_path(gitdir, prefix, arg_path_format, PATH_FORMAT_UNMODIFIED);
 						continue;
 					}
 					if (!prefix) {
-						print_path(".git", prefix, format, DEFAULT_UNMODIFIED);
+						print_path(".git", prefix, arg_path_format, PATH_FORMAT_UNMODIFIED);
 						continue;
 					}
 				} else {		/* --absolute-git-dir */
-					wanted = FORMAT_CANONICAL;
+					wanted = PATH_FORMAT_CANONICAL;
 					if (!gitdir && !prefix)
 						gitdir = ".git";
 					if (gitdir) {
@@ -1055,11 +999,11 @@ int cmd_rev_parse(int argc,
 				strbuf_reset(&buf);
 				strbuf_addf(&buf, "%s%s.git", cwd, len && cwd[len-1] != '/' ? "/" : "");
 				free(cwd);
-				print_path(buf.buf, prefix, wanted, DEFAULT_CANONICAL);
+				print_path(buf.buf, prefix, wanted, PATH_FORMAT_CANONICAL);
 				continue;
 			}
 			if (!strcmp(arg, "--git-common-dir")) {
-				print_path(repo_get_common_dir(the_repository), prefix, format, DEFAULT_RELATIVE_IF_SHARED);
+				print_path(repo_get_common_dir(the_repository), prefix, arg_path_format, PATH_FORMAT_RELATIVE_IF_SHARED);
 				continue;
 			}
 			if (!strcmp(arg, "--is-inside-git-dir")) {
@@ -1089,7 +1033,7 @@ int cmd_rev_parse(int argc,
 				if (the_repository->index->split_index) {
 					const struct object_id *oid = &the_repository->index->split_index->base_oid;
 					const char *path = repo_git_path_replace(the_repository, &buf, "sharedindex.%s", oid_to_hex(oid));
-					print_path(path, prefix, format, DEFAULT_RELATIVE);
+					print_path(path, prefix, arg_path_format, PATH_FORMAT_RELATIVE);
 				}
 				continue;
 			}
-- 
2.54.0


^ permalink raw reply related

* [GSoC Patch v5 1/4] path: introduce append_formatted_path() for shared path formatting
From: K Jayatheerth @ 2026-06-16  4:49 UTC (permalink / raw)
  To: git
  Cc: jltobler, lucasseikioshiro, gitster, phillip.wood, sandals,
	kumarayushjha123, a3205153416, K Jayatheerth
In-Reply-To: <20260616044953.184806-1-jayatheerthkulkarni2005@gmail.com>

The path-formatting logic in builtin/rev-parse.c is tightly coupled
to that command and writes directly to stdout, making it impossible
for other builtins to reuse.

Extract the core algorithm into append_formatted_path() in path.c
and expose a path_format enum in path.h so that any builtin can
format paths consistently without duplicating logic.

Mentored-by: Justin Tobler <jltobler@gmail.com>
Mentored-by: Lucas Seiki Oshiro <lucasseikioshiro@gmail.com>
Signed-off-by: K Jayatheerth <jayatheerthkulkarni2005@gmail.com>
---
 path.c | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 path.h | 36 ++++++++++++++++++++++++++++++
 2 files changed, 106 insertions(+)

diff --git a/path.c b/path.c
index d7e17bf174..5e83e3e4f6 100644
--- a/path.c
+++ b/path.c
@@ -1579,6 +1579,76 @@ char *xdg_cache_home(const char *filename)
 	return NULL;
 }
 
+void append_formatted_path(struct strbuf *dest, const char *path,
+			   const char *prefix, enum path_format format)
+{
+	switch (format) {
+	case PATH_FORMAT_DEFAULT:
+	case PATH_FORMAT_UNMODIFIED:
+		strbuf_addstr(dest, path);
+		break;
+
+	case PATH_FORMAT_RELATIVE: {
+		struct strbuf relative_buf = STRBUF_INIT;
+		struct strbuf real_path = STRBUF_INIT;
+		struct strbuf real_prefix = STRBUF_INIT;
+		char *cwd = NULL;
+
+		/*
+		 * We don't ever produce a relative path if prefix is NULL,
+		 * so set the prefix to the current directory so that we can
+		 * produce a relative path whenever possible.
+		 */
+		if (!prefix)
+			prefix = cwd = xgetcwd();
+
+		if (!is_absolute_path(path)) {
+			strbuf_realpath_forgiving(&real_path, path, 1);
+			path = real_path.buf;
+		}
+		if (!is_absolute_path(prefix)) {
+			strbuf_realpath_forgiving(&real_prefix, prefix, 1);
+			prefix = real_prefix.buf;
+		}
+
+		strbuf_addstr(dest, relative_path(path, prefix, &relative_buf));
+
+		strbuf_release(&relative_buf);
+		strbuf_release(&real_path);
+		strbuf_release(&real_prefix);
+		free(cwd);
+		break;
+	}
+
+	case PATH_FORMAT_RELATIVE_IF_SHARED: {
+		struct strbuf relative_buf = STRBUF_INIT;
+
+		/*
+		 * If we're using RELATIVE_IF_SHARED mode, then we want an
+		 * absolute path unless the two share a common prefix, so don't
+		 * default the prefix to the current working directory. Doing so
+		 * would cause a relative path to always be produced if possible.
+		 */
+		strbuf_addstr(dest, relative_path(path, prefix, &relative_buf));
+		strbuf_release(&relative_buf);
+		break;
+	}
+
+	case PATH_FORMAT_CANONICAL: {
+		struct strbuf canonical_buf = STRBUF_INIT;
+
+		strbuf_realpath_forgiving(&canonical_buf, path, 1);
+		strbuf_addbuf(dest, &canonical_buf);
+
+		strbuf_release(&canonical_buf);
+		break;
+	}
+
+	default:
+		BUG("unknown path_format value %d", format);
+	}
+}
+
 REPO_GIT_PATH_FUNC(squash_msg, "SQUASH_MSG")
 REPO_GIT_PATH_FUNC(merge_msg, "MERGE_MSG")
 REPO_GIT_PATH_FUNC(merge_rr, "MERGE_RR")
diff --git a/path.h b/path.h
index 0434ba5e07..6aca53b100 100644
--- a/path.h
+++ b/path.h
@@ -262,6 +262,42 @@ enum scld_error safe_create_leading_directories_no_share(char *path);
 int safe_create_file_with_leading_directories(struct repository *repo,
 					      const char *path);
 
+/**
+ * The formatting strategy to apply when writing a path into a buffer.
+ */
+enum path_format {
+	/*
+	 * Represents the default formatting behavior. Treated as
+	 * PATH_FORMAT_UNMODIFIED by append_formatted_path().
+	 */
+	PATH_FORMAT_DEFAULT,
+
+	/* Output the path exactly as-is without any modifications. */
+	PATH_FORMAT_UNMODIFIED,
+
+	/* Output a path relative to the provided directory prefix. */
+	PATH_FORMAT_RELATIVE,
+
+	/* Output a relative path only if the path shares a root with the prefix. */
+	PATH_FORMAT_RELATIVE_IF_SHARED,
+
+	/* Output a fully resolved, absolute canonical path. */
+	PATH_FORMAT_CANONICAL
+};
+
+/**
+ * Format a path according to the specified formatting strategy and append
+ * the result to the given strbuf.
+ *
+ * `dest`   : The string buffer to append the formatted path to.
+ * `path`   : The path string that needs to be formatted.
+ * `prefix` : The directory prefix to calculate relative offsets against.
+ * Pass NULL to default to the current working directory where applicable.
+ * `format` : The formatting behavior rule to execute.
+ */
+void append_formatted_path(struct strbuf *dest, const char *path,
+			   const char *prefix, enum path_format format);
+
 # ifdef USE_THE_REPOSITORY_VARIABLE
 #  include "strbuf.h"
 #  include "repository.h"
-- 
2.54.0


^ permalink raw reply related

* [GSoC Patch v5 0/4] teach git repo info to handle path keys
From: K Jayatheerth @ 2026-06-16  4:49 UTC (permalink / raw)
  To: git
  Cc: jltobler, lucasseikioshiro, gitster, phillip.wood, sandals,
	kumarayushjha123, a3205153416, K Jayatheerth
In-Reply-To: <20260601151950.30686-1-jayatheerthkulkarni2005@gmail.com>

Hi!

This series teaches `git repo info` to handle `path.*`
keys, allowing scripts to reliably discover core
repository paths without resorting to `git rev-parse`.

The patches are structured as follows:

1. path: Extract the localized path-formatting logic
   out of `rev-parse` and expose it globally via
   `path.h` using clear append semantics.

2. rev-parse: Refactor the command to leverage the
   newly shared path engine.

3. repo: Introduce `path.commondir.absolute` and
   `path.commondir.relative` alongside a robust,
   isolated test helper.

4. repo: Introduce `path.gitdir.absolute` and
   `path.gitdir.relative` using the same standardized
   formatting rules.

changes since v4:
* Simplified the `test_repo_info_path` helper by dropping the `repo_name` 
  argument and utilizing `test_when_finished "rm -rf repo"` to handle 
  repository setup/teardown inline. This ensures perfect test isolation.
* Condensed the redundant `expect_absolute_suffix` and `expect_relative` 
  test helper arguments into a single `expected_dir` argument, reducing 
  the helper signature to 4 arguments (Justin).
* Added a contextual comment in `builtin/rev-parse.c`'s `print_path()` 
  explaining why `PATH_FORMAT_DEFAULT` is intercepted and overridden with 
  a path-specific fallback (Justin).
* Trimmed the verbose test helper explanations from the commit messages 
  in patches 3 and 4.

K Jayatheerth (4):
  path: introduce append_formatted_path() for shared path formatting
  rev-parse: use append_formatted_path() for path formatting
  repo: add path.commondir with absolute and relative suffix formatting
  repo: add path.gitdir with absolute and relative suffix formatting

 Documentation/git-repo.adoc |  15 ++++++
 builtin/repo.c              |  50 +++++++++++++++++
 builtin/rev-parse.c         | 104 +++++++++---------------------------
 path.c                      |  70 ++++++++++++++++++++++++
 path.h                      |  36 +++++++++++++
 t/t1900-repo-info.sh        |  58 ++++++++++++++++++++
 6 files changed, 253 insertions(+), 80 deletions(-)

Range-diff against v4:
1:  a396b4f8e6 = 1:  a396b4f8e6 path: introduce append_formatted_path() for shared path formatting
2:  16198f96d1 ! 2:  16b42a51d2 rev-parse: use append_formatted_path() for path formatting
    @@ builtin/rev-parse.c: static void handle_ref_opt(const char *pattern, const char
     -	}
     -	free(cwd);
     +	struct strbuf sb = STRBUF_INIT;
    ++	/* If the user didn't explicitly specify a format, fallback to the path-specific default. */
     +	enum path_format fmt = (arg_path_format != PATH_FORMAT_DEFAULT) ? arg_path_format : def_format;
     +
     +	append_formatted_path(&sb, path, prefix, fmt);
3:  b45c6f0d12 ! 3:  38b733ea64 repo: add path.commondir with absolute and relative suffix formatting
    @@ Commit message
         Exposing explicit format variants rather than a single key with a
         default avoids ambiguity for scripts that require predictable output.
     
    -    Add a test helper test_repo_info_path that creates isolated
    -    repositories per test case to prevent state leaks, captures the repo
    -    root before changing directories to avoid eval, and accepts an optional
    -    init_command to cover environment variable overrides such as
    -    GIT_COMMON_DIR and GIT_DIR.
    -
         Mentored-by: Justin Tobler <jltobler@gmail.com>
         Mentored-by: Lucas Seiki Oshiro <lucasseikioshiro@gmail.com>
         Signed-off-by: K Jayatheerth <jayatheerthkulkarni2005@gmail.com>
    @@ t/t1900-repo-info.sh: test_expect_success 'git repo info -h shows only repo info
     +# Helper function to test path keys in both absolute and relative formats.
     +# $1: label for the test
     +# $2: field_name (e.g., commondir)
    -+# $3: unique repo name for isolation
    -+# $4: expect_absolute (suffix appended to repo root)
    -+# $5: expect_relative (the relative path string expected)
    -+# $6: init_command (extra setup like exporting env vars)
    ++# $3: expected_dir (the directory name, e.g., .git or custom-common)
    ++# $4: init_command (extra setup like exporting env vars)
     +test_repo_info_path () {
     +	label=$1
     +	field_name=$2
    -+	repo_name=$3
    -+	expect_absolute_suffix=$4
    -+	expect_relative=$5
    -+	init_command=$6
    -+
    -+	absolute_root="$repo_name-absolute"
    -+	relative_root="$repo_name-relative"
    -+
    -+	test_expect_success "setup: $label" '
    -+		git init "$absolute_root" &&
    -+		git init "$relative_root" &&
    -+		mkdir -p "$absolute_root/sub" "$relative_root/sub"
    -+	'
    ++	expected_dir=$3
    ++	init_command=$4
     +
     +	test_expect_success "absolute: $label" '
    ++		test_when_finished "rm -rf repo" &&
    ++		git init repo &&
     +		(
    -+			cd "$absolute_root/sub" &&
    ++			mkdir -p repo/sub &&
    ++			cd repo/sub &&
     +			ROOT="$(test-tool path-utils real_path ..)" && export ROOT &&
     +			eval "$init_command" &&
    -+			expect_path="$ROOT${expect_absolute_suffix:+/$expect_absolute_suffix}" &&
    -+			echo "path.$field_name.absolute=$expect_path" >expect &&
    ++			echo "path.$field_name.absolute=$ROOT/$expected_dir" >expect &&
     +			git repo info "path.$field_name.absolute" >actual &&
     +			test_cmp expect actual
     +		)
     +	'
     +
     +	test_expect_success "relative: $label" '
    ++		test_when_finished "rm -rf repo" &&
    ++		git init repo &&
     +		(
    -+			cd "$relative_root/sub" &&
    ++			mkdir -p repo/sub &&
    ++			cd repo/sub &&
     +			ROOT="$(test-tool path-utils real_path ..)" && export ROOT &&
     +			eval "$init_command" &&
    -+			echo "path.$field_name.relative=$expect_relative" >expect &&
    ++			echo "path.$field_name.relative=../$expected_dir" >expect &&
     +			git repo info "path.$field_name.relative" >actual &&
     +			test_cmp expect actual
     +		)
     +	'
     +}
     +
    -+test_repo_info_path 'commondir standard' 'commondir' 'commondir-std' \
    -+	'.git' '../.git'
    ++test_repo_info_path 'commondir standard' 'commondir' '.git'
     +
     +test_repo_info_path 'commondir with GIT_COMMON_DIR and GIT_DIR' 'commondir' \
    -+	'commondir-envs' 'custom-common' '../custom-common' \
    ++	'custom-common' \
     +	'GIT_COMMON_DIR="$ROOT/custom-common" && export GIT_COMMON_DIR &&
     +	 GIT_DIR="../.git" && export GIT_DIR &&
     +	 git init --bare "$ROOT/custom-common"'
     +
     +test_repo_info_path 'commondir with only GIT_DIR' 'commondir' \
    -+	'commondir-only-gitdir' '.git' '../.git' \
    ++	'.git' \
     +	'GIT_DIR="../.git" && export GIT_DIR'
     +
      test_done
4:  b5234ffe3e ! 4:  ead1117332 repo: add path.gitdir with absolute and relative suffix formatting
    @@ builtin/repo.c: static const struct repo_info_field repo_info_field[] = {
     
      ## t/t1900-repo-info.sh ##
     @@ t/t1900-repo-info.sh: test_repo_info_path 'commondir with only GIT_DIR' 'commondir' \
    - 	'commondir-only-gitdir' '.git' '../.git' \
    + 	'.git' \
      	'GIT_DIR="../.git" && export GIT_DIR'
      
    -+test_repo_info_path 'gitdir standard' 'gitdir' 'gitdir-std' \
    -+	'.git' '../.git'
    ++test_repo_info_path 'gitdir standard' 'gitdir' '.git'
     +
     +test_repo_info_path 'gitdir with explicit GIT_DIR' 'gitdir' \
    -+	'gitdir-env' '.git' '../.git' \
    ++	'.git' \
     +	'GIT_DIR="../.git" && export GIT_DIR'
     +
      test_done
-- 
2.54.0

^ permalink raw reply

* Re: [GSoC Patch v4 2/4] rev-parse: use append_formatted_path() for path formatting
From: K Jayatheerth @ 2026-06-16  4:19 UTC (permalink / raw)
  To: Justin Tobler
  Cc: git, a3205153416, gitster, kumarayushjha123, lucasseikioshiro,
	phillip.wood, sandals, kristofferhaugsbakk
In-Reply-To: <ajAy1it6CGDQzVes@denethor>

Hey Justin,

>
> Without context, it might be a bit confusing to readers as to why we
> override PATH_FORMAT_DEFAULT without our own provided default. It may be
> worth leaving a comment to provide some breadcrumbs.
>
> The rest of this patch looks good to me.

That makes sense.
That's a minor change.
I will add proper comments!


> > Add a test helper test_repo_info_path that creates isolated
> > repositories per test case to prevent state leaks, captures the repo
> > root before changing directories to avoid eval, and accepts an optional
> > init_command to cover environment variable overrides such as
> > GIT_COMMON_DIR and GIT_DIR.
>
> I'm not sure this last paragraph in the log message provides much value.
> To me it's a bit verbose and focuses mostly on what the test helper is
> doing. Maybe we can just omit this section? If we want to have a note
> though maybe we could say something like:
>
>   Each path key is expected to have an absolute and relative form. To
>   reduce duplication, a test_repo_info_path helper function is
>   introduced to configure and exercise both cases.
>

Now that I think about it
Maybe removing it is a better option.

I mean the patch itself contains the test and it has comments explaining
the test itself.

I am gonna remove the last para in the next series.
Thanks for pointing that out!


> > +test_repo_info_path () {
> > +     label=$1
> > +     field_name=$2
> > +     repo_name=$3
> > +     expect_absolute_suffix=$4
> > +     expect_relative=$5
> > +     init_command=$6
>
> I may be overthinking it, but I can't help but feel this test helper is
> overly complicated. I wonder if we can simlify and reduce the number of
> arguments. For example, could we programatically construct the label
> from the field name and init_command instead of explicitly passing it?
>

That’s a fair question
But I personally don't think the helper is overly complicated.
I think a lot of the current helper can be mapped with test_repo_info's
structure itself.

The existing helper uses a very similar 5-argument signature (label,
init_command, repo_name, key, expected_value)
and separates the setup step from the assertion steps.

Regarding the labels, I'd prefer to keep them explicitly passed in.
Programmatically constructing the label from the init_command could
result in messy
or hard-to-read test descriptions in the console output,
and having explicit strings makes it much easier to debug when a
specific test fails.


> > +     absolute_root="$repo_name-absolute"
> > +     relative_root="$repo_name-relative"
> > +
> > +     test_expect_success "setup: $label" '
> > +             git init "$absolute_root" &&
> > +             git init "$relative_root" &&
> > +             mkdir -p "$absolute_root/sub" "$relative_root/sub"
> > +     '
>
> Do really need this setup test case? Could we instead embed the setup in
> both test cases below? Something like:
>
>         test_when_finished rm -rf repo &&
>         git init repo &&
>         (
>           mkdir repo/sub &&
>           cd repo/sub &&
>           ...
>         )
>

That's a much more elegant way to handle it.
I will incorporate this in v5!

> With something like this, each test case is responsible to creating its
> own repo and cleaning it up when finished. Then we could avoid have to
> provide a separate repo name for each set of test cases and remove the
> repo_name argument.
>

True,
Thanks!


> > +test_repo_info_path 'commondir standard' 'commondir' 'commondir-std' \
> > +     '.git' '../.git'
> > +
> > +test_repo_info_path 'commondir with GIT_COMMON_DIR and GIT_DIR' 'commondir' \
> > +     'commondir-envs' 'custom-common' '../custom-common' \
> > +     'GIT_COMMON_DIR="$ROOT/custom-common" && export GIT_COMMON_DIR &&
> > +      GIT_DIR="../.git" && export GIT_DIR &&
> > +      git init --bare "$ROOT/custom-common"'
> > +
> > +test_repo_info_path 'commondir with only GIT_DIR' 'commondir' \
> > +     'commondir-only-gitdir' '.git' '../.git' \
>
> For each of these test cases, the `expect_absolute_suffix` and
> `expect_relative` and exactly the same. This also appears to be the case
> for the test cases in the next patch. Do these really need to be
> configurable at all? Can we just embed them directly in each test case
> assertion? Or maybe future keys will need this to be configurable?
>

You're right that passing both is completely redundant!
However, the path does still need to be configurable because the directory
name changes between test cases (e.g., `.git` in the standard case vs.
`custom-common` when GIT_COMMON_DIR is exported).

Since the relative path is always just `../` appended to the directory name,
I will condense these two arguments into a single `expected_dir` argument in v5.
The helper will then just construct `$ROOT/$expected_dir` and
`../$expected_dir` internally.

> > +     'GIT_DIR="../.git" && export GIT_DIR'
> > +
> >  test_done
>
> Overall the rest of the patch looks good to me.
>

Thanks again!
These are helpful.

Regards,
- K Jayatheerth

^ permalink raw reply

* Re: [GSoC] [Blog] week 3: Improving the new git repo command
From: K Jayatheerth @ 2026-06-16  3:41 UTC (permalink / raw)
  To: GIT Mailing-list, Justin Tobler, Lucas Seiki Oshiro
In-Reply-To: <CA+rGoLeNzxaTrq50jE=at=0ecnZ5Diy+Q-0McG-R+XFTQ7oMow@mail.gmail.com>

Hi!

My Week 3 GSoC blog is live!
https://jayatheerth.com/blogs/gsoc/week-3-next-steps

Feel free to give it a read and share any feedback ; )

Regards,
- K Jayatheerth

^ permalink raw reply

* SHA-1/SHA-256 interoperability work is functional
From: brian m. carlson @ 2026-06-16  0:17 UTC (permalink / raw)
  To: git

[-- Attachment #1: Type: text/plain, Size: 6043 bytes --]

I'm pleased to announce that I have Git fully passing the testsuite and
CI in interoperability mode, both with SHA-256 and SHA-1 as the main
algorithm.  While this is very exciting, the work is not ready to send
to the list and is effectively a draft, since there is still cleanup
and efficiency work to be done.

What is fully functional:

* The testsuite.
* The protocol, including extensions for mapping objects to support
  shallow clones, submodules, and partial clones and interoperability
  with remotes of different algorithms.
* A full complement of functionality for everyday use cases.

Features which are currently unsupported (and which may or may not be
supported in the future):

* Filtered bundles are unsupported because there is currently no way to provide
	a mapping.
* Multi-pack index cannot be used as the sole pack index format because it does
	not yet provide mappings.
* Pack index v1 and v2 cannot be used because they do not provide object
	mappings.  Git automatically uses pack index v3 instead when necessary, which
	does handle mappings.
* Packfile URIs are not supported because the protocol-provided packfile is not
	complete and its objects cannot be mapped.
* Large object promisors cannot be used if the server does not actually have
	the entire history, since the server must have a complete history in order to
	provide object mappings.
* `git fast-import` does not accept submodules in compatibility mode because
	there is no provision for mappings.
* Remote helpers do not emit signatures in the compatibility algorithm for
	signed tags.
* The WebDAV-based HTTP protocol doesn't support interoperability due to
  the lack of a way to distribute mappings.

Some additional things that may need to be improved:

* We have some recursive delta resolution code in `git index-pack` that
  will need to be made iterative to avoid stack overflows.
* We need to batch object maps whenever we write them, since having too
  many causes `git gc` to kick off frequently (which can be seen in some
  tests).  This will require substantial refactoring of code like `git
  add`, since any time we write an object we must be sure to always
  write the mapping (even if we `die`).
* We will probably want to move the object map repacking code out of
  `git gc` into a separate command that we can call manually.
* We may want some debugging tools for pack index v3 and other data
  formats so that we can show the mapping of objects.
* We will probably want to be able to sign in only one algorithm instead
  of always in both.  Users may not want to sign the SHA-1 format for
  security reasons.
* There is some extremely basic code for `^{sha1}` and `^{sha256}` to
  help `git rev-list` perform connectivity checks for remotes using the
  compatibility algorithm, but it is very much incomplete and will need
  to be completed or fenced off.
* Operations can be rather slow in some cases and we'll want to see what
  speedups we can perform.
* Probably other things I have forgotten.

There is new documentation in `Documentation/gitformat-hash.adoc` that
outlines the requirements for using the protocol.  The protocol
restrictions described there are hard technical limitations that cannot
be avoided; I've intentionally made things as featureful as they can be.
This imposes real restrictions on using protocol interoperability with many
projects, including Git and Linux[0].  Interested parties may wish to look
at t1017 to see what's tested vis-à-vis the protocol and
interoperability.

On the end of the series is a small amount of new Rust code that moves
us in the direction of object file conversion in Rust.  All the pointer
arithmetic makes me very nervous from a security perspective, especially
in network-facing code, so my hope is to eventually port that code over.
Note that Rust is already required for the interoperability work, so
this doesn't add any new dependencies.

I also have some unpublished code that could start on in-place migration
functionality much like is done with `git refs migrate` once the Rust
prerequisites above are ready.  This could also make it possible at some
point to migrate any relevant submodules as part of the migration of the
main repository.  I may or may not complete this work, but perhaps
someone else will want to pick it up if I do not.  I'm certain that such
code would be valuable to many people, including forges.

I intend to rebase and tidy this work for at least a little while, but
don't actually intend to send it upstream, since there are some
technical limitations that prevent me from doing so.  I have, however,
been in contact with someone (who may identify themselves if they
choose) who is interested in getting some of the polishing and
upstreaming work done, which I deeply appreciate.

If you're interested in testing or perusing the work, you may get it
from the `sha256-interop` branch of https://github.com/bk2204/git.git.
Please note that it may be rebased, rewound, or otherwise folded,
spindled, or mutilated at any time.

Even though the testsuite is passing earlier than I expected, I don't
expect it to make Git 3.0, nor do I think we should delay Git 3.0 for
this work.  There are approximately 200 patches currently (and more to
come if we add in-place migration tooling), so it seems very unlikely
that we could get the entire series upstream in any reasonable amount of
time.  We will also very much want to give this time in an experimental
state so people can try it out and report back on things that should be
improved, which is further evidence that it's not right for Git 3.0.

I'm happy to answer any further questions if folks have any.

[0] Git uses submodules, which would need to be rewritten first in order
to migrate, and Linux uses mergetags for which the tagged object is not
in the history (which makes mapping the commit containing it fail).
-- 
brian m. carlson (they/them)
Toronto, Ontario, CA

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 325 bytes --]

^ permalink raw reply

* Re: [PATCH] cat-file: speed up default format
From: René Scharfe @ 2026-06-15 21:53 UTC (permalink / raw)
  To: Jeff King; +Cc: Git List
In-Reply-To: <20260615170652.GB91269@coredump.intra.peff.net>

On 6/15/26 7:06 PM, Jeff King wrote:
> On Mon, Jun 15, 2026 at 12:53:26PM -0400, Jeff King wrote:
> 
>> It uses per-atom callback functions which is nice and clean, though we
>> might be able to do even better with a big ugly switch() statement.
> 
> Being the curious sort, I swapped it out for a big switch statement.
> Patch below, but it does not seem to be any faster.
> 
> So the bottom line is I think you could gain a little bit of performance
> by pre-parsing (versus strbuf_expand() on each object). Around 3% for
> something that actually looks at the objects, though more like 15% if
> for just dumping the objectnames.
> 
> IMHO that is probably not worth it for a custom parsing system just for
> cat-file.  But if we were to finally unify ref-filter and cat-file (and
> even --pretty=format) then it would probably worth doing this kind of
> pre-parsing.
It could be worth it for cat-file alone if we find the right balance, as
it already does do a separate parsing step, but that is awkward with its
mark_query checks all over the place and remembers only object property
requirements and no other format string details.

Making the opcodes small should be beneficial.  We need only a handful
of them, so a byte each should suffice.  We can use a strbuf for that.

We can also store literal characters in there.  An opcode plus with a
payload char incurs an overhead of 50%, which sounds high, but at least
the default format only has two of them and it's much better than
storing pointer plus size for an overhead of more than 90% in case of a
single char.

That gets us closer to native speed, at least on an Apple M1:

Benchmark 1: ./git_fp cat-file --batch-all-objects --batch-check='%(objectname)-%(objecttype)-%(objectsize)'
  Time (mean ± σ):     992.7 ms ±   3.2 ms    [User: 967.5 ms, System: 23.8 ms]
  Range (min … max):   990.1 ms … 1000.7 ms    10 runs

Benchmark 2: ./git_switch cat-file --batch-all-objects --batch-check='%(objectname)-%(objecttype)-%(objectsize)'
  Time (mean ± σ):     991.8 ms ±   1.6 ms    [User: 967.0 ms, System: 23.3 ms]
  Range (min … max):   989.3 ms … 994.4 ms    10 runs

Benchmark 3: ./git cat-file --batch-all-objects --batch-check='%(objectname)-%(objecttype)-%(objectsize)'
  Time (mean ± σ):     985.8 ms ±   2.9 ms    [User: 960.5 ms, System: 23.6 ms]
  Range (min … max):   982.9 ms … 993.0 ms    10 runs

Benchmark 4: ./git cat-file --batch-all-objects --batch-check='%(objectname) %(objecttype) %(objectsize)'
  Time (mean ± σ):     982.1 ms ±   3.2 ms    [User: 956.7 ms, System: 23.6 ms]
  Range (min … max):   979.2 ms … 989.2 ms    10 runs

Summary
  ./git cat-file --batch-all-objects --batch-check='%(objectname) %(objecttype) %(objectsize)' ran
    1.00 ± 0.00 times faster than ./git cat-file --batch-all-objects --batch-check='%(objectname)-%(objecttype)-%(objectsize)'
    1.01 ± 0.00 times faster than ./git_switch cat-file --batch-all-objects --batch-check='%(objectname)-%(objecttype)-%(objectsize)'
    1.01 ± 0.00 times faster than ./git_fp cat-file --batch-all-objects --batch-check='%(objectname)-%(objecttype)-%(objectsize)'


A Ryzen laptop gives me noisy numbers that seem to suggest your
switch-based code already won, but the more compact representation is at
least not worse:

Benchmark 1: ./git_fp cat-file --batch-all-objects --batch-check='%(objectname)-%(objecttype)-%(objectsize)'
  Time (mean ± σ):     397.5 ms ±   8.0 ms    [User: 326.9 ms, System: 39.4 ms]
  Range (min … max):   388.1 ms … 410.0 ms    10 runs

Benchmark 2: ./git_switch cat-file --batch-all-objects --batch-check='%(objectname)-%(objecttype)-%(objectsize)'
  Time (mean ± σ):     388.2 ms ±   4.2 ms    [User: 318.2 ms, System: 39.2 ms]
  Range (min … max):   382.8 ms … 395.7 ms    10 runs

Benchmark 3: ./git cat-file --batch-all-objects --batch-check='%(objectname)-%(objecttype)-%(objectsize)'
  Time (mean ± σ):     385.5 ms ±   5.7 ms    [User: 311.2 ms, System: 43.2 ms]
  Range (min … max):   377.0 ms … 392.9 ms    10 runs

Benchmark 4: ./git cat-file --batch-all-objects --batch-check='%(objectname) %(objecttype) %(objectsize)'
  Time (mean ± σ):     397.5 ms ±   8.4 ms    [User: 321.9 ms, System: 45.2 ms]
  Range (min … max):   382.1 ms … 406.5 ms    10 runs

Summary
  ./git cat-file --batch-all-objects --batch-check='%(objectname)-%(objecttype)-%(objectsize)' ran
    1.01 ± 0.02 times faster than ./git_switch cat-file --batch-all-objects --batch-check='%(objectname)-%(objecttype)-%(objectsize)'
    1.03 ± 0.03 times faster than ./git_fp cat-file --batch-all-objects --batch-check='%(objectname)-%(objecttype)-%(objectsize)'
    1.03 ± 0.03 times faster than ./git cat-file --batch-all-objects --batch-check='%(objectname) %(objecttype) %(objectsize)'

René


diff --git a/builtin/cat-file.c b/builtin/cat-file.c
index 0d1998784c..5667a13e93 100644
--- a/builtin/cat-file.c
+++ b/builtin/cat-file.c
@@ -36,8 +36,6 @@ enum batch_mode {
 	BATCH_MODE_QUEUE_AND_DISPATCH,
 };
 
-struct format_item;
-
 struct batch_options {
 	struct list_objects_filter_options objects_filter;
 	int enabled;
@@ -50,7 +48,7 @@ struct batch_options {
 	char input_delim;
 	char output_delim;
 	const char *format;
-	struct format_item *parsed_format;
+	struct strbuf parsed_format;
 };
 
 static const char *force_path;
@@ -320,30 +318,16 @@ struct expand_data {
 };
 #define EXPAND_DATA_INIT  { .mode = S_IFINVALID }
 
-struct format_item {
-	enum {
-		FORMAT_TYPE_END = 0,
-		FORMAT_TYPE_LITERAL,
-		FORMAT_TYPE_OBJECTNAME,
-		FORMAT_TYPE_OBJECTTYPE,
-		FORMAT_TYPE_OBJECTSIZE,
-		FORMAT_TYPE_OBJECTSIZE_DISK,
-		FORMAT_TYPE_REST,
-		FORMAT_TYPE_DELTABASE,
-		FORMAT_TYPE_OBJECTMODE,
-	} type;
-	union {
-		struct {
-			const char *p;
-			size_t len;
-		} literal;
-	} u;
-	/*
-	 * We could make a true tree here with child/next pointers, which would
-	 * be necessary if we had recursive formats, like %(if). But for our
-	 * simple formats for now it is enough to have a linear set of items,
-	 * so we'll just allocate an array and terminate it with a NULL entry.
-	 */
+
+enum item_type {
+	FORMAT_TYPE_LITERAL,
+	FORMAT_TYPE_OBJECTNAME,
+	FORMAT_TYPE_OBJECTTYPE,
+	FORMAT_TYPE_OBJECTSIZE,
+	FORMAT_TYPE_OBJECTSIZE_DISK,
+	FORMAT_TYPE_REST,
+	FORMAT_TYPE_DELTABASE,
+	FORMAT_TYPE_OBJECTMODE,
 };
 
 static int is_atom(const char *atom, const char *s, int slen)
@@ -352,84 +336,66 @@ static int is_atom(const char *atom, const char *s, int slen)
 	return alen == slen && !memcmp(atom, s, alen);
 }
 
-static int parse_atom(struct format_item *fmt, const char *atom, int len,
+static int parse_atom(struct strbuf *parsed_format, const char *atom, int len,
 		      struct expand_data *data)
 {
 	if (is_atom("objectname", atom, len)) {
-		fmt->type = FORMAT_TYPE_OBJECTNAME;
+		strbuf_addch(parsed_format, FORMAT_TYPE_OBJECTNAME);
 	} else if (is_atom("objecttype", atom, len)) {
 		data->info.typep = &data->type;
-		fmt->type = FORMAT_TYPE_OBJECTTYPE;
+		strbuf_addch(parsed_format, FORMAT_TYPE_OBJECTTYPE);
 	} else if (is_atom("objectsize", atom, len)) {
 		data->info.sizep = &data->size;
-		fmt->type = FORMAT_TYPE_OBJECTSIZE;
+		strbuf_addch(parsed_format, FORMAT_TYPE_OBJECTSIZE);
 	} else if (is_atom("objectsize:disk", atom, len)) {
 		data->info.disk_sizep = &data->disk_size;
-		fmt->type = FORMAT_TYPE_OBJECTSIZE_DISK;
+		strbuf_addch(parsed_format, FORMAT_TYPE_OBJECTSIZE_DISK);
 	} else if (is_atom("rest", atom, len)) {
 		data->split_on_whitespace = 1;
-		fmt->type = FORMAT_TYPE_REST;
+		strbuf_addch(parsed_format, FORMAT_TYPE_REST);
 	} else if (is_atom("deltabase", atom, len)) {
 		data->info.delta_base_oid = &data->delta_base_oid;
-		fmt->type = FORMAT_TYPE_DELTABASE;
+		strbuf_addch(parsed_format, FORMAT_TYPE_DELTABASE);
 	} else if (is_atom("objectmode", atom, len)) {
-		fmt->type = FORMAT_TYPE_OBJECTMODE;
+		strbuf_addch(parsed_format, FORMAT_TYPE_OBJECTMODE);
 	} else
 		return 0;
 	return 1;
 }
 
-static struct format_item *parse_format(const char *start,
-					struct expand_data *data)
+static void parse_format(struct strbuf *parsed_format,
+			 const char *start, struct expand_data *data)
 {
-	struct format_item *ret = NULL;
-	size_t nr = 0, alloc = 0;
-
 	while (1) {
-		const char *percent = strchrnul(start, '%');
 		const char *end;
 
-		if (percent != start) {
-			ALLOC_GROW(ret, nr + 1, alloc);
-			ret[nr].type = FORMAT_TYPE_LITERAL;
-			ret[nr].u.literal.p = start;
-			ret[nr].u.literal.len = percent - start;
-			nr++;
+		while (*start && *start != '%') {
+			strbuf_addch(parsed_format, FORMAT_TYPE_LITERAL);
+			strbuf_addch(parsed_format, *start++);
 		}
 
-		if (!*percent)
+		if (!*start)
 			break;
 
-		start = percent + 1;
+		start++;
 
-		ALLOC_GROW(ret, nr + 1, alloc);
 		if (skip_prefix(start, "%", &start) || *start != '(') {
-			ret[nr].type = FORMAT_TYPE_LITERAL;
-			ret[nr].u.literal.p = "%";
-			ret[nr].u.literal.len = 1;
+			strbuf_addch(parsed_format, FORMAT_TYPE_LITERAL);
+			strbuf_addch(parsed_format, '%');
 		} else if ((end = strchr(start + 1, ')')) &&
-			   parse_atom(&ret[nr], start + 1, end - start - 1, data)) {
+			   parse_atom(parsed_format, start + 1, end - start - 1, data)) {
 			start = end + 1;
 		} else {
 			strbuf_expand_bad_format(start, "cat-file");
 		}
-		nr++;
 	}
-
-	ALLOC_GROW(ret, nr + 1, alloc);
-	ret[nr].type = FORMAT_TYPE_END;
-
-	return ret;
 }
 
-static void expand_format(struct strbuf *sb, struct format_item *fmt,
+static void expand_format(struct strbuf *sb, struct strbuf *parsed_format,
 			  struct expand_data *data)
 {
-	for (; fmt->type; fmt++)
-		switch (fmt->type) {
-		case FORMAT_TYPE_END:
-			BUG("we should have already left the loop!");
-			break;
+	for (size_t i = 0; i < parsed_format->len; i++)
+		switch (parsed_format->buf[i]) {
 		case FORMAT_TYPE_OBJECTNAME:
 			strbuf_add_oid_hex(sb, &data->oid);
 			break;
@@ -453,7 +419,7 @@ static void expand_format(struct strbuf *sb, struct format_item *fmt,
 				strbuf_addf(sb, "%06o", data->mode);
 			break;
 		case FORMAT_TYPE_LITERAL:
-			strbuf_add(sb, fmt->u.literal.p, fmt->u.literal.len);
+			strbuf_addch(sb, parsed_format->buf[++i]);
 		}
 }
 
@@ -641,7 +607,7 @@ static void batch_object_write(const char *obj_name,
 	if (!opt->format) {
 		print_default_format(scratch, data, opt);
 	} else {
-		expand_format(scratch, opt->parsed_format, data);
+		expand_format(scratch, &opt->parsed_format, data);
 		strbuf_addch(scratch, opt->output_delim);
 	}
 
@@ -1010,9 +976,8 @@ static int batch_objects(struct batch_options *opt)
 	int save_warning;
 	int retval = 0;
 
-	opt->parsed_format = parse_format(opt->format ?
-					  opt->format : DEFAULT_FORMAT,
-					  &data);
+	parse_format(&opt->parsed_format,
+		     opt->format ? opt->format : DEFAULT_FORMAT, &data);
 	if (opt->transform_mode)
 		data.split_on_whitespace = 1;
 
@@ -1152,6 +1117,7 @@ int cmd_cat_file(int argc,
 	const char *exp_type = NULL, *obj_name = NULL;
 	struct batch_options batch = {
 		.objects_filter = LIST_OBJECTS_FILTER_INIT,
+		.parsed_format = STRBUF_INIT,
 	};
 	int unknown_type = 0;
 	int input_nul_terminated = 0;


^ permalink raw reply related

* Re: [PATCH] cat-file: speed up default format
From: René Scharfe @ 2026-06-15 21:53 UTC (permalink / raw)
  To: Jeff King; +Cc: Git List
In-Reply-To: <20260615165326.GA91269@coredump.intra.peff.net>

On 6/15/26 6:53 PM, Jeff King wrote:
> 
> +static void rest_add(struct format_item *item UNUSED,
> +		     struct strbuf *sb, struct expand_data *data)
> +{
> +	strbuf_addstr(sb, data->rest);
> +}

>  	} else if (is_atom("rest", atom, len)) {
> -		if (data->mark_query)
> -			data->split_on_whitespace = 1;
> -		else if (data->rest)

This removes support for rest being NULL, breaking t1006.381.

> -			strbuf_addstr(sb, data->rest);
> +		data->split_on_whitespace = 1;
> +		fmt->add = rest_add;
René


^ permalink raw reply

* Re: [PREVIEW v4 0/6] [RFC] diff: add diff.<driver>.process for external hunk providers
From: Michael Montalbo @ 2026-06-15 21:14 UTC (permalink / raw)
  To: Michael Montalbo via GitGitGadget; +Cc: git, Johannes Schindelin
In-Reply-To: <pull.2120.v4.git.1781463332.gitgitgadget@gmail.com>

On Sun, Jun 14, 2026 at 11:55 AM Michael Montalbo via GitGitGadget
<gitgitgadget@gmail.com> wrote:
>
> This series adds diff.<driver>.process, a long-running subprocess protocol
> that lets an external tool control which lines Git considers changed while
> Git handles all output formatting...

Now I'm realizing there are some diff flags that are not properly supported
yet with an external diff provider. Flags like -L and the stat-related
flags should probably behave like blame and consult the external diff
process. In general, there should be a more explicit and comprehensive
explanation for which flags interact with external diff processes and
which do not, included principled reasons for why that is the case.

^ permalink raw reply

* Re: [PATCH v2] commit-reach: remove get_reachable_subset()
From: Kristofer Karlsson @ 2026-06-15 20:58 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Derrick Stolee, Kristofer Karlsson via GitGitGadget, git
In-Reply-To: <xmqq7bo5nf31.fsf@gitster.g>

I think we should park or abandon this patch for now; I initially thought
it would be a somewhat cheap win in code reduction but the risk of
introducing performance regressions for repos without commit graphs
means it's not really worth the time investment and I don't want to
add more maintainer burden for tracking it.

Thanks for looking at it though, I appreciate it!
Kristofer

On Thu, 11 Jun 2026 at 19:48, Junio C Hamano <gitster@pobox.com> wrote:
>
> Derrick Stolee <stolee@gmail.com> writes:
>
> > Finally, a commentary: You seem to have a habit of responding to
> > review feedback only through new patch versions, but I'd rather see
> > some thoughts in the discussion thread as direct replies to the review,
> > especially if you think you will change direction like this. Saying
> > something like "Maybe I should update the method to have two walk modes"
> > in a reply would have given me an opportunity to respond and perhaps
> > avoided a new version that went in this direction.
>
> Thanks for saying this.
>
> I haven't (yet) found it in my exchange with Kristofer, but I did
> find similar irritations during review sessions with other
> contributors.
>
> I wonder if we should talk about it in the SubmittingPatches and/or
> MyFirstContribution document?

^ permalink raw reply

* Re: [PATCH v2 2/4] pack-objects: support reachability bitmaps with `--path-walk`
From: Junio C Hamano @ 2026-06-15 20:57 UTC (permalink / raw)
  To: Taylor Blau
  Cc: git, Michael Montalbo, Derrick Stolee, Jeff King, Elijah Newren
In-Reply-To: <ffad584a43ebf3cb2138e8dce7daef84ab72712f.1780438896.git.me@ttaylorr.com>

Taylor Blau <me@ttaylorr.com> writes:

> diff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh
> index f693cb56691..69c5da1580a 100755
> --- a/t/t5310-pack-bitmaps.sh
> +++ b/t/t5310-pack-bitmaps.sh
> ...
> +		for reuse in true false
> +		do
> +			: >trace.txt &&
> +
> +			GIT_TRACE2_EVENT="$(pwd)/trace.txt" \
> +			git -c pack.allowPackReuse=$reuse pack-objects \
> +				--stdout --revs --path-walk --use-bitmap-index \
> +				<in >out.pack &&
> +			grep "\"category\":\"bitmap\",\"key\":\"bitmap/hits\"" trace.txt &&

This gets flagged by updated test linter X-<.  Use test_grep to
pacify it.


^ permalink raw reply


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