From 818fbfd208f919e7a4fd9c827b65e5ce5372479b Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 5 Mar 2026 15:34:52 -0800 Subject: [PATCH 001/106] sideband: delay sanitizing by default to Git v3.0 The sideband sanitization patches allow ANSI color sequences through by default, preserving compatibility with pre-receive hooks that provide colored output during `git push`. Even so, there is concern that changing any default behavior in a minor release may have unforeseen consequences. To accommodate this, defer the secure-by-default behavior to Git v3.0, where breaking changes are expected. This gives users and tooling time to prepare, while committing to address CVE-2024-52005 in Git v3.0. Signed-off-by: Johannes Schindelin [jc: adjusted for the removal of 'default' value] Signed-off-by: Junio C Hamano --- Documentation/config/sideband.adoc | 12 ++++++++++-- sideband.c | 6 +++++- t/t5409-colorize-remote-messages.sh | 18 +++++++++++++----- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/Documentation/config/sideband.adoc b/Documentation/config/sideband.adoc index 96fade7f5fee39..ddba93393ccadc 100644 --- a/Documentation/config/sideband.adoc +++ b/Documentation/config/sideband.adoc @@ -1,8 +1,16 @@ sideband.allowControlCharacters:: +ifdef::with-breaking-changes[] By default, control characters that are delivered via the sideband are masked, except ANSI color sequences. This prevents potentially - unwanted ANSI escape sequences from being sent to the terminal. Use - this config setting to override this behavior (the value can be + unwanted ANSI escape sequences from being sent to the terminal. +endif::with-breaking-changes[] +ifndef::with-breaking-changes[] + By default, no control characters delivered via the sideband + are masked. This is unsafe and will change in Git v3.* to only + allow ANSI color sequences by default, preventing potentially + unwanted ANSI escape sequences from being sent to the terminal. +endif::with-breaking-changes[] + Use this config setting to override this behavior (the value can be a comma-separated list of the following keywords): + -- diff --git a/sideband.c b/sideband.c index 04282a568edd90..5fb60e52bf00b2 100644 --- a/sideband.c +++ b/sideband.c @@ -34,7 +34,11 @@ static enum { ALLOW_ANSI_CURSOR_MOVEMENTS = 1<<1, ALLOW_ANSI_ERASE = 1<<2, ALLOW_ALL_CONTROL_CHARACTERS = 1<<3, - ALLOW_DEFAULT_ANSI_SEQUENCES = ALLOW_ANSI_COLOR_SEQUENCES +#ifdef WITH_BREAKING_CHANGES + ALLOW_DEFAULT_ANSI_SEQUENCES = ALLOW_ANSI_COLOR_SEQUENCES, +#else + ALLOW_DEFAULT_ANSI_SEQUENCES = ALLOW_ALL_CONTROL_CHARACTERS, +#endif } allow_control_characters = ALLOW_CONTROL_SEQUENCES_UNSET; static inline int skip_prefix_in_csv(const char *value, const char *prefix, diff --git a/t/t5409-colorize-remote-messages.sh b/t/t5409-colorize-remote-messages.sh index 3010913bb113e4..07cbc62736bd26 100755 --- a/t/t5409-colorize-remote-messages.sh +++ b/t/t5409-colorize-remote-messages.sh @@ -98,6 +98,13 @@ test_expect_success 'fallback to color.ui' ' grep "error: error" decoded ' +if test_have_prereq WITH_BREAKING_CHANGES +then + TURN_ON_SANITIZING=already.turned=on +else + TURN_ON_SANITIZING=sideband.allowControlCharacters=color +fi + test_expect_success 'disallow (color) control sequences in sideband' ' write_script .git/color-me-surprised <<-\EOF && printf "error: Have you \\033[31mread\\033[m this?\\a\\n" >&2 @@ -106,7 +113,7 @@ test_expect_success 'disallow (color) control sequences in sideband' ' test_config_global uploadPack.packObjectsHook ./color-me-surprised && test_commit need-at-least-one-commit && - git clone --no-local . throw-away 2>stderr && + git -c $TURN_ON_SANITIZING clone --no-local . throw-away 2>stderr && test_decode_color decoded && test_grep RED decoded && test_grep "\\^G" stderr && @@ -138,7 +145,7 @@ test_decode_csi() { }' } -test_expect_success 'control sequences in sideband allowed by default' ' +test_expect_success 'control sequences in sideband allowed by default (in Git v3.8)' ' write_script .git/color-me-surprised <<-\EOF && printf "error: \\033[31mcolor\\033[m\\033[Goverwrite\\033[Gerase\\033[K\\033?25l\\n" >&2 exec "$@" @@ -147,7 +154,7 @@ test_expect_success 'control sequences in sideband allowed by default' ' test_commit need-at-least-one-commit-at-least && rm -rf throw-away && - git clone --no-local . throw-away 2>stderr && + git -c $TURN_ON_SANITIZING clone --no-local . throw-away 2>stderr && test_decode_color color-decoded && test_decode_csi decoded && test_grep ! "CSI \\[K" decoded && @@ -175,14 +182,15 @@ test_expect_success 'allow all control sequences for a specific URL' ' test_commit one-more-please && rm -rf throw-away && - git clone --no-local . throw-away 2>stderr && + git -c $TURN_ON_SANITIZING clone --no-local . throw-away 2>stderr && test_decode_color color-decoded && test_decode_csi decoded && test_grep ! "CSI \\[K" decoded && test_grep "\\^\\[\\[K" decoded && rm -rf throw-away && - git -c "sideband.file://.allowControlCharacters=true" \ + git -c sideband.allowControlCharacters=false \ + -c "sideband.file://.allowControlCharacters=true" \ clone --no-local "file://$PWD" throw-away 2>stderr && test_decode_color color-decoded && test_decode_csi decoded && From fa1468a1f7c7765a6c7dd1faca4c9dc241d0538c Mon Sep 17 00:00:00 2001 From: Trieu Huynh Date: Tue, 7 Apr 2026 03:30:41 +0900 Subject: [PATCH 002/106] promisor-remote: fix promisor.quiet to use the correct repository fetch_objects() reads the promisor.quiet configuration from the_repository instead of the repo parameter it receives. This means that when git lazy-fetches objects for a non-main repository, eg. a submodule that is itself a partial clone opened via repo_submodule_init(). The submodule's own promisor.quiet setting is ignored and the superproject's setting is used instead. Fix by replacing the_repository with repo in the repo_config_get_bool() call. The practical trigger is git grep --recurse-submodules on a superproject where the submodule is a partial clone. Add a test where promisor.quiet is set only in a partial-clone submodule; a lazy fetch triggered by "git grep --recurse-submodules" must honor that setting. Signed-off-by: Trieu Huynh Signed-off-by: Junio C Hamano --- promisor-remote.c | 2 +- t/t0410-partial-clone.sh | 45 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/promisor-remote.c b/promisor-remote.c index 96fa215b06a924..225260b05f8d65 100644 --- a/promisor-remote.c +++ b/promisor-remote.c @@ -46,7 +46,7 @@ static int fetch_objects(struct repository *repo, "fetch", remote_name, "--no-tags", "--no-write-fetch-head", "--recurse-submodules=no", "--filter=blob:none", "--stdin", NULL); - if (!repo_config_get_bool(the_repository, "promisor.quiet", &quiet) && quiet) + if (!repo_config_get_bool(repo, "promisor.quiet", &quiet) && quiet) strvec_push(&child.args, "--quiet"); if (start_command(&child)) die(_("promisor-remote: unable to fork off fetch subprocess")); diff --git a/t/t0410-partial-clone.sh b/t/t0410-partial-clone.sh index 52e19728a3fca0..dff442da2090b5 100755 --- a/t/t0410-partial-clone.sh +++ b/t/t0410-partial-clone.sh @@ -717,7 +717,29 @@ test_expect_success 'setup for promisor.quiet tests' ' git -C server rm foo.t && git -C server commit -m remove && git -C server config uploadpack.allowanysha1inwant 1 && - git -C server config uploadpack.allowfilter 1 + git -C server config uploadpack.allowfilter 1 && + + # Setup for submodule repo test: superproject whose submodule is a + # partial clone, so that promisor.quiet is read via a non-main repo. + rm -rf sub-pc-src sub-pc-srv.bare super-src super-work && + git init sub-pc-src && + test_commit -C sub-pc-src initial file.txt "hello" && + + git clone --bare sub-pc-src sub-pc-srv.bare && + git -C sub-pc-srv.bare config uploadpack.allowfilter 1 && + git -C sub-pc-srv.bare config uploadpack.allowanysha1inwant 1 && + + git init super-src && + git -C super-src -c protocol.file.allow=always \ + submodule add "file://$(pwd)/sub-pc-srv.bare" sub && + git -C super-src commit -m "add submodule" && + + git -c protocol.file.allow=always clone super-src super-work && + git -C super-work -c protocol.file.allow=always \ + submodule update --init --filter=blob:none sub && + + # Allow file:// in the submodule so that lazy-fetch subprocesses work. + git -C super-work/sub config protocol.file.allow always ' test_expect_success TTY 'promisor.quiet=false shows progress messages' ' @@ -752,6 +774,27 @@ test_expect_success TTY 'promisor.quiet=unconfigured shows progress messages' ' grep "Receiving objects" err ' +test_expect_success 'promisor.quiet from submodule repo is honored' ' + rm -f pc-quiet-trace && + + # Set promisor.quiet only in the submodule, not the superproject. + git -C super-work/sub config promisor.quiet true && + + # Push a new commit+blob to the server; the blob stays missing in the + # partial-clone submodule until a lazy fetch is triggered. + test_commit -C sub-pc-src updated new-file.txt "world" && + git -C sub-pc-src push "$(pwd)/sub-pc-srv.bare" HEAD:master && + git -C super-work/sub -c protocol.file.allow=always fetch origin && + git -C super-work/sub reset --mixed origin/master && + + # grep descends into the submodule and triggers a lazy fetch for the + # missing blob; verify the fetch subprocess carries --quiet. + GIT_TRACE2_EVENT="$(pwd)/pc-quiet-trace" \ + git -C super-work grep --cached --recurse-submodules "world" \ + 2>/dev/null && + grep negotiationAlgorithm pc-quiet-trace | grep -e --quiet +' + . "$TEST_DIRECTORY"/lib-httpd.sh start_httpd From 6f58b42d052e7fb49e7c1ff16875fbfd5b6cb461 Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Mon, 13 Apr 2026 12:21:00 +0200 Subject: [PATCH 003/106] doc: interpret-trailers: stop fixating on RFC 822 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This command handles the trailers metadata format. But the command isn’t introduced as such; it is instead introduced by stating that these trailer lines look similar to RFC 822 email headers. This is overwrought; most people do not deal directly with email headers, and certainly not email RFCs. Trailers are just key–value pairs that, like email headers, use colon as the separator. The format in its simplest form is easy to describe directly without comparing it to anything else; we will do that in the upcoming commit “explain the format after the intro”. For now, let’s: • remove the first mention of email headers; • keep the second, innocuous comparison with email line folding in the middle; and • remove the now-unneeded disclaimer that trailers do not share many of the features of RFC 822 email headers—there is no invitation to speculate that trailers would follow any other email format rules since we do not compare them directly any more. *** Talking about trailers as an RFC 822/2822-like format seems to go back to the `--fixes`/`Fixes:` trailer topic,[1] the thread that precipitated this command and in turn the first trailer support in git(1) beyond adding s-o-b lines. † 1: https://lore.kernel.org/all/20131027071407.GA11683@leaf/ Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- Documentation/git-interpret-trailers.adoc | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Documentation/git-interpret-trailers.adoc b/Documentation/git-interpret-trailers.adoc index 77b4f63b05cf5b..1878848ad2acb9 100644 --- a/Documentation/git-interpret-trailers.adoc +++ b/Documentation/git-interpret-trailers.adoc @@ -14,9 +14,9 @@ git interpret-trailers [--in-place] [--trim-empty] DESCRIPTION ----------- -Add or parse _trailer_ lines that look similar to RFC 822 e-mail -headers, at the end of the otherwise free-form part of a commit -message. For example, in the following commit message +Add or parse _trailer_ lines at the end of the otherwise +free-form part of a commit message. For example, in the following commit +message ------------------------------------------------ subject @@ -107,9 +107,6 @@ key: This is a very long value, with spaces and newlines in it. ------------------------------------------------ -Note that trailers do not follow (nor are they intended to follow) many of the -rules for RFC 822 headers. For example they do not follow the encoding rule. - OPTIONS ------- `--in-place`:: From abb04b0f0daa1df465ec7c71cc42265a8fa0cdf2 Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Mon, 13 Apr 2026 12:21:01 +0200 Subject: [PATCH 004/106] =?UTF-8?q?doc:=20interpret-trailers:=20replace=20?= =?UTF-8?q?=E2=80=9Clines=E2=80=9D=20with=20=E2=80=9Cmetadata=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We removed the initial comparison to email headers in the previous commit. Now the introduction paragraph just says “trailer lines”, and the only hint that this is metadata/structured information is the “otherwise free-form” phrase. Let’s replace “lines” with “metadata” since that is their purpose. This also makes the introduction more consistent with how I chose to define trailers in the glossary:[1] “Key-value metadata”. (We will introduce “key–value” in the upcoming commit “explain the format after the intro”.) † 1: 68e3c69e (Documentation/glossary: describe "trailer", 2024-11-17) Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- Documentation/git-interpret-trailers.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/git-interpret-trailers.adoc b/Documentation/git-interpret-trailers.adoc index 1878848ad2acb9..3f60fd9b720dda 100644 --- a/Documentation/git-interpret-trailers.adoc +++ b/Documentation/git-interpret-trailers.adoc @@ -14,7 +14,7 @@ git interpret-trailers [--in-place] [--trim-empty] DESCRIPTION ----------- -Add or parse _trailer_ lines at the end of the otherwise +Add or parse trailers metadata at the end of the otherwise free-form part of a commit message. For example, in the following commit message From a35523a8398a3dcb65f258b42d323a20fb461361 Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Mon, 13 Apr 2026 12:21:02 +0200 Subject: [PATCH 005/106] =?UTF-8?q?doc:=20interpret-trailers:=20use=20?= =?UTF-8?q?=E2=80=9Cmetadata=E2=80=9D=20in=20Name=20as=20well?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now since the previous commit introduce the format as “trailers metadata”. We can replace “structured information” with “metadata” in the “Name” section to be consistent. While “structured information” does emphasize that the data is not loosely structured, we also say that this command adds to or parses this format. I don’t think that we need to emphasize that it is structured since clearly there is some structure there. Both “metadata” and “structured information” can convey the same information. But “metadata” is shorter and easier to deploy since it’s just one word. Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- Documentation/git-interpret-trailers.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/git-interpret-trailers.adoc b/Documentation/git-interpret-trailers.adoc index 3f60fd9b720dda..4e92c8299bb21b 100644 --- a/Documentation/git-interpret-trailers.adoc +++ b/Documentation/git-interpret-trailers.adoc @@ -3,7 +3,7 @@ git-interpret-trailers(1) NAME ---- -git-interpret-trailers - Add or parse structured information in commit messages +git-interpret-trailers - Add or parse metadata in commit messages SYNOPSIS -------- From 9fb47447e82b6c1b2a1b71b033283ba62f5f6151 Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Mon, 13 Apr 2026 12:21:03 +0200 Subject: [PATCH 006/106] doc: interpret-trailers: not just for commit messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This command doesn’t interface with commits directly. You can interpret or modify any kind of text, even though commit messages are the most relevant. The git(1) suite also isn’t restricted to only direct commit support since git-tag(1) learned `--trailer` in 066cef77 (builtin/tag: add --trailer option, 2024-05-05) Now, we already introduce the command in the “Name” section as dealing with commit messages as well. That is fine since that intro line needs to remain pretty short. Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- Documentation/git-interpret-trailers.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/git-interpret-trailers.adoc b/Documentation/git-interpret-trailers.adoc index 4e92c8299bb21b..7329e710e1a6eb 100644 --- a/Documentation/git-interpret-trailers.adoc +++ b/Documentation/git-interpret-trailers.adoc @@ -15,8 +15,8 @@ git interpret-trailers [--in-place] [--trim-empty] DESCRIPTION ----------- Add or parse trailers metadata at the end of the otherwise -free-form part of a commit message. For example, in the following commit -message +free-form part of a commit message, or any other kind of text. +For example, in the following commit message ------------------------------------------------ subject From d1673e5aa0bae10d08e424f9919c4c7fe4433dd2 Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Mon, 13 Apr 2026 12:21:04 +0200 Subject: [PATCH 007/106] doc: interpret-trailers: explain the format after the intro MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit You need to read the entire “Description” section in order to understand the full trailer format. But there are many nuances, so that’s fine. As a starter though we have an introductory example.[1] That turns out to be crucial; the rest of this section talks about the mechanics of the command and only incidentally the format itself. Now, although the example might arguably be self-explanatory, we can add a little preamble which defines the format in its simplest form as well as define the most important terms. Note that we name the “blank line” rule since I want to use that term every time it comes up. It gets very mildly obfuscated if you call it a “blank line” in one place[2] and “empty (or whitespace-only) ...” in another one.[3] We will define the format of the *key* in the next commit. † 1: from d57fa7fc (doc: trailer: add more examples in DESCRIPTION, 2023-06-15) † 2: `Documentation/git-interpret-trailers.adoc:86` in 5361983c (The 22nd batch, 2026-03-27) † 3: `Documentation/git-interpret-trailers.adoc:93` in 5361983c (The 22nd batch, 2026-03-27) Suggested-by: D. Ben Knoble Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- Documentation/git-interpret-trailers.adoc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Documentation/git-interpret-trailers.adoc b/Documentation/git-interpret-trailers.adoc index 7329e710e1a6eb..bcd79b19bd7752 100644 --- a/Documentation/git-interpret-trailers.adoc +++ b/Documentation/git-interpret-trailers.adoc @@ -16,7 +16,12 @@ DESCRIPTION ----------- Add or parse trailers metadata at the end of the otherwise free-form part of a commit message, or any other kind of text. -For example, in the following commit message + +A _trailer_ in its simplest form is a key-value pair with a colon as a +separator. A _trailer block_ consists of one or more trailers. The +trailer block needs to be preceded by a blank line, where a _blank line_ +is either an empty or a whitespace-only line. For example, in the +following commit message ------------------------------------------------ subject From 975c9a44e305e456a72c48905a805cace521a705 Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Mon, 13 Apr 2026 12:21:05 +0200 Subject: [PATCH 008/106] doc: interpret-trailers: explain key format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A trailer key must consist of ASCII alphanumeric characters and hyphens *only*. Let’s document it explicitly instead of relying on readers being conservative and only basing their trailer keys on the documentation examples.[1] The previous commit provided us with an appropriate paragraph to describe the key format. † 1: Technically they would then miss out on using digits in them since all of the example keys just use letters and hyphens Reported-by: Brendan Jackman Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- Documentation/git-interpret-trailers.adoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Documentation/git-interpret-trailers.adoc b/Documentation/git-interpret-trailers.adoc index bcd79b19bd7752..c35fa9c688d28f 100644 --- a/Documentation/git-interpret-trailers.adoc +++ b/Documentation/git-interpret-trailers.adoc @@ -18,7 +18,8 @@ Add or parse trailers metadata at the end of the otherwise free-form part of a commit message, or any other kind of text. A _trailer_ in its simplest form is a key-value pair with a colon as a -separator. A _trailer block_ consists of one or more trailers. The +separator. The _key_ consists of ASCII alphanumeric characters and +hyphens (`-`). A _trailer block_ consists of one or more trailers. The trailer block needs to be preceded by a blank line, where a _blank line_ is either an empty or a whitespace-only line. For example, in the following commit message From 0e701f8039aff602177db5e7ca525944506253da Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Mon, 13 Apr 2026 12:21:06 +0200 Subject: [PATCH 009/106] doc: interpret-trailers: add key format example All of the examples speak of the Happy Path where everything works as intended. But failure examples can also be instructive. Especially for explaining again, by example, the key format (see previous commit). This also allows us to demonstrate trailer block detection with a concrete example. Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- Documentation/git-interpret-trailers.adoc | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Documentation/git-interpret-trailers.adoc b/Documentation/git-interpret-trailers.adoc index c35fa9c688d28f..f215cba4bf0dea 100644 --- a/Documentation/git-interpret-trailers.adoc +++ b/Documentation/git-interpret-trailers.adoc @@ -405,6 +405,29 @@ mv "\$1.new" "\$1" $ chmod +x .git/hooks/commit-msg ------------ +* Here we try to to use three different trailer keys. But it fails + because two of them are not recognized as trailer keys. ++ +---- +$ cat msg.txt +subject + +Skapad-på: some-branch +Hash-in-v6.11: 45c12d3269fe48f22834320c782ffe86c3560f2c +Reviewed-by: Alice +$ git interpret-trailers --only-trailers Date: Mon, 13 Apr 2026 12:21:07 +0200 Subject: [PATCH 010/106] =?UTF-8?q?doc:=20interpret-trailers:=20commit=20t?= =?UTF-8?q?o=20=E2=80=9Ctrailer=20block=E2=80=9D=20term?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We chose to introduce the term “trailer block” into the documentation a few commits ago.[1] It is used in the code though, so it is not a newly invented term. That term was useful to explain where the trailers are found (they *trail* the message). But it is also useful here, where we explain how trailers are added to existing messages, how trailer blocks are found (beyond the simple case in the introduction), and how the end of the message is found. † 1: in commit “explain the format after the intro” Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- Documentation/git-interpret-trailers.adoc | 26 ++++++++++++----------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/Documentation/git-interpret-trailers.adoc b/Documentation/git-interpret-trailers.adoc index f215cba4bf0dea..b693e89fd96336 100644 --- a/Documentation/git-interpret-trailers.adoc +++ b/Documentation/git-interpret-trailers.adoc @@ -87,19 +87,21 @@ trailer.sign.key "Signed-off-by: " in your configuration, you only need to specify `--trailer="sign: foo"` on the command line instead of `--trailer="Signed-off-by: foo"`. -By default the new trailer will appear at the end of all the existing -trailers. If there is no existing trailer, the new trailer will appear -at the end of the input. A blank line will be added before the new -trailer if there isn't one already. - -Existing trailers are extracted from the input by looking for -a group of one or more lines that (i) is all trailers, or (ii) contains at -least one Git-generated or user-configured trailer and consists of at +By default the new trailer will appear at the end of the trailer block. +A trailer block will be created with only that trailer if a trailer +block does not already exist. Recall that a trailer block needs to be +preceded by a blank line, so a blank line (specifically an empty line) +will be inserted before the new trailer block in that case. + +Existing trailers are extracted from the input by looking for the +trailer block. Concretely, that is a group of one or more lines that (i) +is all trailers, or (ii) contains at least one Git-generated or +user-configured trailer and consists of at least 25% trailers. -The group must be preceded by one or more empty (or whitespace-only) lines. -The group must either be at the end of the input or be the last -non-whitespace lines before a line that starts with `---` (followed by a -space or the end of the line). +The trailer block is by definition at the end the the message. The end +of the message in turn is either (i) at the end of the input, or (ii) +the last non-whitespace lines before a line that starts with `---` +(followed by a space or the end of the line). When reading trailers, there can be no whitespace before or inside the __, but any number of regular space and tab characters are allowed From 4e06417fd8446f1ea7b79dc64221be57f645432e Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Mon, 13 Apr 2026 12:21:08 +0200 Subject: [PATCH 011/106] doc: interpret-trailers: document comment line treatment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comment lines have always been ignored but this is not documented. This is mostly for completeness since this is unlikely to catch anyone by surprise. But we really ought to be reasonably complete here since it’s the only documentation page that documents trailers. Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- Documentation/git-interpret-trailers.adoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Documentation/git-interpret-trailers.adoc b/Documentation/git-interpret-trailers.adoc index b693e89fd96336..b42f957d66638d 100644 --- a/Documentation/git-interpret-trailers.adoc +++ b/Documentation/git-interpret-trailers.adoc @@ -103,6 +103,10 @@ of the message in turn is either (i) at the end of the input, or (ii) the last non-whitespace lines before a line that starts with `---` (followed by a space or the end of the line). +This command ignores comment lines (see `core.commentString` in +linkgit:git-config[1]). This is for use with the `prepare-commit-msg` +and `commit-msg` hooks. + When reading trailers, there can be no whitespace before or inside the __, but any number of regular space and tab characters are allowed between the __ and the separator. There can be whitespaces before, From f7a69261db0f268de967919fa1b7a226571069a9 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Tue, 21 Apr 2026 00:26:07 +0000 Subject: [PATCH 012/106] merge-ort: propagate callback errors from traverse_trees_wrapper() traverse_trees_wrapper() saves entries from a first pass through traverse_trees() and then replays them through the real callback (collect_merge_info_callback). However, the replay loop silently discards the callback return value. This means any error reported by the callback during replay -- including a future check for malformed trees -- would be ignored, allowing the merge to proceed with corrupt state. Capture the return value, stop the loop on negative (error) returns, and propagate the error to the caller. Note that the callback returns a positive mask value on success, so we normalize non-negative returns to 0 for the caller. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- merge-ort.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/merge-ort.c b/merge-ort.c index 00923ce3cd749b..4b8e32209d9b3a 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -1008,18 +1008,20 @@ static int traverse_trees_wrapper(struct index_state *istate, info->traverse_path = renames->callback_data_traverse_path; info->fn = old_fn; for (i = old_offset; i < renames->callback_data_nr; ++i) { - info->fn(n, - renames->callback_data[i].mask, - renames->callback_data[i].dirmask, - renames->callback_data[i].names, - info); + ret = info->fn(n, + renames->callback_data[i].mask, + renames->callback_data[i].dirmask, + renames->callback_data[i].names, + info); + if (ret < 0) + break; } renames->callback_data_nr = old_offset; free(renames->callback_data_traverse_path); renames->callback_data_traverse_path = old_callback_data_traverse_path; info->traverse_path = NULL; - return 0; + return ret < 0 ? ret : 0; } static void setup_path_info(struct merge_options *opt, From 399bf79b7b76b1b408bfe68dd2dd3432c6497a67 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Tue, 21 Apr 2026 00:26:08 +0000 Subject: [PATCH 013/106] merge-ort: drop unnecessary show_all_errors from collect_merge_info() collect_merge_info() has set info.show_all_errors = 1 since d2bc1994f363 (merge-ort: implement a very basic collect_merge_info(), 2020-12-13). This setting was copied from unpack-trees.c where it controls batching of error messages for porcelain display, but merge-ort has no such error-batching logic and never needed it. With show_all_errors set, traverse_trees() captures a negative callback return but continues processing remaining entries rather than stopping immediately. Removing the setting restores the default behavior where a negative return from collect_merge_info_callback() breaks out of the traversal loop right away, allowing a future commit to exit early when a corrupt tree is detected. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- merge-ort.c | 1 - 1 file changed, 1 deletion(-) diff --git a/merge-ort.c b/merge-ort.c index 4b8e32209d9b3a..74e9636020fe40 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -1740,7 +1740,6 @@ static int collect_merge_info(struct merge_options *opt, setup_traverse_info(&info, opt->priv->toplevel_dir); info.fn = collect_merge_info_callback; info.data = opt; - info.show_all_errors = 1; if (repo_parse_tree(opt->repo, merge_base) < 0 || repo_parse_tree(opt->repo, side1) < 0 || From 426fc4f650930846728534e5e710f384708f505f Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Tue, 21 Apr 2026 00:26:09 +0000 Subject: [PATCH 014/106] merge-ort: free diff pairs queue in clear_or_reinit_internal_opts() clear_or_reinit_internal_opts() is responsible for cleaning up the various data structures in merge_options_internal. It already handles many renames-related structures (dirs_removed, dir_renames, relevant_sources, cached_pairs, deferred, etc.) but does not free renames->pairs[].queue. In the normal code path, resolve_and_process_renames() frees pairs[s].queue and reinitializes it with diff_queue_init() before clear_or_reinit_internal_opts() runs, so the omission is harmless. However, if collect_merge_info() encounters an error and returns early (before resolve_and_process_renames() is ever called), any diff pairs already queued by collect_rename_info()/add_pair() will have their backing array leaked. Fix this by freeing renames->pairs[].queue in the cleanup function. In the normal path the pointer is already NULL (from the earlier diff_queue_init() in resolve_and_process_renames()), so free(NULL) is a safe no-op. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- merge-ort.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/merge-ort.c b/merge-ort.c index 74e9636020fe40..8f911cb63979eb 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -728,6 +728,8 @@ static void clear_or_reinit_internal_opts(struct merge_options_internal *opti, strintmap_clear_func(&renames->deferred[i].possible_trivial_merges); strset_clear_func(&renames->deferred[i].target_dirs); renames->deferred[i].trivial_merges_okay = 1; /* 1 == maybe */ + free(renames->pairs[i].queue); + diff_queue_init(&renames->pairs[i]); } renames->cached_pairs_valid_side = 0; renames->dir_rename_mask = 0; From 61388e3ea34663bf0d403f6510cac509cbd88811 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Tue, 21 Apr 2026 00:26:10 +0000 Subject: [PATCH 015/106] merge-ort: abort merge when trees have duplicate entries Trees with duplicate entries are malformed; fsck reports "contains duplicate file entries" for them. merge-ort has from the beginning assumed that we would never hit such trees. It was written with the assumption that traverse_trees() calls collect_merge_info_callback() at most once per path. The "sanity checks" in that callback (added in d2bc1994f363 (merge-ort: implement a very basic collect_merge_info(), 2020-12-13)) verify properties of each individual call but not that invariant. The strmap_put() in setup_path_info() silently overwrites the entry from any prior call for the same path, because it assumed there would be no other path. Unfortunately, supplemental data structures for various optimizations could still be tweaked before the extra paths were overwritten, and those data structures not matching expected state could trip various assertions. Change the return type of setup_path_info() from void to int to allow us to detect this case, and abort the merge with a clear error message when it occurs. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- merge-ort.c | 61 ++++++++++++++++------------ t/t6422-merge-rename-corner-cases.sh | 54 ++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 27 deletions(-) diff --git a/merge-ort.c b/merge-ort.c index 8f911cb63979eb..be0829bbb781ef 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -1026,18 +1026,18 @@ static int traverse_trees_wrapper(struct index_state *istate, return ret < 0 ? ret : 0; } -static void setup_path_info(struct merge_options *opt, - struct string_list_item *result, - const char *current_dir_name, - int current_dir_name_len, - char *fullpath, /* we'll take over ownership */ - struct name_entry *names, - struct name_entry *merged_version, - unsigned is_null, /* boolean */ - unsigned df_conflict, /* boolean */ - unsigned filemask, - unsigned dirmask, - int resolved /* boolean */) +static int setup_path_info(struct merge_options *opt, + struct string_list_item *result, + const char *current_dir_name, + int current_dir_name_len, + char *fullpath, /* we'll take over ownership */ + struct name_entry *names, + struct name_entry *merged_version, + unsigned is_null, /* boolean */ + unsigned df_conflict, /* boolean */ + unsigned filemask, + unsigned dirmask, + int resolved /* boolean */) { /* result->util is void*, so mi is a convenience typed variable */ struct merged_info *mi; @@ -1081,9 +1081,11 @@ static void setup_path_info(struct merge_options *opt, */ mi->is_null = 1; } - strmap_put(&opt->priv->paths, fullpath, mi); + if (strmap_put(&opt->priv->paths, fullpath, mi)) + return error(_("tree has duplicate entries for '%s'"), fullpath); result->string = fullpath; result->util = mi; + return 0; } static void add_pair(struct merge_options *opt, @@ -1350,9 +1352,10 @@ static int collect_merge_info_callback(int n, */ if (side1_matches_mbase && side2_matches_mbase) { /* mbase, side1, & side2 all match; use mbase as resolution */ - setup_path_info(opt, &pi, dirname, info->pathlen, fullpath, - names, names+0, mbase_null, 0 /* df_conflict */, - filemask, dirmask, 1 /* resolved */); + if (setup_path_info(opt, &pi, dirname, info->pathlen, fullpath, + names, names+0, mbase_null, 0 /* df_conflict */, + filemask, dirmask, 1 /* resolved */)) + return -1; /* Quit traversing */ return mask; } @@ -1364,9 +1367,10 @@ static int collect_merge_info_callback(int n, */ if (sides_match && filemask == 0x07) { /* use side1 (== side2) version as resolution */ - setup_path_info(opt, &pi, dirname, info->pathlen, fullpath, - names, names+1, side1_null, 0, - filemask, dirmask, 1); + if (setup_path_info(opt, &pi, dirname, info->pathlen, fullpath, + names, names+1, side1_null, 0, + filemask, dirmask, 1)) + return -1; /* Quit traversing */ return mask; } @@ -1378,18 +1382,20 @@ static int collect_merge_info_callback(int n, */ if (side1_matches_mbase && filemask == 0x07) { /* use side2 version as resolution */ - setup_path_info(opt, &pi, dirname, info->pathlen, fullpath, - names, names+2, side2_null, 0, - filemask, dirmask, 1); + if (setup_path_info(opt, &pi, dirname, info->pathlen, fullpath, + names, names+2, side2_null, 0, + filemask, dirmask, 1)) + return -1; /* Quit traversing */ return mask; } /* Similar to above but swapping sides 1 and 2 */ if (side2_matches_mbase && filemask == 0x07) { /* use side1 version as resolution */ - setup_path_info(opt, &pi, dirname, info->pathlen, fullpath, - names, names+1, side1_null, 0, - filemask, dirmask, 1); + if (setup_path_info(opt, &pi, dirname, info->pathlen, fullpath, + names, names+1, side1_null, 0, + filemask, dirmask, 1)) + return -1; /* Quit traversing */ return mask; } @@ -1413,8 +1419,9 @@ static int collect_merge_info_callback(int n, * unconflict some more cases, but that comes later so all we can * do now is record the different non-null file hashes.) */ - setup_path_info(opt, &pi, dirname, info->pathlen, fullpath, - names, NULL, 0, df_conflict, filemask, dirmask, 0); + if (setup_path_info(opt, &pi, dirname, info->pathlen, fullpath, + names, NULL, 0, df_conflict, filemask, dirmask, 0)) + return -1; /* Quit traversing */ ci = pi.util; VERIFY_CI(ci); diff --git a/t/t6422-merge-rename-corner-cases.sh b/t/t6422-merge-rename-corner-cases.sh index e18d5a227d54f7..81b645bb3bdc5b 100755 --- a/t/t6422-merge-rename-corner-cases.sh +++ b/t/t6422-merge-rename-corner-cases.sh @@ -1525,4 +1525,58 @@ test_expect_success 'submodule/directory preliminary conflict' ' ) ' +# Testcase: submodule/directory conflict with duplicate tree entries +# One side has a path as a gitlink (submodule). The other side replaces +# the gitlink with a directory. A third-party tool creates a tree on the +# submodule side that has *both* a gitlink and a tree entry for the same +# path (adding a file inside the submodule path ignoring that there's a +# gitlink there). collect_merge_info_callback() should detect the +# duplicate and abort rather than silently corrupting its bookkeeping. + +test_expect_success 'duplicate tree entries trigger an error' ' + test_when_finished "rm -rf duplicate-entry" && + git init duplicate-entry && + ( + cd duplicate-entry && + + # Base commit: "docs" is a gitlink (submodule) + empty_tree=$(git mktree file.txt && + git add file.txt && + git commit -m base && + + # side1: remove the gitlink, replace with a directory + git checkout -b side1 && + git rm --cached docs && + mkdir -p docs && + echo hello >docs/requirements.txt && + git add docs/requirements.txt && + git commit -m "side1: submodule to directory" && + + # side2: keep the gitlink but craft a tree that also + # contains a tree entry for "docs" (simulating a tool + # that adds files inside a submodule path without + # removing the gitlink first). + git checkout main && + git checkout -b side2 && + blob_oid=$(echo world | git hash-object -w --stdin) && + docs_tree=$(printf "100644 blob %s\trequirements.txt\n" \ + "$blob_oid" | git mktree) && + cur_tree=$(git rev-parse HEAD^{tree}) && + git cat-file -p $cur_tree >tree-listing && + printf "040000 tree %s\tdocs\n" "$docs_tree" >>tree-listing && + new_tree=$(git mktree err && + test_grep "duplicate entries" err + ) +' + test_done From 60826fdeb137a61e6ae8b80d70509d2bc094f8a5 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Tue, 21 Apr 2026 00:26:11 +0000 Subject: [PATCH 016/106] cache-tree: fix verify_cache() to catch non-adjacent D/F conflicts verify_cache() checks that the index does not contain both "path" and "path/file" before writing a tree. It does this by comparing only adjacent entries, relying on the assumption that "path/file" would immediately follow "path" in sorted order. Unfortunately, this assumption does not always hold. For example: docs <-- submodule entry docs-internal/README.md <-- intervening entry docs/requirements.txt <-- D/F conflict, NOT adjacent to "docs" When this happens, verify_cache() silently misses the D/F conflict and write-tree produces a corrupt tree object containing duplicate entries (one for the submodule "docs" and one for the tree "docs"). I could not find any caller in current git that both allows the index to get into this state and then tries to write it out without doing other checks beyond the verify_cache() call in cache_tree_update(), but verify_cache() is documented as a safety net for preventing corrupt trees and should actually provide that guarantee. A downstream consumer that relied solely on cache_tree_update()'s internal checking via verify_cache() to prevent duplicate tree entries was bitten by the gap. Add a test that constructs a corrupt index directly (bypassing the D/F checks in add_index_entry) and verifies that write-tree now rejects it. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- cache-tree.c | 46 ++++++++++++++++++++++++-- t/meson.build | 1 + t/t0093-direct-index-write.pl | 38 ++++++++++++++++++++++ t/t0093-verify-cache-df-gap.sh | 59 ++++++++++++++++++++++++++++++++++ 4 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 t/t0093-direct-index-write.pl create mode 100755 t/t0093-verify-cache-df-gap.sh diff --git a/cache-tree.c b/cache-tree.c index 7881b42aa24c80..f11844fe72020e 100644 --- a/cache-tree.c +++ b/cache-tree.c @@ -192,22 +192,62 @@ static int verify_cache(struct index_state *istate, int flags) for (i = 0; i + 1 < istate->cache_nr; i++) { /* path/file always comes after path because of the way * the cache is sorted. Also path can appear only once, - * which means conflicting one would immediately follow. + * so path/file is likely the immediately following path + * but might be separated if there is e.g. a + * path-internal/... file. */ const struct cache_entry *this_ce = istate->cache[i]; const struct cache_entry *next_ce = istate->cache[i + 1]; const char *this_name = this_ce->name; const char *next_name = next_ce->name; int this_len = ce_namelen(this_ce); + const char *conflict_name = NULL; + if (this_len < ce_namelen(next_ce) && - next_name[this_len] == '/' && + next_name[this_len] <= '/' && strncmp(this_name, next_name, this_len) == 0) { + if (next_name[this_len] == '/') { + conflict_name = next_name; + } else if (next_name[this_len] < '/') { + /* + * The immediately next entry shares our + * prefix but sorts before "path/" (e.g., + * "path-internal" between "path" and + * "path/file", since '-' (0x2D) < '/' + * (0x2F)). Binary search to find where + * "path/" would be and check for a D/F + * conflict there. + */ + struct cache_entry *other; + struct strbuf probe = STRBUF_INIT; + int pos; + + strbuf_add(&probe, this_name, this_len); + strbuf_addch(&probe, '/'); + pos = index_name_pos_sparse(istate, + probe.buf, + probe.len); + strbuf_release(&probe); + + if (pos < 0) + pos = -pos - 1; + if (pos >= (int)istate->cache_nr) + continue; + other = istate->cache[pos]; + if (ce_namelen(other) > this_len && + other->name[this_len] == '/' && + !strncmp(this_name, other->name, this_len)) + conflict_name = other->name; + } + } + + if (conflict_name) { if (10 < ++funny) { fprintf(stderr, "...\n"); break; } fprintf(stderr, "You have both %s and %s\n", - this_name, next_name); + this_name, conflict_name); } } if (funny) diff --git a/t/meson.build b/t/meson.build index 7528e5cda5fef0..362177999bd342 100644 --- a/t/meson.build +++ b/t/meson.build @@ -124,6 +124,7 @@ integration_tests = [ 't0090-cache-tree.sh', 't0091-bugreport.sh', 't0092-diagnose.sh', + 't0093-verify-cache-df-gap.sh', 't0095-bloom.sh', 't0100-previous.sh', 't0101-at-syntax.sh', diff --git a/t/t0093-direct-index-write.pl b/t/t0093-direct-index-write.pl new file mode 100644 index 00000000000000..2881a3ebb21dcd --- /dev/null +++ b/t/t0093-direct-index-write.pl @@ -0,0 +1,38 @@ +#!/usr/bin/perl +# +# Build a v2 index file from entries listed on stdin. +# Each line: "octalmode hex-oid name" +# Output: binary index written to stdout. +# +# This bypasses all D/F safety checks in add_index_entry(), simulating +# what happens when code uses ADD_CACHE_JUST_APPEND to bulk-load entries. +use strict; +use warnings; +use Digest::SHA qw(sha1 sha256); + +my $hash_algo = $ENV{'GIT_DEFAULT_HASH'} || 'sha1'; +my $hash_func = $hash_algo eq 'sha256' ? \&sha256 : \&sha1; + +my @entries; +while (my $line = ) { + chomp $line; + my ($mode, $oid_hex, $name) = split(/ /, $line, 3); + push @entries, [$mode, $oid_hex, $name]; +} + +my $body = "DIRC" . pack("NN", 2, scalar @entries); + +for my $ent (@entries) { + my ($mode, $oid_hex, $name) = @{$ent}; + # 10 x 32-bit stat fields (zeroed), with mode in position 7 + my $stat = pack("N10", 0, 0, 0, 0, 0, 0, oct($mode), 0, 0, 0); + my $oid = pack("H*", $oid_hex); + my $flags = pack("n", length($name) & 0xFFF); + my $entry = $stat . $oid . $flags . $name . "\0"; + # Pad to 8-byte boundary + while (length($entry) % 8) { $entry .= "\0"; } + $body .= $entry; +} + +binmode STDOUT; +print $body . $hash_func->($body); diff --git a/t/t0093-verify-cache-df-gap.sh b/t/t0093-verify-cache-df-gap.sh new file mode 100755 index 00000000000000..0b6829d805269d --- /dev/null +++ b/t/t0093-verify-cache-df-gap.sh @@ -0,0 +1,59 @@ +#!/bin/sh + +test_description='verify_cache() must catch non-adjacent D/F conflicts + +Ensure that verify_cache() can complain about bad entries like: + + docs <-- submodule + docs-internal/... <-- sorts here because "-" < "/" + docs/... <-- D/F conflict with "docs" above, not adjacent + +In order to test verify_cache, we directly construct a corrupt index +(bypassing the D/F safety checks in add_index_entry) and verify that +write-tree rejects it. +' + +. ./test-lib.sh + +if ! test_have_prereq PERL +then + skip_all='skipping verify_cache D/F tests; Perl not available' + test_done +fi + +# Build a v2 index from entries on stdin, bypassing D/F checks. +# Each line: "octalmode hex-oid name" (entries must be pre-sorted). +build_corrupt_index () { + perl "$TEST_DIRECTORY/t0093-direct-index-write.pl" >"$1" +} + +test_expect_success 'setup objects' ' + test_commit base && + BLOB=$(git rev-parse HEAD:base.t) && + SUB_COMMIT=$(git rev-parse HEAD) +' + +test_expect_success 'adjacent D/F conflict is caught by verify_cache' ' + cat >index-entries <<-EOF && + 0160000 $SUB_COMMIT docs + 0100644 $BLOB docs/requirements.txt + EOF + build_corrupt_index .git/index err && + test_grep "You have both docs and docs/requirements.txt" err +' + +test_expect_success 'non-adjacent D/F conflict is caught by verify_cache' ' + cat >index-entries <<-EOF && + 0160000 $SUB_COMMIT docs + 0100644 $BLOB docs-internal/README.md + 0100644 $BLOB docs/requirements.txt + EOF + build_corrupt_index .git/index err && + test_grep "You have both docs and docs/requirements.txt" err +' + +test_done From eecc860d24564ae8e2c96615649e06e4d636f1aa Mon Sep 17 00:00:00 2001 From: Pablo Sabater Date: Mon, 27 Apr 2026 12:28:38 +0200 Subject: [PATCH 017/106] graph: add indentation for commits preceded by a parentless commit When having a history with multiple root commits or commits that act like roots (they have excluded parents), let's call them parentless, and drawing the history near them, the graphing engine renders the commits one below the other, seeming that they are related. This issue has been attempted multiple times: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/ This happens because for these parentless commits, in the next row the column becomes empty and the engine prints from left to right from the first empty column, filling the gap below these parentless commits. Keep a parentless commit for at least one row more to avoid having the column empty but hide it as indentation, therefore making the next unrelated commit live in the next column (column means even positions where edges live: 0, 2, 4), then clean that "placeholder" column and let the unrelated commit to naturally collapse to the column where the parentless commit was. Add is_placeholder to the struct column to mark if a column is acting as a placeholder for the padding. When a column is parentless, add a column with the parentless commit data to prevent segfaults when 'column->commit' and mark it as a placeholder. Teach rendering functions to print a padding ' ' instead of an edge when a placeholder column is met. Then, unless the next commit is also parentless (then we need to keep cascading the indentation) clean the mapping and columns from the placeholder to allow it to collapse naturally. Add tests for different cases. before this patch: * parentless A * child B * parentless B after this patch: * parentless A * child B / * parentless B Signed-off-by: Pablo Sabater Signed-off-by: Junio C Hamano --- graph.c | 115 ++++++++++++++++++++++++++++++-- t/t4215-log-skewed-merges.sh | 124 +++++++++++++++++++++++++++++++++++ 2 files changed, 233 insertions(+), 6 deletions(-) diff --git a/graph.c b/graph.c index 26f6fbf000aef5..97292df998f5e4 100644 --- a/graph.c +++ b/graph.c @@ -60,6 +60,12 @@ struct column { * index into column_colors. */ unsigned short color; + /* + * A placeholder column keeps the column of a parentless commit filled + * for one extra row, avoiding a next unrelated commit to be printed + * in the same column. + */ + unsigned is_placeholder:1; }; enum graph_state { @@ -563,6 +569,7 @@ static void graph_insert_into_new_columns(struct git_graph *graph, i = graph->num_new_columns++; graph->new_columns[i].commit = commit; graph->new_columns[i].color = graph_find_commit_color(graph, commit); + graph->new_columns[i].is_placeholder = 0; } if (graph->num_parents > 1 && idx > -1 && graph->merge_layout == -1) { @@ -607,7 +614,7 @@ static void graph_update_columns(struct git_graph *graph) { struct commit_list *parent; int max_new_columns; - int i, seen_this, is_commit_in_columns; + int i, seen_this, is_commit_in_columns, is_parentless; /* * Swap graph->columns with graph->new_columns @@ -654,6 +661,26 @@ static void graph_update_columns(struct git_graph *graph) */ seen_this = 0; is_commit_in_columns = 1; + /* + * A commit is "parentless" (is a visual root that starts a new column) + * only if has no visible parents AND it's not a boundary commit. + * + * Boundary commits also have no visible parents, but they are + * NOT a visual root: + * + * 1. A boundary only appears in the output because an included commit + * is its child. Children are always above, and the renderer draws an + * edge down to the boundary from that child. Rather than starting + * a column like a visual root would do, it "inherits" its child + * column. + * + * 2. Included commit CAN'T appear below a boundary. Boundaries are + * ancestors of the exclusion point; if an included commit were an + * ancestor of the boundary it would be excluded and not rendered. + * Boundaries therefore always sink to the bottom. + */ + is_parentless = graph->num_parents == 0 && + !(graph->commit->object.flags & BOUNDARY); for (i = 0; i <= graph->num_columns; i++) { struct commit *col_commit; if (i == graph->num_columns) { @@ -688,11 +715,46 @@ static void graph_update_columns(struct git_graph *graph) * least 2, even if it has no interesting parents. * The current commit always takes up at least 2 * spaces. + * + * Check for the commit to seem like a root, no parents + * rendered and that it is not a boundary commit. If so, + * add a placeholder to keep that column filled for + * at least one row. + * + * Prevents the next commit from being inserted + * just below and making the graph confusing. */ - if (graph->num_parents == 0) + if (is_parentless) { + graph_insert_into_new_columns(graph, graph->commit, i); + graph->new_columns[graph->num_new_columns - 1] + .is_placeholder = 1; + } else if (graph->num_parents == 0) { graph->width += 2; + } } else { - graph_insert_into_new_columns(graph, col_commit, -1); + if (graph->columns[i].is_placeholder) { + /* + * Keep the placeholders if the next commit is + * parentless also, making the indentation cascade. + */ + if (!seen_this && is_parentless) { + graph_insert_into_new_columns(graph, + graph->columns[i].commit, i); + graph->new_columns[graph->num_new_columns - 1] + .is_placeholder = 1; + } else if (!seen_this) { + graph->mapping[graph->width] = -1; + graph->width += 2; + } + /* + * seen_this && is_placeholder means that this + * line is the one after the indented one, the + * placeholder is no longer needed, gets + * dropped and the columns collapses naturally. + */ + } else { + graph_insert_into_new_columns(graph, col_commit, -1); + } } } @@ -846,7 +908,10 @@ static void graph_output_padding_line(struct git_graph *graph, * Output a padding row, that leaves all branch lines unchanged */ for (i = 0; i < graph->num_new_columns; i++) { - graph_line_write_column(line, &graph->new_columns[i], '|'); + if (graph->new_columns[i].is_placeholder) + graph_line_write_column(line, &graph->new_columns[i], ' '); + else + graph_line_write_column(line, &graph->new_columns[i], '|'); graph_line_addch(line, ' '); } } @@ -1058,7 +1123,34 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line graph->mapping[2 * i] < i) { graph_line_write_column(line, col, '/'); } else { - graph_line_write_column(line, col, '|'); + if (col->is_placeholder) { + /* + * When the indented commit is a merge commit, + * the placeholder column adds unwanted padding + * between the commit and its subject. + * + * * parentless commit + * * merge commit + * /| + * | * parent A + * * parent B + * ^^ unwanted padding + * + * Once the current commit has been seen, don't + * let placeholder columns to be rendered: + * + * * parentless commit + * * merge commit + * /| + * | * parent A + * * parent B + */ + if (seen_this) + continue; + graph_line_write_column(line, col, ' '); + } else { + graph_line_write_column(line, col, '|'); + } } graph_line_addch(line, ' '); } @@ -1135,7 +1227,18 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l graph_line_write_column(line, col, '|'); graph_line_addch(line, ' '); } else { - graph_line_write_column(line, col, '|'); + if (col->is_placeholder) { + /* + * Same placeholder handling as in + * graph_output_commit_line(). + */ + if (seen_this) + continue; + graph_line_write_column(line, col, ' '); + } else { + graph_line_write_column(line, col, '|'); + } + if (graph->merge_layout != 0 || i != graph->commit_index - 1) { if (parent_col) graph_line_write_column( diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh index 28d0779a8c599e..0f6f95a6b5aff5 100755 --- a/t/t4215-log-skewed-merges.sh +++ b/t/t4215-log-skewed-merges.sh @@ -370,4 +370,128 @@ test_expect_success 'log --graph with multiple tips' ' EOF ' +test_expect_success 'log --graph with root commit' ' + git checkout --orphan 8_1 && test_commit 8_A && test_commit 8_A1 && + git checkout --orphan 8_2 && test_commit 8_B && + + check_graph 8_2 8_1 <<-\EOF + * 8_B + * 8_A1 + / + * 8_A + EOF +' + +test_expect_success 'log --graph with multiple root commits' ' + test_commit 8_B1 && + git checkout --orphan 8_3 && test_commit 8_C && + + check_graph 8_3 8_2 8_1 <<-\EOF + * 8_C + * 8_B1 + / + * 8_B + * 8_A1 + / + * 8_A + EOF +' + +test_expect_success 'log --graph commit from a two parent merge shifted' ' + git checkout --orphan 9_1 && test_commit 9_B && + git checkout --orphan 9_2 && test_commit 9_C && + git checkout 9_1 && + git merge 9_2 --allow-unrelated-histories -m 9_M && + git checkout --orphan 9_3 && + test_commit 9_A && test_commit 9_A1 && test_commit 9_A2 && + + check_graph 9_3 9_1 <<-\EOF + * 9_A2 + * 9_A1 + * 9_A + * 9_M + /| + | * 9_C + * 9_B + EOF +' + +test_expect_success 'log --graph commit from a three parent merge shifted' ' + git checkout --orphan 10_1 && test_commit 10_B && + git checkout --orphan 10_2 && test_commit 10_C && + git checkout --orphan 10_3 && test_commit 10_D && + git checkout 10_1 && + TREE=$(git write-tree) && + MERGE=$(git commit-tree $TREE -p 10_1 -p 10_2 -p 10_3 -m 10_M) && + git reset --hard $MERGE && + git checkout --orphan 10_4 && + test_commit 10_A && test_commit 10_A1 && test_commit 10_A2 && + + check_graph 10_4 10_1 <<-\EOF + * 10_A2 + * 10_A1 + * 10_A + * 10_M + /|\ + | | * 10_D + | * 10_C + * 10_B + EOF +' + +test_expect_success 'log --graph commit from a four parent merge shifted' ' + git checkout --orphan 11_1 && test_commit 11_B && + git checkout --orphan 11_2 && test_commit 11_C && + git checkout --orphan 11_3 && test_commit 11_D && + git checkout --orphan 11_4 && test_commit 11_E && + git checkout 11_1 && + TREE=$(git write-tree) && + MERGE=$(git commit-tree $TREE -p 11_1 -p 11_2 -p 11_3 -p 11_4 -m 11_M) && + git reset --hard $MERGE && + git checkout --orphan 11_5 && + test_commit 11_A && test_commit 11_A1 && test_commit 11_A2 && + + check_graph 11_5 11_1 <<-\EOF + * 11_A2 + * 11_A1 + * 11_A + *-. 11_M + /|\ \ + | | | * 11_E + | | * 11_D + | * 11_C + * 11_B + EOF +' + +test_expect_success 'log --graph disconnected three roots cascading' ' + git checkout --orphan 12_1 && test_commit 12_D && test_commit 12_D1 && + git checkout --orphan 12_2 && test_commit 12_C && + git checkout --orphan 12_3 && test_commit 12_B && + git checkout --orphan 12_4 && test_commit 12_A && + + check_graph 12_4 12_3 12_2 12_1 <<-\EOF + * 12_A + * 12_B + * 12_C + * 12_D1 + _ / + / + / + * 12_D + EOF +' + +test_expect_success 'log --graph with excluded parent (not a root)' ' + git checkout --orphan 13_1 && test_commit 13_X && test_commit 13_Y && + git checkout --orphan 13_2 && test_commit 13_O && test_commit 13_A && + + check_graph 13_O..13_A 13_1 <<-\EOF + * 13_A + * 13_Y + / + * 13_X + EOF +' + test_done From 6ab1b3b74d02151e7570b82554e9cadebe0ea6b8 Mon Sep 17 00:00:00 2001 From: Usman Akinyemi Date: Sun, 3 May 2026 21:04:00 +0530 Subject: [PATCH 018/106] remote: fix sign-compare warnings in push_cas_option Replace `int` with `size_t` for `nr` and `alloc` in `struct push_cas_option` to avoid -Werror=sign-compare warnings when comparing against size-based values. Suggested-by: Junio C Hamano Signed-off-by: Usman Akinyemi Signed-off-by: Junio C Hamano --- remote.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/remote.h b/remote.h index fc052945ee451d..741d14a9fcefcd 100644 --- a/remote.h +++ b/remote.h @@ -418,8 +418,8 @@ struct push_cas_option { unsigned use_tracking:1; char *refname; } *entry; - int nr; - int alloc; + size_t nr; + size_t alloc; }; int parseopt_push_cas_option(const struct option *, const char *arg, int unset); From 3e7b9dce27b1519f6745c89fe01f0b840acddb0a Mon Sep 17 00:00:00 2001 From: Usman Akinyemi Date: Sun, 3 May 2026 21:04:01 +0530 Subject: [PATCH 019/106] remote: move remote group resolution to remote.c MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `get_remote_group`, `add_remote_or_group`, and the `remote_group_data` struct are currently defined as static helpers inside builtin/fetch.c. They implement generic remote group resolution that is not specific to fetch — they parse `remotes.` config entries and resolve a name to either a list of group members or a single configured remote. Move them to remote.c and declare them in remote.h so that other builtins can use the same logic without duplication. Useful for the next patch. Suggested-by: Junio C Hamano Signed-off-by: Usman Akinyemi Signed-off-by: Junio C Hamano --- builtin/fetch.c | 42 ------------------------------------------ remote.c | 37 +++++++++++++++++++++++++++++++++++++ remote.h | 12 ++++++++++++ 3 files changed, 49 insertions(+), 42 deletions(-) diff --git a/builtin/fetch.c b/builtin/fetch.c index 8a36cf67b5f140..966cc58f730150 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -2138,48 +2138,6 @@ static int get_one_remote_for_fetch(struct remote *remote, void *priv) return 0; } -struct remote_group_data { - const char *name; - struct string_list *list; -}; - -static int get_remote_group(const char *key, const char *value, - const struct config_context *ctx UNUSED, - void *priv) -{ - struct remote_group_data *g = priv; - - if (skip_prefix(key, "remotes.", &key) && !strcmp(key, g->name)) { - /* split list by white space */ - while (*value) { - size_t wordlen = strcspn(value, " \t\n"); - - if (wordlen >= 1) - string_list_append_nodup(g->list, - xstrndup(value, wordlen)); - value += wordlen + (value[wordlen] != '\0'); - } - } - - return 0; -} - -static int add_remote_or_group(const char *name, struct string_list *list) -{ - int prev_nr = list->nr; - struct remote_group_data g; - g.name = name; g.list = list; - - repo_config(the_repository, get_remote_group, &g); - if (list->nr == prev_nr) { - struct remote *remote = remote_get(name); - if (!remote_is_configured(remote, 0)) - return 0; - string_list_append(list, remote->name); - } - return 1; -} - static void add_options_to_argv(struct strvec *argv, const struct fetch_config *config) { diff --git a/remote.c b/remote.c index 7ca2a6501b4920..3d62384792c323 100644 --- a/remote.c +++ b/remote.c @@ -2114,6 +2114,43 @@ int get_fetch_map(const struct ref *remote_refs, return 0; } +int get_remote_group(const char *key, const char *value, + const struct config_context *ctx UNUSED, + void *priv) +{ + struct remote_group_data *g = priv; + + if (skip_prefix(key, "remotes.", &key) && !strcmp(key, g->name)) { + /* split list by white space */ + while (*value) { + size_t wordlen = strcspn(value, " \t\n"); + + if (wordlen >= 1) + string_list_append_nodup(g->list, + xstrndup(value, wordlen)); + value += wordlen + (value[wordlen] != '\0'); + } + } + + return 0; +} + +int add_remote_or_group(const char *name, struct string_list *list) +{ + int prev_nr = list->nr; + struct remote_group_data g; + g.name = name; g.list = list; + + repo_config(the_repository, get_remote_group, &g); + if (list->nr == prev_nr) { + struct remote *remote = remote_get(name); + if (!remote_is_configured(remote, 0)) + return 0; + string_list_append(list, remote->name); + } + return 1; +} + int resolve_remote_symref(struct ref *ref, struct ref *list) { if (!ref->symref) diff --git a/remote.h b/remote.h index 741d14a9fcefcd..7915be3111daa7 100644 --- a/remote.h +++ b/remote.h @@ -347,6 +347,18 @@ int branch_has_merge_config(struct branch *branch); int branch_merge_matches(struct branch *, int n, const char *); +/* list of the remote in a group as configured */ +struct remote_group_data { + const char *name; + struct string_list *list; +}; + +int get_remote_group(const char *key, const char *value, + const struct config_context *ctx, + void *priv); + +int add_remote_or_group(const char *name, struct string_list *list); + /** * Return the fully-qualified refname of the tracking branch for `branch`. * I.e., what "branch@{upstream}" would give you. Returns NULL if no From 8ea82816652d20ac7070a8fcd60980568a8a293c Mon Sep 17 00:00:00 2001 From: Usman Akinyemi Date: Sun, 3 May 2026 21:04:02 +0530 Subject: [PATCH 020/106] push: support pushing to a remote group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `git fetch` accepts a remote group name (configured via `remotes.` in config) and fetches from each member remote. `git push` has no equivalent — it only accepts a single remote name. Teach `git push` to resolve its repository argument through `add_remote_or_group()`, which was made public in the previous patch, so that a user can push to all remotes in a group with: git push When the argument resolves to a single remote, the behaviour is identical to before. When it resolves to a group, each member remote is pushed in sequence. The group push path rebuilds the refspec list (`rs`) from scratch for each member remote so that per-remote push mappings configured via `remote..push` are resolved correctly against each specific remote. Without this, refspec entries would accumulate across iterations and each subsequent remote would receive a growing list of duplicated entries. Mirror detection (`remote->mirror`) is also evaluated per remote using a copy of the flags, so that a mirror remote in the group cannot set TRANSPORT_PUSH_FORCE on subsequent non-mirror remotes in the same group. Suggested-by: Junio C Hamano Signed-off-by: Usman Akinyemi Signed-off-by: Junio C Hamano --- Documentation/git-push.adoc | 80 ++++++++++-- builtin/push.c | 251 +++++++++++++++++++++++++++++++----- t/meson.build | 1 + t/t5566-push-group.sh | 160 +++++++++++++++++++++++ 4 files changed, 451 insertions(+), 41 deletions(-) create mode 100755 t/t5566-push-group.sh diff --git a/Documentation/git-push.adoc b/Documentation/git-push.adoc index e5ba3a67421edc..aa221c3909532e 100644 --- a/Documentation/git-push.adoc +++ b/Documentation/git-push.adoc @@ -18,17 +18,28 @@ git push [--all | --branches | --mirror | --tags] [--follow-tags] [--atomic] [-n DESCRIPTION ----------- - -Updates one or more branches, tags, or other references in a remote -repository from your local repository, and sends all necessary data -that isn't already on the remote. +Updates one or more branches, tags, or other references in one or more +remote repositories from your local repository, and sends all necessary +data that isn't already on the remote. The simplest way to push is `git push `. `git push origin main` will push the local `main` branch to the `main` branch on the remote named `origin`. -The `` argument defaults to the upstream for the current branch, -or `origin` if there's no configured upstream. +You can also push to multiple remotes at once by using a remote group. +A remote group is a named list of remotes configured via `remotes.` +in your git config: + + $ git config remotes.all-remotes "origin gitlab backup" + +Then `git push all-remotes` will push to `origin`, `gitlab`, and +`backup` in turn, as if you had run `git push` against each one +individually. Each remote is pushed independently using its own +push mapping configuration. There is a `remotes.` entry in +the configuration file. (See linkgit:git-config[1]). + +The `` argument defaults to the upstream for the current +branch, or `origin` if there's no configured upstream. To decide which branches, tags, or other refs to push, Git uses (in order of precedence): @@ -55,8 +66,10 @@ OPTIONS __:: The "remote" repository that is the destination of a push operation. This parameter can be either a URL - (see the section <> below) or the name - of a remote (see the section <> below). + (see the section <> below), the name + of a remote (see the section <> below), + or the name of a remote group + (see the section <> below). `...`:: Specify what destination ref to update with what source object. @@ -430,6 +443,57 @@ further recursion will occur. In this case, `only` is treated as `on-demand`. include::urls-remotes.adoc[] +[[REMOTE-GROUPS]] +REMOTE GROUPS +------------- + +A remote group is a named list of remotes configured via `remotes.` +in your git config: + + $ git config remotes.all-remotes "r1 r2 r3" + +When a group name is given as the `` argument, the push is +performed to each member remote in turn. The defining principle is: + + git push all-remotes + +is exactly equivalent to: + + git push r1 + git push r2 + ... + git push rN + +where r1, r2, ..., rN are the members of `all-remotes`. No special +behaviour is added or removed — the group is purely a shorthand for +running the same push command against each member remote individually. + +When pushing to a group of more than one remote, Git spawns a separate +`git push` subprocess for each member remote in sequence. Each subprocess +receives the same flags and refspecs as the original invocation. This +means that per-remote push mappings configured via `remote..push` +and mirror mode (`remote..mirror`) are evaluated independently for +each remote, and a mirror remote in the group cannot affect the push +behaviour of other non-mirror remotes in the same group. + +The `--atomic` option is not supported for group pushes, because atomicity +can only be guaranteed within a single transport connection to a single +remote. Git will refuse the invocation with an error if `--atomic` is +combined with a group name. + +If any member remote fails whether due to a push rejection (e.g. a +non-fast-forward update, a server-side hook refusing a ref) or a connection +error (e.g. the repository does not exist, authentication fails, or the +network is unreachable), Git reports the error and continues pushing to +the remaining remotes in the group. The overall exit code is non-zero if +any member push fails. + +This means the user is responsible for ensuring that the sequence of +individual pushes makes sense. If `git push r1`` would fail for a given +set of options and arguments, then `git push all-remotes` will fail in +the same way when it reaches r1. The group push does not do anything +special to make a failing individual push succeed. + OUTPUT ------ diff --git a/builtin/push.c b/builtin/push.c index 7100ffba5da17e..6021b71d668455 100644 --- a/builtin/push.c +++ b/builtin/push.c @@ -10,6 +10,7 @@ #include "config.h" #include "environment.h" #include "gettext.h" +#include "hex.h" #include "refspec.h" #include "run-command.h" #include "remote.h" @@ -544,6 +545,123 @@ static int git_push_config(const char *k, const char *v, return git_default_config(k, v, ctx, NULL); } +static int push_multiple(struct string_list *list, + const struct string_list *push_options, + int flags, + int tags, + const char **refspecs, + int refspec_nr) +{ + int result = 0; + size_t i; + struct strvec argv = STRVEC_INIT; + + strvec_push(&argv, "push"); + + if (flags & TRANSPORT_PUSH_FORCE) + strvec_push(&argv, "--force"); + if (flags & TRANSPORT_PUSH_DRY_RUN) + strvec_push(&argv, "--dry-run"); + if (flags & TRANSPORT_PUSH_PORCELAIN) + strvec_push(&argv, "--porcelain"); + if (flags & TRANSPORT_PUSH_PRUNE) + strvec_push(&argv, "--prune"); + if (flags & TRANSPORT_PUSH_NO_HOOK) + strvec_push(&argv, "--no-verify"); + if (flags & TRANSPORT_PUSH_FOLLOW_TAGS) + strvec_push(&argv, "--follow-tags"); + if (flags & TRANSPORT_PUSH_SET_UPSTREAM) + strvec_push(&argv, "--set-upstream"); + if (flags & TRANSPORT_PUSH_FORCE_IF_INCLUDES) + strvec_push(&argv, "--force-if-includes"); + if (flags & TRANSPORT_PUSH_ALL) + strvec_push(&argv, "--all"); + if (flags & TRANSPORT_PUSH_MIRROR) + strvec_push(&argv, "--mirror"); + + if (flags & TRANSPORT_PUSH_CERT_ALWAYS) + strvec_push(&argv, "--signed=yes"); + else if (flags & TRANSPORT_PUSH_CERT_IF_ASKED) + strvec_push(&argv, "--signed=if-asked"); + if (!thin) + strvec_push(&argv, "--no-thin"); + + if (deleterefs) + strvec_push(&argv, "--delete"); + + if (receivepack) + strvec_pushf(&argv, "--receive-pack=%s", receivepack); + if (verbosity >= 2) + strvec_push(&argv, "-v"); + if (verbosity >= 1) + strvec_push(&argv, "-v"); + else if (verbosity < 0) + strvec_push(&argv, "-q"); + if (progress > 0) + strvec_push(&argv, "--progress"); + else if (progress == 0) + strvec_push(&argv, "--no-progress"); + + if (family == TRANSPORT_FAMILY_IPV4) + strvec_push(&argv, "--ipv4"); + else if (family == TRANSPORT_FAMILY_IPV6) + strvec_push(&argv, "--ipv6"); + + if (recurse_submodules == RECURSE_SUBMODULES_CHECK) + strvec_push(&argv, "--recurse-submodules=check"); + else if (recurse_submodules == RECURSE_SUBMODULES_ON_DEMAND) + strvec_push(&argv, "--recurse-submodules=on-demand"); + else if (recurse_submodules == RECURSE_SUBMODULES_ONLY) + strvec_push(&argv, "--recurse-submodules=only"); + else if (recurse_submodules == RECURSE_SUBMODULES_OFF) + strvec_push(&argv, "--recurse-submodules=no"); + + + if (tags) + strvec_push(&argv, "--tags"); + + for (i = 0; i < push_options->nr; i++) + strvec_pushf(&argv, "--push-option=%s", + push_options->items[i].string); + + for (i = 0; i < cas.nr; i++) { + if (cas.entry[i].use_tracking) { + strvec_pushf(&argv, "--force-with-lease=%s", + cas.entry[i].refname); + } else if (!is_null_oid(&cas.entry[i].expect)) { + strvec_pushf(&argv, "--force-with-lease=%s:%s", + cas.entry[i].refname, + oid_to_hex(&cas.entry[i].expect)); + } else { + strvec_push(&argv, "--force-with-lease"); + } + } + + for (i = 0; i < list->nr; i++) { + const char *name = list->items[i].string; + struct child_process cmd = CHILD_PROCESS_INIT; + int j; + + strvec_pushv(&cmd.args, argv.v); + strvec_push(&cmd.args, name); + + for (j = 0; j < refspec_nr; j++) + strvec_push(&cmd.args, refspecs[j]); + + if (verbosity >= 0) + printf(_("Pushing to %s\n"), name); + + cmd.git_cmd = 1; + if (run_command(&cmd)) { + error(_("could not push to %s"), name); + result = 1; + } + } + + strvec_clear(&argv); + return result; +} + int cmd_push(int argc, const char **argv, const char *prefix, @@ -552,12 +670,13 @@ int cmd_push(int argc, int flags = 0; int tags = 0; int push_cert = -1; - int rc; + int rc = 0; + int base_flags; const char *repo = NULL; /* default repository */ struct string_list push_options_cmdline = STRING_LIST_INIT_DUP; + struct string_list remote_group = STRING_LIST_INIT_DUP; struct string_list *push_options; const struct string_list_item *item; - struct remote *remote; struct option options[] = { OPT__VERBOSITY(&verbosity), @@ -620,39 +739,45 @@ int cmd_push(int argc, else if (recurse_submodules == RECURSE_SUBMODULES_ONLY) flags |= TRANSPORT_RECURSE_SUBMODULES_ONLY; - if (tags) - refspec_append(&rs, "refs/tags/*"); - if (argc > 0) repo = argv[0]; - remote = pushremote_get(repo); - if (!remote) { - if (repo) - die(_("bad repository '%s'"), repo); - die(_("No configured push destination.\n" - "Either specify the URL from the command-line or configure a remote repository using\n" - "\n" - " git remote add \n" - "\n" - "and then push using the remote name\n" - "\n" - " git push \n")); - } - - if (argc > 0) - set_refspecs(argv + 1, argc - 1, remote); - - if (remote->mirror) - flags |= (TRANSPORT_PUSH_MIRROR|TRANSPORT_PUSH_FORCE); - - if (flags & TRANSPORT_PUSH_ALL) { - if (argc >= 2) - die(_("--all can't be combined with refspecs")); - } - if (flags & TRANSPORT_PUSH_MIRROR) { - if (argc >= 2) - die(_("--mirror can't be combined with refspecs")); + if (repo) { + if (!add_remote_or_group(repo, &remote_group)) { + /* + * Not a configured remote name or group name. + * Try treating it as a direct URL or path, e.g. + * git push /tmp/foo.git + * git push https://github.com/user/repo.git + * pushremote_get() creates an anonymous remote + * from the URL so the loop below can handle it + * identically to a named remote. + */ + struct remote *r = pushremote_get(repo); + if (!r) + die(_("bad repository '%s'"), repo); + string_list_append(&remote_group, r->name); + } + } else { + struct remote *r = pushremote_get(NULL); + if (!r) + die(_("No configured push destination.\n" + "Either specify the URL from the command-line or configure a remote repository using\n" + "\n" + " git remote add \n" + "\n" + "and then push using the remote name\n" + "\n" + " git push \n" + "\n" + "To push to multiple remotes at once, configure a remote group using\n" + "\n" + " git config remotes. \" \"\n" + "\n" + "and then push using the group name\n" + "\n" + " git push \n")); + string_list_append(&remote_group, r->name); } if (!is_empty_cas(&cas) && (flags & TRANSPORT_PUSH_FORCE_IF_INCLUDES)) @@ -662,10 +787,70 @@ int cmd_push(int argc, if (strchr(item->string, '\n')) die(_("push options must not have new line characters")); - rc = do_push(flags, push_options, remote); + if (remote_group.nr == 1) { + /* + * Single remote (the common case): run do_push() directly + * in this process. The loop runs exactly once. + * + * Mirror detection and the --mirror/--all + refspec conflict + * checks are done here. rs is rebuilt so that per-remote push + * mappings (remote.NAME.push config) are resolved against the + * correct remote. inner_flags is a snapshot of flags so that a + * mirror remote cannot bleed TRANSPORT_PUSH_FORCE into any + * subsequent call. + */ + base_flags = flags; + { + int inner_flags = base_flags; + struct remote *r = pushremote_get(remote_group.items[0].string); + if (!r) + die(_("no such remote or remote group: %s"), + remote_group.items[0].string); + + if (r->mirror) + inner_flags |= (TRANSPORT_PUSH_MIRROR|TRANSPORT_PUSH_FORCE); + + if (inner_flags & TRANSPORT_PUSH_ALL) { + if (argc >= 2) + die(_("--all can't be combined with refspecs")); + } + if (inner_flags & TRANSPORT_PUSH_MIRROR) { + if (argc >= 2) + die(_("--mirror can't be combined with refspecs")); + } + + refspec_clear(&rs); + rs = (struct refspec) REFSPEC_INIT_PUSH; + + if (tags) + refspec_append(&rs, "refs/tags/*"); + if (argc > 0) + set_refspecs(argv + 1, argc - 1, r); + + rc = do_push(inner_flags, push_options, r); + } + } else { + /* + * Multiple remotes: spawn one "git push []" + * subprocess per remote, sequentially. + * + * Options that only make sense for a single transport connection + * are rejected here. + */ + if (flags & TRANSPORT_PUSH_ATOMIC) + die(_("--atomic can only be used when pushing to one remote")); + + rc = push_multiple(&remote_group, push_options, flags, + tags, + argc > 1 ? argv + 1 : NULL, + argc > 1 ? argc - 1 : 0); + } + string_list_clear(&push_options_cmdline, 0); string_list_clear(&push_options_config, 0); + string_list_clear(&remote_group, 0); clear_cas_option(&cas); + if (rc == -1) usage_with_options(push_usage, options); else diff --git a/t/meson.build b/t/meson.build index 9b2fa4dee807d6..215df033e07e32 100644 --- a/t/meson.build +++ b/t/meson.build @@ -700,6 +700,7 @@ integration_tests = [ 't5563-simple-http-auth.sh', 't5564-http-proxy.sh', 't5565-push-multiple.sh', + 't5566-push-group.sh', 't5570-git-daemon.sh', 't5571-pre-push-hook.sh', 't5572-pull-submodule.sh', diff --git a/t/t5566-push-group.sh b/t/t5566-push-group.sh new file mode 100755 index 00000000000000..a7d59352b1c03e --- /dev/null +++ b/t/t5566-push-group.sh @@ -0,0 +1,160 @@ +#!/bin/sh + +test_description='push to remote group' + +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=default +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME + +. ./test-lib.sh + +test_expect_success 'setup' ' + for i in 1 2 3 + do + git init --bare dest-$i.git && + git -C dest-$i.git symbolic-ref HEAD refs/heads/not-a-branch || + return 1 + done && + test_tick && + git commit --allow-empty -m "initial" && + git config set remote.remote-1.url "file://$(pwd)/dest-1.git" && + git config set remote.remote-1.fetch "+refs/heads/*:refs/remotes/remote-1/*" && + git config set remote.remote-2.url "file://$(pwd)/dest-2.git" && + git config set remote.remote-2.fetch "+refs/heads/*:refs/remotes/remote-2/*" && + git config set remote.remote-3.url "file://$(pwd)/dest-3.git" && + git config set remote.remote-3.fetch "+refs/heads/*:refs/remotes/remote-3/*" && + git config set remotes.all-remotes "remote-1 remote-2 remote-3" +' + +test_expect_success 'push to remote group updates all members correctly' ' + git push all-remotes HEAD:refs/heads/main && + git rev-parse HEAD >expect && + for i in 1 2 3 + do + git -C dest-$i.git rev-parse refs/heads/main >actual || + return 1 + test_cmp expect actual || return 1 + done +' + +test_expect_success 'push second commit to group updates all members' ' + test_tick && + git commit --allow-empty -m "second" && + git push all-remotes HEAD:refs/heads/main && + git rev-parse HEAD >expect && + for i in 1 2 3 + do + git -C dest-$i.git rev-parse refs/heads/main >actual || + return 1 + test_cmp expect actual || return 1 + done +' + +test_expect_success 'push to single remote in group does not affect others' ' + test_tick && + git commit --allow-empty -m "third" && + git push remote-1 HEAD:refs/heads/main && + git -C dest-1.git rev-parse refs/heads/main >hash-after-1 && + git -C dest-2.git rev-parse refs/heads/main >hash-after-2 && + ! test_cmp hash-after-1 hash-after-2 +' + +test_expect_success 'mirror remote in group with refspec fails' ' + git config set remote.remote-1.mirror true && + test_must_fail git push all-remotes HEAD:refs/heads/main 2>err && + test_grep "mirror" err && + git config unset remote.remote-1.mirror +' + +test_expect_success 'push.default=current works with group push' ' + git config set push.default current && + test_tick && + git commit --allow-empty -m "fifth" && + git push all-remotes && + git config unset push.default +' + +test_expect_success '--atomic is rejected for group push' ' + test_must_fail git push --atomic all-remotes HEAD:refs/heads/main 2>err && + test_grep "atomic" err +' + +test_expect_success 'push continues past rejection to remaining remotes' ' + for i in c1 c2 c3 + do + git init --bare dest-$i.git || return 1 + done && + git config set remote.c1.url "file://$(pwd)/dest-c1.git" && + git config set remote.c2.url "file://$(pwd)/dest-c2.git" && + git config set remote.c3.url "file://$(pwd)/dest-c3.git" && + git config set remotes.continue-group "c1 c2 c3" && + + test_tick && + git commit --allow-empty -m "base for continue test" && + + # initial sync + git push continue-group HEAD:refs/heads/main && + + # advance c2 independently + git clone dest-c2.git tmp-c2 && + ( + cd tmp-c2 && + git checkout -b main origin/main && + test_commit c2_independent && + git push origin HEAD:refs/heads/main + ) && + rm -rf tmp-c2 && + + test_tick && + git commit --allow-empty -m "local diverging commit" && + + # push: c2 rejects, others succeed + test_must_fail git push continue-group HEAD:refs/heads/main && + + git rev-parse HEAD >expect && + git -C dest-c1.git rev-parse refs/heads/main >actual-c1 && + git -C dest-c3.git rev-parse refs/heads/main >actual-c3 && + test_cmp expect actual-c1 && + test_cmp expect actual-c3 && + + # c2 should not have the new commit + git -C dest-c2.git rev-parse refs/heads/main >actual-c2 && + ! test_cmp expect actual-c2 +' + +test_expect_success 'fatal connection error does not stop remaining remotes' ' + for i in f1 f2 f3 + do + git init --bare dest-$i.git || return 1 + done && + git config set remote.f1.url "file://$(pwd)/dest-f1.git" && + git config set remote.f2.url "file://$(pwd)/dest-f2.git" && + git config set remote.f3.url "file://$(pwd)/dest-f3.git" && + git config set remotes.fatal-group "f1 f2 f3" && + + test_tick && + git commit --allow-empty -m "base for fatal test" && + + # initial sync + git push fatal-group HEAD:refs/heads/main && + + # break f2 + git config set remote.f2.url "file:///tmp/does-not-exist-$$" && + + test_tick && + git commit --allow-empty -m "after fatal setup" && + + # overall exit code is non-zero because f2 failed + test_must_fail git push fatal-group HEAD:refs/heads/main && + + git rev-parse HEAD >expect && + + # f1 and f3 should both have the new commit — subprocesses are independent + git -C dest-f1.git rev-parse refs/heads/main >actual-f1 && + test_cmp expect actual-f1 && + git -C dest-f3.git rev-parse refs/heads/main >actual-f3 && + test_cmp expect actual-f3 && + + git config set remote.f2.url "file://$(pwd)/dest-f2.git" +' + +test_done From cdeef283bcf8529fc858cfe7d18a7522294519c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Scharfe?= Date: Tue, 12 May 2026 13:56:00 +0200 Subject: [PATCH 021/106] strbuf: add strbuf_add_uint() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit strbuf_addf() calls vsnprintf(3) underneath, which supports a plethora of formatting options. We can avoid its overhead in basic cases by providing specialized functions like strbuf_addstr() for strings. Add another one, strbuf_add_uint(), for unsigned integers. Prepare the number string in a temporary buffer. Make it big enough for any unsigned integer value: A decimal digit can represent ln(10)/ln(2) ≈ 3.32 bits; dividing the number of bits of uintmax_t by 3.3 and rounding up gives a sufficiently close conservative size estimate. Signed-off-by: René Scharfe Signed-off-by: Junio C Hamano --- strbuf.c | 12 ++++++++++++ strbuf.h | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/strbuf.c b/strbuf.c index 3e04addc22febb..9731ecdc1feb97 100644 --- a/strbuf.c +++ b/strbuf.c @@ -361,6 +361,18 @@ void strbuf_addf(struct strbuf *sb, const char *fmt, ...) va_end(ap); } +void strbuf_add_uint(struct strbuf *sb, uintmax_t value) +{ + char buf[DIV_ROUND_UP(bitsizeof(value) * 10, 33)]; + char *end = buf + sizeof(buf); + char *p = end; + + do + *--p = "0123456789"[value % 10]; + while (value /= 10); + strbuf_add(sb, p, end - p); +} + static void add_lines(struct strbuf *out, const char *prefix, const char *buf, size_t size, diff --git a/strbuf.h b/strbuf.h index 06e284f9cca445..1089ae687bda95 100644 --- a/strbuf.h +++ b/strbuf.h @@ -410,6 +410,12 @@ void strbuf_humanise_rate(struct strbuf *buf, off_t bytes); __attribute__((format (printf,2,3))) void strbuf_addf(struct strbuf *sb, const char *fmt, ...); + +/** + * Add an unsigned decimal number. + */ +void strbuf_add_uint(struct strbuf *sb, uintmax_t value); + /** * Add a formatted string prepended by a comment character and a * blank to the buffer. From 8feb5702163a32384d098e2c9ad3987928f8c447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Scharfe?= Date: Tue, 12 May 2026 13:56:01 +0200 Subject: [PATCH 022/106] cat-file: use strbuf_add_uint() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Speed up printing of objectsize atoms by using the specialized function strbuf_add_uint() instead of the general-purpose function strbuf_addf(): Benchmark 1: ./git_main cat-file --batch-all-objects --batch-check='%(objectsize)' Time (mean ± σ): 751.7 ms ± 1.5 ms [User: 733.5 ms, System: 17.1 ms] Range (min … max): 750.5 ms … 755.0 ms 10 runs Benchmark 2: ./git cat-file --batch-all-objects --batch-check='%(objectsize)' Time (mean ± σ): 720.4 ms ± 0.4 ms [User: 701.9 ms, System: 16.7 ms] Range (min … max): 719.7 ms … 721.2 ms 10 runs Summary ./git cat-file --batch-all-objects --batch-check='%(objectsize)' ran 1.04 ± 0.00 times faster than ./git_main cat-file --batch-all-objects --batch-check='%(objectsize)' Benchmark 1: ./git_main cat-file --batch-all-objects --batch-check='%(objectsize:disk)' Time (mean ± σ): 404.6 ms ± 0.9 ms [User: 397.8 ms, System: 5.7 ms] Range (min … max): 403.3 ms … 405.9 ms 10 runs Benchmark 2: ./git cat-file --batch-all-objects --batch-check='%(objectsize:disk)' Time (mean ± σ): 378.3 ms ± 0.9 ms [User: 371.2 ms, System: 5.9 ms] Range (min … max): 376.8 ms … 380.2 ms 10 runs Summary ./git cat-file --batch-all-objects --batch-check='%(objectsize:disk)' ran 1.07 ± 0.00 times faster than ./git_main cat-file --batch-all-objects --batch-check='%(objectsize:disk)' Signed-off-by: René Scharfe Signed-off-by: Junio C Hamano --- builtin/cat-file.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/cat-file.c b/builtin/cat-file.c index d9fbad535868bb..62160ca9d428eb 100644 --- a/builtin/cat-file.c +++ b/builtin/cat-file.c @@ -330,12 +330,12 @@ static int expand_atom(struct strbuf *sb, const char *atom, int len, if (data->mark_query) data->info.sizep = &data->size; else - strbuf_addf(sb, "%"PRIuMAX , (uintmax_t)data->size); + strbuf_add_uint(sb, data->size); } else if (is_atom("objectsize:disk", atom, len)) { if (data->mark_query) data->info.disk_sizep = &data->disk_size; else - strbuf_addf(sb, "%"PRIuMAX, (uintmax_t)data->disk_size); + strbuf_add_uint(sb, data->disk_size); } else if (is_atom("rest", atom, len)) { if (data->mark_query) data->split_on_whitespace = 1; From f001b4ab3942cbaff4a39662294ee7191e2dbee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Scharfe?= Date: Tue, 12 May 2026 13:56:02 +0200 Subject: [PATCH 023/106] ls-files: use strbuf_add_uint() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Speed up printing of objectsize values by using the specialized function strbuf_add_uint() as well as strbuf_insert() for padding instead of the general-purpose function strbuf_addf(). Here are the numbers I get when listing files in the Linux kernel repo: Benchmark 1: ./git_main -C ../linux ls-files --format='%(objectsize)' Time (mean ± σ): 257.3 ms ± 0.4 ms [User: 197.4 ms, System: 56.7 ms] Range (min … max): 256.7 ms … 258.1 ms 11 runs Benchmark 2: ./git -C ../linux ls-files --format='%(objectsize)' Time (mean ± σ): 253.4 ms ± 0.3 ms [User: 193.6 ms, System: 56.6 ms] Range (min … max): 253.0 ms … 253.8 ms 11 runs Benchmark 3: ./git_main -C ../linux ls-files --format='%(objectsize:padded)' Time (mean ± σ): 257.9 ms ± 0.3 ms [User: 198.0 ms, System: 56.6 ms] Range (min … max): 257.3 ms … 258.5 ms 11 runs Benchmark 4: ./git -C ../linux ls-files --format='%(objectsize:padded)' Time (mean ± σ): 254.6 ms ± 1.0 ms [User: 194.6 ms, System: 56.7 ms] Range (min … max): 253.7 ms … 256.8 ms 11 runs Summary ./git -C ../linux ls-files --format='%(objectsize)' ran 1.00 ± 0.00 times faster than ./git -C ../linux ls-files --format='%(objectsize:padded)' 1.02 ± 0.00 times faster than ./git_main -C ../linux ls-files --format='%(objectsize)' 1.02 ± 0.00 times faster than ./git_main -C ../linux ls-files --format='%(objectsize:padded)' Signed-off-by: René Scharfe Signed-off-by: Junio C Hamano --- builtin/ls-files.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/builtin/ls-files.c b/builtin/ls-files.c index b148607f7a1468..c142ad41562794 100644 --- a/builtin/ls-files.c +++ b/builtin/ls-files.c @@ -250,20 +250,23 @@ static void expand_objectsize(struct repository *repo, struct strbuf *line, const struct object_id *oid, const enum object_type type, unsigned int padded) { + static const char padding[] = " "; + size_t min_len = padded ? strlen(padding) : 0; + size_t orig_len = line->len; + size_t len; + if (type == OBJ_BLOB) { unsigned long size; if (odb_read_object_info(repo->objects, oid, &size) < 0) die(_("could not get object info about '%s'"), oid_to_hex(oid)); - if (padded) - strbuf_addf(line, "%7"PRIuMAX, (uintmax_t)size); - else - strbuf_addf(line, "%"PRIuMAX, (uintmax_t)size); - } else if (padded) { - strbuf_addf(line, "%7s", "-"); + strbuf_add_uint(line, size); } else { strbuf_addstr(line, "-"); } + len = line->len - orig_len; + if (len < min_len) + strbuf_insert(line, orig_len, padding, min_len - len); } static void show_ce_fmt(struct repository *repo, const struct cache_entry *ce, From 4f87748b0d25bdc92b76e453f086204808e8be87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Scharfe?= Date: Tue, 12 May 2026 13:56:03 +0200 Subject: [PATCH 024/106] ls-tree: use strbuf_add_uint() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Speed up printing of objectsize values by using the specialized function strbuf_add_uint() as well as strbuf_insert() for padding instead of the general-purpose function strbuf_addf(). Here are the numbers I get when listing objects in the Linux kernel repo: Benchmark 1: ./git_main -C ../linux ls-tree -r --format='%(objectsize)' HEAD Time (mean ± σ): 294.4 ms ± 0.4 ms [User: 231.5 ms, System: 59.4 ms] Range (min … max): 293.9 ms … 295.0 ms 10 runs Benchmark 2: ./git -C ../linux ls-tree -r --format='%(objectsize)' HEAD Time (mean ± σ): 291.2 ms ± 0.4 ms [User: 227.9 ms, System: 62.1 ms] Range (min … max): 290.6 ms … 292.0 ms 10 runs Benchmark 3: ./git_main -C ../linux ls-tree -r --format='%(objectsize:padded)' HEAD Time (mean ± σ): 295.3 ms ± 0.6 ms [User: 232.0 ms, System: 59.6 ms] Range (min … max): 294.3 ms … 296.3 ms 10 runs Benchmark 4: ./git -C ../linux ls-tree -r --format='%(objectsize:padded)' HEAD Time (mean ± σ): 291.9 ms ± 0.4 ms [User: 228.5 ms, System: 61.5 ms] Range (min … max): 291.2 ms … 292.3 ms 10 runs Summary ./git -C ../linux ls-tree -r --format='%(objectsize)' HEAD ran 1.00 ± 0.00 times faster than ./git -C ../linux ls-tree -r --format='%(objectsize:padded)' HEAD 1.01 ± 0.00 times faster than ./git_main -C ../linux ls-tree -r --format='%(objectsize)' HEAD 1.01 ± 0.00 times faster than ./git_main -C ../linux ls-tree -r --format='%(objectsize:padded)' HEAD Signed-off-by: René Scharfe Signed-off-by: Junio C Hamano --- builtin/ls-tree.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/builtin/ls-tree.c b/builtin/ls-tree.c index 113e4a960dc7dd..57846911ce443f 100644 --- a/builtin/ls-tree.c +++ b/builtin/ls-tree.c @@ -26,20 +26,23 @@ static const char * const ls_tree_usage[] = { static void expand_objectsize(struct strbuf *line, const struct object_id *oid, const enum object_type type, unsigned int padded) { + static const char padding[] = " "; + size_t min_len = padded ? strlen(padding) : 0; + size_t orig_len = line->len; + size_t len; + if (type == OBJ_BLOB) { unsigned long size; if (odb_read_object_info(the_repository->objects, oid, &size) < 0) die(_("could not get object info about '%s'"), oid_to_hex(oid)); - if (padded) - strbuf_addf(line, "%7"PRIuMAX, (uintmax_t)size); - else - strbuf_addf(line, "%"PRIuMAX, (uintmax_t)size); - } else if (padded) { - strbuf_addf(line, "%7s", "-"); + strbuf_add_uint(line, size); } else { strbuf_addstr(line, "-"); } + len = line->len - orig_len; + if (len < min_len) + strbuf_insert(line, orig_len, padding, min_len - len); } struct ls_tree_options { From 63621bcbba81a131794d510bcedfa08d9318219c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Scharfe?= Date: Wed, 13 May 2026 17:49:11 +0200 Subject: [PATCH 025/106] hex: add and use strbuf_add_oid_hex() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a function for adding the full hexadecimal hash value of an object ID to a strbuf. It's thread-safe and slightly more efficient than using strbuf_addstr() with oid_to_hex() because it doesn't have to determine the length of the string or copy it from the intermediate static buffer. Add and apply a semantic patch to use it throughout the code base. I get a tiny speedup for git log showing a single hash per commit: Benchmark 1: ./git_main log --format=%H Time (mean ± σ): 91.2 ms ± 0.7 ms [User: 51.9 ms, System: 38.6 ms] Range (min … max): 89.8 ms … 92.6 ms 31 runs Benchmark 2: ./git log --format=%H Time (mean ± σ): 90.5 ms ± 0.7 ms [User: 51.0 ms, System: 38.8 ms] Range (min … max): 89.2 ms … 92.3 ms 32 runs Summary ./git log --format=%H ran 1.01 ± 0.01 times faster than ./git_main log --format=%H Signed-off-by: René Scharfe Signed-off-by: Junio C Hamano --- bisect.c | 2 +- builtin/bisect.c | 2 +- builtin/cat-file.c | 5 ++--- builtin/replace.c | 2 +- convert.c | 2 +- fsck.c | 2 +- hex.c | 10 ++++++++++ hex.h | 5 +++++ pretty.c | 8 ++++---- refs.c | 2 +- sequencer.c | 4 ++-- shallow.c | 2 +- tools/coccinelle/strbuf.cocci | 6 ++++++ transport-helper.c | 2 +- 14 files changed, 37 insertions(+), 17 deletions(-) diff --git a/bisect.c b/bisect.c index ef17a442e55d2c..e67226a6dcbe69 100644 --- a/bisect.c +++ b/bisect.c @@ -512,7 +512,7 @@ static char *join_oid_array_hex(struct oid_array *array, char delim) int i; for (i = 0; i < array->nr; i++) { - strbuf_addstr(&joined_hexs, oid_to_hex(array->oid + i)); + strbuf_add_oid_hex(&joined_hexs, array->oid + i); if (i + 1 < array->nr) strbuf_addch(&joined_hexs, delim); } diff --git a/builtin/bisect.c b/builtin/bisect.c index 4520e585d0677f..0f679e7af926db 100644 --- a/builtin/bisect.c +++ b/builtin/bisect.c @@ -833,7 +833,7 @@ static enum bisect_error bisect_start(struct bisect_terms *terms, int argc, if (!repo_get_oid(the_repository, head, &head_oid) && !starts_with(head, "refs/heads/")) { strbuf_reset(&start_head); - strbuf_addstr(&start_head, oid_to_hex(&head_oid)); + strbuf_add_oid_hex(&start_head, &head_oid); } else if (!repo_get_oid(the_repository, head, &head_oid) && skip_prefix(head, "refs/heads/", &head)) { strbuf_addstr(&start_head, head); diff --git a/builtin/cat-file.c b/builtin/cat-file.c index d9fbad535868bb..f015e5f415bc3f 100644 --- a/builtin/cat-file.c +++ b/builtin/cat-file.c @@ -320,7 +320,7 @@ static int expand_atom(struct strbuf *sb, const char *atom, int len, { if (is_atom("objectname", atom, len)) { if (!data->mark_query) - strbuf_addstr(sb, oid_to_hex(&data->oid)); + strbuf_add_oid_hex(sb, &data->oid); } else if (is_atom("objecttype", atom, len)) { if (data->mark_query) data->info.typep = &data->type; @@ -345,8 +345,7 @@ static int expand_atom(struct strbuf *sb, const char *atom, int len, if (data->mark_query) data->info.delta_base_oid = &data->delta_base_oid; else - strbuf_addstr(sb, - oid_to_hex(&data->delta_base_oid)); + strbuf_add_oid_hex(sb, &data->delta_base_oid); } else if (is_atom("objectmode", atom, len)) { if (!data->mark_query && !(S_IFINVALID == data->mode)) strbuf_addf(sb, "%06o", data->mode); diff --git a/builtin/replace.c b/builtin/replace.c index 4c62c5ab58bd0a..aed6b2c8debf86 100644 --- a/builtin/replace.c +++ b/builtin/replace.c @@ -127,7 +127,7 @@ static int for_each_replace_name(const char **argv, each_replace_name_fn fn) } strbuf_setlen(&ref, base_len); - strbuf_addstr(&ref, oid_to_hex(&oid)); + strbuf_add_oid_hex(&ref, &oid); full_hex = ref.buf + base_len; if (refs_read_ref(get_main_ref_store(the_repository), ref.buf, &oid)) { diff --git a/convert.c b/convert.c index eae36c8a5936f4..036506842c3d41 100644 --- a/convert.c +++ b/convert.c @@ -1239,7 +1239,7 @@ static int ident_to_worktree(const char *src, size_t len, /* step 4: substitute */ strbuf_addstr(buf, "Id: "); - strbuf_addstr(buf, oid_to_hex(&oid)); + strbuf_add_oid_hex(buf, &oid); strbuf_addstr(buf, " $"); } strbuf_add(buf, src, len); diff --git a/fsck.c b/fsck.c index b72200c352d663..b4ffee6a043474 100644 --- a/fsck.c +++ b/fsck.c @@ -344,7 +344,7 @@ const char *fsck_describe_object(struct fsck_options *options, buf = bufs + b; b = (b + 1) % ARRAY_SIZE(bufs); strbuf_reset(buf); - strbuf_addstr(buf, oid_to_hex(oid)); + strbuf_add_oid_hex(buf, oid); if (name) strbuf_addf(buf, " (%s)", name); diff --git a/hex.c b/hex.c index bc756722ca623b..f02832140d2d43 100644 --- a/hex.c +++ b/hex.c @@ -3,6 +3,7 @@ #include "git-compat-util.h" #include "hash.h" #include "hex.h" +#include "strbuf.h" static int get_hash_hex_algop(const char *hex, unsigned char *hash, const struct git_hash_algo *algop) @@ -122,3 +123,12 @@ char *oid_to_hex(const struct object_id *oid) { return hash_to_hex_algop(oid->hash, &hash_algos[oid->algo]); } + +void strbuf_add_oid_hex(struct strbuf *sb, const struct object_id *oid) +{ + const struct git_hash_algo *algop = oid->algo ? + &hash_algos[oid->algo] : the_hash_algo; + strbuf_grow(sb, algop->hexsz); + hash_to_hex_algop_r(sb->buf + sb->len, oid->hash, algop); + strbuf_setlen(sb, sb->len + algop->hexsz); +} diff --git a/hex.h b/hex.h index 1e9a65d83a4f6b..f15c7e22201cea 100644 --- a/hex.h +++ b/hex.h @@ -33,6 +33,11 @@ char *oid_to_hex_r(char *out, const struct object_id *oid); char *hash_to_hex_algop(const unsigned char *hash, const struct git_hash_algo *); /* static buffer result! */ char *oid_to_hex(const struct object_id *oid); /* same static buffer */ +struct strbuf; + +/* Apply oid_to_hex_r() to a strbuf to append the hexadecimal hash. */ +void strbuf_add_oid_hex(struct strbuf *sb, const struct object_id *oid); + /* * Parse a 40-character hexadecimal object ID starting from hex, updating the * pointer specified by end when parsing stops. The resulting object ID is diff --git a/pretty.c b/pretty.c index 814803980b8d1a..268422394648fe 100644 --- a/pretty.c +++ b/pretty.c @@ -662,7 +662,7 @@ static void add_merge_info(const struct pretty_print_context *pp, if (pp->abbrev) strbuf_add_unique_abbrev(sb, oidp, pp->abbrev); else - strbuf_addstr(sb, oid_to_hex(oidp)); + strbuf_add_oid_hex(sb, oidp); parent = parent->next; } strbuf_addch(sb, '\n'); @@ -1567,7 +1567,7 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */ switch (placeholder[0]) { case 'H': /* commit hash */ strbuf_addstr(sb, diff_get_color(c->auto_color, DIFF_COMMIT)); - strbuf_addstr(sb, oid_to_hex(&commit->object.oid)); + strbuf_add_oid_hex(sb, &commit->object.oid); strbuf_addstr(sb, diff_get_color(c->auto_color, DIFF_RESET)); return 1; case 'h': /* abbreviated commit hash */ @@ -1577,7 +1577,7 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */ strbuf_addstr(sb, diff_get_color(c->auto_color, DIFF_RESET)); return 1; case 'T': /* tree hash */ - strbuf_addstr(sb, oid_to_hex(get_commit_tree_oid(commit))); + strbuf_add_oid_hex(sb, get_commit_tree_oid(commit)); return 1; case 't': /* abbreviated tree hash */ strbuf_add_unique_abbrev(sb, @@ -1588,7 +1588,7 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */ for (p = commit->parents; p; p = p->next) { if (p != commit->parents) strbuf_addch(sb, ' '); - strbuf_addstr(sb, oid_to_hex(&p->item->object.oid)); + strbuf_add_oid_hex(sb, &p->item->object.oid); } return 1; case 'p': /* abbreviated parent hashes */ diff --git a/refs.c b/refs.c index bfcb9c7ac3d38c..d5b968c28ef615 100644 --- a/refs.c +++ b/refs.c @@ -2498,7 +2498,7 @@ int refs_update_symref_extended(struct ref_store *refs, const char *ref, if (referent && refs_read_symbolic_ref(refs, ref, referent) == NOT_A_SYMREF) { struct object_id oid; if (!refs_read_ref(refs, ref, &oid)) { - strbuf_addstr(referent, oid_to_hex(&oid)); + strbuf_add_oid_hex(referent, &oid); ret = NOT_A_SYMREF; } } diff --git a/sequencer.c b/sequencer.c index b7d8dca47f4a58..b4df04b6724206 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2223,7 +2223,7 @@ static void refer_to_commit(struct repository *r, struct strbuf *msgbuf, repo_format_commit_message(r, commit, "%h (%s, %ad)", msgbuf, &ctx); } else { - strbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid)); + strbuf_add_oid_hex(msgbuf, &commit->object.oid); } } @@ -2395,7 +2395,7 @@ static int do_pick_commit(struct repository *r, if (!has_conforming_footer(&ctx->message, NULL, 0)) strbuf_addch(&ctx->message, '\n'); strbuf_addstr(&ctx->message, cherry_picked_prefix); - strbuf_addstr(&ctx->message, oid_to_hex(&commit->object.oid)); + strbuf_add_oid_hex(&ctx->message, &commit->object.oid); strbuf_addstr(&ctx->message, ")\n"); } if (!is_fixup(command)) diff --git a/shallow.c b/shallow.c index a8ad92e303d24d..b4b4e2e32a7600 100644 --- a/shallow.c +++ b/shallow.c @@ -395,7 +395,7 @@ static int write_shallow_commits_1(struct strbuf *out, int use_pack_protocol, if (!extra) return data.count; for (size_t i = 0; i < extra->nr; i++) { - strbuf_addstr(out, oid_to_hex(extra->oid + i)); + strbuf_add_oid_hex(out, extra->oid + i); strbuf_addch(out, '\n'); data.count++; } diff --git a/tools/coccinelle/strbuf.cocci b/tools/coccinelle/strbuf.cocci index f5861283297acd..667903d1d48ad8 100644 --- a/tools/coccinelle/strbuf.cocci +++ b/tools/coccinelle/strbuf.cocci @@ -78,3 +78,9 @@ struct strbuf SB; @@ - SB.buf ? SB.buf : "" + SB.buf + +@@ +expression SB, OID; +@@ +- strbuf_addstr(SB, oid_to_hex(OID)) ++ strbuf_add_oid_hex(SB, OID) diff --git a/transport-helper.c b/transport-helper.c index 4e5d1d914fb12a..145a0cd7e6143a 100644 --- a/transport-helper.c +++ b/transport-helper.c @@ -1053,7 +1053,7 @@ static int push_refs_with_push(struct transport *transport, if (ref->peer_ref) strbuf_addstr(&buf, ref->peer_ref->name); else - strbuf_addstr(&buf, oid_to_hex(&ref->new_oid)); + strbuf_add_oid_hex(&buf, &ref->new_oid); } strbuf_addch(&buf, ':'); strbuf_addstr(&buf, ref->name); From 74216ffe0aa02309e1fc510c0056ec6fd523898c Mon Sep 17 00:00:00 2001 From: Greg Hurrell Date: Thu, 21 May 2026 13:45:09 +0000 Subject: [PATCH 026/106] git-jump: pick a mode automatically when invoked without arguments When `git jump` is invoked with no positional arguments (and no arguments after `--stdout`) it currently prints usage and exits with status 1. But there are two situations where we can usefully infer the most valuable and likely mode that a user would want to use, and select it automatically: 1. When there are unmerged paths in the index, the user likely wants `git jump merge`. 2. When the working tree has unstaged changes, the user likely wants `git jump diff`. In this commit we teach `git jump` a new "auto" mode which detects these cases and dispatches to the corresponding mode automatically. The user can either explicitly spell out `git jump auto`, or just leave it at `git jump` (because "auto" is the default). If none of the interesting cases listed above applies, then auto mode falls back to the existing usage-and-exit behavior. Signed-off-by: Greg Hurrell Signed-off-by: Junio C Hamano --- contrib/git-jump/README | 12 ++++++++++++ contrib/git-jump/git-jump | 26 +++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/contrib/git-jump/README b/contrib/git-jump/README index 3211841305fcb3..aabec4a756e9d4 100644 --- a/contrib/git-jump/README +++ b/contrib/git-jump/README @@ -75,8 +75,20 @@ git jump grep foo_bar # arbitrary grep options git jump grep -i foo_bar +# jump to places with conflict markers or whitespace errors +# (as reported by `git diff --check`) +git jump ws + # use the silver searcher for git jump grep git config jump.grepCmd "ag --column" + +# pick a mode automatically: "merge" if there are unmerged paths, +# "diff" if the worktree has unstaged changes, "ws" if there are +# whitespace problems; otherwise show usage +git jump auto + +# with no explicit mode and no args, same as "auto" +git jump -------------------------------------------------- You can use the optional argument '--stdout' to print the listing to diff --git a/contrib/git-jump/git-jump b/contrib/git-jump/git-jump index 8d1d5d79a69854..79286d811210e3 100755 --- a/contrib/git-jump/git-jump +++ b/contrib/git-jump/git-jump @@ -3,9 +3,11 @@ usage() { cat <<\EOF usage: git jump [--stdout] [] + or: git jump [--stdout] Jump to interesting elements in an editor. -The parameter is one of: +The parameter is one of the following. +With no and no , it defaults to "auto". diff: elements are diff hunks. Arguments are given to diff. @@ -16,6 +18,10 @@ grep: elements are grep hits. Arguments are given to git grep or, if ws: elements are whitespace errors. Arguments are given to diff --check. +auto: select one of the other modes based on worktree state; + "merge" if there are unmerged paths, "diff" if there are + unstaged changes, "ws" if there are whitespace errors. + If the optional argument `--stdout` is given, print the quickfix lines to standard output instead of feeding it to the editor. EOF @@ -82,6 +88,21 @@ mode_ws() { git diff --check "$@" } +mode_auto() { + if test "$(git rev-parse --is-inside-work-tree 2>/dev/null)" != "true"; then + usage >&2 + exit 1 + fi + if test -n "$(git ls-files -u "$@")"; then + mode_merge "$@" + elif ! git diff --quiet "$@"; then + mode_diff "$@" + else + usage >&2 + exit 1 + fi +} + use_stdout= while test $# -gt 0; do case "$1" in @@ -99,8 +120,7 @@ while test $# -gt 0; do shift done if test $# -lt 1; then - usage >&2 - exit 1 + set -- auto fi mode=$1; shift type "mode_$mode" >/dev/null 2>&1 || { usage >&2; exit 1; } From 48513e05e2f226c85a9b88893630c8ae28409772 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Fri, 22 May 2026 23:12:25 +0000 Subject: [PATCH 027/106] stash: reuse cached index entries in --patch temporary index `git stash -p` prepares the interactive selection by creating a temporary index at HEAD, switching `GIT_INDEX_FILE` to it, and then running the `add -p` machinery. That temporary index was created by running `git read-tree HEAD`. The resulting index had no useful cached stat data or fsmonitor-valid bits from the real index. When `run_add_p()` refreshed that temporary index before showing the first prompt, it could end up lstat(2)-ing every tracked file, even in a repository where `git diff` and `git restore -p` can use fsmonitor to avoid that work. Create the temporary index in-process instead. Use `unpack_trees()` to reset the real index contents to HEAD while writing the result to the temporary index path. For paths whose index entries already match HEAD, `oneway_merge()` reuses the existing cache entries, preserving their cached stat data and `CE_FSMONITOR_VALID` state. This makes the refresh performed by `run_add_p()` behave like the one used by `git restore -p`: unchanged paths can be skipped via fsmonitor instead of being scanned again. In a 206k file repository with `core.fsmonitor` enabled and a one-line change in one file, time to first prompt dropped from 34.774 seconds to 0.659 seconds. The new perf test file demonstrates similar improvements, with maen times for without- and with-fsmonitor cases dropping from 6.90 and 6.83 seconds to 0.55 and 0.28 seconds, respectively. Signed-off-by: Adam Johnson Signed-off-by: Junio C Hamano --- builtin/stash.c | 70 +++++++++++++++++++++++++++++++++---- t/perf/p3904-stash-patch.sh | 43 +++++++++++++++++++++++ 2 files changed, 107 insertions(+), 6 deletions(-) create mode 100755 t/perf/p3904-stash-patch.sh diff --git a/builtin/stash.c b/builtin/stash.c index 0d27b2fb1fcb67..640be770c5d02a 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -372,6 +372,56 @@ static int reset_tree(struct object_id *i_tree, int update, int reset) return 0; } +static int create_index_from_tree(const struct object_id *tree_id, + const char *index_path) +{ + int nr_trees = 1; + int ret = 0; + struct unpack_trees_options opts; + struct tree_desc t[MAX_UNPACK_TREES]; + struct tree *tree; + struct index_state dst_istate = INDEX_STATE_INIT(the_repository); + struct lock_file lock_file = LOCK_INIT; + + repo_read_index_preload(the_repository, NULL, 0); + refresh_index(the_repository->index, REFRESH_QUIET, NULL, NULL, NULL); + + hold_lock_file_for_update(&lock_file, index_path, LOCK_DIE_ON_ERROR); + + memset(&opts, 0, sizeof(opts)); + + tree = repo_parse_tree_indirect(the_repository, tree_id); + if (!tree || repo_parse_tree(the_repository, tree)) { + ret = -1; + goto done; + } + + init_tree_desc(t, &tree->object.oid, tree->buffer, tree->size); + + opts.head_idx = 1; + opts.src_index = the_repository->index; + opts.dst_index = &dst_istate; + opts.merge = 1; + opts.reset = UNPACK_RESET_PROTECT_UNTRACKED; + opts.fn = oneway_merge; + + if (unpack_trees(nr_trees, t, &opts)) { + ret = -1; + goto done; + } + + if (write_locked_index(&dst_istate, &lock_file, COMMIT_LOCK)) { + ret = error(_("unable to write new index file")); + goto done; + } + +done: + release_index(&dst_istate); + if (ret) + rollback_lock_file(&lock_file); + return ret; +} + static int diff_tree_binary(struct strbuf *out, struct object_id *w_commit) { struct child_process cp = CHILD_PROCESS_INIT; @@ -1309,18 +1359,26 @@ static int stash_patch(struct stash_info *info, const struct pathspec *ps, struct interactive_options *interactive_opts) { int ret = 0; - struct child_process cp_read_tree = CHILD_PROCESS_INIT; struct child_process cp_diff_tree = CHILD_PROCESS_INIT; + struct commit *head_commit; + const struct object_id *head_tree; struct index_state istate = INDEX_STATE_INIT(the_repository); char *old_index_env = NULL, *old_repo_index_file; remove_path(stash_index_path.buf); - cp_read_tree.git_cmd = 1; - strvec_pushl(&cp_read_tree.args, "read-tree", "HEAD", NULL); - strvec_pushf(&cp_read_tree.env, "GIT_INDEX_FILE=%s", - stash_index_path.buf); - if (run_command(&cp_read_tree)) { + head_commit = lookup_commit(the_repository, &info->b_commit); + if (!head_commit || repo_parse_commit(the_repository, head_commit)) { + ret = -1; + goto done; + } + head_tree = get_commit_tree_oid(head_commit); + if (!head_tree) { + ret = -1; + goto done; + } + + if (create_index_from_tree(head_tree, stash_index_path.buf)) { ret = -1; goto done; } diff --git a/t/perf/p3904-stash-patch.sh b/t/perf/p3904-stash-patch.sh new file mode 100755 index 00000000000000..4cfce638bed6f4 --- /dev/null +++ b/t/perf/p3904-stash-patch.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +test_description="Performance tests for git stash -p" + +. ./perf-lib.sh + +test_perf_fresh_repo + +test_expect_success "setup" ' + mkdir files && + test_seq 1 100000 | while read i; do + echo "content $i" >files/$i.txt || return 1 + done && + git add files/ && + git commit -q -m "add tracked files" && + echo modified >files/1.txt +' + +test_perf "stash -p, no fsmonitor" \ + --setup 'echo modified >files/1.txt' ' + printf "q\n" | git stash -p >/dev/null 2>&1 || true +' + +if test_have_prereq FSMONITOR_DAEMON +then + test_expect_success "enable builtin fsmonitor" ' + git config core.fsmonitor true && + git fsmonitor--daemon start && + git update-index --fsmonitor && + git status >/dev/null 2>&1 + ' + + test_perf "stash -p, builtin fsmonitor" \ + --setup 'echo modified >files/1.txt && git status >/dev/null 2>&1' ' + printf "q\n" | git stash -p >/dev/null 2>&1 || true + ' + + test_expect_success "stop builtin fsmonitor" ' + git fsmonitor--daemon stop + ' +fi + +test_done From 7c9b38d267129625adeced9f66140e802c345261 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Mon, 25 May 2026 11:58:48 +0900 Subject: [PATCH 028/106] SubmittingPatches: proactively monitor GHCI pages Even those contributors who do not come from GGG and do not first push their changes to their repositories on GitHub with CI enabled, can still monitor the CI runs triggered by integration of their topic to 'seen' and other branches to notice a breakage their topic caused to the system. Encourage them to help the project by keeping an eye on these CI runs. Signed-off-by: Junio C Hamano --- Documentation/SubmittingPatches | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches index e270ccbe85b087..ad2dce1998120f 100644 --- a/Documentation/SubmittingPatches +++ b/Documentation/SubmittingPatches @@ -792,6 +792,17 @@ relevant for debugging. Then fix the problem and push your fix to your GitHub fork. This will trigger a new CI build to ensure all tests pass. +Even if you do not use GitHub CI to test your changes, pay close +attention to new failures on the branches when the maintainer pushes +out after your topic gets merged to the 'seen' branch to make sure +that your topic is not breaking the CI, and retract your breaking +topic quickly while you fix the breakage you caused. + +To see maintainer's push, keep an eye on this page: + + `https://github.com/git/git/actions/workflows/main.yml?query=event%3Apush+actor%3Agitster` + + [[mua]] == MUA specific hints From 50cd5219d2b63f4896a3d142f83fadf8e47a6c3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Avila?= Date: Mon, 25 May 2026 10:28:22 +0000 Subject: [PATCH 029/106] doc: convert git-bisect to synopsis style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert Documentation/git-bisect.adoc to the modern synopsis style. - Replace [verse] with [synopsis] in the SYNOPSIS block - Remove single quotes around command names in the synopsis - Use backticks for inline commands, options, refs, and special values - Apply [synopsis] attribute to in-body command-form code blocks - Format OPTIONS entries with backtick-quoted terms and direct - Add synopsis-style formatting to listing blocks - Format man page references as `command`(N) Signed-off-by: Jean-Noël Avila Signed-off-by: Junio C Hamano --- Documentation/asciidoc.conf.in | 6 +++ Documentation/git-bisect.adoc | 90 ++++++++++++++++------------------ 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/Documentation/asciidoc.conf.in b/Documentation/asciidoc.conf.in index 31b883a72c5739..93c63b284a9669 100644 --- a/Documentation/asciidoc.conf.in +++ b/Documentation/asciidoc.conf.in @@ -84,6 +84,9 @@ ifdef::doctype-manpage[] [blockdef-open] synopsis-style=template="verseparagraph",filter="sed 's!…\\(\\]\\|$\\)!\\0!g;s!\\([\\[ |()]\\|^\\|\\]\\|>\\)\\([-=a-zA-Z0-9:+@,\\/_^\\$.\\\\\\*]\\+\\|…\\)!\\1\\2!g;s!<[-a-zA-Z0-9.]\\+>!\\0!g'" +[blockdef-listing] +synopsis-style=template="verseparagraph",filter="sed 's!…\\(\\]\\|$\\)!\\0!g;s!\\([\\[ |()]\\|^\\|\\]\\|>\\)\\([-=a-zA-Z0-9:+@,\\/_^\\$.\\\\\\*]\\+\\|…\\)!\\1\\2!g;s!<[-a-zA-Z0-9.]\\+>!\\0!g'" + [paradef-default] synopsis-style=template="verseparagraph",filter="sed 's!…\\(\\]\\|$\\)!\\0!g;s!\\([\\[ |()]\\|^\\|\\]\\|>\\)\\([-=a-zA-Z0-9:+@,\\/_^\\$.\\\\\\*]\\+\\|…\\)!\\1\\2!g;s!<[-a-zA-Z0-9.]\\+>!\\0!g'" endif::doctype-manpage[] @@ -93,6 +96,9 @@ ifdef::backend-xhtml11[] [blockdef-open] synopsis-style=template="verseparagraph",filter="sed 's!…\\(\\]\\|$\\)!\\0!g;s!\\([\\[ |()]\\|^\\|\\]\\|>\\)\\([-=a-zA-Z0-9:+@,\\/_^\\$.\\\\\\*]\\+\\|…\\)!\\1\\2!g;s!<[-a-zA-Z0-9.]\\+>!\\0!g'" +[blockdef-listing] +synopsis-style=template="verseparagraph",filter="sed 's!…\\(\\]\\|$\\)!\\0!g;s!\\([\\[ |()]\\|^\\|\\]\\|>\\)\\([-=a-zA-Z0-9:+@,\\/_^\\$.\\\\\\*]\\+\\|…\\)!\\1\\2!g;s!<[-a-zA-Z0-9.]\\+>!\\0!g'" + [paradef-default] synopsis-style=template="verseparagraph",filter="sed 's!…\\(\\]\\|$\\)!\\0!g;s!\\([\\[ |()]\\|^\\|\\]\\|>\\)\\([-=a-zA-Z0-9:+@,\\/_^\\$.\\\\\\*]\\+\\|…\\)!\\1\\2!g;s!<[-a-zA-Z0-9.]\\+>!\\0!g'" endif::backend-xhtml11[] diff --git a/Documentation/git-bisect.adoc b/Documentation/git-bisect.adoc index b0078dda0eb050..4765d3b9691a10 100644 --- a/Documentation/git-bisect.adoc +++ b/Documentation/git-bisect.adoc @@ -8,20 +8,20 @@ git-bisect - Use binary search to find the commit that introduced a bug SYNOPSIS -------- -[verse] -'git bisect' start [--term-(bad|new)= --term-(good|old)=] - [--no-checkout] [--first-parent] [ [...]] [--] [...] -'git bisect' (bad|new|) [] -'git bisect' (good|old|) [...] -'git bisect' terms [--term-(good|old) | --term-(bad|new)] -'git bisect' skip [(|)...] -'git bisect' next -'git bisect' reset [] -'git bisect' (visualize|view) -'git bisect' replay -'git bisect' log -'git bisect' run [...] -'git bisect' help +[synopsis] +git bisect start [--term-(bad|new)= --term-(good|old)=] + [--no-checkout] [--first-parent] [ [...]] [--] [...] +git bisect (bad|new|) [] +git bisect (good|old|) [...] +git bisect terms [--term-(good|old) | --term-(bad|new)] +git bisect skip [(|)...] +git bisect next +git bisect reset [] +git bisect (visualize|view) +git bisect replay +git bisect log +git bisect run [...] +git bisect help DESCRIPTION ----------- @@ -94,7 +94,7 @@ Bisect reset ~~~~~~~~~~~~ After a bisect session, to clean up the bisection state and return to -the original HEAD, issue the following command: +the original `HEAD`, issue the following command: ------------------------------------------------ $ git bisect reset @@ -107,9 +107,8 @@ that, as it cleans up the old bisection state.) With an optional argument, you can return to a different commit instead: ------------------------------------------------- +[synopsis] $ git bisect reset ------------------------------------------------- For example, `git bisect reset bisect/bad` will check out the first bad revision, while `git bisect reset HEAD` will leave you on the @@ -143,23 +142,20 @@ To use "old" and "new" instead of "good" and bad, you must run `git bisect start` without commits as argument and then run the following commands to add the commits: ------------------------------------------------- +[synopsis] git bisect old [] ------------------------------------------------- to indicate that a commit was before the sought change, or ------------------------------------------------- +[synopsis] git bisect new [...] ------------------------------------------------- to indicate that it was after. To get a reminder of the currently used terms, use ------------------------------------------------- +[synopsis] git bisect terms ------------------------------------------------- You can get just the old term with `git bisect terms --term-old` or `git bisect terms --term-good`; `git bisect terms --term-new` @@ -171,9 +167,8 @@ If you would like to use your own terms instead of "bad"/"good" or subcommands like `reset`, `start`, ...) by starting the bisection using ------------------------------------------------- +[synopsis] git bisect start --term-old --term-new ------------------------------------------------- For example, if you are looking for a commit that introduced a performance regression, you might use @@ -194,7 +189,7 @@ of `git bisect good` and `git bisect bad` to mark commits. Bisect visualize/view ~~~~~~~~~~~~~~~~~~~~~ -To see the currently remaining suspects in 'gitk', issue the following +To see the currently remaining suspects in `gitk`, issue the following command during the bisection process (the subcommand `view` can be used as an alternative to `visualize`): @@ -203,12 +198,13 @@ $ git bisect visualize ------------ Git detects a graphical environment through various environment variables: -`DISPLAY`, which is set in X Window System environments on Unix systems. -`SESSIONNAME`, which is set under Cygwin in interactive desktop sessions. -`MSYSTEM`, which is set under Msys2 and Git for Windows. -`SECURITYSESSIONID`, which may be set on macOS in interactive desktop sessions. -If none of these environment variables is set, 'git log' is used instead. +`DISPLAY`:: which is set in X Window System environments on Unix systems. +`SESSIONNAME`:: which is set under Cygwin in interactive desktop sessions. +`MSYSTEM`:: which is set under Msys2 and Git for Windows. +`SECURITYSESSIONID`:: which may be set on macOS in interactive desktop sessions. + +If none of these environment variables is set, `git log` is used instead. You can also give command-line options such as `-p` and `--stat`. ------------ @@ -342,8 +338,8 @@ code between 1 and 127 (inclusive), except 125, if the current source code is bad/new. Any other exit code will abort the bisect process. It should be noted -that a program that terminates via `exit(-1)` leaves $? = 255, (see the -exit(3) manual page), as the value is chopped with `& 0377`. +that a program that terminates via `exit(-1)` leaves `$?` = 255, (see the +`exit`(3) manual page), as the value is chopped with `& 0377`. The special exit code 125 should be used when the current source code cannot be tested. If the script exits with this code, the current @@ -355,12 +351,12 @@ details do not matter, as they are normal errors in the script, as far as `bisect run` is concerned). You may often find that during a bisect session you want to have -temporary modifications (e.g. s/#define DEBUG 0/#define DEBUG 1/ in a +temporary modifications (e.g. `s/#define DEBUG 0/#define DEBUG 1/` in a header file, or "revision that does not have this commit needs this patch applied to work around another problem this bisection is not interested in") applied to the revision being tested. -To cope with such a situation, after the inner 'git bisect' finds the +To cope with such a situation, after the inner `git bisect` finds the next revision to test, the script can apply the patch before compiling, run the real test, and afterwards decide if the revision (possibly with the needed patch) passed the test and then @@ -370,20 +366,18 @@ determine the eventual outcome of the bisect session. OPTIONS ------- ---no-checkout:: -+ -Do not checkout the new working tree at each iteration of the bisection -process. Instead just update the reference named `BISECT_HEAD` to make -it point to the commit that should be tested. +`--no-checkout`:: + Do not checkout the new working tree at each iteration of the bisection + process. Instead just update the reference named `BISECT_HEAD` to make + it point to the commit that should be tested. + This option may be useful when the test you would perform in each step does not require a checked out tree. + If the repository is bare, `--no-checkout` is assumed. ---first-parent:: -+ -Follow only the first parent commit upon seeing a merge commit. +`--first-parent`:: + Follow only the first parent commit upon seeing a merge commit. + In detecting regressions introduced through the merging of a branch, the merge commit will be identified as introduction of the bug and its ancestors will be @@ -395,7 +389,7 @@ branch contained broken or non-buildable commits, but the merge itself was OK. EXAMPLES -------- -* Automatically bisect a broken build between v1.2 and HEAD: +* Automatically bisect a broken build between v1.2 and `HEAD`: + ------------ $ git bisect start HEAD v1.2 -- # HEAD is bad, v1.2 is good @@ -403,7 +397,7 @@ $ git bisect run make # "make" builds the app $ git bisect reset # quit the bisect session ------------ -* Automatically bisect a test failure between origin and HEAD: +* Automatically bisect a test failure between origin and `HEAD`: + ------------ $ git bisect start HEAD origin -- # HEAD is bad, origin is good @@ -430,7 +424,7 @@ and `exit 1` otherwise. + It is safer if both `test.sh` and `check_test_case.sh` are outside the repository to prevent interactions between the bisect, -make and test processes and the scripts. +`make` and test processes and the scripts. * Automatically bisect with temporary modifications (hot-fix): + @@ -491,9 +485,9 @@ $ git bisect run sh -c ' $ git bisect reset # quit the bisect session ------------ + -In this case, when 'git bisect run' finishes, bisect/bad will refer to a commit that +In this case, when `git bisect run` finishes, `bisect/bad` will refer to a commit that has at least one parent whose reachable graph is fully traversable in the sense -required by 'git pack objects'. +required by `git pack-objects`. * Look for a fix instead of a regression in the code + From ed31e2872a7306f52de1e2b9ab0065b9d91f3338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Avila?= Date: Mon, 25 May 2026 10:28:23 +0000 Subject: [PATCH 030/106] doc: git bisect: clarify the usage of the synopsis vs actual command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The difference between a synopsis and an actual command is that the synopsis is a more abstract representation of the command, which may include placeholders for arguments and options. The actual command is the specific instance of the command with all the arguments and options filled in. The formatting of an actual command is a code block, with the command prefixed by a dollar sign ($) to indicate that it is a command to be run in the terminal. It can also include comments with a hash sign (#) to explain the command or provide additional information, just like in a regular terminal session. Signed-off-by: Jean-Noël Avila Signed-off-by: Junio C Hamano --- Documentation/git-bisect.adoc | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/Documentation/git-bisect.adoc b/Documentation/git-bisect.adoc index 4765d3b9691a10..d2115b29905f41 100644 --- a/Documentation/git-bisect.adoc +++ b/Documentation/git-bisect.adoc @@ -96,9 +96,8 @@ Bisect reset After a bisect session, to clean up the bisection state and return to the original `HEAD`, issue the following command: ------------------------------------------------- -$ git bisect reset ------------------------------------------------- +[synopsis] +git bisect reset By default, this will return your tree to the commit that was checked out before `git bisect start`. (A new `git bisect start` will also do @@ -108,7 +107,8 @@ With an optional argument, you can return to a different commit instead: [synopsis] -$ git bisect reset +git bisect reset + For example, `git bisect reset bisect/bad` will check out the first bad revision, while `git bisect reset HEAD` will leave you on the @@ -174,13 +174,13 @@ For example, if you are looking for a commit that introduced a performance regression, you might use ------------------------------------------------ -git bisect start --term-old fast --term-new slow +$ git bisect start --term-old fast --term-new slow ------------------------------------------------ Or if you are looking for the commit that fixed a bug, you might use ------------------------------------------------ -git bisect start --term-new fixed --term-old broken +$ git bisect start --term-new fixed --term-old broken ------------------------------------------------ Then, use `git bisect ` and `git bisect ` instead @@ -328,11 +328,10 @@ Bisect run If you have a script that can tell if the current source code is good or bad, you can bisect by issuing the command: ------------- -$ git bisect run my_script arguments ------------- +[synopsis] +git bisect run [...] -Note that the script (`my_script` in the above example) should exit +Note that __ run with __ should exit with code 0 if the current source code is good/old, and exit with a code between 1 and 127 (inclusive), except 125, if the current source code is bad/new. From 25d5d60958f1486cb8439863062f66ffef605f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Avila?= Date: Mon, 25 May 2026 10:28:24 +0000 Subject: [PATCH 031/106] doc: convert git-grep synopsis and options to new style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert git-grep.adoc from [verse]/single-quote style to the modern synopsis-block style: - Replace [verse] with [synopsis] in SYNOPSIS block - Change 'git grep' to git grep (no single quotes) - Backtick-quote all OPTIONS terms - Convert inline man page refs: grep(1) -> `grep`(1) - Convert inline command refs: 'git diff' -> `git diff` - Convert prose placeholders: -> __ Signed-off-by: Jean-Noël Avila Signed-off-by: Junio C Hamano --- Documentation/config/grep.adoc | 36 +++--- Documentation/git-grep.adoc | 196 ++++++++++++++++----------------- 2 files changed, 116 insertions(+), 116 deletions(-) diff --git a/Documentation/config/grep.adoc b/Documentation/config/grep.adoc index 10041f27b0c8e2..83d4b76dd351e1 100644 --- a/Documentation/config/grep.adoc +++ b/Documentation/config/grep.adoc @@ -1,28 +1,28 @@ -grep.lineNumber:: - If set to true, enable `-n` option by default. +`grep.lineNumber`:: + If set to `true`, enable `-n` option by default. -grep.column:: - If set to true, enable the `--column` option by default. +`grep.column`:: + If set to `true`, enable the `--column` option by default. -grep.patternType:: - Set the default matching behavior. Using a value of 'basic', 'extended', - 'fixed', or 'perl' will enable the `--basic-regexp`, `--extended-regexp`, +`grep.patternType`:: + Set the default matching behavior. Using a value of `basic`, `extended`, + `fixed`, or `perl` will enable the `--basic-regexp`, `--extended-regexp`, `--fixed-strings`, or `--perl-regexp` option accordingly, while the - value 'default' will use the `grep.extendedRegexp` option to choose - between 'basic' and 'extended'. + value `default` will use the `grep.extendedRegexp` option to choose + between `basic` and `extended`. -grep.extendedRegexp:: - If set to true, enable `--extended-regexp` option by default. This +`grep.extendedRegexp`:: + If set to `true`, enable `--extended-regexp` option by default. This option is ignored when the `grep.patternType` option is set to a value - other than 'default'. + other than `default`. -grep.threads:: +`grep.threads`:: Number of grep worker threads to use. If unset (or set to 0), Git will use as many threads as the number of logical cores available. -grep.fullName:: - If set to true, enable `--full-name` option by default. +`grep.fullName`:: + If set to `true`, enable `--full-name` option by default. -grep.fallbackToNoIndex:: - If set to true, fall back to `git grep --no-index` if `git grep` - is executed outside of a git repository. Defaults to false. +`grep.fallbackToNoIndex`:: + If set to `true`, fall back to `git grep --no-index` if `git grep` + is executed outside of a git repository. Defaults to `false`. diff --git a/Documentation/git-grep.adoc b/Documentation/git-grep.adoc index a548585d4cbad5..19b3ade16dd51a 100644 --- a/Documentation/git-grep.adoc +++ b/Documentation/git-grep.adoc @@ -8,8 +8,8 @@ git-grep - Print lines matching a pattern SYNOPSIS -------- -[verse] -'git grep' [-a | --text] [-I] [--textconv] [-i | --ignore-case] [-w | --word-regexp] +[synopsis] +git grep [-a | --text] [-I] [--textconv] [-i | --ignore-case] [-w | --word-regexp] [-v | --invert-match] [-h|-H] [--full-name] [-E | --extended-regexp] [-G | --basic-regexp] [-P | --perl-regexp] @@ -41,139 +41,139 @@ characters. An empty string as search expression matches all lines. OPTIONS ------- ---cached:: +`--cached`:: Instead of searching tracked files in the working tree, search blobs registered in the index file. ---untracked:: +`--untracked`:: In addition to searching in the tracked files in the working tree, search also in untracked files. ---no-index:: +`--no-index`:: Search files in the current directory that is not managed by Git, or by ignoring that the current directory is managed by Git. This - is rather similar to running the regular `grep(1)` utility with its + is rather similar to running the regular `grep`(1) utility with its `-r` option specified, but with some additional benefits, such as - using pathspec patterns to limit paths; see the 'pathspec' entry + using pathspec patterns to limit paths; see the `pathspec` entry in linkgit:gitglossary[7] for more information. + This option cannot be used together with `--cached` or `--untracked`. See also `grep.fallbackToNoIndex` in 'CONFIGURATION' below. ---no-exclude-standard:: +`--no-exclude-standard`:: Also search in ignored files by not honoring the `.gitignore` mechanism. Only useful with `--untracked`. ---exclude-standard:: +`--exclude-standard`:: Do not pay attention to ignored files specified via the `.gitignore` mechanism. Only useful when searching files in the current directory with `--no-index`. ---recurse-submodules:: +`--recurse-submodules`:: Recursively search in each submodule that is active and checked out in the repository. When used in combination with the __ option the prefix of all submodule output will be the name of the parent project's __ object. This option cannot be used together with `--untracked`, and it has no effect if `--no-index` is specified. --a:: ---text:: +`-a`:: +`--text`:: Process binary files as if they were text. ---textconv:: +`--textconv`:: Honor textconv filter settings. ---no-textconv:: +`--no-textconv`:: Do not honor textconv filter settings. This is the default. --i:: ---ignore-case:: +`-i`:: +`--ignore-case`:: Ignore case differences between the patterns and the files. --I:: +`-I`:: Don't match the pattern in binary files. ---max-depth :: - For each given on command line, descend at most +`--max-depth `:: + For each __ given on command line, descend at most __ levels of directories. A value of -1 means no limit. - This option is ignored if contains active wildcards. + This option is ignored if __ contains active wildcards. In other words if "a*" matches a directory named "a*", - "*" is matched literally so --max-depth is still effective. + "*" is matched literally so `--max-depth` is still effective. --r:: ---recursive:: +`-r`:: +`--recursive`:: Same as `--max-depth=-1`; this is the default. ---no-recursive:: +`--no-recursive`:: Same as `--max-depth=0`. --w:: ---word-regexp:: +`-w`:: +`--word-regexp`:: Match the pattern only at word boundary (either begin at the beginning of a line, or preceded by a non-word character; end at the end of a line or followed by a non-word character). --v:: ---invert-match:: +`-v`:: +`--invert-match`:: Select non-matching lines. --h:: --H:: +`-h`:: +`-H`:: By default, the command shows the filename for each match. `-h` option is used to suppress this output. `-H` is there for completeness and does not do anything except it overrides `-h` given earlier on the command line. ---full-name:: +`--full-name`:: When run from a subdirectory, the command usually outputs paths relative to the current directory. This option forces paths to be output relative to the project top directory. --E:: ---extended-regexp:: --G:: ---basic-regexp:: +`-E`:: +`--extended-regexp`:: +`-G`:: +`--basic-regexp`:: Use POSIX extended/basic regexp for patterns. Default is to use basic regexp. --P:: ---perl-regexp:: +`-P`:: +`--perl-regexp`:: Use Perl-compatible regular expressions for patterns. + Support for these types of regular expressions is an optional compile-time dependency. If Git wasn't compiled with support for them providing this option will cause it to die. --F:: ---fixed-strings:: +`-F`:: +`--fixed-strings`:: Use fixed strings for patterns (don't interpret pattern as a regex). --n:: ---line-number:: +`-n`:: +`--line-number`:: Prefix the line number to matching lines. ---column:: +`--column`:: Prefix the 1-indexed byte-offset of the first match from the start of the matching line. --l:: ---files-with-matches:: ---name-only:: --L:: ---files-without-match:: +`-l`:: +`--files-with-matches`:: +`--name-only`:: +`-L`:: +`--files-without-match`:: Instead of showing every matched line, show only the names of files that contain (or do not contain) matches. - For better compatibility with 'git diff', `--name-only` is a + For better compatibility with `git diff`, `--name-only` is a synonym for `--files-with-matches`. --O[]:: ---open-files-in-pager[=]:: - Open the matching files in the pager (not the output of 'grep'). +`-O[]`:: +`--open-files-in-pager[=]`:: + Open the matching files in the pager (not the output of `grep`). If the pager happens to be "less" or "vi", and the user specified only one pattern, the first file is positioned at the first match automatically. The `pager` argument is @@ -181,65 +181,65 @@ providing this option will cause it to die. without a space. If `pager` is unspecified, the default pager will be used (see `core.pager` in linkgit:git-config[1]). --z:: ---null:: +`-z`:: +`--null`:: Use \0 as the delimiter for pathnames in the output, and print them verbatim. Without this option, pathnames with "unusual" characters are quoted as explained for the configuration variable `core.quotePath` (see linkgit:git-config[1]). --o:: ---only-matching:: +`-o`:: +`--only-matching`:: Print only the matched (non-empty) parts of a matching line, with each such part on a separate output line. --c:: ---count:: +`-c`:: +`--count`:: Instead of showing every matched line, show the number of lines that match. ---color[=]:: +`--color[=]`:: Show colored matches. - The value must be always (the default), never, or auto. + The value must be `always` (the default), `never`, or `auto`. ---no-color:: +`--no-color`:: Turn off match highlighting, even when the configuration file gives the default to color output. Same as `--color=never`. ---break:: +`--break`:: Print an empty line between matches from different files. ---heading:: +`--heading`:: Show the filename above the matches in that file instead of at the start of each shown line. --p:: ---show-function:: +`-p`:: +`--show-function`:: Show the preceding line that contains the function name of the match, unless the matching line is a function name itself. The name is determined in the same way as `git diff` works out patch hunk headers (see 'Defining a custom hunk-header' in linkgit:gitattributes[5]). --:: --C :: ---context :: - Show leading and trailing lines, and place a line +`-`:: +`-C `:: +`--context `:: + Show __ leading and trailing lines, and place a line containing `--` between contiguous groups of matches. --A :: ---after-context :: - Show trailing lines, and place a line containing +`-A `:: +`--after-context `:: + Show __ trailing lines, and place a line containing `--` between contiguous groups of matches. --B :: ---before-context :: - Show leading lines, and place a line containing +`-B `:: +`--before-context `:: + Show __ leading lines, and place a line containing `--` between contiguous groups of matches. --W:: ---function-context:: +`-W`:: +`--function-context`:: Show the surrounding text from the previous line containing a function name up to the one before the next function name, effectively showing the whole function in which the match was @@ -247,22 +247,22 @@ providing this option will cause it to die. `git diff` works out patch hunk headers (see 'Defining a custom hunk-header' in linkgit:gitattributes[5]). --m :: ---max-count :: +`-m `:: +`--max-count `:: Limit the amount of matches per file. When using the `-v` or `--invert-match` option, the search stops after the specified number of non-matches. A value of -1 will return unlimited results (the default). A value of 0 will exit immediately with a non-zero status. ---threads :: - Number of `grep` worker threads to use. See 'NOTES ON THREADS' +`--threads `:: + Number of `grep` worker threads to use. See `NOTES ON THREADS` and `grep.threads` in 'CONFIGURATION' for more information. --f :: - Read patterns from , one per line. +`-f `:: + Read patterns from __, one per line. + -Passing the pattern via allows for providing a search pattern +Passing the pattern via __ allows for providing a search pattern containing a \0. + Not all pattern types support patterns containing \0. Git will error @@ -279,44 +279,44 @@ In future versions we may learn to support patterns containing \0 for more search backends, until then we'll die when the pattern type in question doesn't support them. --e:: +`-e`:: The next parameter is the pattern. This option has to be used for patterns starting with `-` and should be used in scripts passing user input to grep. Multiple patterns are - combined by 'or'. + combined by `or`. ---and:: ---or:: ---not:: -( ... ):: +`--and`:: +`--or`:: +`--not`:: +`( ... )`:: Specify how multiple patterns are combined using Boolean expressions. `--or` is the default operator. `--and` has higher precedence than `--or`. `-e` has to be used for all patterns. ---all-match:: +`--all-match`:: When giving multiple pattern expressions combined with `--or`, this flag is specified to limit the match to files that have lines to match all of them. --q:: ---quiet:: +`-q`:: +`--quiet`:: Do not output matched lines; instead, exit with status 0 when there is a match and with non-zero status when there isn't. -...:: +`...`:: Instead of searching tracked files in the working tree, search blobs in the given trees. -\--:: +`--`:: Signals the end of options; the rest of the parameters - are limiters. + are __ limiters. -...:: +`...`:: If given, limit the search to paths matching at least one pattern. - Both leading paths match and glob(7) patterns are supported. + Both leading paths match and `glob`(7) patterns are supported. + -For more details about the syntax, see the 'pathspec' entry +For more details about the __ syntax, see the `pathspec` entry in linkgit:gitglossary[7]. EXAMPLES From 242d3aa317ccaa3e7c5f6bf2218ccbdd8a0a26e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Avila?= Date: Mon, 25 May 2026 10:28:25 +0000 Subject: [PATCH 032/106] doc: convert git-am synopsis and options to new style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert git-am from [verse]/single-quote style to the modern synopsis-block style: - Replace [verse] with [synopsis] in SYNOPSIS block - Backtick-quote all OPTIONS terms - Convert inline man page refs - Convert inline command refs - Convert prose placeholders: Signed-off-by: Jean-Noël Avila Signed-off-by: Junio C Hamano --- Documentation/config/am.adoc | 6 +- Documentation/format-patch-caveats.adoc | 2 +- .../format-patch-end-of-commit-message.adoc | 4 +- Documentation/git-am.adoc | 132 +++++++++--------- 4 files changed, 72 insertions(+), 72 deletions(-) diff --git a/Documentation/config/am.adoc b/Documentation/config/am.adoc index e9561e12d7467e..250e6b5047365d 100644 --- a/Documentation/config/am.adoc +++ b/Documentation/config/am.adoc @@ -1,11 +1,11 @@ -am.keepcr:: +`am.keepcr`:: If true, linkgit:git-am[1] will call linkgit:git-mailsplit[1] for patches in mbox format with parameter `--keep-cr`. In this case linkgit:git-mailsplit[1] will not remove `\r` from lines ending with `\r\n`. Can be overridden by giving `--no-keep-cr` from the command line. -am.threeWay:: +`am.threeWay`:: By default, linkgit:git-am[1] will fail if the patch does not apply cleanly. When set to true, this setting tells linkgit:git-am[1] to fall back on 3-way merge if the patch @@ -13,7 +13,7 @@ am.threeWay:: have those blobs available locally (equivalent to giving the `--3way` option from the command line). Defaults to `false`. -am.messageId:: +`am.messageId`:: Add a `Message-ID` trailer based on the email header to the commit when using linkgit:git-am[1] (see linkgit:git-interpret-trailers[1]). See also the `--message-id` diff --git a/Documentation/format-patch-caveats.adoc b/Documentation/format-patch-caveats.adoc index 807a65b885b09a..133e4757e7946c 100644 --- a/Documentation/format-patch-caveats.adoc +++ b/Documentation/format-patch-caveats.adoc @@ -28,6 +28,6 @@ repositories. This goes to show that this behavior does not only impact email workflows. Given these limitations, one might be tempted to use a general-purpose -utility like patch(1) instead. However, patch(1) will not only look for +utility like `patch`(1) instead. However, `patch`(1) will not only look for unindented diffs (like linkgit:git-am[1]) but will try to apply indented diffs as well. diff --git a/Documentation/format-patch-end-of-commit-message.adoc b/Documentation/format-patch-end-of-commit-message.adoc index ec1ef79f5e3308..a1a624d2ac5bc2 100644 --- a/Documentation/format-patch-end-of-commit-message.adoc +++ b/Documentation/format-patch-end-of-commit-message.adoc @@ -1,8 +1,8 @@ Any line that is of the form: * three-dashes and end-of-line, or -* a line that begins with "diff -", or -* a line that begins with "Index: " +* a line that begins with `diff -`, or +* a line that begins with `Index: ` is taken as the beginning of a patch, and the commit log message is terminated before the first occurrence of such a line. diff --git a/Documentation/git-am.adoc b/Documentation/git-am.adoc index ac65852918f3ee..28adf4cf656651 100644 --- a/Documentation/git-am.adoc +++ b/Documentation/git-am.adoc @@ -8,17 +8,17 @@ git-am - Apply a series of patches from a mailbox SYNOPSIS -------- -[verse] -'git am' [--signoff] [--keep] [--[no-]keep-cr] [--[no-]utf8] [--[no-]verify] +[synopsis] +git am [--signoff] [--keep] [--[no-]keep-cr] [--[no-]utf8] [--[no-]verify] [--[no-]3way] [--interactive] [--committer-date-is-author-date] [--ignore-date] [--ignore-space-change | --ignore-whitespace] [--whitespace=] [-C] [-p] [--directory=] [--exclude=] [--include=] [--reject] [-q | --quiet] - [--[no-]scissors] [-S[]] [--patch-format=] + [--[no-]scissors] [-S[]] [--patch-format=] [--quoted-cr=] [--empty=(stop|drop|keep)] [( | )...] -'git am' (--continue | --skip | --abort | --quit | --retry | --show-current-patch[=(diff|raw)] | --allow-empty) +git am (--continue | --skip | --abort | --quit | --retry | --show-current-patch[=(diff|raw)] | --allow-empty) DESCRIPTION ----------- @@ -30,45 +30,45 @@ history without merges. OPTIONS ------- -(|)...:: +`(|)...`:: The list of mailbox files to read patches from. If you do not supply this argument, the command reads from the standard input. If you supply directories, they will be treated as Maildirs. --s:: ---signoff:: +`-s`:: +`--signoff`:: Add a `Signed-off-by` trailer to the commit message (see linkgit:git-interpret-trailers[1]), using the committer identity of yourself. See the signoff option in linkgit:git-commit[1] for more information. --k:: ---keep:: +`-k`:: +`--keep`:: Pass `-k` flag to linkgit:git-mailinfo[1]. ---keep-non-patch:: +`--keep-non-patch`:: Pass `-b` flag to linkgit:git-mailinfo[1]. ---keep-cr:: ---no-keep-cr:: +`--keep-cr`:: +`--no-keep-cr`:: With `--keep-cr`, call linkgit:git-mailsplit[1] with the same option, to prevent it from stripping CR at the end of lines. `am.keepcr` configuration variable can be used to specify the default behaviour. `--no-keep-cr` is useful to override `am.keepcr`. --c:: ---scissors:: +`-c`:: +`--scissors`:: Remove everything in body before a scissors line (see linkgit:git-mailinfo[1]). Can be activated by default using the `mailinfo.scissors` configuration variable. ---no-scissors:: +`--no-scissors`:: Ignore scissors lines (see linkgit:git-mailinfo[1]). ---quoted-cr=:: +`--quoted-cr=`:: This flag will be passed down to linkgit:git-mailinfo[1]. ---empty=(drop|keep|stop):: +`--empty=(drop|keep|stop)`:: How to handle an e-mail message lacking a patch: + -- @@ -82,23 +82,23 @@ OPTIONS session. This is the default behavior. -- --m:: ---message-id:: +`-m`:: +`--message-id`:: Pass the `-m` flag to linkgit:git-mailinfo[1], so that the `Message-ID` header is added to the commit message. The `am.messageid` configuration variable can be used to specify the default behaviour. ---no-message-id:: +`--no-message-id`:: Do not add the Message-ID header to the commit message. `--no-message-id` is useful to override `am.messageid`. --q:: ---quiet:: +`-q`:: +`--quiet`:: Be quiet. Only print error messages. --u:: ---utf8:: +`-u`:: +`--utf8`:: Pass `-u` flag to linkgit:git-mailinfo[1]. The proposed commit log message taken from the e-mail is re-coded into UTF-8 encoding (configuration variable @@ -108,57 +108,57 @@ OPTIONS This was optional in prior versions of git, but now it is the default. You can use `--no-utf8` to override this. ---no-utf8:: +`--no-utf8`:: Pass `-n` flag to linkgit:git-mailinfo[1]. --3:: ---3way:: ---no-3way:: +`-3`:: +`--3way`:: +`--no-3way`:: When the patch does not apply cleanly, fall back on 3-way merge if the patch records the identity of blobs it is supposed to apply to and we have those blobs available locally. `--no-3way` can be used to override - am.threeWay configuration variable. For more information, - see am.threeWay in linkgit:git-config[1]. + `am.threeWay` configuration variable. For more information, + see `am.threeWay` in linkgit:git-config[1]. include::rerere-options.adoc[] ---ignore-space-change:: ---ignore-whitespace:: ---whitespace=:: --C:: --p:: ---directory=:: ---exclude=:: ---include=:: ---reject:: +`--ignore-space-change`:: +`--ignore-whitespace`:: +`--whitespace=`:: +`-C`:: +`-p`:: +`--directory=`:: +`--exclude=`:: +`--include=`:: +`--reject`:: These flags are passed to the linkgit:git-apply[1] program that applies the patch. + -Valid for the `--whitespace` option are: +Valid __ for the `--whitespace` option are: `nowarn`, `warn`, `fix`, `error`, and `error-all`. ---patch-format:: +`--patch-format`:: By default the command will try to detect the patch format automatically. This option allows the user to bypass the automatic detection and specify the patch format that the patch(es) should be interpreted as. Valid formats are mbox, mboxrd, stgit, stgit-series, and hg. --i:: ---interactive:: +`-i`:: +`--interactive`:: Run interactively. ---verify:: --n:: ---no-verify:: +`--verify`:: +`-n`:: +`--no-verify`:: Run the `pre-applypatch` and `applypatch-msg` hooks. This is the default. Skip these hooks with `-n` or `--no-verify`. See also linkgit:githooks[5]. + Note that `post-applypatch` cannot be skipped. ---committer-date-is-author-date:: +`--committer-date-is-author-date`:: By default the command records the date from the e-mail message as the commit author date, and uses the time of commit creation as the committer date. This allows the @@ -172,29 +172,29 @@ committer date when applying commits on top of a base which commit is older (in terms of the commit date) than the oldest patch you are applying. ---ignore-date:: +`--ignore-date`:: By default the command records the date from the e-mail message as the commit author date, and uses the time of commit creation as the committer date. This allows the user to lie about the author date by using the same value as the committer date. ---skip:: +`--skip`:: Skip the current patch. This is only meaningful when restarting an aborted patch. --S[]:: ---gpg-sign[=]:: ---no-gpg-sign:: - GPG-sign commits. The `keyid` argument is optional and +`-S[]`:: +`--gpg-sign[=]`:: +`--no-gpg-sign`:: + GPG-sign commits. The __ is optional and defaults to the committer identity; if specified, it must be stuck to the option without a space. `--no-gpg-sign` is useful to countermand both `commit.gpgSign` configuration variable, and earlier `--gpg-sign`. ---continue:: --r:: ---resolved:: +`--continue`:: +`-r`:: +`--resolved`:: After a patch failure (e.g. attempting to apply conflicting patch), the user has applied it by hand and the index file stores the result of the application. @@ -202,36 +202,36 @@ applying. extracted from the e-mail message and the current index file, and continue. ---resolvemsg=:: - When a patch failure occurs, will be printed +`--resolvemsg=`:: + When a patch failure occurs, __ will be printed to the screen before exiting. This overrides the standard message informing you to use `--continue` or `--skip` to handle the failure. This is solely for internal use between linkgit:git-rebase[1] and linkgit:git-am[1]. ---abort:: +`--abort`:: Restore the original branch and abort the patching operation. Revert the contents of files involved in the am operation to their pre-am state. ---quit:: - Abort the patching operation but keep HEAD and the index +`--quit`:: + Abort the patching operation but keep `HEAD` and the index untouched. ---retry:: +`--retry`:: Try to apply the last conflicting patch again. This is generally only useful for passing extra options to the retry attempt (e.g., `--3way`), since otherwise you'll just see the same failure again. ---show-current-patch[=(diff|raw)]:: +`--show-current-patch[=(diff|raw)]`:: Show the message at which linkgit:git-am[1] has stopped due to conflicts. If `raw` is specified, show the raw contents of the e-mail message; if `diff`, show the diff portion only. Defaults to `raw`. ---allow-empty:: +`--allow-empty`:: After a patch failure on an input e-mail message lacking a patch, create an empty commit with the contents of the e-mail message as its log message. @@ -278,11 +278,11 @@ operation is finished, so if you decide to start over from scratch, run `git am --abort` before running the command with mailbox names. -Before any patches are applied, ORIG_HEAD is set to the tip of the +Before any patches are applied, `ORIG_HEAD` is set to the tip of the current branch. This is useful if you have problems with multiple commits, like running linkgit:git-am[1] on the wrong branch or an error in the commits that is more easily fixed by changing the mailbox (e.g. -errors in the "From:" lines). +errors in the `From:` lines). [[caveats]] CAVEATS From ba1c516edabccd3e195746a8be842a70cdea6502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Avila?= Date: Mon, 25 May 2026 10:28:26 +0000 Subject: [PATCH 033/106] doc: convert git-apply synopsis and options to new style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert git-apply from [verse]/single-quote style to the modern synopsis-block style: - Replace [verse] with [synopsis] in SYNOPSIS block - Backtick-quote all OPTIONS terms and config keys in config/apply.adoc - Convert single-quoted inline commands ('git apply', 'diff', etc.) - Wrap standalone placeholders in underscores (, , ) - Backtick-quote `*.rej` and GNU `patch` tool references Signed-off-by: Jean-Noël Avila Signed-off-by: Junio C Hamano --- Documentation/config/apply.adoc | 17 +++-- Documentation/git-apply.adoc | 125 ++++++++++++++++---------------- 2 files changed, 74 insertions(+), 68 deletions(-) diff --git a/Documentation/config/apply.adoc b/Documentation/config/apply.adoc index f9908e210a838d..36fcea62914ae6 100644 --- a/Documentation/config/apply.adoc +++ b/Documentation/config/apply.adoc @@ -1,11 +1,16 @@ -apply.ignoreWhitespace:: - When set to 'change', tells 'git apply' to ignore changes in +`apply.ignoreWhitespace`:: + When set to `change`, tells `git apply` to ignore changes in whitespace, in the same way as the `--ignore-space-change` option. - When set to one of: no, none, never, false, it tells 'git apply' to + When set to one of: `no`, `none`, `never`, `false`, it tells `git apply` to respect all whitespace differences. +ifndef::git-apply[] See linkgit:git-apply[1]. +endif::git-apply[] -apply.whitespace:: - Tells 'git apply' how to handle whitespace, in the same way - as the `--whitespace` option. See linkgit:git-apply[1]. +`apply.whitespace`:: + Tells `git apply` how to handle whitespace, in the same way + as the `--whitespace` option. +ifndef::git-apply[] + See linkgit:git-apply[1]. +endif::git-apply[] diff --git a/Documentation/git-apply.adoc b/Documentation/git-apply.adoc index 6c71ee69da977d..3f22dac1ce8edd 100644 --- a/Documentation/git-apply.adoc +++ b/Documentation/git-apply.adoc @@ -8,8 +8,8 @@ git-apply - Apply a patch to files and/or to the index SYNOPSIS -------- -[verse] -'git apply' [--stat] [--numstat] [--summary] [--check] +[synopsis] +git apply [--stat] [--numstat] [--summary] [--check] [--index | --intent-to-add] [--3way] [--ours | --theirs | --union] [--apply] [--no-add] [--build-fake-ancestor=] [-R | --reverse] [--allow-binary-replacement | --binary] [--reject] [-z] @@ -35,33 +35,33 @@ linkgit:git-format-patch[1] and/or received by email. OPTIONS ------- -...:: - The files to read the patch from. '-' can be used to read +`...`:: + The files to read the patch from. `-` can be used to read from the standard input. ---stat:: +`--stat`:: Instead of applying the patch, output diffstat for the input. Turns off "apply". ---numstat:: +`--numstat`:: Similar to `--stat`, but shows the number of added and deleted lines in decimal notation and the pathname without abbreviation, to make it more machine friendly. For binary files, outputs two `-` instead of saying `0 0`. Turns off "apply". ---summary:: +`--summary`:: Instead of applying the patch, output a condensed summary of information obtained from git diff extended headers, such as creations, renames, and mode changes. Turns off "apply". ---check:: +`--check`:: Instead of applying the patch, see if the patch is applicable to the current working tree and/or the index file and detects errors. Turns off "apply". ---index:: +`--index`:: Apply the patch to both the index and the working tree (or merely check that it would apply cleanly to both if `--check` is in effect). Note that `--index` expects index entries and @@ -70,13 +70,13 @@ OPTIONS raise an error if they are not, even if the patch would apply cleanly to both the index and the working tree in isolation. ---cached:: +`--cached`:: Apply the patch to just the index, without touching the working tree. If `--check` is in effect, merely check that it would apply cleanly to the index entry. --N:: ---intent-to-add:: +`-N`:: +`--intent-to-add`:: When applying the patch only to the working tree, mark new files to be added to the index later (see `--intent-to-add` option in linkgit:git-add[1]). This option is ignored if @@ -84,8 +84,8 @@ OPTIONS repository. Note that `--index` could be implied by other options such as `--3way`. --3:: ---3way:: +`-3`:: +`--3way`:: Attempt 3-way merge if the patch records the identity of blobs it is supposed to apply to and we have those blobs available locally, possibly leaving the conflict markers in the files in the working tree for the user to @@ -94,14 +94,14 @@ OPTIONS When used with the `--cached` option, any conflicts are left at higher stages in the cache. ---ours:: ---theirs:: ---union:: +`--ours`:: +`--theirs`:: +`--union`:: Instead of leaving conflicts in the file, resolve conflicts favouring - our (or their or both) side of the lines. Requires --3way. + our (or their or both) side of the lines. Requires `--3way`. ---build-fake-ancestor=:: - Newer 'git diff' output has embedded 'index information' +`--build-fake-ancestor=`:: + Newer `git diff` output has embedded 'index information' for each blob to help identify the original version that the patch applies to. When this flag is given, and if the original versions of the blobs are available locally, @@ -110,18 +110,18 @@ OPTIONS When a pure mode change is encountered (which has no index information), the information is read from the current index instead. --R:: ---reverse:: +`-R`:: +`--reverse`:: Apply the patch in reverse. ---reject:: - For atomicity, 'git apply' by default fails the whole patch and +`--reject`:: + For atomicity, `git apply` by default fails the whole patch and does not touch the working tree when some of the hunks do not apply. This option makes it apply the parts of the patch that are applicable, and leave the - rejected hunks in corresponding *.rej files. + rejected hunks in corresponding `*.rej` files. --z:: +`-z`:: When `--numstat` has been given, do not munge pathnames, but use a NUL-terminated machine-readable format. + @@ -129,20 +129,20 @@ Without this option, pathnames with "unusual" characters are quoted as explained for the configuration variable `core.quotePath` (see linkgit:git-config[1]). --p:: - Remove leading path components (separated by slashes) from +`-p`:: + Remove __ leading path components (separated by slashes) from traditional diff paths. E.g., with `-p2`, a patch against `a/dir/file` will be applied directly to `file`. The default is 1. --C:: - Ensure at least lines of surrounding context match before +`-C`:: + Ensure at least __ lines of surrounding context match before and after each change. When fewer lines of surrounding context exist they all must match. By default no context is ever ignored. ---unidiff-zero:: - By default, 'git apply' expects that the patch being +`--unidiff-zero`:: + By default, `git apply` expects that the patch being applied is a unified diff with at least one line of context. This provides good safety measures, but breaks down when applying a diff generated with `--unified=0`. To bypass these @@ -151,34 +151,34 @@ linkgit:git-config[1]). Note, for the reasons stated above, the usage of context-free patches is discouraged. ---apply:: +`--apply`:: If you use any of the options marked "Turns off - 'apply'" above, 'git apply' reads and outputs the + 'apply'" above, `git apply` reads and outputs the requested information without actually applying the patch. Give this flag after those flags to also apply the patch. ---no-add:: +`--no-add`:: When applying a patch, ignore additions made by the patch. This can be used to extract the common part between - two files by first running 'diff' on them and applying + two files by first running `diff` on them and applying the result with this option, which would apply the deletion part but not the addition part. ---allow-binary-replacement:: ---binary:: +`--allow-binary-replacement`:: +`--binary`:: Historically we did not allow binary patch application without an explicit permission from the user, and this flag was the way to do so. Currently, we always allow binary patch application, so this is a no-op. ---exclude=:: - Don't apply changes to files matching the given path pattern. This can +`--exclude=`:: + Don't apply changes to files matching __. This can be useful when importing patchsets, where you want to exclude certain files or directories. ---include=:: - Apply changes to files matching the given path pattern. This can +`--include=`:: + Apply changes to files matching the __. This can be useful when importing patchsets, where you want to include certain files or directories. + @@ -188,15 +188,15 @@ patch to each path is used. A patch to a path that does not match any include/exclude pattern is used by default if there is no include pattern on the command line, and ignored if there is any include pattern. ---ignore-space-change:: ---ignore-whitespace:: +`--ignore-space-change`:: +`--ignore-whitespace`:: When applying a patch, ignore changes in whitespace in context lines if necessary. Context lines will preserve their whitespace, and they will not undergo whitespace fixing regardless of the value of the `--whitespace` option. New lines will still be fixed, though. ---whitespace=:: +`--whitespace=`:: When applying a patch, detect a new or modified line that has whitespace errors. What are considered whitespace errors is controlled by `core.whitespace` configuration. By default, @@ -209,7 +209,7 @@ By default, the command outputs warning messages but applies the patch. When `git-apply` is used for statistics and not applying a patch, it defaults to `nowarn`. + -You can use different `` values to control this +You can use different __ values to control this behavior: + * `nowarn` turns off the trailing whitespace warning. @@ -223,48 +223,48 @@ behavior: to apply the patch. * `error-all` is similar to `error` but shows all errors. ---inaccurate-eof:: - Under certain circumstances, some versions of 'diff' do not correctly +`--inaccurate-eof`:: + Under certain circumstances, some versions of `diff` do not correctly detect a missing new-line at the end of the file. As a result, patches - created by such 'diff' programs do not record incomplete lines + created by such `diff` programs do not record incomplete lines correctly. This option adds support for applying such patches by working around this bug. --v:: ---verbose:: +`-v`:: +`--verbose`:: Report progress to stderr. By default, only a message about the current patch being applied will be printed. This option will cause additional information to be reported. --q:: ---quiet:: +`-q`:: +`--quiet`:: Suppress stderr output. Messages about patch status and progress will not be printed. ---recount:: +`--recount`:: Do not trust the line counts in the hunk headers, but infer them by inspecting the patch (e.g. after editing the patch without adjusting the hunk headers appropriately). ---directory=:: - Prepend to all filenames. If a "-p" argument was also passed, +`--directory=`:: + Prepend __ to all filenames. If a `-p` argument was also passed, it is applied before prepending the new root. + For example, a patch that talks about updating `a/git-gui.sh` to `b/git-gui.sh` can be applied to the file in the working tree `modules/git-gui/git-gui.sh` by running `git apply --directory=modules/git-gui`. ---unsafe-paths:: +`--unsafe-paths`:: By default, a patch that affects outside the working area (either a Git controlled working tree, or the current working - directory when "git apply" is used as a replacement of GNU - patch) is rejected as a mistake (or a mischief). + directory when `git apply` is used as a replacement of GNU + `patch`) is rejected as a mistake (or a mischief). + -When `git apply` is used as a "better GNU patch", the user can pass +When `git apply` is used as a "better GNU `patch`", the user can pass the `--unsafe-paths` option to override this safety check. This option has no effect when `--index` or `--cached` is in use. ---allow-empty:: +`--allow-empty`:: Don't return an error for patches containing no diff. This includes empty patches and patches with commit text only. @@ -273,11 +273,12 @@ CONFIGURATION include::includes/cmd-config-section-all.adoc[] +:git-apply: 1 include::config/apply.adoc[] SUBMODULES ---------- -If the patch contains any changes to submodules then 'git apply' +If the patch contains any changes to submodules then `git apply` treats these changes as follows. If `--index` is specified (explicitly or implicitly), then the submodule From 2ef248ae45bdcfbb027108bf87fcc3375cb5daba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Avila?= Date: Mon, 25 May 2026 10:28:27 +0000 Subject: [PATCH 034/106] doc: convert git-imap-send synopsis and options to new style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert git-imap-send from [verse]/single-quote style to the modern synopsis-block style: - Replace [verse] with [synopsis] in SYNOPSIS block - Backtick-quote all OPTIONS terms - Backtick-quote all config keys in config/imap.adoc - Backtick-quote bare config key references in prose Signed-off-by: Jean-Noël Avila Signed-off-by: Junio C Hamano --- Documentation/config/imap.adoc | 30 +++++++++++++++--------------- Documentation/git-imap-send.adoc | 24 ++++++++++++------------ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Documentation/config/imap.adoc b/Documentation/config/imap.adoc index 4682a6bd039755..cb8f5e2700ae13 100644 --- a/Documentation/config/imap.adoc +++ b/Documentation/config/imap.adoc @@ -1,44 +1,44 @@ -imap.folder:: +`imap.folder`:: The folder to drop the mails into, which is typically the Drafts folder. For example: `INBOX.Drafts`, `INBOX/Drafts` or `[Gmail]/Drafts`. The IMAP folder to interact with MUST be specified; the value of this configuration variable is used as the fallback default value when the `--folder` option is not given. -imap.tunnel:: +`imap.tunnel`:: Command used to set up a tunnel to the IMAP server through which commands will be piped instead of using a direct network connection - to the server. Required when imap.host is not set. + to the server. Required when `imap.host` is not set. -imap.host:: +`imap.host`:: A URL identifying the server. Use an `imap://` prefix for non-secure connections and an `imaps://` prefix for secure connections. - Ignored when imap.tunnel is set, but required otherwise. + Ignored when `imap.tunnel` is set, but required otherwise. -imap.user:: +`imap.user`:: The username to use when logging in to the server. -imap.pass:: +`imap.pass`:: The password to use when logging in to the server. -imap.port:: +`imap.port`:: An integer port number to connect to on the server. - Defaults to 143 for imap:// hosts and 993 for imaps:// hosts. - Ignored when imap.tunnel is set. + Defaults to 143 for `imap://` hosts and 993 for `imaps://` hosts. + Ignored when `imap.tunnel` is set. -imap.sslverify:: +`imap.sslverify`:: A boolean to enable/disable verification of the server certificate used by the SSL/TLS connection. Default is `true`. Ignored when - imap.tunnel is set. + `imap.tunnel` is set. -imap.preformattedHTML:: +`imap.preformattedHTML`:: A boolean to enable/disable the use of html encoding when sending - a patch. An html encoded patch will be bracketed with
+	a patch.  An html encoded patch will be bracketed with `
`
 	and have a content type of text/html.  Ironically, enabling this
 	option causes Thunderbird to send the patch as a plain/text,
 	format=fixed email.  Default is `false`.
 
-imap.authMethod::
+`imap.authMethod`::
 	Specify the authentication method for authenticating with the IMAP server.
 	If Git was built with the NO_CURL option, or if your curl version is older
 	than 7.34.0, or if you're running git-imap-send with the `--no-curl`
diff --git a/Documentation/git-imap-send.adoc b/Documentation/git-imap-send.adoc
index 278e5ccd36b9fa..538b91afc06dde 100644
--- a/Documentation/git-imap-send.adoc
+++ b/Documentation/git-imap-send.adoc
@@ -8,9 +8,9 @@ git-imap-send - Send a collection of patches from stdin to an IMAP folder
 
 SYNOPSIS
 --------
-[verse]
-'git imap-send' [-v] [-q] [--[no-]curl] [(--folder|-f) ]
-'git imap-send' --list
+[synopsis]
+git imap-send [-v] [-q] [--[no-]curl] [(--folder|-f) ]
+git imap-send --list
 
 
 DESCRIPTION
@@ -32,30 +32,30 @@ $ git format-patch --signoff --stdout --attach origin | git imap-send
 OPTIONS
 -------
 
--v::
---verbose::
+`-v`::
+`--verbose`::
 	Be verbose.
 
--q::
---quiet::
+`-q`::
+`--quiet`::
 	Be quiet.
 
--f ::
---folder=::
+`-f `::
+`--folder=`::
 	Specify the folder in which the emails have to saved.
 	For example: `--folder=[Gmail]/Drafts` or `-f INBOX/Drafts`.
 
---curl::
+`--curl`::
 	Use libcurl to communicate with the IMAP server, unless tunneling
 	into it.  Ignored if Git was built without the USE_CURL_FOR_IMAP_SEND
 	option set.
 
---no-curl::
+`--no-curl`::
 	Talk to the IMAP server using git's own IMAP routines instead of
 	using libcurl.  Ignored if Git was built with the NO_OPENSSL option
 	set.
 
---list::
+`--list`::
 	Run the IMAP LIST command to output a list of all the folders present.
 
 CONFIGURATION

From 85a30b5b26c2953aa836c554af5fc58eed707e4a Mon Sep 17 00:00:00 2001
From: Kristofer Karlsson 
Date: Mon, 25 May 2026 14:28:03 +0000
Subject: [PATCH 035/106] object.h: fix stale entries in object flag allocation
 table

Update three stale entries found during an audit of the flag
allocation table:

 - sha1-name.c was renamed to object-name.c
 - builtin/show-branch.c uses bits 0 and 2-28, not 0-26
   (REV_SHIFT=2, MAX_REVS=FLAG_BITS-REV_SHIFT=27)
 - negotiator/skipping.c uses bits 2-5 like negotiator/default.c
   (ADVERTISED on bit 3 instead of COMMON_REF)

Signed-off-by: Kristofer Karlsson 
Signed-off-by: Junio C Hamano 
---
 object.h | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/object.h b/object.h
index d814647ebe6c18..2b26de3044846b 100644
--- a/object.h
+++ b/object.h
@@ -67,6 +67,7 @@ void object_array_init(struct object_array *array);
  * revision.h:               0---------10         15               23--------28
  * fetch-pack.c:             01    67
  * negotiator/default.c:       2--5
+ * negotiator/skipping.c:      2--5
  * walker.c:                 0-2
  * upload-pack.c:                4       11-----14  16-----19
  * builtin/blame.c:                        12-13
@@ -76,13 +77,13 @@ void object_array_init(struct object_array *array);
  * commit-graph.c:                                15
  * commit-reach.c:                                  16-----19
  * builtin/last-modified.c:                         1617
- * sha1-name.c:                                              20
+ * object-name.c:                                            20
  * list-objects-filter.c:                                      21
  * bloom.c:                                                    2122
  * builtin/fsck.c:           0--3
  * builtin/index-pack.c:                                     2021
  * reflog.c:                           10--12
- * builtin/show-branch.c:    0-------------------------------------------26
+ * builtin/show-branch.c:    0-----------------------------------------------28
  * builtin/unpack-objects.c:                                 2021
  * pack-bitmap.h:                                              2122
  */

From f767dae3e6c8359128d0ec83acd009751e92e419 Mon Sep 17 00:00:00 2001
From: Kristofer Karlsson 
Date: Mon, 25 May 2026 14:28:04 +0000
Subject: [PATCH 036/106] commit-reach: deduplicate queue entries in
 paint_down_to_common

paint_down_to_common() can enqueue the same commit multiple times
when it is reached through different parents with different flag
combinations. Add an ENQUEUED flag to track whether a commit is
currently in the priority queue, and skip it if already present.

Introduce prio_queue_put_dedup() and prio_queue_get_dedup()
wrappers that manage the ENQUEUED flag on enqueue and dequeue.

This change is performance-neutral on its own: the O(n)
queue_has_nonstale() scan still dominates the per-iteration cost.
However, the deduplication guarantee (each commit appears in the
queue at most once) is a prerequisite for the next commit, which
replaces that scan with O(1) tracking.

Signed-off-by: Kristofer Karlsson 
Signed-off-by: Junio C Hamano 
---
 commit-reach.c | 27 ++++++++++++++++++++++-----
 object.h       |  2 +-
 2 files changed, 23 insertions(+), 6 deletions(-)

diff --git a/commit-reach.c b/commit-reach.c
index d3a9b3ed6fe561..31e6110b138d8f 100644
--- a/commit-reach.c
+++ b/commit-reach.c
@@ -17,8 +17,9 @@
 #define PARENT2		(1u<<17)
 #define STALE		(1u<<18)
 #define RESULT		(1u<<19)
+#define ENQUEUED	(1u<<20)
 
-static const unsigned all_flags = (PARENT1 | PARENT2 | STALE | RESULT);
+static const unsigned all_flags = (PARENT1 | PARENT2 | STALE | RESULT | ENQUEUED);
 
 static int compare_commits_by_gen(const void *_a, const void *_b)
 {
@@ -39,6 +40,22 @@ static int compare_commits_by_gen(const void *_a, const void *_b)
 	return 0;
 }
 
+static void prio_queue_put_dedup(struct prio_queue *queue, struct commit *c)
+{
+	if (c->object.flags & ENQUEUED)
+		return;
+	c->object.flags |= ENQUEUED;
+	prio_queue_put(queue, c);
+}
+
+static struct commit *prio_queue_get_dedup(struct prio_queue *queue)
+{
+	struct commit *commit = prio_queue_get(queue);
+	if (commit)
+		commit->object.flags &= ~ENQUEUED;
+	return commit;
+}
+
 static int queue_has_nonstale(struct prio_queue *queue)
 {
 	for (size_t i = 0; i < queue->nr; i++) {
@@ -70,15 +87,15 @@ static int paint_down_to_common(struct repository *r,
 		commit_list_append(one, result);
 		return 0;
 	}
-	prio_queue_put(&queue, one);
+	prio_queue_put_dedup(&queue, one);
 
 	for (i = 0; i < n; i++) {
 		twos[i]->object.flags |= PARENT2;
-		prio_queue_put(&queue, twos[i]);
+		prio_queue_put_dedup(&queue, twos[i]);
 	}
 
 	while (queue_has_nonstale(&queue)) {
-		struct commit *commit = prio_queue_get(&queue);
+		struct commit *commit = prio_queue_get_dedup(&queue);
 		struct commit_list *parents;
 		int flags;
 		timestamp_t generation = commit_graph_generation(commit);
@@ -124,7 +141,7 @@ static int paint_down_to_common(struct repository *r,
 					     oid_to_hex(&p->object.oid));
 			}
 			p->object.flags |= flags;
-			prio_queue_put(&queue, p);
+			prio_queue_put_dedup(&queue, p);
 		}
 	}
 
diff --git a/object.h b/object.h
index 2b26de3044846b..8fb03ff90a3e56 100644
--- a/object.h
+++ b/object.h
@@ -75,7 +75,7 @@ void object_array_init(struct object_array *array);
  * bundle.c:                                        16
  * http-push.c:                          11-----14
  * commit-graph.c:                                15
- * commit-reach.c:                                  16-----19
+ * commit-reach.c:                                  16-------20
  * builtin/last-modified.c:                         1617
  * object-name.c:                                            20
  * list-objects-filter.c:                                      21

From a186b7797a8bd4b9ca09b9cb326a2dccee00f90e Mon Sep 17 00:00:00 2001
From: Kristofer Karlsson 
Date: Mon, 25 May 2026 14:28:05 +0000
Subject: [PATCH 037/106] commit-reach: replace queue_has_nonstale() scan with
 O(1) tracking
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

paint_down_to_common() and ahead_behind() call queue_has_nonstale()
on every iteration to decide whether to continue the walk.
queue_has_nonstale() performs a linear scan of the priority queue,
making the overall walk O(n*m) where n is the number of commits
walked and m is the queue size.

Introduce 'struct nonstale_queue', a thin wrapper around prio_queue
that maintains a 'max_nonstale' pointer — the lowest-priority
(oldest) non-stale commit seen so far. When this commit is popped,
every remaining queue entry is known to be stale, so the walk can
stop. This reduces the per-iteration termination check from O(m)
to O(1).

Uses <= 0 (not < 0) when comparing priorities so that among distinct
commits with equal priority (same generation and timestamp) the
last-enqueued one is tracked. Since prio_queue breaks ties by
insertion order, this ensures max_nonstale is always the last in its
priority class to be popped, making pointer equality on pop
sufficient for correctness.

The previous commit's ENQUEUED deduplication guarantees each commit
appears at most once in the queue, which is required for the pointer
equality check to be unambiguous.

On a large monorepo (3.7M commits), this yields ~2x end-to-end
speedup for merge-base calculations on deep import branches.
Profiling shows paint_down_to_common() drops from 50% to 4% of
total runtime (~27x faster), with the remaining time in commit
graph lookups and heap operations:

  Before: 8536ms / 5757ms / 4743ms  (three test cases)
  After:  3956ms / 4383ms / 1927ms

Suggested-by: Jeff King 
Signed-off-by: Kristofer Karlsson 
Signed-off-by: Junio C Hamano 
---
 commit-reach.c | 96 ++++++++++++++++++++++++++++++++++----------------
 1 file changed, 65 insertions(+), 31 deletions(-)

diff --git a/commit-reach.c b/commit-reach.c
index 31e6110b138d8f..2e86e1e80316a9 100644
--- a/commit-reach.c
+++ b/commit-reach.c
@@ -40,32 +40,62 @@ static int compare_commits_by_gen(const void *_a, const void *_b)
 	return 0;
 }
 
-static void prio_queue_put_dedup(struct prio_queue *queue, struct commit *c)
+/*
+ * A prio_queue with O(1) termination check.  'max_nonstale' tracks
+ * the lowest-priority non-stale commit enqueued so far; once it is
+ * popped, every remaining entry is known to be STALE.
+ */
+struct nonstale_queue {
+	struct prio_queue pq;
+	struct commit *max_nonstale;
+};
+
+static void nonstale_queue_put(struct nonstale_queue *queue,
+			       struct commit *c)
+{
+	struct commit *old = queue->max_nonstale;
+
+	prio_queue_put(&queue->pq, c);
+	if (c->object.flags & STALE)
+		return;
+	if (!old || queue->pq.compare(old, c, queue->pq.cb_data) <= 0)
+		queue->max_nonstale = c;
+}
+
+static struct commit *nonstale_queue_get(struct nonstale_queue *queue)
+{
+	struct commit *commit = prio_queue_get(&queue->pq);
+
+	if (commit == queue->max_nonstale)
+		queue->max_nonstale = NULL;
+
+	return commit;
+}
+
+static void clear_nonstale_queue(struct nonstale_queue *queue)
+{
+	clear_prio_queue(&queue->pq);
+	queue->max_nonstale = NULL;
+}
+
+static void nonstale_queue_put_dedup(struct nonstale_queue *queue,
+				     struct commit *c)
 {
 	if (c->object.flags & ENQUEUED)
 		return;
 	c->object.flags |= ENQUEUED;
-	prio_queue_put(queue, c);
+	nonstale_queue_put(queue, c);
 }
 
-static struct commit *prio_queue_get_dedup(struct prio_queue *queue)
+static struct commit *nonstale_queue_get_dedup(struct nonstale_queue *queue)
 {
-	struct commit *commit = prio_queue_get(queue);
+	struct commit *commit = nonstale_queue_get(queue);
+
 	if (commit)
 		commit->object.flags &= ~ENQUEUED;
 	return commit;
 }
 
-static int queue_has_nonstale(struct prio_queue *queue)
-{
-	for (size_t i = 0; i < queue->nr; i++) {
-		struct commit *commit = queue->array[i].data;
-		if (!(commit->object.flags & STALE))
-			return 1;
-	}
-	return 0;
-}
-
 /* all input commits in one and twos[] must have been parsed! */
 static int paint_down_to_common(struct repository *r,
 				struct commit *one, int n,
@@ -74,28 +104,30 @@ static int paint_down_to_common(struct repository *r,
 				int ignore_missing_commits,
 				struct commit_list **result)
 {
-	struct prio_queue queue = { compare_commits_by_gen_then_commit_date };
+	struct nonstale_queue queue = {
+		{ compare_commits_by_gen_then_commit_date }
+	};
 	int i;
 	timestamp_t last_gen = GENERATION_NUMBER_INFINITY;
 	struct commit_list **tail = result;
 
 	if (!min_generation && !corrected_commit_dates_enabled(r))
-		queue.compare = compare_commits_by_commit_date;
+		queue.pq.compare = compare_commits_by_commit_date;
 
 	one->object.flags |= PARENT1;
 	if (!n) {
 		commit_list_append(one, result);
 		return 0;
 	}
-	prio_queue_put_dedup(&queue, one);
+	nonstale_queue_put_dedup(&queue, one);
 
 	for (i = 0; i < n; i++) {
 		twos[i]->object.flags |= PARENT2;
-		prio_queue_put_dedup(&queue, twos[i]);
+		nonstale_queue_put_dedup(&queue, twos[i]);
 	}
 
-	while (queue_has_nonstale(&queue)) {
-		struct commit *commit = prio_queue_get_dedup(&queue);
+	while (queue.max_nonstale) {
+		struct commit *commit = nonstale_queue_get_dedup(&queue);
 		struct commit_list *parents;
 		int flags;
 		timestamp_t generation = commit_graph_generation(commit);
@@ -125,7 +157,7 @@ static int paint_down_to_common(struct repository *r,
 			if ((p->object.flags & flags) == flags)
 				continue;
 			if (repo_parse_commit(r, p)) {
-				clear_prio_queue(&queue);
+				clear_nonstale_queue(&queue);
 				commit_list_free(*result);
 				*result = NULL;
 				/*
@@ -141,11 +173,11 @@ static int paint_down_to_common(struct repository *r,
 					     oid_to_hex(&p->object.oid));
 			}
 			p->object.flags |= flags;
-			prio_queue_put_dedup(&queue, p);
+			nonstale_queue_put_dedup(&queue, p);
 		}
 	}
 
-	clear_prio_queue(&queue);
+	clear_nonstale_queue(&queue);
 	commit_list_sort_by_date(result);
 	return 0;
 }
@@ -1039,11 +1071,11 @@ struct commit_list *get_reachable_subset(struct commit **from, size_t nr_from,
 define_commit_slab(bit_arrays, struct bitmap *);
 static struct bit_arrays bit_arrays;
 
-static void insert_no_dup(struct prio_queue *queue, struct commit *c)
+static void insert_no_dup(struct nonstale_queue *queue, struct commit *c)
 {
 	if (c->object.flags & PARENT2)
 		return;
-	prio_queue_put(queue, c);
+	nonstale_queue_put(queue, c);
 	c->object.flags |= PARENT2;
 }
 
@@ -1068,7 +1100,9 @@ void ahead_behind(struct repository *r,
 		  struct commit **commits, size_t commits_nr,
 		  struct ahead_behind_count *counts, size_t counts_nr)
 {
-	struct prio_queue queue = { .compare = compare_commits_by_gen_then_commit_date };
+	struct nonstale_queue queue = {
+		{ .compare = compare_commits_by_gen_then_commit_date }
+	};
 	size_t width = DIV_ROUND_UP(commits_nr, BITS_IN_EWORD);
 
 	if (!commits_nr || !counts_nr)
@@ -1091,8 +1125,8 @@ void ahead_behind(struct repository *r,
 		insert_no_dup(&queue, c);
 	}
 
-	while (queue_has_nonstale(&queue)) {
-		struct commit *c = prio_queue_get(&queue);
+	while (queue.max_nonstale) {
+		struct commit *c = nonstale_queue_get(&queue);
 		struct commit_list *p;
 		struct bitmap *bitmap_c = get_bit_array(c, width);
 
@@ -1134,10 +1168,10 @@ void ahead_behind(struct repository *r,
 
 	/* STALE is used here, PARENT2 is used by insert_no_dup(). */
 	repo_clear_commit_marks(r, PARENT2 | STALE);
-	for (size_t i = 0; i < queue.nr; i++)
-		free_bit_array(queue.array[i].data);
+	for (size_t i = 0; i < queue.pq.nr; i++)
+		free_bit_array(queue.pq.array[i].data);
 	clear_bit_arrays(&bit_arrays);
-	clear_prio_queue(&queue);
+	clear_nonstale_queue(&queue);
 }
 
 struct commit_and_index {

From 44d04e442683b53ed618b74861f056f93fcbd783 Mon Sep 17 00:00:00 2001
From: Alyssa Ross 
Date: Mon, 25 May 2026 18:23:12 +0200
Subject: [PATCH 038/106] receive-pack: fix updateInstead with core.worktree

Before a8cc594333 (hooks: fix an obscure TOCTOU "did we just run a
hook?" race, 2022-03-07), when receive.denyCurrentBranch is set to
updateInstead, only one of push_to_checkout() or push_to_deploy()
was called.  That commit changed to always call push_to_checkout(),
and then to call push_to_deploy() if push_to_checkout() didn't run
anything.

This change didn't take into account that push_to_checkout() had a
side effect of modifying env, and that modified env broke updating
the worktree in push_to_deploy() if core.worktree was configured.
To fix this, only mutate the environment used inside
push_to_commit(), rather than the environment that might later be
passed to push_to_deploy().

Signed-off-by: Alyssa Ross 
Signed-off-by: Junio C Hamano 
---
 builtin/receive-pack.c |  2 +-
 t/t5516-fetch-push.sh  | 11 +++++++++++
 2 files changed, 12 insertions(+), 1 deletion(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 9c491746168a6f..9fbfa15a516077 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -1427,8 +1427,8 @@ static const char *push_to_checkout(unsigned char *hash,
 	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
 	opt.invoked_hook = invoked_hook;
 
-	strvec_pushf(env, "GIT_WORK_TREE=%s", absolute_path(work_tree));
 	strvec_pushv(&opt.env, env->v);
+	strvec_pushf(&opt.env, "GIT_WORK_TREE=%s", absolute_path(work_tree));
 	strvec_push(&opt.args, hash_to_hex(hash));
 	if (run_hooks_opt(the_repository, push_to_checkout_hook, &opt))
 		return "push-to-checkout hook declined";
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 46926e7bbd3a9a..a85b1fbf8b5d47 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1791,6 +1791,17 @@ test_expect_success 'updateInstead with push-to-checkout hook' '
 	)
 '
 
+test_expect_success 'denyCurrentBranch and core.worktree' '
+	test_when_finished "rm -fr cloned cloned.git" &&
+	git clone --separate-git-dir cloned.git . cloned &&
+	git --git-dir cloned.git config receive.denyCurrentBranch updateInstead &&
+	git --git-dir cloned.git config core.worktree "$PWD/cloned" &&
+	test_commit raspberry &&
+	git push cloned.git HEAD:main &&
+	test_path_exists cloned/raspberry.t &&
+	test_must_fail git push --delete cloned.git main
+'
+
 test_expect_success 'denyCurrentBranch and worktrees' '
 	git worktree add new-wt &&
 	git clone . cloned &&

From b2040bfafe0f7bbbd21cf65a903d2346d602f421 Mon Sep 17 00:00:00 2001
From: Ivan Baluta 
Date: Tue, 26 May 2026 03:58:07 +0000
Subject: [PATCH 039/106] doc: clarify push.default=simple behavior

The documentation for the 'simple' push mode currently singles out
the centralized workflow, which can cause confusion about its
behavior in other scenarios, such as triangular workflows.

Clarify that 'simple' always pushes the current branch to a branch
of the same name, but only enforces the strict upstream tracking
requirement when pushing back to the same remote being pulled from.

Suggested-by: Junio C Hamano 
Signed-off-by: Ivan Baluta 
Signed-off-by: Junio C Hamano 
---
 Documentation/config/push.adoc | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/Documentation/config/push.adoc b/Documentation/config/push.adoc
index d9112b22609b51..28132eedfee6c0 100644
--- a/Documentation/config/push.adoc
+++ b/Documentation/config/push.adoc
@@ -41,9 +41,10 @@ this is a deprecated synonym for `upstream`.
 `simple`;;
 push the current branch with the same name on the remote.
 +
-If you are working on a centralized workflow (pushing to the same repository you
-pull from, which is typically `origin`), then you need to configure an upstream
-branch with the same name.
+This mode requires that the remote repository to be pushed to is
+known.  When pushing back to the same remote you pull from, the
+current branch must also have an upstream tracking branch with the
+same name.
 +
 This mode is the default since Git 2.0, and is the safest option suited for
 beginners.

From 96d1225ad904bf865fecc89ddfde62e1f4281c19 Mon Sep 17 00:00:00 2001
From: Zakariyah Ali 
Date: Tue, 26 May 2026 15:23:07 +0000
Subject: [PATCH 040/106] completion: hide dotfiles for selected path
 completion

The completion helper for index paths uses git ls-files rather than
shell filename completion. As a result, leading-dot paths such as a
tracked .gitignore were offered even when the user had not started the
path with ".".

Hide leading-dot path components for git rm, git mv, and git ls-files
when completing an empty path component. Explicit dot completion is
still preserved, so git rm . can still complete .gitignore.

This matches standard shell filename completion behavior, where dotfiles
are hidden by default unless the user starts their input with a dot.
This also resolves four TODO comments in t/9902-completion.sh which
have been present since 2013 (commit ddf07bddef9a, "completion: add file
completion tests", 2013-04-27), expecting that .gitignore would not be
shown when completing on an empty path component.

Signed-off-by: Zakariyah Ali 
Signed-off-by: Junio C Hamano 
---
 contrib/completion/git-completion.bash | 36 +++++++++++++++++---------
 t/t9902-completion.sh                  | 10 ++-----
 2 files changed, 26 insertions(+), 20 deletions(-)

diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
index a8e7c6ddbfb2b1..e8f8fab125b42b 100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -638,25 +638,33 @@ __git_ls_files_helper ()
 }
 
 
-# __git_index_files accepts 1 or 2 arguments:
+# __git_index_files accepts 1 to 4 arguments:
 # 1: Options to pass to ls-files (required).
 # 2: A directory path (optional).
 #    If provided, only files within the specified directory are listed.
 #    Sub directories are never recursed.  Path must have a trailing
 #    slash.
 # 3: List only paths matching this path component (optional).
+# 4: Hide paths whose first component starts with a dot if this is
+#    "hide-dotfiles" and the third argument is empty (optional).
 __git_index_files ()
 {
-	local root="$2" match="$3"
+	local root="$2" match="$3" hide_dotfiles="${4-}"
+	local hide_dotfiles_awk=0
+	if [ "$hide_dotfiles" = "hide-dotfiles" ] && [ -z "$match" ]; then
+		hide_dotfiles_awk=1
+	fi
 
 	__git_ls_files_helper "$root" "$1" "${match:-?}" |
-	awk -F / -v pfx="${2//\\/\\\\}" '{
+	awk -F / -v pfx="${2//\\/\\\\}" -v hide_dotfiles="$hide_dotfiles_awk" '{
 		paths[$1] = 1
 	}
 	END {
 		for (p in paths) {
 			if (substr(p, 1, 1) != "\"") {
 				# No special characters, easy!
+				if (hide_dotfiles == 1 && substr(p, 1, 1) == ".")
+					continue
 				print pfx p
 				continue
 			}
@@ -675,8 +683,10 @@ __git_index_files ()
 				# We have seen the same directory unquoted,
 				# skip it.
 				continue
-			else
-				print pfx p
+
+			if (hide_dotfiles == 1 && substr(p, 1, 1) == ".")
+				continue
+			print pfx p
 		}
 	}
 	function dequote(p,    bs_idx, out, esc, esc_idx, dec) {
@@ -721,13 +731,15 @@ __git_index_files ()
 	}'
 }
 
-# __git_complete_index_file requires 1 argument:
+# __git_complete_index_file accepts 1 or 2 arguments:
 # 1: the options to pass to ls-file
+# 2: Hide paths whose first component starts with a dot if this is
+#    "hide-dotfiles" and the current word is empty (optional).
 #
 # The exception is --committable, which finds the files appropriate commit.
 __git_complete_index_file ()
 {
-	local dequoted_word pfx="" cur_
+	local dequoted_word pfx="" cur_ hide_dotfiles="${2-}"
 
 	__git_dequote "$cur"
 
@@ -740,7 +752,7 @@ __git_complete_index_file ()
 		cur_="$dequoted_word"
 	esac
 
-	__gitcomp_file_direct "$(__git_index_files "$1" "$pfx" "$cur_")"
+	__gitcomp_file_direct "$(__git_index_files "$1" "$pfx" "$cur_" "$hide_dotfiles")"
 }
 
 # Lists branches from the local repository.
@@ -2164,7 +2176,7 @@ _git_ls_files ()
 
 	# XXX ignore options like --modified and always suggest all cached
 	# files.
-	__git_complete_index_file "--cached"
+	__git_complete_index_file "--cached" hide-dotfiles
 }
 
 _git_ls_remote ()
@@ -2397,9 +2409,9 @@ _git_mv ()
 	if [ $(__git_count_arguments "mv") -gt 0 ]; then
 		# We need to show both cached and untracked files (including
 		# empty directories) since this may not be the last argument.
-		__git_complete_index_file "--cached --others --directory"
+		__git_complete_index_file "--cached --others --directory" hide-dotfiles
 	else
-		__git_complete_index_file "--cached"
+		__git_complete_index_file "--cached" hide-dotfiles
 	fi
 }
 
@@ -3219,7 +3231,7 @@ _git_rm ()
 		;;
 	esac
 
-	__git_complete_index_file "--cached"
+	__git_complete_index_file "--cached" hide-dotfiles
 }
 
 _git_shortlog ()
diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh
index 28f61f08fb4cec..02aaf71876ea0a 100755
--- a/t/t9902-completion.sh
+++ b/t/t9902-completion.sh
@@ -2811,17 +2811,15 @@ test_expect_success 'complete files' '
 
 	touch untracked &&
 
-	: TODO .gitignore should not be here &&
 	test_completion "git rm " <<-\EOF &&
-	.gitignore
 	modified
 	EOF
 
+	test_completion "git rm ." ".gitignore" &&
+
 	test_completion "git clean " "untracked" &&
 
-	: TODO .gitignore should not be here &&
 	test_completion "git mv " <<-\EOF &&
-	.gitignore
 	modified
 	EOF
 
@@ -2832,9 +2830,7 @@ test_expect_success 'complete files' '
 
 	mkdir untracked-dir &&
 
-	: TODO .gitignore should not be here &&
 	test_completion "git mv modified " <<-\EOF &&
-	.gitignore
 	dir
 	modified
 	untracked
@@ -2843,9 +2839,7 @@ test_expect_success 'complete files' '
 
 	test_completion "git commit " "modified" &&
 
-	: TODO .gitignore should not be here &&
 	test_completion "git ls-files " <<-\EOF &&
-	.gitignore
 	dir
 	modified
 	EOF

From ca7b9ae3403b1a46fffbdae312092c4029470eee Mon Sep 17 00:00:00 2001
From: Derrick Stolee 
Date: Tue, 26 May 2026 20:26:33 +0000
Subject: [PATCH 041/106] t1092: test 'git restore' with sparse index

A user reported that 'git restore --staged .' causes the sparse index to
expand. This is somewhat natural because the '.' pathspec means 'check
every path'. However, the restore will not update paths marked with the
SKIP_WORKTREE bit, so we shouldn't need to process such entries.

For now, establish the current behavior, including the sparse index
expansion, in the t1092 test case as a baseline.

Signed-off-by: Derrick Stolee 
Signed-off-by: Junio C Hamano 
---
 t/t1092-sparse-checkout-compatibility.sh | 50 ++++++++++++++++++++++++
 1 file changed, 50 insertions(+)

diff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh
index d98cb4ac113c67..d69434e7ab6ce0 100755
--- a/t/t1092-sparse-checkout-compatibility.sh
+++ b/t/t1092-sparse-checkout-compatibility.sh
@@ -2573,4 +2573,54 @@ test_expect_success 'sparse-index is not expanded: merge-ours' '
 	ensure_not_expanded merge -s ours merge-right
 '
 
+test_expect_success 'restore --staged with sparse definition' '
+	init_repos &&
+
+	# Stage changes within the sparse definition
+	test_all_match git checkout -b restore-staged-1 base &&
+	test_all_match git reset --soft update-deep &&
+	test_all_match git restore --staged . &&
+	test_all_match git status --porcelain=v2 &&
+	test_all_match git diff --cached
+'
+
+test_expect_success 'restore --staged with outside sparse definition' '
+	init_repos &&
+
+	# Stage changes that include paths outside the sparse definition.
+	# Although the working tree differs between full and sparse checkouts
+	# after restore, the state of the index should be the same.
+	test_all_match git checkout -b restore-staged-2 base &&
+	test_all_match git reset --soft update-folder1 &&
+	test_sparse_match git restore --staged . &&
+	git -C full-checkout restore --staged . &&
+	test_all_match git ls-files -s -- folder1 &&
+	test_all_match git diff --cached -- folder1
+'
+
+test_expect_success 'restore --staged with wildcards' '
+	init_repos &&
+
+	test_all_match git checkout -b restore-staged-3 base &&
+	test_all_match git reset --soft update-deep &&
+	test_all_match git restore --staged "deep/*" &&
+	test_all_match git status --porcelain=v2 &&
+	test_all_match git diff --cached
+'
+
+test_expect_success 'sparse-index is expanded: restore --staged' '
+	init_repos &&
+
+	git -C sparse-index checkout -b restore-staged-exp base &&
+	git -C sparse-index reset --soft update-folder1 &&
+	ensure_expanded restore --staged .
+'
+
+test_expect_success 'sparse-index is expanded: restore --source --staged' '
+	init_repos &&
+
+	git -C sparse-index checkout -b restore-source-staged base &&
+	ensure_expanded restore --source update-folder1 --staged .
+'
+
 test_done

From 105aacd072e41c55948d016b46f86f75db2487b3 Mon Sep 17 00:00:00 2001
From: Derrick Stolee 
Date: Tue, 26 May 2026 20:26:34 +0000
Subject: [PATCH 042/106] restore: avoid sparse index expansion

Teach update_some() to handle sparse directory entries at the tree
level rather than expanding the entire sparse index. When iterating a
source tree during checkout/restore operations:

 - If a directory matches a sparse directory entry with the same OID,
   skip it entirely (no change needed).

 - If the OID differs and we are in non-overlay mode (e.g., restore
   --staged), update the sparse directory entry's OID in place. This
   is semantically correct because non-overlay mode removes paths not
   in the source tree anyway.

 - In overlay mode (e.g., checkout  -- .), fall through to
   recursive descent so individual file entries are preserved
   correctly.

Also switch from index_name_pos() to index_name_pos_sparse() for
individual file lookups to avoid triggering ensure_full_index() when
the file is already individually tracked in the index.

Update the test expectation in t1092 to assert that 'restore --staged'
no longer expands the sparse index.

Signed-off-by: Derrick Stolee 
Signed-off-by: Junio C Hamano 
---
 builtin/checkout.c                       | 65 +++++++++++++++++++++---
 t/t1092-sparse-checkout-compatibility.sh |  8 +--
 2 files changed, 63 insertions(+), 10 deletions(-)

diff --git a/builtin/checkout.c b/builtin/checkout.c
index 1345e8574a79c8..86e23a07b11695 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -31,6 +31,7 @@
 #include "revision.h"
 #include "sequencer.h"
 #include "setup.h"
+#include "sparse-index.h"
 #include "strvec.h"
 #include "submodule.h"
 #include "symlinks.h"
@@ -141,15 +142,65 @@ static int post_checkout_hook(struct commit *old_commit, struct commit *new_comm
 	return run_hooks_opt(the_repository, "post-checkout", &opt);
 }
 
+/*
+ * Handle a tree object and determine if we need to recurse into the
+ * tree (READ_TREE_RECURSIVE) or skip it (0).
+ */
+static int try_update_sparse_directory(const struct object_id *oid,
+				       struct strbuf *base,
+				       const char *pathname,
+				       int overlay_mode)
+{
+	struct strbuf dirpath = STRBUF_INIT;
+	struct cache_entry *old;
+	int pos, result = READ_TREE_RECURSIVE;
+
+	if (!the_repository->index->sparse_index)
+		return result;
+
+	strbuf_addbuf(&dirpath, base);
+	strbuf_addstr(&dirpath, pathname);
+	strbuf_addch(&dirpath, '/');
+
+	pos = index_name_pos_sparse(the_repository->index,
+				    dirpath.buf, dirpath.len);
+	if (pos < 0)
+		goto cleanup;
+
+	old = the_repository->index->cache[pos];
+	if (!S_ISSPARSEDIR(old->ce_mode))
+		goto cleanup;
+
+	if (oideq(oid, &old->oid)) {
+		/* Tree content already matches; no need to descend. */
+		result = 0;
+	} else if (!overlay_mode) {
+		/*
+		 * In non-overlay mode (e.g., restore --staged), replace the
+		 * sparse directory OID directly since files not present in
+		 * the source tree should be removed anyway.
+		 */
+		oidcpy(&old->oid, oid);
+		old->ce_flags |= CE_UPDATE;
+		result = 0;
+	}
+
+cleanup:
+	strbuf_release(&dirpath);
+	return result;
+}
+
 static int update_some(const struct object_id *oid, struct strbuf *base,
-		       const char *pathname, unsigned mode, void *context UNUSED)
+		       const char *pathname, unsigned mode, void *context)
 {
 	int len;
 	struct cache_entry *ce;
 	int pos;
+	int overlay_mode = context ? *((int *)context) : 1;
 
 	if (S_ISDIR(mode))
-		return READ_TREE_RECURSIVE;
+		return try_update_sparse_directory(oid, base, pathname,
+						   overlay_mode);
 
 	len = base->len + strlen(pathname);
 	ce = make_empty_cache_entry(the_repository->index, len);
@@ -165,7 +216,7 @@ static int update_some(const struct object_id *oid, struct strbuf *base,
 	 * entry in place. Whether it is UPTODATE or not, checkout_entry will
 	 * do the right thing.
 	 */
-	pos = index_name_pos(the_repository->index, ce->name, ce->ce_namelen);
+	pos = index_name_pos_sparse(the_repository->index, ce->name, ce->ce_namelen);
 	if (pos >= 0) {
 		struct cache_entry *old = the_repository->index->cache[pos];
 		if (ce->ce_mode == old->ce_mode &&
@@ -182,10 +233,11 @@ static int update_some(const struct object_id *oid, struct strbuf *base,
 	return 0;
 }
 
-static int read_tree_some(struct tree *tree, const struct pathspec *pathspec)
+static int read_tree_some(struct tree *tree, const struct pathspec *pathspec,
+			  int overlay_mode)
 {
 	read_tree(the_repository, tree,
-		  pathspec, update_some, NULL);
+		  pathspec, update_some, &overlay_mode);
 
 	/* update the index with the given tree's info
 	 * for all args, expanding wildcards, and exit
@@ -580,7 +632,8 @@ static int checkout_paths(const struct checkout_opts *opts,
 		return error(_("index file corrupt"));
 
 	if (opts->source_tree)
-		read_tree_some(opts->source_tree, &opts->pathspec);
+		read_tree_some(opts->source_tree, &opts->pathspec,
+			       opts->overlay_mode);
 	if (opts->merge)
 		unmerge_index(the_repository->index, &opts->pathspec, CE_MATCHED);
 
diff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh
index d69434e7ab6ce0..8186da5c887c56 100755
--- a/t/t1092-sparse-checkout-compatibility.sh
+++ b/t/t1092-sparse-checkout-compatibility.sh
@@ -2608,19 +2608,19 @@ test_expect_success 'restore --staged with wildcards' '
 	test_all_match git diff --cached
 '
 
-test_expect_success 'sparse-index is expanded: restore --staged' '
+test_expect_success 'sparse-index is not expanded: restore --staged' '
 	init_repos &&
 
 	git -C sparse-index checkout -b restore-staged-exp base &&
 	git -C sparse-index reset --soft update-folder1 &&
-	ensure_expanded restore --staged .
+	ensure_not_expanded restore --staged .
 '
 
-test_expect_success 'sparse-index is expanded: restore --source --staged' '
+test_expect_success 'sparse-index is not expanded: restore --source --staged' '
 	init_repos &&
 
 	git -C sparse-index checkout -b restore-source-staged base &&
-	ensure_expanded restore --source update-folder1 --staged .
+	ensure_not_expanded restore --source update-folder1 --staged .
 '
 
 test_done

From e3959cc78c968d8f029daa48d4aadcb486da0629 Mon Sep 17 00:00:00 2001
From: Taylor Blau 
Date: Wed, 27 May 2026 15:55:50 -0400
Subject: [PATCH 043/106] pack-bitmap: pass object position to
 `fill_bitmap_tree()`

In the following commit, callers of `fill_bitmap_tree()` will be
required to check the bit corresponding to their tree before calling
that function. That change will reduce the overhead of setting up and
tearing down stack frames for trees whose bits are already set.

To prepare for that change, have callers pass in the tree's bit position
in `fill_bitmap_tree()`, which will make the next commit easier to read.

In the meantime, this change has a surprising and measurable benefit
during bitmap generation, particularly on very large repositories.

When processing sub-trees within `fill_bitmap_tree()`, the preimage of
this patch did the following:

    while (tree_entry(&desc, entry)) {
        switch (object_type(entry.mode)) {
        case OBJ_TREE:
            if (fill_bitmap_tree(writer, bitmap,
                                 lookup_tree(writer->repo,
                                             &entry.oid)) < 0) {
                /* ... */
            }
            /* ... */
        }
    }

, first performing the object lookup via `lookup_tree()`, and then
locating its bit position within the recursive call. This patch
effectively reorders those two calls so that we first discover the
sub-tree's bit position, *then* load its tree.

By reordering these two operations, we spend fewer CPU cycles per
instruction, likely due to improved CPU dependency/cache/pipeline
behavior. Comparing the results of: running `perf stat` before and after
this commit, we have:

    +--------------+-------------+-------------+-------------------+
    |              | HEAD^       | HEAD        | Delta             |
    +--------------+-------------+-------------+-------------------+
    | elapsed      |   612.5 s   |   582.4 s   |  -30.1 s  (-4.9%) |
    | cycles       | 2,857.3 B   | 2,713.3 B   | -144.0 B  (-5.0%) |
    | instructions | 2,413.2 B   | 2,415.5 B   |   +2.3 B  (+0.1%) |
    | CPI          |     1.184   |     1.123   |  -0.061   (-5.1%) |
    +--------------+-------------+-------------+-------------------+

In a large repository with ~4.8M commit, and ~37.1M tree objects this
change improves timing from ~612.5 seconds down to ~582.4 seconds, or a
~4.9% improvement. More importantly, the number of CPU cycles spent
dropped off significantly as a result of this commit, lowering our
cycles-per-instruction ratio by about ~5.1%.

Signed-off-by: Taylor Blau 
Signed-off-by: Junio C Hamano 
---
 pack-bitmap-write.c | 23 +++++++++++++++--------
 1 file changed, 15 insertions(+), 8 deletions(-)

diff --git a/pack-bitmap-write.c b/pack-bitmap-write.c
index 1c8070f99c03ca..2d5ff8fd406db9 100644
--- a/pack-bitmap-write.c
+++ b/pack-bitmap-write.c
@@ -456,10 +456,10 @@ static void bitmap_builder_clear(struct bitmap_builder *bb)
 
 static int fill_bitmap_tree(struct bitmap_writer *writer,
 			    struct bitmap *bitmap,
-			    struct tree *tree)
+			    struct tree *tree,
+			    uint32_t pos)
 {
 	int found;
-	uint32_t pos;
 	struct tree_desc desc;
 	struct name_entry entry;
 
@@ -467,9 +467,6 @@ static int fill_bitmap_tree(struct bitmap_writer *writer,
 	 * If our bit is already set, then there is nothing to do. Both this
 	 * tree and all of its children will be set.
 	 */
-	pos = find_object_pos(writer, &tree->object.oid, &found);
-	if (!found)
-		return -1;
 	if (bitmap_get(bitmap, pos))
 		return 0;
 	bitmap_set(bitmap, pos);
@@ -482,8 +479,12 @@ static int fill_bitmap_tree(struct bitmap_writer *writer,
 	while (tree_entry(&desc, &entry)) {
 		switch (object_type(entry.mode)) {
 		case OBJ_TREE:
+			pos = find_object_pos(writer, &entry.oid, &found);
+			if (!found)
+				return -1;
 			if (fill_bitmap_tree(writer, bitmap,
-					     lookup_tree(writer->repo, &entry.oid)) < 0)
+					     lookup_tree(writer->repo,
+							 &entry.oid), pos) < 0)
 				return -1;
 			break;
 		case OBJ_BLOB:
@@ -575,8 +576,14 @@ static int fill_bitmap_commit(struct bitmap_writer *writer,
 	}
 
 	while (tree_queue->nr) {
-		if (fill_bitmap_tree(writer, ent->bitmap,
-				     prio_queue_get(tree_queue)) < 0)
+		struct tree *t = prio_queue_get(tree_queue);
+		int found;
+
+		pos = find_object_pos(writer, &t->object.oid, &found);
+		if (!found)
+			return -1;
+
+		if (fill_bitmap_tree(writer, ent->bitmap, t, pos) < 0)
 			return -1;
 	}
 	return 0;

From 1760c372589af09ff0b986c57bfe0b9101275674 Mon Sep 17 00:00:00 2001
From: Taylor Blau 
Date: Wed, 27 May 2026 15:55:53 -0400
Subject: [PATCH 044/106] pack-bitmap: check subtree bits before recursing

In the previous commit, we adjusted the callers of `fill_bitmap_tree()`
to pass in the bit position of the tree they wish to fill.

This commit makes use of that information at the call site to avoid
setting up a stack frame for fill_bitmap_tree() entirely whenever a
tree's bit position is already set.

Since this is such a hot path, the avoided cost of setting up and
tearing down stack frames for each noop'd call to `fill_bitmap_tree()`
is significant:

    +--------------+-------------+-------------+-------------------+
    |              | HEAD^       | HEAD        | Delta             |
    +--------------+-------------+-------------+-------------------+
    | elapsed      |   582.4 s   |   562.8 s   |  -19.6 s  (-3.4%) |
    | cycles       | 2,713.3 B   | 2,621.3 B   |  -92.0 B  (-3.4%) |
    | instructions | 2,415.5 B   | 2,348.9 B   |  -66.6 B  (-2.8%) |
    | CPI          |     1.123   |     1.116   |  -0.007   (-0.7%) |
    +--------------+-------------+-------------+-------------------+

In the same repository as in the previous commit, our timings dropped
from ~582.4 seconds down to ~562.77 seconds.

While the cycles-per-instruction ratio is basically unchanged, we
execute significantly fewer instructions, and correspondingly fewer
cycles.

Signed-off-by: Taylor Blau 
Signed-off-by: Junio C Hamano 
---
 pack-bitmap-write.c | 23 +++++++++++++++++------
 1 file changed, 17 insertions(+), 6 deletions(-)

diff --git a/pack-bitmap-write.c b/pack-bitmap-write.c
index 2d5ff8fd406db9..72610397020664 100644
--- a/pack-bitmap-write.c
+++ b/pack-bitmap-write.c
@@ -463,12 +463,6 @@ static int fill_bitmap_tree(struct bitmap_writer *writer,
 	struct tree_desc desc;
 	struct name_entry entry;
 
-	/*
-	 * If our bit is already set, then there is nothing to do. Both this
-	 * tree and all of its children will be set.
-	 */
-	if (bitmap_get(bitmap, pos))
-		return 0;
 	bitmap_set(bitmap, pos);
 
 	if (repo_parse_tree(writer->repo, tree) < 0)
@@ -482,6 +476,15 @@ static int fill_bitmap_tree(struct bitmap_writer *writer,
 			pos = find_object_pos(writer, &entry.oid, &found);
 			if (!found)
 				return -1;
+			if (bitmap_get(bitmap, pos)) {
+				/*
+				 * If our bit is already set, then there
+				 * is nothing to do. Both this tree and
+				 * all of its children will be set.
+				 */
+				break;
+			}
+
 			if (fill_bitmap_tree(writer, bitmap,
 					     lookup_tree(writer->repo,
 							 &entry.oid), pos) < 0)
@@ -582,6 +585,14 @@ static int fill_bitmap_commit(struct bitmap_writer *writer,
 		pos = find_object_pos(writer, &t->object.oid, &found);
 		if (!found)
 			return -1;
+		if (bitmap_get(ent->bitmap, pos)) {
+			/*
+			 * If our bit is already set, then there is
+			 * nothing to do. Both this tree and all of its
+			 * children will be set.
+			 */
+			continue;
+		}
 
 		if (fill_bitmap_tree(writer, ent->bitmap, t, pos) < 0)
 			return -1;

From 3ea5fe8482e44fe8636b2725edffcadc81b22161 Mon Sep 17 00:00:00 2001
From: Taylor Blau 
Date: Wed, 27 May 2026 15:55:56 -0400
Subject: [PATCH 045/106] pack-bitmap: reuse stored selected bitmaps

When `fill_bitmap_commit()` reaches an ancestor that was selected for
its own bitmap and processed earlier, its object closure is already
stored in `writer->bitmaps` as an EWAH bitmap. As a result, walking
through that commit's tree and parents again is redundant.

Teach `fill_bitmap_commit()` to notice that case. For non-root commits in
the walk, look for a stored selected bitmap and OR it into the bitmap
being built. If one exists, skip the commit, its tree, and its parents.

Building bitmaps from scratch on the same test repository from the
previous commits yields a significant speed-up:

    +------------------+-------------+-------------+---------------------+
    |                  | HEAD^       | HEAD        | Delta               |
    +------------------+-------------+-------------+---------------------+
    | elapsed          |   562.8 s   |   324.8 s   |   -237.9 s (-42.3%) |
    | cycles           | 2,621.3 B   | 1,508.6 B   | -1,112.7 B (-42.4%) |
    | instructions     | 2,348.9 B   | 1,436.6 B   |   -912.3 B (-38.8%) |
    | CPI              |     1.116   |     1.050   |   -0.066    (-5.9%) |
    +------------------+-------------+-------------+---------------------+

In our testing repository, there are 1,261 commits selected for bitmap
coverage, and 1,382 maximal commits induced as a result of that. Of the
1,382 calls made to `fill_bitmap_commit()` (one per maximal commit), 131
of them can be short-circuited at some point during their traversal as a
consequence of this change.

In large repositories where the cost of filling the bitmap for any
individual commit is large, being able to short-circuit even ~9.5% of
the calls to `fill_bitmap_commit()` results in a significant savings.

Signed-off-by: Taylor Blau 
Signed-off-by: Junio C Hamano 
---
 pack-bitmap-write.c | 34 ++++++++++++++++++++++++++++++++++
 1 file changed, 34 insertions(+)

diff --git a/pack-bitmap-write.c b/pack-bitmap-write.c
index 72610397020664..651ad467469f44 100644
--- a/pack-bitmap-write.c
+++ b/pack-bitmap-write.c
@@ -509,6 +509,9 @@ static int fill_bitmap_tree(struct bitmap_writer *writer,
 static int reused_bitmaps_nr;
 static int reused_pseudo_merge_bitmaps_nr;
 
+static int fill_bitmap_commit_calls_nr;
+static int fill_bitmap_commit_found_ancestor_nr;
+
 static int fill_bitmap_commit(struct bitmap_writer *writer,
 			      struct bb_commit *ent,
 			      struct commit *commit,
@@ -519,6 +522,9 @@ static int fill_bitmap_commit(struct bitmap_writer *writer,
 {
 	int found;
 	uint32_t pos;
+
+	fill_bitmap_commit_calls_nr++;
+
 	if (!ent->bitmap)
 		ent->bitmap = bitmap_new();
 
@@ -553,6 +559,28 @@ static int fill_bitmap_commit(struct bitmap_writer *writer,
 			bitmap_free(remapped);
 		}
 
+		/*
+		 * If we encounter an ancestor for which we have already
+		 * computed a bitmap during this build (i.e. a regular
+		 * selected commit processed earlier in topo order), we can
+		 * short-circuit the walk: its stored bitmap already covers
+		 * the commit itself, its tree, and all of its ancestors.
+		 */
+		if (c != commit) {
+			khiter_t hash_pos = kh_get_oid_map(writer->bitmaps,
+							   c->object.oid);
+			if (hash_pos != kh_end(writer->bitmaps)) {
+				struct bitmapped_commit *stored =
+					kh_value(writer->bitmaps, hash_pos);
+				if (stored && stored->bitmap) {
+					fill_bitmap_commit_found_ancestor_nr++;
+					bitmap_or_ewah(ent->bitmap,
+						       stored->bitmap);
+					continue;
+				}
+			}
+		}
+
 		/*
 		 * Mark ourselves and queue our tree. The commit
 		 * walk ensures we cover all parents.
@@ -692,6 +720,12 @@ int bitmap_writer_build(struct bitmap_writer *writer)
 	trace2_data_intmax("pack-bitmap-write", writer->repo,
 			   "building_bitmaps_pseudo_merge_reused",
 			   reused_pseudo_merge_bitmaps_nr);
+	trace2_data_intmax("pack-bitmap-write", writer->repo,
+			   "fill_bitmap_commit_calls_nr",
+			   fill_bitmap_commit_calls_nr);
+	trace2_data_intmax("pack-bitmap-write", writer->repo,
+			   "fill_bitmap_commit_found_ancestor_nr",
+			   fill_bitmap_commit_found_ancestor_nr);
 
 	stop_progress(&writer->progress);
 

From ece3465d44157157a03eb7cd5de955e552e7831c Mon Sep 17 00:00:00 2001
From: Taylor Blau 
Date: Wed, 27 May 2026 15:55:59 -0400
Subject: [PATCH 046/106] pack-bitmap: consolidate `find_object_pos()` success
 path

Both sides of `find_object_pos()` report success in the same way by
setting the optional `found` out-parameter and return the resolved
bitmap position.

Prepare for adding more bookkeeping around object-position lookups by
storing the result in a local `pos` variable and sharing the success
return path between the packlist and MIDX cases.

Signed-off-by: Taylor Blau 
Signed-off-by: Junio C Hamano 
---
 pack-bitmap-write.c | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/pack-bitmap-write.c b/pack-bitmap-write.c
index 651ad467469f44..42ed22feacc702 100644
--- a/pack-bitmap-write.c
+++ b/pack-bitmap-write.c
@@ -217,6 +217,7 @@ static uint32_t find_object_pos(struct bitmap_writer *writer,
 				const struct object_id *oid, int *found)
 {
 	struct object_entry *entry;
+	uint32_t pos;
 
 	entry = packlist_find(writer->to_pack, oid);
 	if (entry) {
@@ -224,23 +225,22 @@ static uint32_t find_object_pos(struct bitmap_writer *writer,
 		if (writer->midx)
 			base_objects = writer->midx->num_objects +
 				writer->midx->num_objects_in_base;
-
-		if (found)
-			*found = 1;
-		return oe_in_pack_pos(writer->to_pack, entry) + base_objects;
+		pos = oe_in_pack_pos(writer->to_pack, entry) + base_objects;
 	} else if (writer->midx) {
-		uint32_t at, pos;
+		uint32_t at;
 
 		if (!bsearch_midx(oid, writer->midx, &at))
 			goto missing;
 		if (midx_to_pack_pos(writer->midx, at, &pos) < 0)
 			goto missing;
-
-		if (found)
-			*found = 1;
-		return pos;
+	} else {
+		goto missing;
 	}
 
+	if (found)
+		*found = 1;
+	return pos;
+
 missing:
 	if (found)
 		*found = 0;

From c720bbcc53f223236220c7a879f0a0e73e5d3739 Mon Sep 17 00:00:00 2001
From: Taylor Blau 
Date: Wed, 27 May 2026 15:56:02 -0400
Subject: [PATCH 047/106] pack-bitmap: cache object positions during fill

The previous commits removed some redundant work from bitmap generation
by avoiding unnecessary tree recursion and by reusing selected bitmaps
that have already been computed.

Even with those changes in place, there is still an extremely hot path
from `fill_bitmap_commit()` and `fill_bitmap_tree()` to translate object
IDs into their corresponding bit positions in order to generate their
bitmaps.

In a small repository, this overhead is not significant. However, in a
very large repository (e.g., the one that we have been using as a
benchmark over the past several commits with ~57M total objects), the
overhead of locating object bit positions (often repeatedly) adds up
significantly.

Combat this by adding a small, direct-mapped cache to the bitmap writer
which maps object IDs to their corresponding bit positions. Size the
cache according to the number of objects being written, with fixed lower
and upper bounds so small repositories do not pay for a large table and
large repositories can avoid most repeated packlist and MIDX lookups.

On my machine with (a somewhat outdated) GCC 15.2.0, each entry in the
cache is 40 bytes wide:

    $ pahole -C bitmap_pos_cache_entry pack-bitmap-write.o
    struct bitmap_pos_cache_entry {
            struct object_id           oid;                  /*     0    36 */
            uint32_t                   pos;                  /*    36     4 */

            /* size: 40, cachelines: 1, members: 2 */
            /* last cacheline: 40 bytes */
    };

, and we will allocate up to 2^21 entries for a maximum total of 80 MiB
of cache overhead.

In our example repository from above and in earlier commits, this
results in a ~9.4% reduction in runtime relative to the previous commit:

    +------------------+-------------+-------------+---------------------+
    |                  | HEAD^       | HEAD        | Delta               |
    +------------------+-------------+-------------+---------------------+
    | elapsed          |   324.8 s   |   294.1 s   |    -30.7 s  (-9.4%) |
    | cycles           | 1,508.6 B   | 1,365.5 B   |   -143.0 B  (-9.5%) |
    | instructions     | 1,436.6 B   | 1,389.8 B   |    -46.9 B  (-3.3%) |
    | CPI              |     1.050   |     0.983   |   -0.068    (-6.4%) |
    +------------------+-------------+-------------+---------------------+

When generating bitmaps on this repository (to produce the above
timings), the cache grew to its maximum size of 80 MiB, and resulted in
1.024B cache hits and 59.957M cache misses.

Signed-off-by: Taylor Blau 
Signed-off-by: Junio C Hamano 
---
 pack-bitmap-write.c | 88 ++++++++++++++++++++++++++++++++++++++++++++-
 pack-bitmap.h       |  7 ++++
 2 files changed, 94 insertions(+), 1 deletion(-)

diff --git a/pack-bitmap-write.c b/pack-bitmap-write.c
index 42ed22feacc702..4b6fb07edd71c9 100644
--- a/pack-bitmap-write.c
+++ b/pack-bitmap-write.c
@@ -89,6 +89,7 @@ void bitmap_writer_free(struct bitmap_writer *writer)
 	ewah_free(writer->tags);
 
 	kh_destroy_oid_map(writer->bitmaps);
+	free(writer->pos_cache);
 
 	kh_foreach_value(writer->pseudo_merge_commits, idx,
 			 free_pseudo_merge_commit_idx(idx));
@@ -213,15 +214,92 @@ void bitmap_writer_push_commit(struct bitmap_writer *writer,
 	writer->selected_nr++;
 }
 
+struct bitmap_pos_cache_entry {
+	struct object_id oid;
+	uint32_t pos;
+};
+
+#define BITMAP_POS_MIN_CACHE_SIZE (1U << 10)
+#define BITMAP_POS_MAX_CACHE_SIZE (1U << 21)
+#define BITMAP_POS_CACHE_VALID    (1U << 31)
+
+static void bitmap_writer_init_pos_cache(struct bitmap_writer *writer)
+{
+	if (writer->pos_cache)
+		return;
+
+	writer->pos_cache_nr = BITMAP_POS_MIN_CACHE_SIZE;
+
+	while (writer->pos_cache_nr < writer->to_pack->nr_objects &&
+	       writer->pos_cache_nr < BITMAP_POS_MAX_CACHE_SIZE)
+		writer->pos_cache_nr <<= 1;
+
+	CALLOC_ARRAY(writer->pos_cache, writer->pos_cache_nr);
+}
+
+static size_t bitmap_writer_pos_cache_slot(struct bitmap_writer *writer,
+					   const struct object_id *oid)
+{
+	return oidhash(oid) & (writer->pos_cache_nr - 1);
+}
+
+static bool bitmap_writer_pos_cache_valid(struct bitmap_writer *writer,
+					  size_t slot)
+{
+	return !!(writer->pos_cache[slot].pos & BITMAP_POS_CACHE_VALID);
+}
+
+static int find_cached_object_pos(struct bitmap_writer *writer,
+				  const struct object_id *oid, uint32_t *pos)
+{
+	size_t slot = bitmap_writer_pos_cache_slot(writer, oid);
+
+	if (bitmap_writer_pos_cache_valid(writer, slot) &&
+	    oideq(&writer->pos_cache[slot].oid, oid)) {
+		writer->pos_cache_hits++;
+		*pos = writer->pos_cache[slot].pos & ~BITMAP_POS_CACHE_VALID;
+		return 1;
+	}
+
+	writer->pos_cache_misses++;
+	return 0;
+}
+
+static uint32_t store_cached_object_pos(struct bitmap_writer *writer,
+					const struct object_id *oid,
+					uint32_t pos)
+{
+	size_t slot;
+
+	if (pos & BITMAP_POS_CACHE_VALID)
+		return pos; /* too large to cache */
+
+	slot = bitmap_writer_pos_cache_slot(writer, oid);
+
+	oidcpy(&writer->pos_cache[slot].oid, oid);
+	writer->pos_cache[slot].pos = pos | BITMAP_POS_CACHE_VALID;
+
+	return pos;
+}
+
 static uint32_t find_object_pos(struct bitmap_writer *writer,
 				const struct object_id *oid, int *found)
 {
 	struct object_entry *entry;
 	uint32_t pos;
 
+	bitmap_writer_init_pos_cache(writer);
+
+	if (find_cached_object_pos(writer, oid, &pos)) {
+		if (found)
+			*found = 1;
+		return pos;
+	}
+
 	entry = packlist_find(writer->to_pack, oid);
 	if (entry) {
 		uint32_t base_objects = 0;
+
 		if (writer->midx)
 			base_objects = writer->midx->num_objects +
 				writer->midx->num_objects_in_base;
@@ -239,7 +317,7 @@ static uint32_t find_object_pos(struct bitmap_writer *writer,
 
 	if (found)
 		*found = 1;
-	return pos;
+	return store_cached_object_pos(writer, oid, pos);
 
 missing:
 	if (found)
@@ -662,6 +740,10 @@ int bitmap_writer_build(struct bitmap_writer *writer)
 		writer->progress = start_progress(writer->repo,
 						  "Building bitmaps",
 						  writer->selected_nr);
+
+	writer->pos_cache_hits = 0;
+	writer->pos_cache_misses = 0;
+
 	trace2_region_enter("pack-bitmap-write", "building_bitmaps_total",
 			    writer->repo);
 
@@ -726,6 +808,10 @@ int bitmap_writer_build(struct bitmap_writer *writer)
 	trace2_data_intmax("pack-bitmap-write", writer->repo,
 			   "fill_bitmap_commit_found_ancestor_nr",
 			   fill_bitmap_commit_found_ancestor_nr);
+	trace2_data_intmax("pack-bitmap-write", writer->repo,
+			   "bitmap_pos_cache_hits", writer->pos_cache_hits);
+	trace2_data_intmax("pack-bitmap-write", writer->repo,
+			   "bitmap_pos_cache_misses", writer->pos_cache_misses);
 
 	stop_progress(&writer->progress);
 
diff --git a/pack-bitmap.h b/pack-bitmap.h
index a95e1c2d115a31..19a86554579f7c 100644
--- a/pack-bitmap.h
+++ b/pack-bitmap.h
@@ -132,6 +132,8 @@ int bitmap_has_oid_in_uninteresting(struct bitmap_index *, const struct object_i
 
 off_t get_disk_usage_from_bitmap(struct bitmap_index *, struct rev_info *);
 
+struct bitmap_pos_cache_entry;
+
 struct bitmap_writer {
 	struct repository *repo;
 	struct ewah_bitmap *commits;
@@ -143,6 +145,11 @@ struct bitmap_writer {
 	struct packing_data *to_pack;
 	struct multi_pack_index *midx; /* if appending to a MIDX chain */
 
+	struct bitmap_pos_cache_entry *pos_cache;
+	size_t pos_cache_nr;
+	uint64_t pos_cache_hits;
+	uint64_t pos_cache_misses;
+
 	struct bitmapped_commit *selected;
 	unsigned int selected_nr, selected_alloc;
 

From dcccd997462e2130bcc35f933285ff087454275e Mon Sep 17 00:00:00 2001
From: Taylor Blau 
Date: Wed, 27 May 2026 15:56:05 -0400
Subject: [PATCH 048/106] pack-bitmap: sort bitmaps before XORing

Reachability bitmaps may be stored as XORs against nearby bitmaps, up to
10 away. However, when callers provide selected commits in an arbitrary
order, the writer may miss good ancestor/descendant pairs and produce
much larger bitmap files without changing query coverage.

Sort the selected bitmaps in date order (from oldest to newest) before
computing XOR offsets, leaving pseudo-merge bitmaps alone (which we will
deal with separately in following commits).

On our same testing repository from previous commits, this change shrunk
our selection of 1,261 bitmaps from ~635.46 MiB to 176.4 MiB for a
~72.24% reduction in the on-disk size of our *.bitmap file. The time to
generate the smaller bitmap file decreased by ~3.69 seconds, though this
is likely mostly noise.

Signed-off-by: Taylor Blau 
Signed-off-by: Junio C Hamano 
---
 pack-bitmap-write.c | 29 +++++++++++++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/pack-bitmap-write.c b/pack-bitmap-write.c
index 4b6fb07edd71c9..66282ea14b5123 100644
--- a/pack-bitmap-write.c
+++ b/pack-bitmap-write.c
@@ -327,11 +327,40 @@ static uint32_t find_object_pos(struct bitmap_writer *writer,
 	return 0;
 }
 
+static int bitmapped_commit_date_cmp(const void *_a, const void *_b)
+{
+	const struct bitmapped_commit *a = _a;
+	const struct bitmapped_commit *b = _b;
+
+	if (a->commit->date < b->commit->date)
+		return -1;
+	if (a->commit->date > b->commit->date)
+		return 1;
+	return 0;
+}
+
 static void compute_xor_offsets(struct bitmap_writer *writer)
 {
 	static const int MAX_XOR_OFFSET_SEARCH = 10;
 
 	int i, next = 0;
+	int nr = bitmap_writer_nr_selected_commits(writer);
+
+	if (nr > 1) {
+		QSORT(writer->selected, nr, bitmapped_commit_date_cmp);
+
+		for (i = 0; i < nr; i++) {
+			struct bitmapped_commit *stored = &writer->selected[i];
+			khiter_t hash_pos = kh_get_oid_map(writer->bitmaps,
+							   stored->commit->object.oid);
+
+			if (hash_pos == kh_end(writer->bitmaps))
+				BUG("selected commit missing from bitmap map: %s",
+				    oid_to_hex(&stored->commit->object.oid));
+
+			kh_value(writer->bitmaps, hash_pos) = stored;
+		}
+	}
 
 	while (next < writer->selected_nr) {
 		struct bitmapped_commit *stored = &writer->selected[next];

From b04d26607de35b88cf9c62ca11931d4f8cc4ac05 Mon Sep 17 00:00:00 2001
From: Taylor Blau 
Date: Wed, 27 May 2026 15:56:08 -0400
Subject: [PATCH 049/106] pack-bitmap: remember pseudo-merge parents

write_pseudo_merges() currently builds an array of temporary bitmaps for
the parent set of each pseudo-merge, then serializes those bitmaps later
while writing the extension.

Move those parent bitmaps onto the corresponding bitmapped_commit
entries instead. This keeps the on-disk output unchanged, but gives the
parent bitmap the same lifetime and access pattern that later changes
will use when pseudo-merge object bitmaps are built before the write
step.

Signed-off-by: Taylor Blau 
Signed-off-by: Junio C Hamano 
---
 pack-bitmap-write.c | 30 +++++++++++++++++-------------
 1 file changed, 17 insertions(+), 13 deletions(-)

diff --git a/pack-bitmap-write.c b/pack-bitmap-write.c
index 66282ea14b5123..8200aed610135b 100644
--- a/pack-bitmap-write.c
+++ b/pack-bitmap-write.c
@@ -32,6 +32,7 @@ struct bitmapped_commit {
 	struct commit *commit;
 	struct ewah_bitmap *bitmap;
 	struct ewah_bitmap *write_as;
+	struct ewah_bitmap *pseudo_merge_parents;
 	int flags;
 	int xor_offset;
 	uint32_t commit_pos;
@@ -102,6 +103,7 @@ void bitmap_writer_free(struct bitmap_writer *writer)
 		if (bc->write_as != bc->bitmap)
 			ewah_free(bc->write_as);
 		ewah_free(bc->bitmap);
+		ewah_free(bc->pseudo_merge_parents);
 	}
 	free(writer->selected);
 }
@@ -210,6 +212,7 @@ void bitmap_writer_push_commit(struct bitmap_writer *writer,
 	writer->selected[writer->selected_nr].write_as = NULL;
 	writer->selected[writer->selected_nr].flags = 0;
 	writer->selected[writer->selected_nr].pseudo_merge = pseudo_merge;
+	writer->selected[writer->selected_nr].pseudo_merge_parents = NULL;
 
 	writer->selected_nr++;
 }
@@ -1004,42 +1007,47 @@ static void write_pseudo_merges(struct bitmap_writer *writer,
 				struct hashfile *f)
 {
 	struct oid_array commits = OID_ARRAY_INIT;
-	struct bitmap **commits_bitmap = NULL;
 	off_t *pseudo_merge_ofs = NULL;
 	off_t start, table_start, next_ext;
 
 	uint32_t base = bitmap_writer_nr_selected_commits(writer);
 	size_t i, j = 0;
 
-	CALLOC_ARRAY(commits_bitmap, writer->pseudo_merges_nr);
 	CALLOC_ARRAY(pseudo_merge_ofs, writer->pseudo_merges_nr);
 
 	for (i = 0; i < writer->pseudo_merges_nr; i++) {
 		struct bitmapped_commit *merge = &writer->selected[base + i];
 		struct commit_list *p;
+		struct bitmap *parents = bitmap_new();
 
 		if (!merge->pseudo_merge)
 			BUG("found non-pseudo merge commit at %"PRIuMAX, (uintmax_t)i);
 
-		commits_bitmap[i] = bitmap_new();
-
 		for (p = merge->commit->parents; p; p = p->next)
-			bitmap_set(commits_bitmap[i],
+			bitmap_set(parents,
 				   find_object_pos(writer, &p->item->object.oid,
 						   NULL));
+
+		merge->pseudo_merge_parents = bitmap_to_ewah(parents);
+		bitmap_free(parents);
 	}
 
 	start = hashfile_total(f);
 
 	for (i = 0; i < writer->pseudo_merges_nr; i++) {
-		struct ewah_bitmap *commits_ewah = bitmap_to_ewah(commits_bitmap[i]);
+		struct bitmapped_commit *merge = &writer->selected[base + i];
+
+		if (!merge->pseudo_merge)
+			BUG("found non-pseudo merge commit at %"PRIuMAX, (uintmax_t)i);
+
+		if (!merge->pseudo_merge_parents)
+			BUG("missing pseudo-merge parents bitmap for commit %s",
+			    oid_to_hex(&merge->commit->object.oid));
 
 		pseudo_merge_ofs[i] = hashfile_total(f);
 
-		dump_bitmap(f, commits_ewah);
+		dump_bitmap(f, merge->pseudo_merge_parents);
 		dump_bitmap(f, writer->selected[base+i].write_as);
-
-		ewah_free(commits_ewah);
 	}
 
 	next_ext = st_add(hashfile_total(f),
@@ -1122,12 +1130,8 @@ static void write_pseudo_merges(struct bitmap_writer *writer,
 	hashwrite_be64(f, table_start - start);
 	hashwrite_be64(f, hashfile_total(f) - start + sizeof(uint64_t));
 
-	for (i = 0; i < writer->pseudo_merges_nr; i++)
-		bitmap_free(commits_bitmap[i]);
-
 	oid_array_clear(&commits);
 	free(pseudo_merge_ofs);
-	free(commits_bitmap);
 }
 
 static int table_cmp(const void *_va, const void *_vb, void *_data)

From 49633dc88c14008f9a405f215b60994362b36d6c Mon Sep 17 00:00:00 2001
From: Taylor Blau 
Date: Wed, 27 May 2026 15:56:11 -0400
Subject: [PATCH 050/106] pack-bitmap: build pseudo-merge bitmaps after regular
 bitmaps
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

When generating bitmaps, `bitmap_builder_init()` starts with an initial
selection of commits to receive bitmap coverage, and then determines a
set of "maximal" commits based on its input.

Commit 089f751360f (pack-bitmap-write: build fewer intermediate bitmaps,
2020-12-08) has extensive details, but the gist is as follows:

Each selected commit starts with one commit_mask bit in its "commit
mask" bitmap. Then, we walk the first-parent history in topological
order and OR each commit's mask into its (first) parent. Whenever that
OR results in the parent having more bits set, the child is deemed to be
non-maximal, and the frontier is pushed further back along the first
parent history.

That approach works extremely well for ordinary selected commits, whose
first-parent histories often describe real sharing between the bitmaps
we are going to write.

It struggles, however, to efficiently generate pseudo-merge bitmaps.
Unlike ordinary commits for which the above algorithm is designed,
pseudo-merges don't represent any "real" commit in history, just a
grouping of non-bitmapped reference tips. In that sense, their first
parent is just a part of a larger set, and treating them like ordinary
selected commits imposes a significant slow-down when generating bitmaps
with pseudo-merges enabled.

Consider partitioning all non-bitmapped reference tips into eight
individual pseudo-merges via the following configuration:

    [bitmapPseudoMerge "all"]
        pattern=refs/
        threshold=now
        stableSize=10000000
        maxMerges=8

, the cost of generating a bitmap from scratch rises significantly:

    +------------------+-----------------+---------------+---------------------+
    |                  | no pseudo-merge | pseudo-merges | Delta               |
    |                  |                 | (HEAD^)       |                     |
    +------------------+-----------------+---------------+---------------------+
    | elapsed          |   294.1 s       |   575.0 s     |   +280.9 s (+95.5%) |
    | cycles           | 1,365.5 B       | 2,686.9 B     | +1,321.4 B (+96.8%) |
    | instructions     | 1,389.8 B       | 2,546.6 B     | +1,156.8 B (+83.2%) |
    | CPI              |     0.983       |     1.055     |   +0.073    (+7.4%) |
    +------------------+-----------------+---------------+---------------------+

This is a particularly poor trade-off, because the time saved by these
pseudo-merges during, e.g.,

    $ git rev-list --count --all --objects --use-bitmap-index

is only:

    $ hyperfine -L v true,false -n 'pseudo-merges: {v}' '
        GIT_TEST_USE_PSEUDO_MERGES={v} git.compile rev-list --count \
          --objects --all --use-bitmap-index
      '

    Benchmark 1: pseudo-merges: true
      Time (mean ± σ):      2.613 s ±  0.012 s    [User: 2.308 s, System: 0.305 s]
      Range (min … max):    2.594 s …  2.633 s    10 runs

    Benchmark 2: pseudo-merges: false
      Time (mean ± σ):     52.205 s ±  0.170 s    [User: 51.500 s, System: 0.697 s]
      Range (min … max):   51.956 s … 52.458 s    10 runs

    Summary
      pseudo-merges: true ran
       19.98 ± 0.11 times faster than pseudo-merges: false

In other words, we pay a nearly ~5 minute penalty to generate
pseudo-merge bitmaps, but only save ~50 seconds during traversal.

The problem stems from injecting pseudo-merges into the bitmap builder
as if they were normal commits. The maximal commit selection algorithm
was simply not designed for that case, and performs predictably poorly.

The only reason we reused the maximal commit selection routine for
pseudo-merges alongside regular non-pseudo-merge commits is because we
represent them both as commit objects (where the pseudo-merge commits
just represent a made-up commit as opposed to one that actually exists
in a repository's object store).

Instead, build the regular selected commit bitmaps first, considering
only non-pseudo-merge commits in `bitmap_builder_init()`. Once those
bitmaps have been stored, build each pseudo-merge bitmap separately and
attach its parent and object bitmaps to the corresponding pseudo-merge
entry before writing the extension.

This keeps the regular bitmap build shaped like the no-pseudo-merge
case. The later pseudo-merge fill can still stop at stored selected
ancestor bitmaps, so it does not have to rewalk each pseudo-merge
closure from scratch.

When an existing bitmap has the same pseudo-merge parent set, reuse and
remap that whole pseudo-merge bitmap before falling back to
fill_bitmap_commit(). This preserves the benefit of stable pseudo-merges
while keeping the on-disk format and reader behavior unchanged.

As a result, the overhead cost for generating pseudo-merges in the above
configuration is much smaller:

    +------------------+-----------------+---------------+-------------------+
    |                  | no pseudo-merge | pseudo-merges | Delta             |
    |                  |                 | (HEAD)        |                   |
    +------------------+-----------------+---------------+-------------------+
    | elapsed          |   294.1 s       |   328.4 s     |  +34.3 s (+11.7%) |
    | cycles           | 1,365.5 B       | 1,529.3 B     | +163.7 B (+12.0%) |
    | instructions     | 1,389.8 B       | 1,552.8 B     | +163.0 B (+11.7%) |
    | CPI              |     0.983       |     0.985     |  +0.002   (+0.2%) |
    +------------------+-----------------+---------------+-------------------+

Recall that at the start of this series, generating reachability bitmaps
took 612.5 seconds *without* pseudo-merges. With this commit, it is
still ~46.38% *faster* to generate reachability bitmaps *with*
pseudo-merges than it was to generate bitmaps wihtout them at the
beginning of this series.

The changes to implement this are mostly straightforward. We exclude
pseudo-merge commits from the existing bitmap generation, and walk over
them in a separate pass, by either reusing an existing on-disk
pseudo-merge, or passing the pseudo-merge commit itself back to the
existing routine in `fill_bitmap_commit()`.

(Note that the routine to build pseudo-merge bitmaps is the same both
before and after this change, the difference is only that we do not let
psuedo-merges participate in determining the set of maximal commits.)

The only wrinkle is that `fill_bitmap_commit()` must be taught to not
expect that all tree objects have been parsed, which is the case for any
portion of history reachable by one or more pseudo-merge(s), but not by
any non-pseudo-merge commit selected for bitmapping.

Signed-off-by: Taylor Blau 
Signed-off-by: Junio C Hamano 
---
 pack-bitmap-write.c | 210 ++++++++++++++++++++++++++++++++++++--------
 1 file changed, 174 insertions(+), 36 deletions(-)

diff --git a/pack-bitmap-write.c b/pack-bitmap-write.c
index 8200aed610135b..1bcb3f98a42518 100644
--- a/pack-bitmap-write.c
+++ b/pack-bitmap-write.c
@@ -446,13 +446,17 @@ static void bitmap_builder_init(struct bitmap_builder *bb,
 	revs.topo_order = 1;
 	revs.first_parent_only = 1;
 
-	for (i = 0; i < writer->selected_nr; i++) {
+	for (i = 0; i < bitmap_writer_nr_selected_commits(writer); i++) {
 		struct bitmapped_commit *bc = &writer->selected[i];
 		struct bb_commit *ent = bb_data_at(&bb->data, bc->commit);
 
+		if (bc->pseudo_merge)
+			BUG("unexpected pseudo-merge at %"PRIuMAX,
+			    (uintmax_t)i);
+
 		ent->selected = 1;
 		ent->maximal = 1;
-		ent->pseudo_merge = bc->pseudo_merge;
+		ent->pseudo_merge = 0;
 		ent->idx = i;
 
 		ent->commit_mask = bitmap_new();
@@ -618,6 +622,8 @@ static int fill_bitmap_tree(struct bitmap_writer *writer,
 
 static int reused_bitmaps_nr;
 static int reused_pseudo_merge_bitmaps_nr;
+static int pseudo_merge_bitmap_nr;
+static int pseudo_merge_bitmap_parents;
 
 static int fill_bitmap_commit_calls_nr;
 static int fill_bitmap_commit_found_ancestor_nr;
@@ -631,8 +637,12 @@ static int fill_bitmap_commit(struct bitmap_writer *writer,
 			      const uint32_t *mapping)
 {
 	int found;
+	int from_pseudo_merge = commit->object.flags & BITMAP_PSEUDO_MERGE;
 	uint32_t pos;
 
+	if (ent->pseudo_merge)
+		BUG("unexpected pseudo-merge commit in fill_bitmap_commit()");
+
 	fill_bitmap_commit_calls_nr++;
 
 	if (!ent->bitmap)
@@ -648,10 +658,7 @@ static int fill_bitmap_commit(struct bitmap_writer *writer,
 			struct ewah_bitmap *old;
 			struct bitmap *remapped = bitmap_new();
 
-			if (commit->object.flags & BITMAP_PSEUDO_MERGE)
-				old = pseudo_merge_bitmap_for_commit(old_bitmap, c);
-			else
-				old = bitmap_for_commit(old_bitmap, c);
+			old = bitmap_for_commit(old_bitmap, c);
 			/*
 			 * If this commit has an old bitmap, then translate that
 			 * bitmap and add its bits to this one. No need to walk
@@ -660,10 +667,7 @@ static int fill_bitmap_commit(struct bitmap_writer *writer,
 			if (old && !rebuild_bitmap(mapping, old, remapped)) {
 				bitmap_or(ent->bitmap, remapped);
 				bitmap_free(remapped);
-				if (commit->object.flags & BITMAP_PSEUDO_MERGE)
-					reused_pseudo_merge_bitmaps_nr++;
-				else
-					reused_bitmaps_nr++;
+				reused_bitmaps_nr++;
 				continue;
 			}
 			bitmap_free(remapped);
@@ -696,12 +700,32 @@ static int fill_bitmap_commit(struct bitmap_writer *writer,
 		 * walk ensures we cover all parents.
 		 */
 		if (!(c->object.flags & BITMAP_PSEUDO_MERGE)) {
+			struct tree *tree;
+
+			if (from_pseudo_merge && !c->object.parsed) {
+				/*
+				 * Commits reachable from selected
+				 * non-pseudo-merges are already parsed
+				 * by the regular bitmap build.
+				 *
+				 * However, pseudo-merge fills can also
+				 * reach commits that were not covered
+				 * there, so parse any such leftovers
+				 * before reading their tree or parents.
+				 */
+				if (repo_parse_commit(writer->repo, c))
+					return -1;
+			}
+
 			pos = find_object_pos(writer, &c->object.oid, &found);
 			if (!found)
 				return -1;
 			bitmap_set(ent->bitmap, pos);
-			prio_queue_put(tree_queue,
-				       repo_get_commit_tree(writer->repo, c));
+
+			tree = repo_get_commit_tree(writer->repo, c);
+			if (!tree)
+				return -1;
+			prio_queue_put(tree_queue, tree);
 		}
 
 		for (p = c->parents; p; p = p->next) {
@@ -738,6 +762,137 @@ static int fill_bitmap_commit(struct bitmap_writer *writer,
 	return 0;
 }
 
+static int reuse_pseudo_merge_bitmap(struct bitmap_index *old_bitmap,
+				     const uint32_t *mapping,
+				     struct commit *merge,
+				     struct ewah_bitmap **out)
+{
+	struct ewah_bitmap *old;
+	struct bitmap *remapped;
+
+	if (!old_bitmap || !mapping)
+		return 0;
+
+	old = pseudo_merge_bitmap_for_commit(old_bitmap, merge);
+	if (!old)
+		return 0;
+
+	remapped = bitmap_new();
+	if (rebuild_bitmap(mapping, old, remapped) < 0) {
+		bitmap_free(remapped);
+		return 0;
+	}
+
+	*out = bitmap_to_ewah(remapped);
+	bitmap_free(remapped);
+	reused_pseudo_merge_bitmaps_nr++;
+	return 1;
+}
+
+static int build_pseudo_merge_bitmap(struct bitmap_writer *writer,
+				     struct bitmap_index *old_bitmap,
+				     const uint32_t *mapping,
+				     struct commit *merge,
+				     struct ewah_bitmap **out)
+{
+	struct bb_commit ent = { 0 };
+	struct prio_queue queue = { NULL };
+	struct prio_queue tree_queue = { NULL };
+	unsigned parents = commit_list_count(merge->parents);
+	int ret;
+
+	ent.bitmap = bitmap_new();
+
+	pseudo_merge_bitmap_nr++;
+	pseudo_merge_bitmap_parents += parents;
+
+	if (reuse_pseudo_merge_bitmap(old_bitmap, mapping, merge, out)) {
+		ret = 0;
+		goto done;
+	}
+
+	ret = fill_bitmap_commit(writer, &ent, merge, &queue, &tree_queue,
+				 old_bitmap, mapping);
+
+	if (!ret)
+		*out = bitmap_to_ewah(ent.bitmap);
+
+done:
+	bitmap_free(ent.bitmap);
+	clear_prio_queue(&queue);
+	clear_prio_queue(&tree_queue);
+
+	return ret;
+}
+
+static int build_pseudo_merge_bitmaps(struct bitmap_writer *writer,
+				      struct bitmap_index *old_bitmap,
+				      const uint32_t *mapping,
+				      int *nr_stored)
+{
+	size_t i = bitmap_writer_nr_selected_commits(writer);
+	int ret = 0;
+
+	if (!writer->pseudo_merges_nr)
+		return 0;
+
+	trace2_region_enter("pack-bitmap-write", "building_pseudo_merge_bitmaps",
+			    writer->repo);
+
+	for (; i < writer->selected_nr; i++) {
+		struct bitmapped_commit *merge = &writer->selected[i];
+		struct commit_list *p;
+		struct bitmap *parents = bitmap_new();
+		struct ewah_bitmap *objects = NULL;
+
+		if (!merge->pseudo_merge)
+			BUG("found non-pseudo merge commit at %"PRIuMAX,
+			    (uintmax_t)i);
+
+		for (p = merge->commit->parents; p; p = p->next) {
+			int found;
+			uint32_t pos = find_object_pos(writer,
+						       &p->item->object.oid,
+						       &found);
+			if (!found) {
+				bitmap_free(parents);
+				ret = -1;
+				goto done;
+			}
+			bitmap_set(parents, pos);
+		}
+
+		merge->pseudo_merge_parents = bitmap_to_ewah(parents);
+		bitmap_free(parents);
+
+		if (build_pseudo_merge_bitmap(writer, old_bitmap, mapping,
+					      merge->commit, &objects) < 0) {
+			ret = -1;
+			goto done;
+		}
+		merge->bitmap = objects;
+
+		(*nr_stored)++;
+		display_progress(writer->progress, *nr_stored);
+	}
+
+done:
+	trace2_region_leave("pack-bitmap-write", "building_pseudo_merge_bitmaps",
+			    writer->repo);
+
+	trace2_data_intmax("pack-bitmap-write", writer->repo,
+			   "pseudo_merge_bitmap_nr",
+			   pseudo_merge_bitmap_nr);
+	trace2_data_intmax("pack-bitmap-write", writer->repo,
+			   "building_bitmaps_pseudo_merge_reused",
+			   reused_pseudo_merge_bitmaps_nr);
+	trace2_data_intmax("pack-bitmap-write", writer->repo,
+			   "pseudo_merge_bitmap_parents",
+			   pseudo_merge_bitmap_parents);
+
+	return ret;
+}
+
 static void store_selected(struct bitmap_writer *writer,
 			   struct bb_commit *ent, struct commit *commit)
 {
@@ -821,6 +976,10 @@ int bitmap_writer_build(struct bitmap_writer *writer)
 			bitmap_free(ent->bitmap);
 		ent->bitmap = NULL;
 	}
+	if (closed &&
+	    build_pseudo_merge_bitmaps(writer, old_bitmap, mapping,
+				       &nr_stored) < 0)
+		closed = 0;
 	clear_prio_queue(&queue);
 	clear_prio_queue(&tree_queue);
 	bitmap_builder_clear(&bb);
@@ -831,9 +990,6 @@ int bitmap_writer_build(struct bitmap_writer *writer)
 			    writer->repo);
 	trace2_data_intmax("pack-bitmap-write", writer->repo,
 			   "building_bitmaps_reused", reused_bitmaps_nr);
-	trace2_data_intmax("pack-bitmap-write", writer->repo,
-			   "building_bitmaps_pseudo_merge_reused",
-			   reused_pseudo_merge_bitmaps_nr);
 	trace2_data_intmax("pack-bitmap-write", writer->repo,
 			   "fill_bitmap_commit_calls_nr",
 			   fill_bitmap_commit_calls_nr);
@@ -1015,23 +1171,6 @@ static void write_pseudo_merges(struct bitmap_writer *writer,
 
 	CALLOC_ARRAY(pseudo_merge_ofs, writer->pseudo_merges_nr);
 
-	for (i = 0; i < writer->pseudo_merges_nr; i++) {
-		struct bitmapped_commit *merge = &writer->selected[base + i];
-		struct commit_list *p;
-		struct bitmap *parents = bitmap_new();
-
-		if (!merge->pseudo_merge)
-			BUG("found non-pseudo merge commit at %"PRIuMAX, (uintmax_t)i);
-
-		for (p = merge->commit->parents; p; p = p->next)
-			bitmap_set(parents,
-				   find_object_pos(writer, &p->item->object.oid,
-						   NULL));
-
-		merge->pseudo_merge_parents = bitmap_to_ewah(parents);
-		bitmap_free(parents);
-	}
-
 	start = hashfile_total(f);
 
 	for (i = 0; i < writer->pseudo_merges_nr; i++) {
@@ -1040,14 +1179,13 @@ static void write_pseudo_merges(struct bitmap_writer *writer,
 		if (!merge->pseudo_merge)
 			BUG("found non-pseudo merge commit at %"PRIuMAX, (uintmax_t)i);
 
-		if (!merge->pseudo_merge_parents)
-			BUG("missing pseudo-merge parents bitmap for commit %s",
+		if (!merge->pseudo_merge_parents || !merge->bitmap)
+			BUG("missing pseudo-merge bitmap for commit %s",
 			    oid_to_hex(&merge->commit->object.oid));
 
 		pseudo_merge_ofs[i] = hashfile_total(f);
-
 		dump_bitmap(f, merge->pseudo_merge_parents);
-		dump_bitmap(f, writer->selected[base+i].write_as);
+		dump_bitmap(f, merge->bitmap);
 	}
 
 	next_ext = st_add(hashfile_total(f),

From 9f4e170dfc3bd8cdd284f1c4411b25ce1d09737f Mon Sep 17 00:00:00 2001
From: Kristofer Karlsson 
Date: Wed, 27 May 2026 15:50:00 +0000
Subject: [PATCH 051/106] pack-objects: call release_revisions() after cruft
 traversal

enumerate_and_traverse_cruft_objects() initializes a rev_info on the
stack but never calls release_revisions() afterwards.  This is not
visible on master but becomes a leak once the revision walking
machinery uses dynamically allocated structures.

Add the missing release_revisions() call.

Signed-off-by: Kristofer Karlsson 
Signed-off-by: Junio C Hamano 
---
 builtin/pack-objects.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c
index 480cc0bd8c8d22..67025e86256cfd 100644
--- a/builtin/pack-objects.c
+++ b/builtin/pack-objects.c
@@ -4275,6 +4275,7 @@ static void enumerate_and_traverse_cruft_objects(struct string_list *fresh_packs
 	traverse_commit_list(&revs, show_cruft_commit, show_cruft_object, NULL);
 
 	stop_progress(&progress_state);
+	release_revisions(&revs);
 }
 
 static void read_cruft_objects(void)

From d877b1af507a6aaf55e8643eb73277a30d3a800b Mon Sep 17 00:00:00 2001
From: Kristofer Karlsson 
Date: Wed, 27 May 2026 15:50:01 +0000
Subject: [PATCH 052/106] revision: introduce rev_walk_mode to clarify
 get_revision_1()

get_revision_1() dispatches to different walk strategies based on a
combination of rev_info flags: reflog_info, topo_walk_info, and
limited.  These conditions are checked in multiple places within
the function -- once to select the next commit, and again to decide
how to expand parents -- and the two chains must stay in sync.

Extract the mode selection into a rev_walk_mode enum and a small
get_walk_mode() helper, resolved once at the top of get_revision_1().
Both dispatch sites now switch on the same mode variable, making it
obvious that they agree and easier to verify that all modes are
handled.

No functional change.

Signed-off-by: Kristofer Karlsson 
Signed-off-by: Junio C Hamano 
---
 revision.c | 62 ++++++++++++++++++++++++++++++++++++++++++------------
 1 file changed, 48 insertions(+), 14 deletions(-)

diff --git a/revision.c b/revision.c
index e1970b9c5d34ed..9d0fc696d09937 100644
--- a/revision.c
+++ b/revision.c
@@ -4327,22 +4327,48 @@ static void track_linear(struct rev_info *revs, struct commit *commit)
 	revs->previous_parents = commit_list_copy(commit->parents);
 }
 
+enum rev_walk_mode {
+	REV_WALK_REFLOG,
+	REV_WALK_TOPO,
+	REV_WALK_LIMITED,
+	REV_WALK_STREAMING,
+};
+
+static enum rev_walk_mode get_walk_mode(struct rev_info *revs)
+{
+	if (revs->reflog_info)
+		return REV_WALK_REFLOG;
+	if (revs->topo_walk_info)
+		return REV_WALK_TOPO;
+	if (revs->limited)
+		return REV_WALK_LIMITED;
+	return REV_WALK_STREAMING;
+}
+
 static struct commit *get_revision_1(struct rev_info *revs)
 {
+	enum rev_walk_mode mode = get_walk_mode(revs);
+
 	while (1) {
 		struct commit *commit;
 
-		if (revs->reflog_info)
+		switch (mode) {
+		case REV_WALK_REFLOG:
 			commit = next_reflog_entry(revs->reflog_info);
-		else if (revs->topo_walk_info)
+			break;
+		case REV_WALK_TOPO:
 			commit = next_topo_commit(revs);
-		else
+			break;
+		case REV_WALK_LIMITED:
+		case REV_WALK_STREAMING:
 			commit = pop_commit(&revs->commits);
+			break;
+		}
 
 		if (!commit)
 			return NULL;
 
-		if (revs->reflog_info)
+		if (mode == REV_WALK_REFLOG)
 			commit->object.flags &= ~(ADDED | SEEN | SHOWN);
 
 		/*
@@ -4350,20 +4376,28 @@ static struct commit *get_revision_1(struct rev_info *revs)
 		 * the parents here. We also need to do the date-based limiting
 		 * that we'd otherwise have done in limit_list().
 		 */
-		if (!revs->limited) {
-			if (revs->max_age != -1 &&
-			    comparison_date(revs, commit) < revs->max_age)
-				continue;
+		if (mode != REV_WALK_LIMITED &&
+		    revs->max_age != -1 &&
+		    comparison_date(revs, commit) < revs->max_age)
+			continue;
 
-			if (revs->reflog_info)
-				try_to_simplify_commit(revs, commit);
-			else if (revs->topo_walk_info)
-				expand_topo_walk(revs, commit);
-			else if (process_parents(revs, commit, &revs->commits, NULL) < 0) {
+		switch (mode) {
+		case REV_WALK_REFLOG:
+			try_to_simplify_commit(revs, commit);
+			break;
+		case REV_WALK_TOPO:
+			expand_topo_walk(revs, commit);
+			break;
+		case REV_WALK_STREAMING:
+			if (process_parents(revs, commit,
+					    &revs->commits, NULL) < 0) {
 				if (!revs->ignore_missing_links)
 					die("Failed to traverse parents of commit %s",
-						oid_to_hex(&commit->object.oid));
+					    oid_to_hex(&commit->object.oid));
 			}
+			break;
+		case REV_WALK_LIMITED:
+			break;
 		}
 
 		switch (simplify_commit(revs, commit)) {

From dd4bc01c0a8fc871a68a5027ed5ac953fa47fc6e Mon Sep 17 00:00:00 2001
From: Kristofer Karlsson 
Date: Wed, 27 May 2026 15:50:02 +0000
Subject: [PATCH 053/106] revision: use priority queue for non-limited
 streaming walks

The streaming (non-limited) walk in get_revision_1() inserts newly
discovered parent commits into a date-sorted queue via
commit_list_insert_by_date(), which scans the linked list to find the
insertion point -- O(w) per insert, where w is the width of the active
walk frontier.  Replace this with an O(log w) priority queue.

Add a commit_queue field to rev_info alongside the existing commits
linked list.  The two representations are mutually exclusive: setup
and external callers that need list access use the linked list, then
get_revision_1() lazily drains it into the priority queue on first
call.  Add a REV_WALK_NO_WALK enum value to distinguish the no_walk
case (which still uses the commit list) from the streaming case.

The conversion function rev_info_commit_list_to_queue() is public so
callers that know they will iterate can convert early.

Combined with the limit_list() priority queue change already in
master, this eliminates all O(w) sorted linked-list insertion from
the revision walk machinery.

Signed-off-by: Kristofer Karlsson 
Signed-off-by: Junio C Hamano 
---
 commit.c   | 13 -------------
 commit.h   |  2 --
 revision.c | 55 +++++++++++++++++++++++++++++-------------------------
 revision.h | 12 +++++++++++-
 4 files changed, 41 insertions(+), 41 deletions(-)

diff --git a/commit.c b/commit.c
index e3e7352e69682d..5112c7b2af31b1 100644
--- a/commit.c
+++ b/commit.c
@@ -729,19 +729,6 @@ void commit_list_free(struct commit_list *list)
 		pop_commit(&list);
 }
 
-struct commit_list * commit_list_insert_by_date(struct commit *item, struct commit_list **list)
-{
-	struct commit_list **pp = list;
-	struct commit_list *p;
-	while ((p = *pp) != NULL) {
-		if (p->item->date < item->date) {
-			break;
-		}
-		pp = &p->next;
-	}
-	return commit_list_insert(item, pp);
-}
-
 static int commit_list_compare_by_date(const struct commit_list *a,
 				       const struct commit_list *b)
 {
diff --git a/commit.h b/commit.h
index 58150045afafed..385492fbb1ecc5 100644
--- a/commit.h
+++ b/commit.h
@@ -191,8 +191,6 @@ int commit_list_contains(struct commit *item,
 struct commit_list **commit_list_append(struct commit *commit,
 					struct commit_list **next);
 unsigned commit_list_count(const struct commit_list *l);
-struct commit_list *commit_list_insert_by_date(struct commit *item,
-				    struct commit_list **list);
 void commit_list_sort_by_date(struct commit_list **list);
 
 /* Shallow copy of the input list */
diff --git a/revision.c b/revision.c
index 9d0fc696d09937..4bb3b16e43acb9 100644
--- a/revision.c
+++ b/revision.c
@@ -1116,7 +1116,7 @@ static void try_to_simplify_commit(struct rev_info *revs, struct commit *commit)
 }
 
 static int process_parents(struct rev_info *revs, struct commit *commit,
-			   struct commit_list **list, struct prio_queue *queue)
+			   struct prio_queue *queue)
 {
 	struct commit_list *parent = commit->parents;
 	unsigned pass_flags;
@@ -1158,8 +1158,6 @@ static int process_parents(struct rev_info *revs, struct commit *commit,
 			if (p->object.flags & SEEN)
 				continue;
 			p->object.flags |= (SEEN | NOT_USER_GIVEN);
-			if (list)
-				commit_list_insert_by_date(p, list);
 			if (queue)
 				prio_queue_put(queue, p);
 			if (revs->exclude_first_parent_only)
@@ -1207,8 +1205,6 @@ static int process_parents(struct rev_info *revs, struct commit *commit,
 		p->object.flags |= pass_flags | CHILD_VISITED;
 		if (!(p->object.flags & SEEN)) {
 			p->object.flags |= (SEEN | NOT_USER_GIVEN);
-			if (list)
-				commit_list_insert_by_date(p, list);
 			if (queue)
 				prio_queue_put(queue, p);
 		}
@@ -1470,7 +1466,7 @@ static int limit_list(struct rev_info *revs)
 
 		if (revs->max_age != -1 && (commit->date < revs->max_age))
 			obj->flags |= UNINTERESTING;
-		if (process_parents(revs, commit, NULL, &queue) < 0) {
+		if (process_parents(revs, commit, &queue) < 0) {
 			clear_prio_queue(&queue);
 			return -1;
 		}
@@ -3257,6 +3253,7 @@ static void free_void_commit_list(void *list)
 void release_revisions(struct rev_info *revs)
 {
 	commit_list_free(revs->commits);
+	clear_prio_queue(&revs->commit_queue);
 	commit_list_free(revs->ancestry_path_bottoms);
 	release_display_notes(&revs->notes_opt);
 	object_array_clear(&revs->pending);
@@ -3726,7 +3723,7 @@ static void explore_walk_step(struct rev_info *revs)
 	if (revs->max_age != -1 && (c->date < revs->max_age))
 		c->object.flags |= UNINTERESTING;
 
-	if (process_parents(revs, c, NULL, NULL) < 0)
+	if (process_parents(revs, c, NULL) < 0)
 		return;
 
 	if (c->object.flags & UNINTERESTING)
@@ -3902,7 +3899,7 @@ static void expand_topo_walk(struct rev_info *revs, struct commit *commit)
 {
 	struct commit_list *p;
 	struct topo_walk_info *info = revs->topo_walk_info;
-	if (process_parents(revs, commit, NULL, NULL) < 0) {
+	if (process_parents(revs, commit, NULL) < 0) {
 		if (!revs->ignore_missing_links)
 			die("Failed to traverse parents of commit %s",
 			    oid_to_hex(&commit->object.oid));
@@ -3938,6 +3935,13 @@ static void expand_topo_walk(struct rev_info *revs, struct commit *commit)
 	}
 }
 
+void rev_info_commit_list_to_queue(struct rev_info *revs)
+{
+	while (revs->commits)
+		prio_queue_put(&revs->commit_queue, pop_commit(&revs->commits));
+}
+
+
 int prepare_revision_walk(struct rev_info *revs)
 {
 	int i;
@@ -4006,7 +4010,7 @@ static enum rewrite_result rewrite_one_1(struct rev_info *revs,
 	for (;;) {
 		struct commit *p = *pp;
 		if (!revs->limited)
-			if (process_parents(revs, p, NULL, queue) < 0)
+			if (process_parents(revs, p, queue) < 0)
 				return rewrite_one_error;
 		if (p->object.flags & UNINTERESTING)
 			return rewrite_one_ok;
@@ -4020,27 +4024,18 @@ static enum rewrite_result rewrite_one_1(struct rev_info *revs,
 	}
 }
 
-static void merge_queue_into_list(struct prio_queue *q, struct commit_list **list)
+static void merge_queue_into_prio_queue(struct prio_queue *from,
+					struct prio_queue *to)
 {
-	while (q->nr) {
-		struct commit *item = prio_queue_peek(q);
-		struct commit_list *p = *list;
-
-		if (p && p->item->date >= item->date)
-			list = &p->next;
-		else {
-			p = commit_list_insert(item, list);
-			list = &p->next; /* skip newly added item */
-			prio_queue_get(q); /* pop item */
-		}
-	}
+	while (from->nr)
+		prio_queue_put(to, prio_queue_get(from));
 }
 
 static enum rewrite_result rewrite_one(struct rev_info *revs, struct commit **pp)
 {
 	struct prio_queue queue = { compare_commits_by_commit_date };
 	enum rewrite_result ret = rewrite_one_1(revs, pp, &queue);
-	merge_queue_into_list(&queue, &revs->commits);
+	merge_queue_into_prio_queue(&queue, &revs->commit_queue);
 	clear_prio_queue(&queue);
 	return ret;
 }
@@ -4331,6 +4326,7 @@ enum rev_walk_mode {
 	REV_WALK_REFLOG,
 	REV_WALK_TOPO,
 	REV_WALK_LIMITED,
+	REV_WALK_NO_WALK,
 	REV_WALK_STREAMING,
 };
 
@@ -4342,6 +4338,8 @@ static enum rev_walk_mode get_walk_mode(struct rev_info *revs)
 		return REV_WALK_TOPO;
 	if (revs->limited)
 		return REV_WALK_LIMITED;
+	if (revs->no_walk)
+		return REV_WALK_NO_WALK;
 	return REV_WALK_STREAMING;
 }
 
@@ -4349,6 +4347,9 @@ static struct commit *get_revision_1(struct rev_info *revs)
 {
 	enum rev_walk_mode mode = get_walk_mode(revs);
 
+	if (mode == REV_WALK_STREAMING && revs->commits)
+		rev_info_commit_list_to_queue(revs);
+
 	while (1) {
 		struct commit *commit;
 
@@ -4360,9 +4361,12 @@ static struct commit *get_revision_1(struct rev_info *revs)
 			commit = next_topo_commit(revs);
 			break;
 		case REV_WALK_LIMITED:
-		case REV_WALK_STREAMING:
+		case REV_WALK_NO_WALK:
 			commit = pop_commit(&revs->commits);
 			break;
+		case REV_WALK_STREAMING:
+			commit = prio_queue_get(&revs->commit_queue);
+			break;
 		}
 
 		if (!commit)
@@ -4390,12 +4394,13 @@ static struct commit *get_revision_1(struct rev_info *revs)
 			break;
 		case REV_WALK_STREAMING:
 			if (process_parents(revs, commit,
-					    &revs->commits, NULL) < 0) {
+					    &revs->commit_queue) < 0) {
 				if (!revs->ignore_missing_links)
 					die("Failed to traverse parents of commit %s",
 					    oid_to_hex(&commit->object.oid));
 			}
 			break;
+		case REV_WALK_NO_WALK:
 		case REV_WALK_LIMITED:
 			break;
 		}
diff --git a/revision.h b/revision.h
index 584f1338b5e323..04982a3d47f28f 100644
--- a/revision.h
+++ b/revision.h
@@ -12,6 +12,7 @@
 #include "decorate.h"
 #include "ident.h"
 #include "list-objects-filter-options.h"
+#include "prio-queue.h"
 #include "strvec.h"
 
 /**
@@ -122,8 +123,14 @@ struct oidset;
 struct topo_walk_info;
 
 struct rev_info {
-	/* Starting list */
+	/*
+	 * Work queue of commits, stored as either a linked list or a
+	 * priority queue, but never both at the same time.
+	 * rev_info_commit_list_to_queue() converts list to queue.
+	 */
 	struct commit_list *commits;
+	struct prio_queue commit_queue;
+
 	struct object_array pending;
 	struct repository *repo;
 
@@ -400,6 +407,7 @@ struct rev_info {
  * uninitialized.
  */
 #define REV_INFO_INIT { \
+	.commit_queue = { .compare = compare_commits_by_commit_date }, \
 	.abbrev = DEFAULT_ABBREV, \
 	.simplify_history = 1, \
 	.pruning.flags.recursive = 1, \
@@ -478,6 +486,8 @@ void reset_revision_walk(void);
  */
 int prepare_revision_walk(struct rev_info *revs);
 
+/* Drain the commits linked list into the priority queue. */
+void rev_info_commit_list_to_queue(struct rev_info *revs);
 /**
  * Takes a pointer to a `rev_info` structure and iterates over it, returning a
  * `struct commit *` each time you call it. The end of the revision list is

From 7dd898a92ddc2318c3516c06fb6769c3e64216ed Mon Sep 17 00:00:00 2001
From: Kristoffer Haugsbakk 
Date: Thu, 28 May 2026 09:00:10 +0200
Subject: [PATCH 054/106] *: replace deprecated free_commit_list
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Replace `free_commit_list` with `commit_list_free`. The former was
deprecated in 9f18d089 (commit: rename `free_commit_list()` to conform
to coding guidelines, 2026-01-15).

This allows us to remove all the deprecated functions in the
next commit:

• `copy_commit_list`
• `reverse_commit_list`
• `free_commit_list`

Acked-by: Patrick Steinhardt 
Signed-off-by: Kristoffer Haugsbakk 
Signed-off-by: Junio C Hamano 
---
 builtin/history.c | 4 ++--
 replay.c          | 2 +-
 upload-pack.c     | 4 ++--
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/builtin/history.c b/builtin/history.c
index 952693808574b7..4c47ce494689ac 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -272,7 +272,7 @@ static int setup_revwalk(struct repository *repo,
 
 		commit_list_insert(original, &from_list);
 		ret = repo_is_descendant_of(repo, head, from_list);
-		free_commit_list(from_list);
+		commit_list_free(from_list);
 
 		if (ret < 0) {
 			ret = error(_("cannot determine descendance"));
@@ -649,7 +649,7 @@ static int split_commit(struct repository *repo,
 	if (index_file.len)
 		unlink(index_file.buf);
 	strbuf_release(&index_file);
-	free_commit_list(parents);
+	commit_list_free(parents);
 	release_index(&index);
 	return ret;
 }
diff --git a/replay.c b/replay.c
index f96f1f6551ae63..4f1996485fdd9a 100644
--- a/replay.c
+++ b/replay.c
@@ -120,7 +120,7 @@ static struct commit *create_commit(struct repository *repo,
 out:
 	repo_unuse_commit_buffer(repo, based_on, message);
 	free_commit_extra_headers(extra);
-	free_commit_list(parents);
+	commit_list_free(parents);
 	strbuf_release(&msg);
 	free(author);
 	return (struct commit *)obj;
diff --git a/upload-pack.c b/upload-pack.c
index 9f6d6fe48c8c58..2bf450ab2880c7 100644
--- a/upload-pack.c
+++ b/upload-pack.c
@@ -886,7 +886,7 @@ static void deepen(struct upload_pack_data *data, int depth)
 					     data->deepen_relative, depth,
 					     SHALLOW, NOT_SHALLOW);
 		send_shallow(data, result);
-		free_commit_list(result);
+		commit_list_free(result);
 	}
 
 	send_unshallow(data);
@@ -900,7 +900,7 @@ static void deepen_by_rev_list(struct upload_pack_data *data,
 	disable_commit_graph(the_repository);
 	result = get_shallow_commits_by_rev_list(argv, SHALLOW, NOT_SHALLOW);
 	send_shallow(data, result);
-	free_commit_list(result);
+	commit_list_free(result);
 	send_unshallow(data);
 }
 

From 83e7f3bd2bac934c21f39175b965c37810a41ea5 Mon Sep 17 00:00:00 2001
From: Kristoffer Haugsbakk 
Date: Thu, 28 May 2026 09:00:11 +0200
Subject: [PATCH 055/106] commit: remove deprecated functions

These functions were deprecated in a series of commits merged in
52882024 (Merge branch 'ps/commit-list-functions-renamed', 2026-02-13).

The compatibility was for in-flight topics at the time.

Acked-by: Patrick Steinhardt 
Signed-off-by: Kristoffer Haugsbakk 
Signed-off-by: Junio C Hamano 
---
 commit.h | 19 -------------------
 1 file changed, 19 deletions(-)

diff --git a/commit.h b/commit.h
index 58150045afafed..5352056f87abfa 100644
--- a/commit.h
+++ b/commit.h
@@ -203,25 +203,6 @@ struct commit_list *commit_list_reverse(struct commit_list *list);
 
 void commit_list_free(struct commit_list *list);
 
-/*
- * Deprecated compatibility functions for `struct commit_list`, to be removed
- * once Git 2.53 is released.
- */
-static inline struct commit_list *copy_commit_list(struct commit_list *l)
-{
-	return commit_list_copy(l);
-}
-
-static inline struct commit_list *reverse_commit_list(struct commit_list *l)
-{
-	return commit_list_reverse(l);
-}
-
-static inline void free_commit_list(struct commit_list *l)
-{
-	commit_list_free(l);
-}
-
 struct rev_info; /* in revision.h, it circularly uses enum cmit_fmt */
 
 const char *repo_logmsg_reencode(struct repository *r,

From 8c84e6802c0e23503bfe655dadcdc4a15de7373a Mon Sep 17 00:00:00 2001
From: Kristofer Karlsson 
Date: Thu, 28 May 2026 09:00:48 +0000
Subject: [PATCH 056/106] t3070: skip ls-files tests with backslash patterns on
 Windows
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

On Windows (MINGW), backslashes in pathspecs are silently converted to
forward slashes (directory separators), which changes the glob semantics.
This causes 36 test failures in t3070-wildmatch when the "via ls-files"
variants test patterns containing backslash escapes (e.g. '\[ab]',
'[\-_]', '[A-\\]').

The wildmatch function itself handles these patterns correctly — only the
ls-files code path fails because pathspec parsing converts the
backslashes before they reach the glob matcher.

Skip these ls-files tests on platforms where BSLASHPSPEC is not set,
which is the existing prereq that captures exactly this semantic:
"backslashes in pathspec are not directory separators."

Signed-off-by: Kristofer Karlsson 
Signed-off-by: Junio C Hamano 
---
 t/t3070-wildmatch.sh | 19 +++++++++++++------
 1 file changed, 13 insertions(+), 6 deletions(-)

diff --git a/t/t3070-wildmatch.sh b/t/t3070-wildmatch.sh
index 655bb1a0f21031..33941222189663 100755
--- a/t/t3070-wildmatch.sh
+++ b/t/t3070-wildmatch.sh
@@ -99,6 +99,13 @@ match_with_ls_files() {
 	match_function=$4
 	ls_files_args=$5
 
+	prereqs=EXPENSIVE_ON_WINDOWS
+	case "$pattern" in
+	*\\*)
+		prereqs="$prereqs,BSLASHPSPEC"
+		;;
+	esac
+
 	match_stdout_stderr_cmp="
 		tr -d '\0' actual &&
 		test_must_be_empty actual.err &&
@@ -108,36 +115,36 @@ match_with_ls_files() {
 	then
 		if test -e .git/created_test_file
 		then
-			test_expect_success EXPENSIVE_ON_WINDOWS "$match_function (via ls-files): match dies on '$pattern' '$text'" "
+			test_expect_success $prereqs "$match_function (via ls-files): match dies on '$pattern' '$text'" "
 				printf '%s' '$text' >expect &&
 				test_must_fail git$ls_files_args ls-files -z -- '$pattern'
 			"
 		else
-			test_expect_failure EXPENSIVE_ON_WINDOWS "$match_function (via ls-files): match skip '$pattern' '$text'" 'false'
+			test_expect_failure $prereqs "$match_function (via ls-files): match skip '$pattern' '$text'" 'false'
 		fi
 	elif test "$match_expect" = 1
 	then
 		if test -e .git/created_test_file
 		then
-			test_expect_success EXPENSIVE_ON_WINDOWS "$match_function (via ls-files): match '$pattern' '$text'" "
+			test_expect_success $prereqs "$match_function (via ls-files): match '$pattern' '$text'" "
 				printf '%s' '$text' >expect &&
 				git$ls_files_args ls-files -z -- '$pattern' >actual.raw 2>actual.err &&
 				$match_stdout_stderr_cmp
 			"
 		else
-			test_expect_failure EXPENSIVE_ON_WINDOWS "$match_function (via ls-files): match skip '$pattern' '$text'" 'false'
+			test_expect_failure $prereqs "$match_function (via ls-files): match skip '$pattern' '$text'" 'false'
 		fi
 	elif test "$match_expect" = 0
 	then
 		if test -e .git/created_test_file
 		then
-			test_expect_success EXPENSIVE_ON_WINDOWS "$match_function (via ls-files): no match '$pattern' '$text'" "
+			test_expect_success $prereqs "$match_function (via ls-files): no match '$pattern' '$text'" "
 				>expect &&
 				git$ls_files_args ls-files -z -- '$pattern' >actual.raw 2>actual.err &&
 				$match_stdout_stderr_cmp
 			"
 		else
-			test_expect_failure EXPENSIVE_ON_WINDOWS "$match_function (via ls-files): no match skip '$pattern' '$text'" 'false'
+			test_expect_failure $prereqs "$match_function (via ls-files): no match skip '$pattern' '$text'" 'false'
 		fi
 	else
 		test_expect_success "PANIC: Test framework error. Unknown matches value $match_expect" 'false'

From 1ec041bebb46159562c4beeb2e6980284e0f9a28 Mon Sep 17 00:00:00 2001
From: Michael Montalbo 
Date: Thu, 28 May 2026 19:21:45 +0000
Subject: [PATCH 057/106] doc: clarify that --word-diff operates on line-level
 hunks

The --word-diff documentation describes the output modes and
word-regex mechanics but does not explain that word-diff operates
within the hunks produced by the line-level diff rather than
performing an independent word-stream comparison.  This can
surprise users when the line-level alignment causes word-level
changes to appear even though the words in both files are
identical.

Add an implementation note explaining the two-stage relationship
and that the output may change if Git acquires a different
implementation in the future.

Signed-off-by: Michael Montalbo 
Signed-off-by: Junio C Hamano 
---
 Documentation/diff-options.adoc | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/Documentation/diff-options.adoc b/Documentation/diff-options.adoc
index 9cdad6f72a0c7d..88b724b8c6dba4 100644
--- a/Documentation/diff-options.adoc
+++ b/Documentation/diff-options.adoc
@@ -455,6 +455,14 @@ endif::git-diff[]
 +
 Note that despite the name of the first mode, color is used to
 highlight the changed parts in all modes if enabled.
++
+The `--word-diff` option operates by taking the same line-by-line
+diff that is produced without the option and computing
+word-by-word changes within each hunk.  This may produce a
+larger diff than a dedicated word-diff tool would.  If Git
+acquires a different implementation in the future, the output
+may change.  Note that this is similar to the `--diff-algorithm`
+option, which may also change the output.
 
 `--word-diff-regex=`::
 	Use __ to decide what a word is, instead of considering

From 558057cf4f43ea3b28c5e0b1b2250cab362f1a6a Mon Sep 17 00:00:00 2001
From: Michael Montalbo 
Date: Thu, 28 May 2026 20:47:44 +0000
Subject: [PATCH 058/106] revision: move -L setup before output_format-to-diff
 derivation

The line_level_traverse block sets a default DIFF_FORMAT_PATCH when
no output format has been explicitly requested.  This default must
be visible to the "Did the user ask for any diff output?" check
that derives revs->diff from revs->diffopt.output_format.

Currently the -L block runs after that derivation, so revs->diff
stays 0 when no explicit format is given.  This does not matter yet
because log_tree_commit() short-circuits into line_log_print()
before consulting revs->diff, but the next commit will route -L
through the normal log_tree_diff() path, which checks revs->diff.

Move the block above the derivation so the default DIFF_FORMAT_PATCH
is in place when revs->diff is computed.  No behavior change on its
own.

Signed-off-by: Michael Montalbo 
Signed-off-by: Junio C Hamano 
---
 revision.c | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/revision.c b/revision.c
index 599b3a66c369ca..4a8e24bc38d572 100644
--- a/revision.c
+++ b/revision.c
@@ -3112,6 +3112,14 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s
 		object_context_release(&oc);
 	}
 
+	if (revs->line_level_traverse) {
+		if (want_ancestry(revs))
+			revs->limited = 1;
+		revs->topo_order = 1;
+		if (!revs->diffopt.output_format)
+			revs->diffopt.output_format = DIFF_FORMAT_PATCH;
+	}
+
 	/* Did the user ask for any diff output? Run the diff! */
 	if (revs->diffopt.output_format & ~DIFF_FORMAT_NO_OUTPUT)
 		revs->diff = 1;
@@ -3125,14 +3133,6 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s
 	if (revs->diffopt.objfind)
 		revs->simplify_history = 0;
 
-	if (revs->line_level_traverse) {
-		if (want_ancestry(revs))
-			revs->limited = 1;
-		revs->topo_order = 1;
-		if (!revs->diffopt.output_format)
-			revs->diffopt.output_format = DIFF_FORMAT_PATCH;
-	}
-
 	if (revs->topo_order && !generation_numbers_enabled(the_repository))
 		revs->limited = 1;
 

From 42d960748efa79a31e72cc36d983aca244dc167e Mon Sep 17 00:00:00 2001
From: Michael Montalbo 
Date: Thu, 28 May 2026 20:47:45 +0000
Subject: [PATCH 059/106] line-log: integrate -L output with the standard
 log-tree pipeline

`git log -L` has bypassed log_tree_diff() and log_tree_diff_flush()
since the feature was introduced, short-circuiting from
log_tree_commit() directly into line_log_print().  This skips the
no_free save/restore (noted in a NEEDSWORK comment added by
f8781bfda3), the always_show_header fallback, show_diff_of_diff(),
and diff_free() cleanup.

Restructure so that -L flows through log_tree_diff() ->
log_tree_diff_flush(), the same path used by the normal
single-parent and merge diff codepaths:

 - Rename line_log_print() to line_log_queue_pairs() and strip it
   down to just queuing pre-computed filepairs.  The show_log(),
   separator, diffcore_std(), and diff_flush() calls are removed
   since log_tree_diff_flush() handles all of those.

 - In log_tree_diff(), call line_log_queue_pairs() then
   log_tree_diff_flush(), mirroring the diff_tree_oid() + flush
   pattern used by the single-parent and merge codepaths.

 - Remove the early return in log_tree_commit() that is no longer
   needed now that -L output flows through log_tree_diff() and
   log_tree_diff_flush(); this restores no_free save/restore,
   always_show_header, and diff_free() cleanup.

Because show_log() is now deferred until after diffcore_std() inside
log_tree_diff_flush(), pickaxe (-S, -G, --find-object) and
--diff-filter now properly suppress commits when all pairs are
filtered out.

The blank-line separator between commit header and diff changes
slightly: the old code printed one unconditionally, while
log_tree_diff_flush() only emits one for verbose headers.  This
matches the rest of log output.

Also reject --full-diff, which is not yet supported with -L: the
filepairs are pre-computed during the history walk and scoped to
tracked line ranges, so there is currently no full-tree diff to
fall back to for display.

Update tests accordingly.

Signed-off-by: Michael Montalbo 
Signed-off-by: Junio C Hamano 
---
 line-log.c                                    | 30 ++++-------
 line-log.h                                    |  2 +-
 log-tree.c                                    | 10 ++--
 revision.c                                    |  6 ++-
 t/t4211-line-log.sh                           | 53 ++++++++++++++-----
 t/t4211/sha1/expect.parallel-change-f-to-main |  1 -
 .../sha256/expect.parallel-change-f-to-main   |  1 -
 7 files changed, 60 insertions(+), 43 deletions(-)

diff --git a/line-log.c b/line-log.c
index 858a899cd2a61d..7ee55b05cc5077 100644
--- a/line-log.c
+++ b/line-log.c
@@ -13,7 +13,6 @@
 #include "revision.h"
 #include "xdiff-interface.h"
 #include "strbuf.h"
-#include "log-tree.h"
 #include "line-log.h"
 #include "setup.h"
 #include "strvec.h"
@@ -1004,29 +1003,18 @@ static int process_all_files(struct line_log_data **range_out,
 	return changed;
 }
 
-int line_log_print(struct rev_info *rev, struct commit *commit)
+void line_log_queue_pairs(struct rev_info *rev, struct commit *commit)
 {
-	show_log(rev);
-	if (!(rev->diffopt.output_format & DIFF_FORMAT_NO_OUTPUT)) {
-		struct line_log_data *range = lookup_line_range(rev, commit);
-		struct line_log_data *r;
-		const char *prefix = diff_line_prefix(&rev->diffopt);
-
-		fprintf(rev->diffopt.file, "%s\n", prefix);
-
-		for (r = range; r; r = r->next) {
-			if (r->pair) {
-				struct diff_filepair *p =
-					diff_filepair_dup(r->pair);
-				p->line_ranges = &r->ranges;
-				diff_q(&diff_queued_diff, p);
-			}
-		}
+	struct line_log_data *range = lookup_line_range(rev, commit);
+	struct line_log_data *r;
 
-		diffcore_std(&rev->diffopt);
-		diff_flush(&rev->diffopt);
+	for (r = range; r; r = r->next) {
+		if (r->pair) {
+			struct diff_filepair *p = diff_filepair_dup(r->pair);
+			p->line_ranges = &r->ranges;
+			diff_q(&diff_queued_diff, p);
+		}
 	}
-	return 1;
 }
 
 static int bloom_filter_check(struct rev_info *rev,
diff --git a/line-log.h b/line-log.h
index 04a6ea64d3d68f..99e1755ce3d568 100644
--- a/line-log.h
+++ b/line-log.h
@@ -46,7 +46,7 @@ int line_log_filter(struct rev_info *rev);
 int line_log_process_ranges_arbitrary_commit(struct rev_info *rev,
 						    struct commit *commit);
 
-int line_log_print(struct rev_info *rev, struct commit *commit);
+void line_log_queue_pairs(struct rev_info *rev, struct commit *commit);
 
 void line_log_free(struct rev_info *rev);
 
diff --git a/log-tree.c b/log-tree.c
index 7e048701d0c5b4..88b3019293b725 100644
--- a/log-tree.c
+++ b/log-tree.c
@@ -1105,6 +1105,12 @@ static int log_tree_diff(struct rev_info *opt, struct commit *commit, struct log
 	if (!all_need_diff && !opt->merges_need_diff)
 		return 0;
 
+	if (opt->line_level_traverse) {
+		line_log_queue_pairs(opt, commit);
+		log_tree_diff_flush(opt);
+		return !opt->loginfo;
+	}
+
 	parse_commit_or_die(commit);
 	oid = get_commit_tree_oid(commit);
 
@@ -1179,10 +1185,6 @@ int log_tree_commit(struct rev_info *opt, struct commit *commit)
 	opt->loginfo = &log;
 	opt->diffopt.no_free = 1;
 
-	/* NEEDSWORK: no restoring of no_free?  Why? */
-	if (opt->line_level_traverse)
-		return line_log_print(opt, commit);
-
 	if (opt->track_linear && !opt->linear && !opt->reverse_output_stage)
 		fprintf(opt->diffopt.file, "\n%s\n", opt->break_bar);
 	shown = log_tree_diff(opt, commit, &log);
diff --git a/revision.c b/revision.c
index 4a8e24bc38d572..c903f7a1b4c4c8 100644
--- a/revision.c
+++ b/revision.c
@@ -3179,8 +3179,10 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s
 		die(_("the option '%s' requires '%s'"), "--grep-reflog", "--walk-reflogs");
 
 	if (revs->line_level_traverse &&
-	    (revs->diffopt.output_format & ~(DIFF_FORMAT_PATCH | DIFF_FORMAT_NO_OUTPUT)))
-		die(_("-L does not yet support diff formats besides -p and -s"));
+	    (revs->full_diff ||
+	     (revs->diffopt.output_format &
+	      ~(DIFF_FORMAT_PATCH | DIFF_FORMAT_NO_OUTPUT))))
+		die(_("-L does not yet support the requested diff format"));
 
 	if (revs->expand_tabs_in_log < 0)
 		revs->expand_tabs_in_log = revs->expand_tabs_in_log_default;
diff --git a/t/t4211-line-log.sh b/t/t4211-line-log.sh
index aaf197d2edc4d8..e3937138a94055 100755
--- a/t/t4211-line-log.sh
+++ b/t/t4211-line-log.sh
@@ -368,7 +368,6 @@ test_expect_success '-L diff output includes index and new file mode' '
 
 test_expect_success '-L with --word-diff' '
 	cat >expect <<-\EOF &&
-
 	diff --git a/file.c b/file.c
 	--- a/file.c
 	+++ b/file.c
@@ -377,7 +376,6 @@ test_expect_success '-L with --word-diff' '
 	{
 	    return [-F2;-]{+F2 + 2;+}
 	}
-
 	diff --git a/file.c b/file.c
 	new file mode 100644
 	--- /dev/null
@@ -433,7 +431,6 @@ test_expect_success 'show line-log with graph' '
 	null_blob=$(test_oid zero | cut -c1-7) &&
 	qz_to_tab_space >expect <<-EOF &&
 	* $head_oid Modify func2() in file.c
-	|Z
 	| diff --git a/file.c b/file.c
 	| index $head_blob_old..$head_blob_new 100644
 	| --- a/file.c
@@ -445,7 +442,6 @@ test_expect_success 'show line-log with graph' '
 	| +    return F2 + 2;
 	|  }
 	* $root_oid Add func1() and func2() in file.c
-	ZZ
 	  diff --git a/file.c b/file.c
 	  new file mode 100644
 	  index $null_blob..$root_blob
@@ -494,23 +490,17 @@ test_expect_success '-L --find-object does not crash with merge and rename' '
 		--find-object=$(git rev-parse HEAD:file) >actual
 '
 
-# Commit-level filtering with pickaxe does not yet work for -L.
-# show_log() prints the commit header before diffcore_std() runs
-# pickaxe, so commits cannot be suppressed even when no diff pairs
-# survive filtering.  Fixing this would require deferring show_log()
-# until after diffcore_std(), which is a larger restructuring of the
-# log-tree output pipeline.
-test_expect_failure '-L -G should filter commits by pattern' '
+test_expect_success '-L -G should filter commits by pattern' '
 	git log --format="%s" --no-patch -L 1,1:file -G "nomatch" >actual &&
 	test_must_be_empty actual
 '
 
-test_expect_failure '-L -S should filter commits by pattern' '
+test_expect_success '-L -S should filter commits by pattern' '
 	git log --format="%s" --no-patch -L 1,1:file -S "nomatch" >actual &&
 	test_must_be_empty actual
 '
 
-test_expect_failure '-L --find-object should filter commits by object' '
+test_expect_success '-L --find-object should filter commits by object' '
 	git log --format="%s" --no-patch -L 1,1:file \
 		--find-object=$ZERO_OID >actual &&
 	test_must_be_empty actual
@@ -711,4 +701,41 @@ test_expect_success '-L with -G filters to diff-text matches' '
 	grep "F2 + 2" actual
 '
 
+test_expect_success '-L with --diff-filter=M excludes root commit' '
+	git checkout parent-oids &&
+	git log -L:func2:file.c --diff-filter=M --format=%s --no-patch >actual &&
+	# Root commit is an Add (A), not a Modify (M), so it should
+	# be excluded; only the modification commit remains.
+	echo "Modify func2() in file.c" >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success '-L with --diff-filter=A shows only root commit' '
+	git checkout parent-oids &&
+	git log -L:func2:file.c --diff-filter=A --format=%s --no-patch >actual &&
+	echo "Add func1() and func2() in file.c" >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success '-L with -S suppresses non-matching commits' '
+	git checkout parent-oids &&
+	git log -L:func2:file.c -S "F2 + 2" --format=%s --no-patch >actual &&
+	# Only the commit that changes the count of "F2 + 2" should appear.
+	echo "Modify func2() in file.c" >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success '--full-diff is not yet supported with -L' '
+	test_must_fail git log -L1,24:b.c --full-diff 2>err &&
+	test_grep "does not yet support" err
+'
+
+test_expect_success '-L --oneline has no extra blank line before diff' '
+	git checkout parent-oids &&
+	git log --oneline -L:func2:file.c -1 >actual &&
+	# Oneline header on line 1, diff starts immediately on line 2
+	sed -n 2p actual >line2 &&
+	test_grep "^diff --git" line2
+'
+
 test_done
diff --git a/t/t4211/sha1/expect.parallel-change-f-to-main b/t/t4211/sha1/expect.parallel-change-f-to-main
index 65a8cc673a6fca..6d7a20103631cc 100644
--- a/t/t4211/sha1/expect.parallel-change-f-to-main
+++ b/t/t4211/sha1/expect.parallel-change-f-to-main
@@ -5,7 +5,6 @@ Date:   Fri Apr 12 16:16:24 2013 +0200
 
     Merge across the rename
 
-
 commit 6ce3c4ff690136099bb17e1a8766b75764726ea7
 Author: Thomas Rast 
 Date:   Thu Feb 28 10:49:50 2013 +0100
diff --git a/t/t4211/sha256/expect.parallel-change-f-to-main b/t/t4211/sha256/expect.parallel-change-f-to-main
index 3178989253a885..c93e03bef40544 100644
--- a/t/t4211/sha256/expect.parallel-change-f-to-main
+++ b/t/t4211/sha256/expect.parallel-change-f-to-main
@@ -5,7 +5,6 @@ Date:   Fri Apr 12 16:16:24 2013 +0200
 
     Merge across the rename
 
-
 commit 4f7a58195a92c400e28a2354328587f1ff14fb77f5cf894536f17ccbc72931b9
 Author: Thomas Rast 
 Date:   Thu Feb 28 10:49:50 2013 +0100

From 4b5d8a0163fe4e9a4ac074f407e0599ba27acf68 Mon Sep 17 00:00:00 2001
From: Michael Montalbo 
Date: Thu, 28 May 2026 20:47:46 +0000
Subject: [PATCH 060/106] line-log: allow non-patch diff formats with -L

Now that -L flows through log_tree_diff_flush() and diff_flush(),
metadata-only diff formats work because they only read filepair
fields (status, mode, path, oid) already set on the pre-computed
pairs.

Expand the allowlist in setup_revisions() to also accept --raw,
--name-only, --name-status, and --summary.  Diff stat formats
(--stat, --numstat, --shortstat, --dirstat) remain blocked because
they call compute_diffstat() on full blob content and would show
whole-file statistics rather than range-scoped ones.

Signed-off-by: Michael Montalbo 
Signed-off-by: Junio C Hamano 
---
 Documentation/line-range-options.adoc | 10 +++---
 revision.c                            |  4 ++-
 t/t4211-line-log.sh                   | 47 +++++++++++++++++++++++++--
 3 files changed, 54 insertions(+), 7 deletions(-)

diff --git a/Documentation/line-range-options.adoc b/Documentation/line-range-options.adoc
index ecb2c79fb9bde8..72f639b5e79ea4 100644
--- a/Documentation/line-range-options.adoc
+++ b/Documentation/line-range-options.adoc
@@ -8,12 +8,14 @@
 	give zero or one positive revision arguments, and
 	__ and __ (or __) must exist in the starting revision.
 	You can specify this option more than once. Implies `--patch`.
-	Patch output can be suppressed using `--no-patch`, but other diff formats
-	(namely `--raw`, `--numstat`, `--shortstat`, `--dirstat`, `--summary`,
-	`--name-only`, `--name-status`, `--check`) are not currently implemented.
+	Patch output can be suppressed using `--no-patch`.
+	Non-patch diff formats `--raw`, `--name-only`, `--name-status`,
+	and `--summary` are supported.  Diff stat formats
+	(`--stat`, `--numstat`, `--shortstat`, `--dirstat`) are not
+	currently implemented.
 +
 Patch formatting options such as `--word-diff`, `--color-moved`,
 `--no-prefix`, and whitespace options (`-w`, `-b`) are supported,
-as are pickaxe options (`-S`, `-G`).
+as are pickaxe options (`-S`, `-G`) and `--diff-filter`.
 +
 include::line-range-format.adoc[]
diff --git a/revision.c b/revision.c
index c903f7a1b4c4c8..f26fc1f4d5e48e 100644
--- a/revision.c
+++ b/revision.c
@@ -3181,7 +3181,9 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s
 	if (revs->line_level_traverse &&
 	    (revs->full_diff ||
 	     (revs->diffopt.output_format &
-	      ~(DIFF_FORMAT_PATCH | DIFF_FORMAT_NO_OUTPUT))))
+	      ~(DIFF_FORMAT_PATCH | DIFF_FORMAT_NO_OUTPUT |
+		DIFF_FORMAT_RAW | DIFF_FORMAT_NAME |
+		DIFF_FORMAT_NAME_STATUS | DIFF_FORMAT_SUMMARY))))
 		die(_("-L does not yet support the requested diff format"));
 
 	if (revs->expand_tabs_in_log < 0)
diff --git a/t/t4211-line-log.sh b/t/t4211-line-log.sh
index e3937138a94055..ca4eb7bbc713ef 100755
--- a/t/t4211-line-log.sh
+++ b/t/t4211-line-log.sh
@@ -155,8 +155,45 @@ test_expect_success '-p shows the default patch output' '
 	test_cmp expect actual
 '
 
-test_expect_success '--raw is forbidden' '
-	test_must_fail git log -L1,24:b.c --raw
+test_expect_success '--raw shows mode, oid, status and path' '
+	git log -L1,24:b.c --raw --format= >actual &&
+	test_grep "^:100644 100644 [0-9a-f]\{7\} [0-9a-f]\{7\} M	b.c$" actual &&
+	test_grep ! "^diff --git" actual &&
+	test_grep ! "^@@" actual
+'
+
+test_expect_success '--name-only shows path' '
+	git log -L1,24:b.c --name-only --format= >actual &&
+	test_grep "^b.c$" actual &&
+	test_grep ! "^diff --git" actual &&
+	test_grep ! "^@@" actual
+'
+
+test_expect_success '--name-status shows status and path' '
+	git log -L1,24:b.c --name-status --format= >actual &&
+	test_grep "^M	b.c$" actual &&
+	test_grep ! "^diff --git" actual &&
+	test_grep ! "^@@" actual
+'
+
+test_expect_success '--stat is not yet supported with -L' '
+	test_must_fail git log -L1,24:b.c --stat 2>err &&
+	test_grep "does not yet support" err
+'
+
+test_expect_success '--numstat is not yet supported with -L' '
+	test_must_fail git log -L1,24:b.c --numstat 2>err &&
+	test_grep "does not yet support" err
+'
+
+test_expect_success '--shortstat is not yet supported with -L' '
+	test_must_fail git log -L1,24:b.c --shortstat 2>err &&
+	test_grep "does not yet support" err
+'
+
+test_expect_success '--dirstat is not yet supported with -L' '
+	test_must_fail git log -L1,24:b.c --dirstat 2>err &&
+	test_grep "does not yet support" err
 '
 
 test_expect_success 'setup for checking fancy rename following' '
@@ -738,4 +775,10 @@ test_expect_success '-L --oneline has no extra blank line before diff' '
 	test_grep "^diff --git" line2
 '
 
+test_expect_success '--summary shows new file on root commit' '
+	git checkout parent-oids &&
+	git log -L:func2:file.c --summary --format= >actual &&
+	test_grep "create mode 100644 file.c" actual
+'
+
 test_done

From b8cda126b4e0fbfd514b26dec4ee8a1c6849abe9 Mon Sep 17 00:00:00 2001
From: Sebastien Tardif 
Date: Thu, 28 May 2026 02:56:54 +0000
Subject: [PATCH 061/106] daemon: fix IPv6 address corruption in
 lookup_hostname()

getaddrinfo() is called with AF_UNSPEC hints, so it may return IPv6
results. However, the code unconditionally casts ai_addr to
sockaddr_in and passes AF_INET to inet_ntop(). On IPv6-only hosts,
this reads from the wrong struct offset, producing garbage IP
addresses.

Fix this by checking ai_family and extracting the address pointer
into a local variable before calling inet_ntop() once with the
correct family. Die on unexpected address families.

Signed-off-by: Sebastien Tardif 
Signed-off-by: Junio C Hamano 
---
 daemon.c | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)

diff --git a/daemon.c b/daemon.c
index 0a7b1aae447912..80fa0226d89f03 100644
--- a/daemon.c
+++ b/daemon.c
@@ -674,9 +674,20 @@ static void lookup_hostname(struct hostinfo *hi)
 
 		gai = getaddrinfo(hi->hostname.buf, NULL, &hints, &ai);
 		if (!gai) {
-			struct sockaddr_in *sin_addr = (void *)ai->ai_addr;
+			void *addr;
+
+			if (ai->ai_family == AF_INET) {
+				struct sockaddr_in *sa = (void *)ai->ai_addr;
+				addr = &sa->sin_addr;
+			} else if (ai->ai_family == AF_INET6) {
+				struct sockaddr_in6 *sa6 = (void *)ai->ai_addr;
+				addr = &sa6->sin6_addr;
+			} else {
+				die("unexpected address family: %d",
+				    ai->ai_family);
+			}
 
-			inet_ntop(AF_INET, &sin_addr->sin_addr,
+			inet_ntop(ai->ai_family, addr,
 				  addrbuf, sizeof(addrbuf));
 			strbuf_addstr(&hi->ip_address, addrbuf);
 

From 30c8fda1ab6d55d3b0129bb1686c23bf06cd5b0d Mon Sep 17 00:00:00 2001
From: Sebastien Tardif 
Date: Thu, 28 May 2026 02:56:55 +0000
Subject: [PATCH 062/106] daemon: fix IPv6 address truncation in ip2str()

The sockaddr struct size (ai_addrlen) is passed as the output buffer
size to inet_ntop(). For IPv6, sizeof(sockaddr_in6) is 28 bytes but
INET6_ADDRSTRLEN is 46, so long IPv6 addresses are silently truncated.

Fix this by passing sizeof(ip) instead, which is the actual size of
the destination buffer. Drop the now-unused len parameter from
ip2str() and update all callers.

Signed-off-by: Sebastien Tardif 
Signed-off-by: Junio C Hamano 
---
 daemon.c | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/daemon.c b/daemon.c
index 80fa0226d89f03..103c08d868d5de 100644
--- a/daemon.c
+++ b/daemon.c
@@ -947,7 +947,7 @@ struct socketlist {
 	size_t alloc;
 };
 
-static const char *ip2str(int family, struct sockaddr *sin, socklen_t len)
+static const char *ip2str(int family, struct sockaddr *sin)
 {
 #ifdef NO_IPV6
 	static char ip[INET_ADDRSTRLEN];
@@ -958,11 +958,11 @@ static const char *ip2str(int family, struct sockaddr *sin, socklen_t len)
 	switch (family) {
 #ifndef NO_IPV6
 	case AF_INET6:
-		inet_ntop(family, &((struct sockaddr_in6*)sin)->sin6_addr, ip, len);
+		inet_ntop(family, &((struct sockaddr_in6*)sin)->sin6_addr, ip, sizeof(ip));
 		break;
 #endif
 	case AF_INET:
-		inet_ntop(family, &((struct sockaddr_in*)sin)->sin_addr, ip, len);
+		inet_ntop(family, &((struct sockaddr_in*)sin)->sin_addr, ip, sizeof(ip));
 		break;
 	default:
 		xsnprintf(ip, sizeof(ip), "");
@@ -1019,14 +1019,14 @@ static int setup_named_sock(char *listen_addr, int listen_port, struct socketlis
 
 		if (bind(sockfd, ai->ai_addr, ai->ai_addrlen) < 0) {
 			logerror("Could not bind to %s: %s",
-				 ip2str(ai->ai_family, ai->ai_addr, ai->ai_addrlen),
+				 ip2str(ai->ai_family, ai->ai_addr),
 				 strerror(errno));
 			close(sockfd);
 			continue;	/* not fatal */
 		}
 		if (listen(sockfd, 5) < 0) {
 			logerror("Could not listen to %s: %s",
-				 ip2str(ai->ai_family, ai->ai_addr, ai->ai_addrlen),
+				 ip2str(ai->ai_family, ai->ai_addr),
 				 strerror(errno));
 			close(sockfd);
 			continue;	/* not fatal */
@@ -1080,7 +1080,7 @@ static int setup_named_sock(char *listen_addr, int listen_port, struct socketlis
 
 	if ( bind(sockfd, (struct sockaddr *)&sin, sizeof sin) < 0 ) {
 		logerror("Could not bind to %s: %s",
-			 ip2str(AF_INET, (struct sockaddr *)&sin, sizeof(sin)),
+			 ip2str(AF_INET, (struct sockaddr *)&sin),
 			 strerror(errno));
 		close(sockfd);
 		return 0;
@@ -1088,7 +1088,7 @@ static int setup_named_sock(char *listen_addr, int listen_port, struct socketlis
 
 	if (listen(sockfd, 5) < 0) {
 		logerror("Could not listen to %s: %s",
-			 ip2str(AF_INET, (struct sockaddr *)&sin, sizeof(sin)),
+			 ip2str(AF_INET, (struct sockaddr *)&sin),
 			 strerror(errno));
 		close(sockfd);
 		return 0;

From 422a5bf57575a8c5d06faedfd77376501917e22c Mon Sep 17 00:00:00 2001
From: Sebastien Tardif 
Date: Thu, 28 May 2026 02:56:56 +0000
Subject: [PATCH 063/106] daemon: guard NULL REMOTE_PORT in execute() logging

REMOTE_ADDR and REMOTE_PORT are both set by the same code path in
handle(), so when the existing REMOTE_ADDR check passes, REMOTE_PORT
is guaranteed to be non-NULL.  Guard REMOTE_PORT as well so that a
future change that breaks this invariant does not pass NULL to
printf's %s, which is undefined behavior.

Signed-off-by: Sebastien Tardif 
Signed-off-by: Junio C Hamano 
---
 daemon.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/daemon.c b/daemon.c
index 103c08d868d5de..78cca8673fdb34 100644
--- a/daemon.c
+++ b/daemon.c
@@ -753,7 +753,7 @@ static int execute(void)
 	struct strvec env = STRVEC_INIT;
 
 	if (addr)
-		loginfo("Connection from %s:%s", addr, port);
+		loginfo("Connection from %s:%s", addr, port ? port : "?");
 
 	set_keep_alive(0);
 	alarm(init_timeout ? init_timeout : timeout);

From 514f039c9052c23047c310f911ba8c0c2e74a1c7 Mon Sep 17 00:00:00 2001
From: Patrick Steinhardt 
Date: Mon, 1 Jun 2026 10:20:24 +0200
Subject: [PATCH 064/106] odb/source-loose: move loose source into "odb/"
 subsystem

In subsequent patches we'll be turning `struct odb_source_loose` into a
proper `struct odb_source`. As a first step towards this goal, move its
struct out of "object-file.c" and into "odb/source-loose.c".

This detaches the implementation of the loose object source from the
generic object file code, following the same convention already used by
the "files" and "in-memory" sources.

No functional changes are intended.

Signed-off-by: Patrick Steinhardt 
Signed-off-by: Junio C Hamano 
---
 Makefile           |  1 +
 meson.build        |  1 +
 object-file.c      |  8 --------
 object-file.h      | 21 +--------------------
 odb/source-loose.c | 10 ++++++++++
 odb/source-loose.h | 34 ++++++++++++++++++++++++++++++++++
 6 files changed, 47 insertions(+), 28 deletions(-)
 create mode 100644 odb/source-loose.c
 create mode 100644 odb/source-loose.h

diff --git a/Makefile b/Makefile
index a43b8ee0674df8..01356235c3e11d 100644
--- a/Makefile
+++ b/Makefile
@@ -1217,6 +1217,7 @@ LIB_OBJS += odb.o
 LIB_OBJS += odb/source.o
 LIB_OBJS += odb/source-files.o
 LIB_OBJS += odb/source-inmemory.o
+LIB_OBJS += odb/source-loose.o
 LIB_OBJS += odb/streaming.o
 LIB_OBJS += odb/transaction.o
 LIB_OBJS += oid-array.o
diff --git a/meson.build b/meson.build
index 664d8313295a26..c85e5988351b1f 100644
--- a/meson.build
+++ b/meson.build
@@ -405,6 +405,7 @@ libgit_sources = [
   'odb/source.c',
   'odb/source-files.c',
   'odb/source-inmemory.c',
+  'odb/source-loose.c',
   'odb/streaming.c',
   'odb/transaction.c',
   'oid-array.c',
diff --git a/object-file.c b/object-file.c
index 90f995d0000bf6..641bd9c0799dec 100644
--- a/object-file.c
+++ b/object-file.c
@@ -2205,14 +2205,6 @@ struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)
 	return &transaction->base;
 }
 
-struct odb_source_loose *odb_source_loose_new(struct odb_source *source)
-{
-	struct odb_source_loose *loose;
-	CALLOC_ARRAY(loose, 1);
-	loose->source = source;
-	return loose;
-}
-
 void odb_source_loose_free(struct odb_source_loose *loose)
 {
 	if (!loose)
diff --git a/object-file.h b/object-file.h
index 5241b8dd5c564d..1d8312cf7f9ff9 100644
--- a/object-file.h
+++ b/object-file.h
@@ -4,6 +4,7 @@
 #include "git-zlib.h"
 #include "object.h"
 #include "odb.h"
+#include "odb/source-loose.h"
 
 struct index_state;
 
@@ -20,26 +21,6 @@ struct object_info;
 struct odb_read_stream;
 struct odb_source;
 
-struct odb_source_loose {
-	struct odb_source *source;
-
-	/*
-	 * Used to store the results of readdir(3) calls when we are OK
-	 * sacrificing accuracy due to races for speed. That includes
-	 * object existence with OBJECT_INFO_QUICK, as well as
-	 * our search for unique abbreviated hashes. Don't use it for tasks
-	 * requiring greater accuracy!
-	 *
-	 * Be sure to call odb_load_loose_cache() before using.
-	 */
-	uint32_t subdir_seen[8]; /* 256 bits */
-	struct oidtree *cache;
-
-	/* Map between object IDs for loose objects. */
-	struct loose_object_map *map;
-};
-
-struct odb_source_loose *odb_source_loose_new(struct odb_source *source);
 void odb_source_loose_free(struct odb_source_loose *loose);
 
 /* Reprepare the loose source by emptying the loose object cache. */
diff --git a/odb/source-loose.c b/odb/source-loose.c
new file mode 100644
index 00000000000000..b944d2181324ce
--- /dev/null
+++ b/odb/source-loose.c
@@ -0,0 +1,10 @@
+#include "git-compat-util.h"
+#include "odb/source-loose.h"
+
+struct odb_source_loose *odb_source_loose_new(struct odb_source *source)
+{
+	struct odb_source_loose *loose;
+	CALLOC_ARRAY(loose, 1);
+	loose->source = source;
+	return loose;
+}
diff --git a/odb/source-loose.h b/odb/source-loose.h
new file mode 100644
index 00000000000000..8b4bac77ea39e8
--- /dev/null
+++ b/odb/source-loose.h
@@ -0,0 +1,34 @@
+#ifndef ODB_SOURCE_LOOSE_H
+#define ODB_SOURCE_LOOSE_H
+
+#include "odb/source.h"
+
+struct object_database;
+struct oidtree;
+
+/*
+ * An object database source that stores its objects in loose format, one
+ * file per object. This source is part of the files source.
+ */
+struct odb_source_loose {
+	struct odb_source *source;
+
+	/*
+	 * Used to store the results of readdir(3) calls when we are OK
+	 * sacrificing accuracy due to races for speed. That includes
+	 * object existence with OBJECT_INFO_QUICK, as well as
+	 * our search for unique abbreviated hashes. Don't use it for tasks
+	 * requiring greater accuracy!
+	 *
+	 * Be sure to call odb_load_loose_cache() before using.
+	 */
+	uint32_t subdir_seen[8]; /* 256 bits */
+	struct oidtree *cache;
+
+	/* Map between object IDs for loose objects. */
+	struct loose_object_map *map;
+};
+
+struct odb_source_loose *odb_source_loose_new(struct odb_source *source);
+
+#endif

From 1d451ba6fec076d357abf62607b97f585283030a Mon Sep 17 00:00:00 2001
From: Patrick Steinhardt 
Date: Mon, 1 Jun 2026 10:20:25 +0200
Subject: [PATCH 065/106] odb/source-loose: store pointer to "files" instead of
 generic source

The `struct odb_source_loose` holds a pointer to its owning parent
source. The way that Git is currently structured, this parent is always
the "files" source. In subsequent commits we're going to detangle that
so that the "loose" source doesn't have any owning parent source at all
so that it can be used as a completely standalone source.

Detangling this mess is somewhat intricate though, and is made even more
intricate because it's not always clear which kind of source one is
holding at a specific point in time -- either the parent "files" source,
or the child "loose" source.

Make this relationship more explicit by storing a pointer to the "files"
source instead of storing a pointer to a generic `struct odb_source`.
This will help make subsequent steps a bit clearer.

Note that this is a temporary step, only. At the end of this series
we will have dropped the parent pointer completely.

Signed-off-by: Patrick Steinhardt 
Signed-off-by: Junio C Hamano 
---
 object-file.c      | 4 ++--
 odb/source-files.c | 2 +-
 odb/source-loose.c | 4 ++--
 odb/source-loose.h | 5 +++--
 4 files changed, 8 insertions(+), 7 deletions(-)

diff --git a/object-file.c b/object-file.c
index 641bd9c0799dec..7a1908bfc05cb5 100644
--- a/object-file.c
+++ b/object-file.c
@@ -178,7 +178,7 @@ static int open_loose_object(struct odb_source_loose *loose,
 	static struct strbuf buf = STRBUF_INIT;
 	int fd;
 
-	*path = odb_loose_path(loose->source, &buf, oid);
+	*path = odb_loose_path(&loose->files->base, &buf, oid);
 	fd = git_open(*path);
 	if (fd >= 0)
 		return fd;
@@ -189,7 +189,7 @@ static int open_loose_object(struct odb_source_loose *loose,
 static int quick_has_loose(struct odb_source_loose *loose,
 			   const struct object_id *oid)
 {
-	return !!oidtree_contains(odb_source_loose_cache(loose->source, oid), oid);
+	return !!oidtree_contains(odb_source_loose_cache(&loose->files->base, oid), oid);
 }
 
 /*
diff --git a/odb/source-files.c b/odb/source-files.c
index b5abd20e971e78..185cc6903e35f2 100644
--- a/odb/source-files.c
+++ b/odb/source-files.c
@@ -264,7 +264,7 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb,
 
 	CALLOC_ARRAY(files, 1);
 	odb_source_init(&files->base, odb, ODB_SOURCE_FILES, path, local);
-	files->loose = odb_source_loose_new(&files->base);
+	files->loose = odb_source_loose_new(files);
 	files->packed = packfile_store_new(&files->base);
 
 	files->base.free = odb_source_files_free;
diff --git a/odb/source-loose.c b/odb/source-loose.c
index b944d2181324ce..c9e7414814814d 100644
--- a/odb/source-loose.c
+++ b/odb/source-loose.c
@@ -1,10 +1,10 @@
 #include "git-compat-util.h"
 #include "odb/source-loose.h"
 
-struct odb_source_loose *odb_source_loose_new(struct odb_source *source)
+struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)
 {
 	struct odb_source_loose *loose;
 	CALLOC_ARRAY(loose, 1);
-	loose->source = source;
+	loose->files = files;
 	return loose;
 }
diff --git a/odb/source-loose.h b/odb/source-loose.h
index 8b4bac77ea39e8..bf61e767c8aab4 100644
--- a/odb/source-loose.h
+++ b/odb/source-loose.h
@@ -3,6 +3,7 @@
 
 #include "odb/source.h"
 
+struct odb_source_files;
 struct object_database;
 struct oidtree;
 
@@ -11,7 +12,7 @@ struct oidtree;
  * file per object. This source is part of the files source.
  */
 struct odb_source_loose {
-	struct odb_source *source;
+	struct odb_source_files *files;
 
 	/*
 	 * Used to store the results of readdir(3) calls when we are OK
@@ -29,6 +30,6 @@ struct odb_source_loose {
 	struct loose_object_map *map;
 };
 
-struct odb_source_loose *odb_source_loose_new(struct odb_source *source);
+struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files);
 
 #endif

From ead691927b05dbbd2655db9a7183d5fcb935bf3b Mon Sep 17 00:00:00 2001
From: Patrick Steinhardt 
Date: Mon, 1 Jun 2026 10:20:26 +0200
Subject: [PATCH 066/106] odb/source-loose: start converting to a proper
 `struct odb_source`

Start converting `struct odb_source_loose` into a proper pluggable
`struct odb_source` by embedding the base struct and assigning it the
new `ODB_SOURCE_LOOSE` type. Furthermore, wire up lifecycle management
of this source by implementing the `free` callback and taking ownership
of the chdir notifications.

Note that the loose source is not yet functional as a standalone `struct
odb_source`, as it's missing all of the callback implementations. These
will be wired up in subsequent commits.

Signed-off-by: Patrick Steinhardt 
Signed-off-by: Junio C Hamano 
---
 object-file.c      | 17 -----------------
 object-file.h      |  2 --
 odb/source-files.c |  2 +-
 odb/source-loose.c | 45 +++++++++++++++++++++++++++++++++++++++++++++
 odb/source-loose.h | 14 ++++++++++++++
 odb/source.h       |  3 +++
 6 files changed, 63 insertions(+), 20 deletions(-)

diff --git a/object-file.c b/object-file.c
index 7a1908bfc05cb5..977d959d333166 100644
--- a/object-file.c
+++ b/object-file.c
@@ -2041,14 +2041,6 @@ static struct oidtree *odb_source_loose_cache(struct odb_source *source,
 	return files->loose->cache;
 }
 
-static void odb_source_loose_clear_cache(struct odb_source_loose *loose)
-{
-	oidtree_clear(loose->cache);
-	FREE_AND_NULL(loose->cache);
-	memset(&loose->subdir_seen, 0,
-	       sizeof(loose->subdir_seen));
-}
-
 void odb_source_loose_reprepare(struct odb_source *source)
 {
 	struct odb_source_files *files = odb_source_files_downcast(source);
@@ -2205,15 +2197,6 @@ struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)
 	return &transaction->base;
 }
 
-void odb_source_loose_free(struct odb_source_loose *loose)
-{
-	if (!loose)
-		return;
-	odb_source_loose_clear_cache(loose);
-	loose_object_map_clear(&loose->map);
-	free(loose);
-}
-
 struct odb_loose_read_stream {
 	struct odb_read_stream base;
 	git_zstream z;
diff --git a/object-file.h b/object-file.h
index 1d8312cf7f9ff9..02c9680980ab0f 100644
--- a/object-file.h
+++ b/object-file.h
@@ -21,8 +21,6 @@ struct object_info;
 struct odb_read_stream;
 struct odb_source;
 
-void odb_source_loose_free(struct odb_source_loose *loose);
-
 /* Reprepare the loose source by emptying the loose object cache. */
 void odb_source_loose_reprepare(struct odb_source *source);
 
diff --git a/odb/source-files.c b/odb/source-files.c
index 185cc6903e35f2..ccc637311b9c21 100644
--- a/odb/source-files.c
+++ b/odb/source-files.c
@@ -27,7 +27,7 @@ static void odb_source_files_free(struct odb_source *source)
 {
 	struct odb_source_files *files = odb_source_files_downcast(source);
 	chdir_notify_unregister(NULL, odb_source_files_reparent, files);
-	odb_source_loose_free(files->loose);
+	odb_source_free(&files->loose->base);
 	packfile_store_free(files->packed);
 	odb_source_release(&files->base);
 	free(files);
diff --git a/odb/source-loose.c b/odb/source-loose.c
index c9e7414814814d..92e18f5adb2b89 100644
--- a/odb/source-loose.c
+++ b/odb/source-loose.c
@@ -1,10 +1,55 @@
 #include "git-compat-util.h"
+#include "abspath.h"
+#include "chdir-notify.h"
+#include "loose.h"
+#include "odb.h"
+#include "odb/source-files.h"
 #include "odb/source-loose.h"
+#include "oidtree.h"
+
+void odb_source_loose_clear_cache(struct odb_source_loose *loose)
+{
+	oidtree_clear(loose->cache);
+	FREE_AND_NULL(loose->cache);
+	memset(&loose->subdir_seen, 0,
+	       sizeof(loose->subdir_seen));
+}
+
+static void odb_source_loose_reparent(const char *name UNUSED,
+				      const char *old_cwd,
+				      const char *new_cwd,
+				      void *cb_data)
+{
+	struct odb_source_loose *loose = cb_data;
+	char *path = reparent_relative_path(old_cwd, new_cwd,
+					    loose->base.path);
+	free(loose->base.path);
+	loose->base.path = path;
+}
+
+static void odb_source_loose_free(struct odb_source *source)
+{
+	struct odb_source_loose *loose = odb_source_loose_downcast(source);
+	odb_source_loose_clear_cache(loose);
+	loose_object_map_clear(&loose->map);
+	chdir_notify_unregister(NULL, odb_source_loose_reparent, loose);
+	odb_source_release(&loose->base);
+	free(loose);
+}
 
 struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)
 {
 	struct odb_source_loose *loose;
+
 	CALLOC_ARRAY(loose, 1);
+	odb_source_init(&loose->base, files->base.odb, ODB_SOURCE_LOOSE,
+			files->base.path, files->base.local);
 	loose->files = files;
+
+	loose->base.free = odb_source_loose_free;
+
+	if (!is_absolute_path(loose->base.path))
+		chdir_notify_register(NULL, odb_source_loose_reparent, loose);
+
 	return loose;
 }
diff --git a/odb/source-loose.h b/odb/source-loose.h
index bf61e767c8aab4..bd989f0728e622 100644
--- a/odb/source-loose.h
+++ b/odb/source-loose.h
@@ -12,6 +12,7 @@ struct oidtree;
  * file per object. This source is part of the files source.
  */
 struct odb_source_loose {
+	struct odb_source base;
 	struct odb_source_files *files;
 
 	/*
@@ -32,4 +33,17 @@ struct odb_source_loose {
 
 struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files);
 
+/*
+ * Cast the given object database source to the loose backend. This will cause
+ * a BUG in case the source doesn't use this backend.
+ */
+static inline struct odb_source_loose *odb_source_loose_downcast(struct odb_source *source)
+{
+	if (source->type != ODB_SOURCE_LOOSE)
+		BUG("trying to downcast source of type '%d' to loose", source->type);
+	return container_of(source, struct odb_source_loose, base);
+}
+
+void odb_source_loose_clear_cache(struct odb_source_loose *loose);
+
 #endif
diff --git a/odb/source.h b/odb/source.h
index 0a440884e4f0ab..8bcb67787ebafd 100644
--- a/odb/source.h
+++ b/odb/source.h
@@ -14,6 +14,9 @@ enum odb_source_type {
 	/* The "files" backend that uses loose objects and packfiles. */
 	ODB_SOURCE_FILES,
 
+	/* The "loose" backend that uses loose objects, only. */
+	ODB_SOURCE_LOOSE,
+
 	/* The "in-memory" backend that stores objects in memory. */
 	ODB_SOURCE_INMEMORY,
 };

From a2b7db9bc8d52f133fe8fcb317788d9fe8696f07 Mon Sep 17 00:00:00 2001
From: Patrick Steinhardt 
Date: Mon, 1 Jun 2026 10:20:27 +0200
Subject: [PATCH 067/106] odb/source-loose: wire up `reprepare()` callback

Move `odb_source_loose_reprepare()` from "object-file.c" into
"odb/source-loose.c" and wire it up as the `reprepare()` callback of the
loose source.

While at it, make `odb_source_loose_clear_cache()` static, as it is no
longer needed outside of its file.

Signed-off-by: Patrick Steinhardt 
Signed-off-by: Junio C Hamano 
---
 object-file.c      | 6 ------
 object-file.h      | 3 ---
 odb/source-files.c | 2 +-
 odb/source-loose.c | 9 ++++++++-
 odb/source-loose.h | 2 --
 5 files changed, 9 insertions(+), 13 deletions(-)

diff --git a/object-file.c b/object-file.c
index 977d959d333166..0f4f1e7bdc0733 100644
--- a/object-file.c
+++ b/object-file.c
@@ -2041,12 +2041,6 @@ static struct oidtree *odb_source_loose_cache(struct odb_source *source,
 	return files->loose->cache;
 }
 
-void odb_source_loose_reprepare(struct odb_source *source)
-{
-	struct odb_source_files *files = odb_source_files_downcast(source);
-	odb_source_loose_clear_cache(files->loose);
-}
-
 static int check_stream_oid(git_zstream *stream,
 			    const char *hdr,
 			    unsigned long size,
diff --git a/object-file.h b/object-file.h
index 02c9680980ab0f..420a0fff2e7d7e 100644
--- a/object-file.h
+++ b/object-file.h
@@ -21,9 +21,6 @@ struct object_info;
 struct odb_read_stream;
 struct odb_source;
 
-/* Reprepare the loose source by emptying the loose object cache. */
-void odb_source_loose_reprepare(struct odb_source *source);
-
 int odb_source_loose_read_object_info(struct odb_source *source,
 				      const struct object_id *oid,
 				      struct object_info *oi,
diff --git a/odb/source-files.c b/odb/source-files.c
index ccc637311b9c21..10832e81e4e206 100644
--- a/odb/source-files.c
+++ b/odb/source-files.c
@@ -42,7 +42,7 @@ static void odb_source_files_close(struct odb_source *source)
 static void odb_source_files_reprepare(struct odb_source *source)
 {
 	struct odb_source_files *files = odb_source_files_downcast(source);
-	odb_source_loose_reprepare(&files->base);
+	odb_source_reprepare(&files->loose->base);
 	packfile_store_reprepare(files->packed);
 }
 
diff --git a/odb/source-loose.c b/odb/source-loose.c
index 92e18f5adb2b89..e0fe0d513d2532 100644
--- a/odb/source-loose.c
+++ b/odb/source-loose.c
@@ -7,7 +7,7 @@
 #include "odb/source-loose.h"
 #include "oidtree.h"
 
-void odb_source_loose_clear_cache(struct odb_source_loose *loose)
+static void odb_source_loose_clear_cache(struct odb_source_loose *loose)
 {
 	oidtree_clear(loose->cache);
 	FREE_AND_NULL(loose->cache);
@@ -15,6 +15,12 @@ void odb_source_loose_clear_cache(struct odb_source_loose *loose)
 	       sizeof(loose->subdir_seen));
 }
 
+static void odb_source_loose_reprepare(struct odb_source *source)
+{
+	struct odb_source_loose *loose = odb_source_loose_downcast(source);
+	odb_source_loose_clear_cache(loose);
+}
+
 static void odb_source_loose_reparent(const char *name UNUSED,
 				      const char *old_cwd,
 				      const char *new_cwd,
@@ -47,6 +53,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)
 	loose->files = files;
 
 	loose->base.free = odb_source_loose_free;
+	loose->base.reprepare = odb_source_loose_reprepare;
 
 	if (!is_absolute_path(loose->base.path))
 		chdir_notify_register(NULL, odb_source_loose_reparent, loose);
diff --git a/odb/source-loose.h b/odb/source-loose.h
index bd989f0728e622..4dd4fd6ce30a7e 100644
--- a/odb/source-loose.h
+++ b/odb/source-loose.h
@@ -44,6 +44,4 @@ static inline struct odb_source_loose *odb_source_loose_downcast(struct odb_sour
 	return container_of(source, struct odb_source_loose, base);
 }
 
-void odb_source_loose_clear_cache(struct odb_source_loose *loose);
-
 #endif

From 337b7fccba1cca8b7d9232b5e6e9ff53271f0398 Mon Sep 17 00:00:00 2001
From: Patrick Steinhardt 
Date: Mon, 1 Jun 2026 10:20:28 +0200
Subject: [PATCH 068/106] odb/source-loose: wire up `close()` callback

Wire up a new `close()` callback for the loose source and call it from
the "files" source via the generic `odb_source_close()` interface. The
callback itself is a no-op as the loose source has no resources that
need to be released on close.

Signed-off-by: Patrick Steinhardt 
Signed-off-by: Junio C Hamano 
---
 odb/source-files.c | 1 +
 odb/source-loose.c | 6 ++++++
 2 files changed, 7 insertions(+)

diff --git a/odb/source-files.c b/odb/source-files.c
index 10832e81e4e206..59e3a70d80d355 100644
--- a/odb/source-files.c
+++ b/odb/source-files.c
@@ -36,6 +36,7 @@ static void odb_source_files_free(struct odb_source *source)
 static void odb_source_files_close(struct odb_source *source)
 {
 	struct odb_source_files *files = odb_source_files_downcast(source);
+	odb_source_close(&files->loose->base);
 	packfile_store_close(files->packed);
 }
 
diff --git a/odb/source-loose.c b/odb/source-loose.c
index e0fe0d513d2532..65c1076659b8fd 100644
--- a/odb/source-loose.c
+++ b/odb/source-loose.c
@@ -21,6 +21,11 @@ static void odb_source_loose_reprepare(struct odb_source *source)
 	odb_source_loose_clear_cache(loose);
 }
 
+static void odb_source_loose_close(struct odb_source *source UNUSED)
+{
+	/* Nothing to do. */
+}
+
 static void odb_source_loose_reparent(const char *name UNUSED,
 				      const char *old_cwd,
 				      const char *new_cwd,
@@ -53,6 +58,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)
 	loose->files = files;
 
 	loose->base.free = odb_source_loose_free;
+	loose->base.close = odb_source_loose_close;
 	loose->base.reprepare = odb_source_loose_reprepare;
 
 	if (!is_absolute_path(loose->base.path))

From 584338ed92735f3be768c16b53266d5bad439a7a Mon Sep 17 00:00:00 2001
From: Patrick Steinhardt 
Date: Mon, 1 Jun 2026 10:20:29 +0200
Subject: [PATCH 069/106] odb/source-loose: wire up `read_object_info()`
 callback

Move `odb_source_loose_read_object_info()` from "object-file.c" into
"odb/source-loose.c" and wire it up as the `read_object_info()` callback
of the loose source. Callers that previously invoked it directly now go
through the generic `odb_source_read_object_info()` interface instead.

The function `read_object_info_from_path()` cannot be moved along with
it because it is still called by `for_each_object_wrapper_cb()`. It is
therefore kept in place, but adjusted to take a loose source to clarify
that it's always operating on this structure.

Signed-off-by: Patrick Steinhardt 
Signed-off-by: Junio C Hamano 
---
 object-file.c      | 46 +++++++++++++---------------------------------
 object-file.h      | 11 ++++++-----
 odb/source-files.c |  2 +-
 odb/source-loose.c | 24 ++++++++++++++++++++++++
 4 files changed, 44 insertions(+), 39 deletions(-)

diff --git a/object-file.c b/object-file.c
index 0f4f1e7bdc0733..fa174512a43c75 100644
--- a/object-file.c
+++ b/object-file.c
@@ -396,13 +396,12 @@ static int parse_loose_header(const char *hdr, struct object_info *oi)
 	return 0;
 }
 
-static int read_object_info_from_path(struct odb_source *source,
-				      const char *path,
-				      const struct object_id *oid,
-				      struct object_info *oi,
-				      enum object_info_flags flags)
+int read_object_info_from_path(struct odb_source_loose *loose,
+			       const char *path,
+			       const struct object_id *oid,
+			       struct object_info *oi,
+			       enum object_info_flags flags)
 {
-	struct odb_source_files *files = odb_source_files_downcast(source);
 	int ret;
 	int fd;
 	unsigned long mapsize;
@@ -425,7 +424,7 @@ static int read_object_info_from_path(struct odb_source *source,
 		struct stat st;
 
 		if ((!oi || (!oi->disk_sizep && !oi->mtimep)) && (flags & OBJECT_INFO_QUICK)) {
-			ret = quick_has_loose(files->loose, oid) ? 0 : -1;
+			ret = quick_has_loose(loose, oid) ? 0 : -1;
 			goto out;
 		}
 
@@ -532,7 +531,7 @@ static int read_object_info_from_path(struct odb_source *source,
 		if (oi->typep == &type_scratch)
 			oi->typep = NULL;
 		if (oi->delta_base_oid)
-			oidclr(oi->delta_base_oid, source->odb->repo->hash_algo);
+			oidclr(oi->delta_base_oid, loose->base.odb->repo->hash_algo);
 		if (!ret)
 			oi->whence = OI_LOOSE;
 	}
@@ -540,26 +539,6 @@ static int read_object_info_from_path(struct odb_source *source,
 	return ret;
 }
 
-int odb_source_loose_read_object_info(struct odb_source *source,
-				      const struct object_id *oid,
-				      struct object_info *oi,
-				      enum object_info_flags flags)
-{
-	static struct strbuf buf = STRBUF_INIT;
-
-	/*
-	 * The second read shouldn't cause new loose objects to show up, unless
-	 * there was a race condition with a secondary process. We don't care
-	 * about this case though, so we simply skip reading loose objects a
-	 * second time.
-	 */
-	if (flags & OBJECT_INFO_SECOND_READ)
-		return -1;
-
-	odb_loose_path(source, &buf, oid);
-	return read_object_info_from_path(source, buf.buf, oid, oi, flags);
-}
-
 static void hash_object_body(const struct git_hash_algo *algo, struct git_hash_ctx *c,
 			     const void *buf, unsigned long len,
 			     struct object_id *oid,
@@ -1833,7 +1812,7 @@ int for_each_loose_file_in_source(struct odb_source *source,
 }
 
 struct for_each_object_wrapper_data {
-	struct odb_source *source;
+	struct odb_source_loose *loose;
 	const struct object_info *request;
 	odb_for_each_object_cb cb;
 	void *cb_data;
@@ -1848,7 +1827,7 @@ static int for_each_object_wrapper_cb(const struct object_id *oid,
 	if (data->request) {
 		struct object_info oi = *data->request;
 
-		if (read_object_info_from_path(data->source, path, oid, &oi, 0) < 0)
+		if (read_object_info_from_path(data->loose, path, oid, &oi, 0) < 0)
 			return -1;
 
 		return data->cb(oid, &oi, data->cb_data);
@@ -1865,8 +1844,8 @@ static int for_each_prefixed_object_wrapper_cb(const struct object_id *oid,
 	if (data->request) {
 		struct object_info oi = *data->request;
 
-		if (odb_source_loose_read_object_info(data->source,
-						      oid, &oi, 0) < 0)
+		if (odb_source_read_object_info(&data->loose->base,
+						oid, &oi, 0) < 0)
 			return -1;
 
 		return data->cb(oid, &oi, data->cb_data);
@@ -1881,8 +1860,9 @@ int odb_source_loose_for_each_object(struct odb_source *source,
 				     void *cb_data,
 				     const struct odb_for_each_object_options *opts)
 {
+	struct odb_source_files *files = odb_source_files_downcast(source);
 	struct for_each_object_wrapper_data data = {
-		.source = source,
+		.loose = files->loose,
 		.request = request,
 		.cb = cb,
 		.cb_data = cb_data,
diff --git a/object-file.h b/object-file.h
index 420a0fff2e7d7e..8ac2832dac3439 100644
--- a/object-file.h
+++ b/object-file.h
@@ -21,11 +21,6 @@ struct object_info;
 struct odb_read_stream;
 struct odb_source;
 
-int odb_source_loose_read_object_info(struct odb_source *source,
-				      const struct object_id *oid,
-				      struct object_info *oi,
-				      enum object_info_flags flags);
-
 int odb_source_loose_read_object_stream(struct odb_read_stream **out,
 					struct odb_source *source,
 					const struct object_id *oid);
@@ -198,6 +193,12 @@ int read_loose_object(struct repository *repo,
 		      void **contents,
 		      struct object_info *oi);
 
+int read_object_info_from_path(struct odb_source_loose *loose,
+			       const char *path,
+			       const struct object_id *oid,
+			       struct object_info *oi,
+			       enum object_info_flags flags);
+
 struct odb_transaction;
 
 /*
diff --git a/odb/source-files.c b/odb/source-files.c
index 59e3a70d80d355..8d6924755ffb70 100644
--- a/odb/source-files.c
+++ b/odb/source-files.c
@@ -55,7 +55,7 @@ static int odb_source_files_read_object_info(struct odb_source *source,
 	struct odb_source_files *files = odb_source_files_downcast(source);
 
 	if (!packfile_store_read_object_info(files->packed, oid, oi, flags) ||
-	    !odb_source_loose_read_object_info(source, oid, oi, flags))
+	    !odb_source_read_object_info(&files->loose->base, oid, oi, flags))
 		return 0;
 
 	return -1;
diff --git a/odb/source-loose.c b/odb/source-loose.c
index 65c1076659b8fd..50f387ecf31e38 100644
--- a/odb/source-loose.c
+++ b/odb/source-loose.c
@@ -2,10 +2,33 @@
 #include "abspath.h"
 #include "chdir-notify.h"
 #include "loose.h"
+#include "object-file.h"
 #include "odb.h"
 #include "odb/source-files.h"
 #include "odb/source-loose.h"
 #include "oidtree.h"
+#include "strbuf.h"
+
+static int odb_source_loose_read_object_info(struct odb_source *source,
+					     const struct object_id *oid,
+					     struct object_info *oi,
+					     enum object_info_flags flags)
+{
+	struct odb_source_loose *loose = odb_source_loose_downcast(source);
+	static struct strbuf buf = STRBUF_INIT;
+
+	/*
+	 * The second read shouldn't cause new loose objects to show up, unless
+	 * there was a race condition with a secondary process. We don't care
+	 * about this case though, so we simply skip reading loose objects a
+	 * second time.
+	 */
+	if (flags & OBJECT_INFO_SECOND_READ)
+		return -1;
+
+	odb_loose_path(source, &buf, oid);
+	return read_object_info_from_path(loose, buf.buf, oid, oi, flags);
+}
 
 static void odb_source_loose_clear_cache(struct odb_source_loose *loose)
 {
@@ -60,6 +83,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)
 	loose->base.free = odb_source_loose_free;
 	loose->base.close = odb_source_loose_close;
 	loose->base.reprepare = odb_source_loose_reprepare;
+	loose->base.read_object_info = odb_source_loose_read_object_info;
 
 	if (!is_absolute_path(loose->base.path))
 		chdir_notify_register(NULL, odb_source_loose_reparent, loose);

From 727a935a71c29524c936520d8aba4de7098f7566 Mon Sep 17 00:00:00 2001
From: Patrick Steinhardt 
Date: Mon, 1 Jun 2026 10:20:30 +0200
Subject: [PATCH 070/106] odb/source-loose: wire up `read_object_stream()`
 callback

Move `odb_source_loose_read_object_stream()` and its associated helpers
from "object-file.c" into "odb/source-loose.c" and wire it up as the
`read_object_stream()` callback of the loose source.

As part of the move we are also forced to expose a couple of functions
from "object-file.h" that parse object headers in a somewhat-generic
way, as those functions are now used by both subsystems.

Signed-off-by: Patrick Steinhardt 
Signed-off-by: Junio C Hamano 
---
 object-file.c      | 200 ++-------------------------------------------
 object-file.h      |  31 +++++--
 odb/source-files.c |   2 +-
 odb/source-loose.c | 189 ++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 222 insertions(+), 200 deletions(-)

diff --git a/object-file.c b/object-file.c
index fa174512a43c75..adfb6724936452 100644
--- a/object-file.c
+++ b/object-file.c
@@ -164,28 +164,6 @@ int stream_object_signature(struct repository *r,
 	return !oideq(oid, &real_oid) ? -1 : 0;
 }
 
-/*
- * Find "oid" as a loose object in given source, open the object and return its
- * file descriptor. Returns the file descriptor on success, negative on failure.
- *
- * The "path" out-parameter will give the path of the object we found (if any).
- * Note that it may point to static storage and is only valid until another
- * call to stat_loose_object().
- */
-static int open_loose_object(struct odb_source_loose *loose,
-			     const struct object_id *oid, const char **path)
-{
-	static struct strbuf buf = STRBUF_INIT;
-	int fd;
-
-	*path = odb_loose_path(&loose->files->base, &buf, oid);
-	fd = git_open(*path);
-	if (fd >= 0)
-		return fd;
-
-	return -1;
-}
-
 static int quick_has_loose(struct odb_source_loose *loose,
 			   const struct object_id *oid)
 {
@@ -215,42 +193,11 @@ static void *map_fd(int fd, const char *path, unsigned long *size)
 	return map;
 }
 
-static void *odb_source_loose_map_object(struct odb_source *source,
-					 const struct object_id *oid,
-					 unsigned long *size)
-{
-	struct odb_source_files *files = odb_source_files_downcast(source);
-	const char *p;
-	int fd = open_loose_object(files->loose, oid, &p);
-
-	if (fd < 0)
-		return NULL;
-	return map_fd(fd, p, size);
-}
-
-enum unpack_loose_header_result {
-	ULHR_OK,
-	ULHR_BAD,
-	ULHR_TOO_LONG,
-};
-
-/**
- * unpack_loose_header() initializes the data stream needed to unpack
- * a loose object header.
- *
- * Returns:
- *
- * - ULHR_OK on success
- * - ULHR_BAD on error
- * - ULHR_TOO_LONG if the header was too long
- *
- * It will only parse up to MAX_HEADER_LEN bytes.
- */
-static enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,
-							   unsigned char *map,
-							   unsigned long mapsize,
-							   void *buffer,
-							   unsigned long bufsiz)
+enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,
+						    unsigned char *map,
+						    unsigned long mapsize,
+						    void *buffer,
+						    unsigned long bufsiz)
 {
 	int status;
 
@@ -340,7 +287,7 @@ static void *unpack_loose_rest(git_zstream *stream,
  * too permissive for what we want to check. So do an anal
  * object header parse by hand.
  */
-static int parse_loose_header(const char *hdr, struct object_info *oi)
+int parse_loose_header(const char *hdr, struct object_info *oi)
 {
 	const char *type_buf = hdr;
 	size_t size;
@@ -2170,138 +2117,3 @@ struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)
 
 	return &transaction->base;
 }
-
-struct odb_loose_read_stream {
-	struct odb_read_stream base;
-	git_zstream z;
-	enum {
-		ODB_LOOSE_READ_STREAM_INUSE,
-		ODB_LOOSE_READ_STREAM_DONE,
-		ODB_LOOSE_READ_STREAM_ERROR,
-	} z_state;
-	void *mapped;
-	unsigned long mapsize;
-	char hdr[32];
-	int hdr_avail;
-	int hdr_used;
-};
-
-static ssize_t read_istream_loose(struct odb_read_stream *_st, char *buf, size_t sz)
-{
-	struct odb_loose_read_stream *st =
-		container_of(_st, struct odb_loose_read_stream, base);
-	size_t total_read = 0;
-
-	switch (st->z_state) {
-	case ODB_LOOSE_READ_STREAM_DONE:
-		return 0;
-	case ODB_LOOSE_READ_STREAM_ERROR:
-		return -1;
-	default:
-		break;
-	}
-
-	if (st->hdr_used < st->hdr_avail) {
-		size_t to_copy = st->hdr_avail - st->hdr_used;
-		if (sz < to_copy)
-			to_copy = sz;
-		memcpy(buf, st->hdr + st->hdr_used, to_copy);
-		st->hdr_used += to_copy;
-		total_read += to_copy;
-	}
-
-	while (total_read < sz) {
-		int status;
-
-		st->z.next_out = (unsigned char *)buf + total_read;
-		st->z.avail_out = sz - total_read;
-		status = git_inflate(&st->z, Z_FINISH);
-
-		total_read = st->z.next_out - (unsigned char *)buf;
-
-		if (status == Z_STREAM_END) {
-			git_inflate_end(&st->z);
-			st->z_state = ODB_LOOSE_READ_STREAM_DONE;
-			break;
-		}
-		if (status != Z_OK && (status != Z_BUF_ERROR || total_read < sz)) {
-			git_inflate_end(&st->z);
-			st->z_state = ODB_LOOSE_READ_STREAM_ERROR;
-			return -1;
-		}
-	}
-	return total_read;
-}
-
-static int close_istream_loose(struct odb_read_stream *_st)
-{
-	struct odb_loose_read_stream *st =
-		container_of(_st, struct odb_loose_read_stream, base);
-
-	if (st->z_state == ODB_LOOSE_READ_STREAM_INUSE)
-		git_inflate_end(&st->z);
-	munmap(st->mapped, st->mapsize);
-	return 0;
-}
-
-int odb_source_loose_read_object_stream(struct odb_read_stream **out,
-					struct odb_source *source,
-					const struct object_id *oid)
-{
-	struct object_info oi = OBJECT_INFO_INIT;
-	struct odb_loose_read_stream *st;
-	unsigned long mapsize;
-	unsigned long size_ul;
-	void *mapped;
-
-	mapped = odb_source_loose_map_object(source, oid, &mapsize);
-	if (!mapped)
-		return -1;
-
-	/*
-	 * Note: we must allocate this structure early even though we may still
-	 * fail. This is because we need to initialize the zlib stream, and it
-	 * is not possible to copy the stream around after the fact because it
-	 * has self-referencing pointers.
-	 */
-	CALLOC_ARRAY(st, 1);
-
-	switch (unpack_loose_header(&st->z, mapped, mapsize, st->hdr,
-				    sizeof(st->hdr))) {
-	case ULHR_OK:
-		break;
-	case ULHR_BAD:
-	case ULHR_TOO_LONG:
-		goto error;
-	}
-
-	/*
-	 * object_info.sizep is unsigned long* (32-bit on Windows), but
-	 * st->base.size is size_t (64-bit). Use temporary variable.
-	 * Note: loose objects >4GB would still truncate here, but such
-	 * large loose objects are uncommon (they'd normally be packed).
-	 */
-	oi.sizep = &size_ul;
-	oi.typep = &st->base.type;
-
-	if (parse_loose_header(st->hdr, &oi) < 0 || st->base.type < 0)
-		goto error;
-	st->base.size = size_ul;
-
-	st->mapped = mapped;
-	st->mapsize = mapsize;
-	st->hdr_used = strlen(st->hdr) + 1;
-	st->hdr_avail = st->z.total_out;
-	st->z_state = ODB_LOOSE_READ_STREAM_INUSE;
-	st->base.close = close_istream_loose;
-	st->base.read = read_istream_loose;
-
-	*out = &st->base;
-
-	return 0;
-error:
-	git_inflate_end(&st->z);
-	munmap(mapped, mapsize);
-	free(st);
-	return -1;
-}
diff --git a/object-file.h b/object-file.h
index 8ac2832dac3439..d93b7ffad704b0 100644
--- a/object-file.h
+++ b/object-file.h
@@ -18,13 +18,8 @@ int index_fd(struct index_state *istate, struct object_id *oid, int fd, struct s
 int index_path(struct index_state *istate, struct object_id *oid, const char *path, struct stat *st, unsigned flags);
 
 struct object_info;
-struct odb_read_stream;
 struct odb_source;
 
-int odb_source_loose_read_object_stream(struct odb_read_stream **out,
-					struct odb_source *source,
-					const struct object_id *oid);
-
 /*
  * Return true iff an object database source has a loose object
  * with the specified name.  This function does not respect replace
@@ -199,6 +194,32 @@ int read_object_info_from_path(struct odb_source_loose *loose,
 			       struct object_info *oi,
 			       enum object_info_flags flags);
 
+enum unpack_loose_header_result {
+	ULHR_OK,
+	ULHR_BAD,
+	ULHR_TOO_LONG,
+};
+
+/**
+ * unpack_loose_header() initializes the data stream needed to unpack
+ * a loose object header.
+ *
+ * Returns:
+ *
+ * - ULHR_OK on success
+ * - ULHR_BAD on error
+ * - ULHR_TOO_LONG if the header was too long
+ *
+ * It will only parse up to MAX_HEADER_LEN bytes.
+ */
+enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,
+						    unsigned char *map,
+						    unsigned long mapsize,
+						    void *buffer,
+						    unsigned long bufsiz);
+
+int parse_loose_header(const char *hdr, struct object_info *oi);
+
 struct odb_transaction;
 
 /*
diff --git a/odb/source-files.c b/odb/source-files.c
index 8d6924755ffb70..90806ddf86b662 100644
--- a/odb/source-files.c
+++ b/odb/source-files.c
@@ -67,7 +67,7 @@ static int odb_source_files_read_object_stream(struct odb_read_stream **out,
 {
 	struct odb_source_files *files = odb_source_files_downcast(source);
 	if (!packfile_store_read_object_stream(out, files->packed, oid) ||
-	    !odb_source_loose_read_object_stream(out, source, oid))
+	    !odb_source_read_object_stream(out, &files->loose->base, oid))
 		return 0;
 	return -1;
 }
diff --git a/odb/source-loose.c b/odb/source-loose.c
index 50f387ecf31e38..4b82c6f316512e 100644
--- a/odb/source-loose.c
+++ b/odb/source-loose.c
@@ -1,11 +1,13 @@
 #include "git-compat-util.h"
 #include "abspath.h"
 #include "chdir-notify.h"
+#include "gettext.h"
 #include "loose.h"
 #include "object-file.h"
 #include "odb.h"
 #include "odb/source-files.h"
 #include "odb/source-loose.h"
+#include "odb/streaming.h"
 #include "oidtree.h"
 #include "strbuf.h"
 
@@ -30,6 +32,192 @@ static int odb_source_loose_read_object_info(struct odb_source *source,
 	return read_object_info_from_path(loose, buf.buf, oid, oi, flags);
 }
 
+/*
+ * Find "oid" as a loose object in given source, open the object and return its
+ * file descriptor. Returns the file descriptor on success, negative on failure.
+ *
+ * The "path" out-parameter will give the path of the object we found (if any).
+ * Note that it may point to static storage and is only valid until another
+ * call to open_loose_object().
+ */
+static int open_loose_object(struct odb_source_loose *loose,
+			     const struct object_id *oid, const char **path)
+{
+	static struct strbuf buf = STRBUF_INIT;
+	int fd;
+
+	*path = odb_loose_path(&loose->base, &buf, oid);
+	fd = git_open(*path);
+	if (fd >= 0)
+		return fd;
+
+	return -1;
+}
+
+static void *odb_source_loose_map_object(struct odb_source_loose *loose,
+					 const struct object_id *oid,
+					 unsigned long *size)
+{
+	const char *p;
+	int fd = open_loose_object(loose, oid, &p);
+	void *map = NULL;
+	struct stat st;
+
+	if (fd < 0)
+		return NULL;
+
+	if (!fstat(fd, &st)) {
+		*size = xsize_t(st.st_size);
+		if (!*size) {
+			/* mmap() is forbidden on empty files */
+			error(_("object file %s is empty"), p);
+			goto out;
+		}
+
+		map = xmmap(NULL, *size, PROT_READ, MAP_PRIVATE, fd, 0);
+	}
+
+out:
+	close(fd);
+	return map;
+}
+
+struct odb_loose_read_stream {
+	struct odb_read_stream base;
+	git_zstream z;
+	enum {
+		ODB_LOOSE_READ_STREAM_INUSE,
+		ODB_LOOSE_READ_STREAM_DONE,
+		ODB_LOOSE_READ_STREAM_ERROR,
+	} z_state;
+	void *mapped;
+	unsigned long mapsize;
+	char hdr[32];
+	int hdr_avail;
+	int hdr_used;
+};
+
+static ssize_t read_istream_loose(struct odb_read_stream *_st, char *buf, size_t sz)
+{
+	struct odb_loose_read_stream *st =
+		container_of(_st, struct odb_loose_read_stream, base);
+	size_t total_read = 0;
+
+	switch (st->z_state) {
+	case ODB_LOOSE_READ_STREAM_DONE:
+		return 0;
+	case ODB_LOOSE_READ_STREAM_ERROR:
+		return -1;
+	default:
+		break;
+	}
+
+	if (st->hdr_used < st->hdr_avail) {
+		size_t to_copy = st->hdr_avail - st->hdr_used;
+		if (sz < to_copy)
+			to_copy = sz;
+		memcpy(buf, st->hdr + st->hdr_used, to_copy);
+		st->hdr_used += to_copy;
+		total_read += to_copy;
+	}
+
+	while (total_read < sz) {
+		int status;
+
+		st->z.next_out = (unsigned char *)buf + total_read;
+		st->z.avail_out = sz - total_read;
+		status = git_inflate(&st->z, Z_FINISH);
+
+		total_read = st->z.next_out - (unsigned char *)buf;
+
+		if (status == Z_STREAM_END) {
+			git_inflate_end(&st->z);
+			st->z_state = ODB_LOOSE_READ_STREAM_DONE;
+			break;
+		}
+		if (status != Z_OK && (status != Z_BUF_ERROR || total_read < sz)) {
+			git_inflate_end(&st->z);
+			st->z_state = ODB_LOOSE_READ_STREAM_ERROR;
+			return -1;
+		}
+	}
+	return total_read;
+}
+
+static int close_istream_loose(struct odb_read_stream *_st)
+{
+	struct odb_loose_read_stream *st =
+		container_of(_st, struct odb_loose_read_stream, base);
+
+	if (st->z_state == ODB_LOOSE_READ_STREAM_INUSE)
+		git_inflate_end(&st->z);
+	munmap(st->mapped, st->mapsize);
+	return 0;
+}
+
+static int odb_source_loose_read_object_stream(struct odb_read_stream **out,
+					       struct odb_source *source,
+					       const struct object_id *oid)
+{
+	struct odb_source_loose *loose = odb_source_loose_downcast(source);
+	struct object_info oi = OBJECT_INFO_INIT;
+	struct odb_loose_read_stream *st;
+	unsigned long mapsize;
+	unsigned long size_ul;
+	void *mapped;
+
+	mapped = odb_source_loose_map_object(loose, oid, &mapsize);
+	if (!mapped)
+		return -1;
+
+	/*
+	 * Note: we must allocate this structure early even though we may still
+	 * fail. This is because we need to initialize the zlib stream, and it
+	 * is not possible to copy the stream around after the fact because it
+	 * has self-referencing pointers.
+	 */
+	CALLOC_ARRAY(st, 1);
+
+	switch (unpack_loose_header(&st->z, mapped, mapsize, st->hdr,
+				    sizeof(st->hdr))) {
+	case ULHR_OK:
+		break;
+	case ULHR_BAD:
+	case ULHR_TOO_LONG:
+		goto error;
+	}
+
+	/*
+	 * object_info.sizep is unsigned long* (32-bit on Windows), but
+	 * st->base.size is size_t (64-bit). Use temporary variable.
+	 * Note: loose objects >4GB would still truncate here, but such
+	 * large loose objects are uncommon (they'd normally be packed).
+	 */
+	oi.sizep = &size_ul;
+	oi.typep = &st->base.type;
+
+	if (parse_loose_header(st->hdr, &oi) < 0 || st->base.type < 0)
+		goto error;
+	st->base.size = size_ul;
+
+	st->mapped = mapped;
+	st->mapsize = mapsize;
+	st->hdr_used = strlen(st->hdr) + 1;
+	st->hdr_avail = st->z.total_out;
+	st->z_state = ODB_LOOSE_READ_STREAM_INUSE;
+	st->base.close = close_istream_loose;
+	st->base.read = read_istream_loose;
+
+	*out = &st->base;
+
+	return 0;
+error:
+	git_inflate_end(&st->z);
+	munmap(mapped, mapsize);
+	free(st);
+	return -1;
+}
+
 static void odb_source_loose_clear_cache(struct odb_source_loose *loose)
 {
 	oidtree_clear(loose->cache);
@@ -84,6 +272,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)
 	loose->base.close = odb_source_loose_close;
 	loose->base.reprepare = odb_source_loose_reprepare;
 	loose->base.read_object_info = odb_source_loose_read_object_info;
+	loose->base.read_object_stream = odb_source_loose_read_object_stream;
 
 	if (!is_absolute_path(loose->base.path))
 		chdir_notify_register(NULL, odb_source_loose_reparent, loose);

From e4f1d9ba5714957389bee87dd5f9fedb69d8a764 Mon Sep 17 00:00:00 2001
From: Patrick Steinhardt 
Date: Mon, 1 Jun 2026 10:20:31 +0200
Subject: [PATCH 071/106] odb/source-loose: wire up `for_each_object()`
 callback

Move `odb_source_loose_for_each_object()` and its associated helpers
from "object-file.c" into "odb/source-loose.c" and wire it up as the
`for_each_object()` callback of the loose source.

Again, as in the preceding commit, we are forced to expose a couple of
functions from "object-file.c" that are now used by both subsystems.

Signed-off-by: Patrick Steinhardt 
Signed-off-by: Junio C Hamano 
---
 builtin/cat-file.c |   5 +-
 object-file.c      | 299 +++------------------------------------------
 object-file.h      |  32 ++---
 odb/source-files.c |   2 +-
 odb/source-loose.c | 264 +++++++++++++++++++++++++++++++++++++++
 5 files changed, 297 insertions(+), 305 deletions(-)

diff --git a/builtin/cat-file.c b/builtin/cat-file.c
index d9fbad535868bb..2958fc53579336 100644
--- a/builtin/cat-file.c
+++ b/builtin/cat-file.c
@@ -862,8 +862,9 @@ static void batch_each_object(struct batch_options *opt,
 	 */
 	odb_prepare_alternates(the_repository->objects);
 	for (source = the_repository->objects->sources; source; source = source->next) {
-		int ret = odb_source_loose_for_each_object(source, NULL, batch_one_object_oi,
-							   &payload, &opts);
+		struct odb_source_files *files = odb_source_files_downcast(source);
+		int ret = odb_source_for_each_object(&files->loose->base, NULL, batch_one_object_oi,
+						     &payload, &opts);
 		if (ret)
 			break;
 	}
diff --git a/object-file.c b/object-file.c
index adfb6724936452..157ecad3ea204a 100644
--- a/object-file.c
+++ b/object-file.c
@@ -22,7 +22,6 @@
 #include "odb.h"
 #include "odb/streaming.h"
 #include "odb/transaction.h"
-#include "oidtree.h"
 #include "pack.h"
 #include "packfile.h"
 #include "path.h"
@@ -31,12 +30,6 @@
 #include "tempfile.h"
 #include "tmp-objdir.h"
 
-/* The maximum size for an object header. */
-#define MAX_HEADER_LEN 32
-
-static struct oidtree *odb_source_loose_cache(struct odb_source *source,
-					      const struct object_id *oid);
-
 static int get_conv_flags(unsigned flags)
 {
 	if (flags & INDEX_RENORMALIZE)
@@ -164,12 +157,6 @@ int stream_object_signature(struct repository *r,
 	return !oideq(oid, &real_oid) ? -1 : 0;
 }
 
-static int quick_has_loose(struct odb_source_loose *loose,
-			   const struct object_id *oid)
-{
-	return !!oidtree_contains(odb_source_loose_cache(&loose->files->base, oid), oid);
-}
-
 /*
  * Map and close the given loose object fd. The path argument is used for
  * error reporting.
@@ -227,9 +214,9 @@ enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,
 	return ULHR_TOO_LONG;
 }
 
-static void *unpack_loose_rest(git_zstream *stream,
-			       void *buffer, unsigned long size,
-			       const struct object_id *oid)
+void *unpack_loose_rest(git_zstream *stream,
+			void *buffer, unsigned long size,
+			const struct object_id *oid)
 {
 	size_t bytes = strlen(buffer) + 1, n;
 	unsigned char *buf = xmallocz(size);
@@ -343,149 +330,6 @@ int parse_loose_header(const char *hdr, struct object_info *oi)
 	return 0;
 }
 
-int read_object_info_from_path(struct odb_source_loose *loose,
-			       const char *path,
-			       const struct object_id *oid,
-			       struct object_info *oi,
-			       enum object_info_flags flags)
-{
-	int ret;
-	int fd;
-	unsigned long mapsize;
-	void *map = NULL;
-	git_zstream stream, *stream_to_end = NULL;
-	char hdr[MAX_HEADER_LEN];
-	unsigned long size_scratch;
-	enum object_type type_scratch;
-	struct stat st;
-
-	/*
-	 * If we don't care about type or size, then we don't
-	 * need to look inside the object at all. Note that we
-	 * do not optimize out the stat call, even if the
-	 * caller doesn't care about the disk-size, since our
-	 * return value implicitly indicates whether the
-	 * object even exists.
-	 */
-	if (!oi || (!oi->typep && !oi->sizep && !oi->contentp)) {
-		struct stat st;
-
-		if ((!oi || (!oi->disk_sizep && !oi->mtimep)) && (flags & OBJECT_INFO_QUICK)) {
-			ret = quick_has_loose(loose, oid) ? 0 : -1;
-			goto out;
-		}
-
-		if (lstat(path, &st) < 0) {
-			ret = -1;
-			goto out;
-		}
-
-		if (oi) {
-			if (oi->disk_sizep)
-				*oi->disk_sizep = st.st_size;
-			if (oi->mtimep)
-				*oi->mtimep = st.st_mtime;
-		}
-
-		ret = 0;
-		goto out;
-	}
-
-	fd = git_open(path);
-	if (fd < 0) {
-		if (errno != ENOENT)
-			error_errno(_("unable to open loose object %s"), oid_to_hex(oid));
-		ret = -1;
-		goto out;
-	}
-
-	if (fstat(fd, &st)) {
-		close(fd);
-		ret = -1;
-		goto out;
-	}
-
-	mapsize = xsize_t(st.st_size);
-	if (!mapsize) {
-		close(fd);
-		ret = error(_("object file %s is empty"), path);
-		goto out;
-	}
-
-	map = xmmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, fd, 0);
-	close(fd);
-	if (!map) {
-		ret = -1;
-		goto out;
-	}
-
-	if (oi->disk_sizep)
-		*oi->disk_sizep = mapsize;
-	if (oi->mtimep)
-		*oi->mtimep = st.st_mtime;
-
-	stream_to_end = &stream;
-
-	switch (unpack_loose_header(&stream, map, mapsize, hdr, sizeof(hdr))) {
-	case ULHR_OK:
-		if (!oi->sizep)
-			oi->sizep = &size_scratch;
-		if (!oi->typep)
-			oi->typep = &type_scratch;
-
-		if (parse_loose_header(hdr, oi) < 0) {
-			ret = error(_("unable to parse %s header"), oid_to_hex(oid));
-			goto corrupt;
-		}
-
-		if (*oi->typep < 0)
-			die(_("invalid object type"));
-
-		if (oi->contentp) {
-			*oi->contentp = unpack_loose_rest(&stream, hdr, *oi->sizep, oid);
-			if (!*oi->contentp) {
-				ret = -1;
-				goto corrupt;
-			}
-		}
-
-		break;
-	case ULHR_BAD:
-		ret = error(_("unable to unpack %s header"),
-			    oid_to_hex(oid));
-		goto corrupt;
-	case ULHR_TOO_LONG:
-		ret = error(_("header for %s too long, exceeds %d bytes"),
-			    oid_to_hex(oid), MAX_HEADER_LEN);
-		goto corrupt;
-	}
-
-	ret = 0;
-
-corrupt:
-	if (ret && (flags & OBJECT_INFO_DIE_IF_CORRUPT))
-		die(_("loose object %s (stored in %s) is corrupt"),
-		    oid_to_hex(oid), path);
-
-out:
-	if (stream_to_end)
-		git_inflate_end(stream_to_end);
-	if (map)
-		munmap(map, mapsize);
-	if (oi) {
-		if (oi->sizep == &size_scratch)
-			oi->sizep = NULL;
-		if (oi->typep == &type_scratch)
-			oi->typep = NULL;
-		if (oi->delta_base_oid)
-			oidclr(oi->delta_base_oid, loose->base.odb->repo->hash_algo);
-		if (!ret)
-			oi->whence = OI_LOOSE;
-	}
-
-	return ret;
-}
-
 static void hash_object_body(const struct git_hash_algo *algo, struct git_hash_ctx *c,
 			     const void *buf, unsigned long len,
 			     struct object_id *oid,
@@ -1667,13 +1511,13 @@ int read_pack_header(int fd, struct pack_header *header)
 	return 0;
 }
 
-static int for_each_file_in_obj_subdir(unsigned int subdir_nr,
-				       struct strbuf *path,
-				       const struct git_hash_algo *algop,
-				       each_loose_object_fn obj_cb,
-				       each_loose_cruft_fn cruft_cb,
-				       each_loose_subdir_fn subdir_cb,
-				       void *data)
+int for_each_file_in_obj_subdir(unsigned int subdir_nr,
+				struct strbuf *path,
+				const struct git_hash_algo *algop,
+				each_loose_object_fn obj_cb,
+				each_loose_cruft_fn cruft_cb,
+				each_loose_subdir_fn subdir_cb,
+				void *data)
 {
 	size_t origlen, baselen;
 	DIR *dir;
@@ -1758,78 +1602,6 @@ int for_each_loose_file_in_source(struct odb_source *source,
 	return r;
 }
 
-struct for_each_object_wrapper_data {
-	struct odb_source_loose *loose;
-	const struct object_info *request;
-	odb_for_each_object_cb cb;
-	void *cb_data;
-};
-
-static int for_each_object_wrapper_cb(const struct object_id *oid,
-				      const char *path,
-				      void *cb_data)
-{
-	struct for_each_object_wrapper_data *data = cb_data;
-
-	if (data->request) {
-		struct object_info oi = *data->request;
-
-		if (read_object_info_from_path(data->loose, path, oid, &oi, 0) < 0)
-			return -1;
-
-		return data->cb(oid, &oi, data->cb_data);
-	} else {
-		return data->cb(oid, NULL, data->cb_data);
-	}
-}
-
-static int for_each_prefixed_object_wrapper_cb(const struct object_id *oid,
-					       void *node_data UNUSED,
-					       void *cb_data)
-{
-	struct for_each_object_wrapper_data *data = cb_data;
-	if (data->request) {
-		struct object_info oi = *data->request;
-
-		if (odb_source_read_object_info(&data->loose->base,
-						oid, &oi, 0) < 0)
-			return -1;
-
-		return data->cb(oid, &oi, data->cb_data);
-	} else {
-		return data->cb(oid, NULL, data->cb_data);
-	}
-}
-
-int odb_source_loose_for_each_object(struct odb_source *source,
-				     const struct object_info *request,
-				     odb_for_each_object_cb cb,
-				     void *cb_data,
-				     const struct odb_for_each_object_options *opts)
-{
-	struct odb_source_files *files = odb_source_files_downcast(source);
-	struct for_each_object_wrapper_data data = {
-		.loose = files->loose,
-		.request = request,
-		.cb = cb,
-		.cb_data = cb_data,
-	};
-
-	/* There are no loose promisor objects, so we can return immediately. */
-	if ((opts->flags & ODB_FOR_EACH_OBJECT_PROMISOR_ONLY))
-		return 0;
-	if ((opts->flags & ODB_FOR_EACH_OBJECT_LOCAL_ONLY) && !source->local)
-		return 0;
-
-	if (opts->prefix)
-		return oidtree_each(odb_source_loose_cache(source, opts->prefix),
-				    opts->prefix, opts->prefix_hex_len,
-				    for_each_prefixed_object_wrapper_cb, &data);
-
-	return for_each_loose_file_in_source(source, for_each_object_wrapper_cb,
-					     NULL, NULL, &data);
-}
-
 static int count_loose_object(const struct object_id *oid UNUSED,
 			      struct object_info *oi UNUSED,
 			      void *payload)
@@ -1843,6 +1615,7 @@ int odb_source_loose_count_objects(struct odb_source *source,
 				   enum odb_count_objects_flags flags,
 				   unsigned long *out)
 {
+	struct odb_source_files *files = odb_source_files_downcast(source);
 	const unsigned hexsz = source->odb->repo->hash_algo->hexsz - 2;
 	char *path = NULL;
 	DIR *dir = NULL;
@@ -1878,8 +1651,8 @@ int odb_source_loose_count_objects(struct odb_source *source,
 	} else {
 		struct odb_for_each_object_options opts = { 0 };
 		*out = 0;
-		ret = odb_source_loose_for_each_object(source, NULL, count_loose_object,
-						       out, &opts);
+		ret = odb_source_for_each_object(&files->loose->base, NULL, count_loose_object,
+						 out, &opts);
 	}
 
 out:
@@ -1910,6 +1683,7 @@ int odb_source_loose_find_abbrev_len(struct odb_source *source,
 				     unsigned min_len,
 				     unsigned *out)
 {
+	struct odb_source_files *files = odb_source_files_downcast(source);
 	struct odb_for_each_object_options opts = {
 		.prefix = oid,
 		.prefix_hex_len = min_len,
@@ -1920,54 +1694,13 @@ int odb_source_loose_find_abbrev_len(struct odb_source *source,
 	};
 	int ret;
 
-	ret = odb_source_loose_for_each_object(source, NULL, find_abbrev_len_cb,
-					       &data, &opts);
+	ret = odb_source_for_each_object(&files->loose->base, NULL, find_abbrev_len_cb,
+					 &data, &opts);
 	*out = data.len;
 
 	return ret;
 }
 
-static int append_loose_object(const struct object_id *oid,
-			       const char *path UNUSED,
-			       void *data)
-{
-	oidtree_insert(data, oid, NULL);
-	return 0;
-}
-
-static struct oidtree *odb_source_loose_cache(struct odb_source *source,
-					      const struct object_id *oid)
-{
-	struct odb_source_files *files = odb_source_files_downcast(source);
-	int subdir_nr = oid->hash[0];
-	struct strbuf buf = STRBUF_INIT;
-	size_t word_bits = bitsizeof(files->loose->subdir_seen[0]);
-	size_t word_index = subdir_nr / word_bits;
-	size_t mask = (size_t)1u << (subdir_nr % word_bits);
-	uint32_t *bitmap;
-
-	if (subdir_nr < 0 ||
-	    (size_t) subdir_nr >= bitsizeof(files->loose->subdir_seen))
-		BUG("subdir_nr out of range");
-
-	bitmap = &files->loose->subdir_seen[word_index];
-	if (*bitmap & mask)
-		return files->loose->cache;
-	if (!files->loose->cache) {
-		ALLOC_ARRAY(files->loose->cache, 1);
-		oidtree_init(files->loose->cache);
-	}
-	strbuf_addstr(&buf, source->path);
-	for_each_file_in_obj_subdir(subdir_nr, &buf,
-				    source->odb->repo->hash_algo,
-				    append_loose_object,
-				    NULL, NULL,
-				    files->loose->cache);
-	*bitmap |= mask;
-	strbuf_release(&buf);
-	return files->loose->cache;
-}
-
 static int check_stream_oid(git_zstream *stream,
 			    const char *hdr,
 			    unsigned long size,
diff --git a/object-file.h b/object-file.h
index d93b7ffad704b0..9ee5649220931b 100644
--- a/object-file.h
+++ b/object-file.h
@@ -6,6 +6,9 @@
 #include "odb.h"
 #include "odb/source-loose.h"
 
+/* The maximum size for an object header. */
+#define MAX_HEADER_LEN 32
+
 struct index_state;
 
 enum {
@@ -85,19 +88,13 @@ int for_each_loose_file_in_source(struct odb_source *source,
 				  each_loose_cruft_fn cruft_cb,
 				  each_loose_subdir_fn subdir_cb,
 				  void *data);
-
-/*
- * Iterate through all loose objects in the given object database source and
- * invoke the callback function for each of them. If an object info request is
- * given, then the object info will be read for every individual object and
- * passed to the callback as if `odb_source_loose_read_object_info()` was
- * called for the object.
- */
-int odb_source_loose_for_each_object(struct odb_source *source,
-				     const struct object_info *request,
-				     odb_for_each_object_cb cb,
-				     void *cb_data,
-				     const struct odb_for_each_object_options *opts);
+int for_each_file_in_obj_subdir(unsigned int subdir_nr,
+				struct strbuf *path,
+				const struct git_hash_algo *algop,
+				each_loose_object_fn obj_cb,
+				each_loose_cruft_fn cruft_cb,
+				each_loose_subdir_fn subdir_cb,
+				void *data);
 
 /*
  * Count the number of loose objects in this source.
@@ -188,12 +185,6 @@ int read_loose_object(struct repository *repo,
 		      void **contents,
 		      struct object_info *oi);
 
-int read_object_info_from_path(struct odb_source_loose *loose,
-			       const char *path,
-			       const struct object_id *oid,
-			       struct object_info *oi,
-			       enum object_info_flags flags);
-
 enum unpack_loose_header_result {
 	ULHR_OK,
 	ULHR_BAD,
@@ -217,6 +208,9 @@ enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,
 						    unsigned long mapsize,
 						    void *buffer,
 						    unsigned long bufsiz);
+void *unpack_loose_rest(git_zstream *stream,
+			void *buffer, unsigned long size,
+			const struct object_id *oid);
 
 int parse_loose_header(const char *hdr, struct object_info *oi);
 
diff --git a/odb/source-files.c b/odb/source-files.c
index 90806ddf86b662..676a641739bcbf 100644
--- a/odb/source-files.c
+++ b/odb/source-files.c
@@ -82,7 +82,7 @@ static int odb_source_files_for_each_object(struct odb_source *source,
 	int ret;
 
 	if (!(opts->flags & ODB_FOR_EACH_OBJECT_PROMISOR_ONLY)) {
-		ret = odb_source_loose_for_each_object(source, request, cb, cb_data, opts);
+		ret = odb_source_for_each_object(&files->loose->base, request, cb, cb_data, opts);
 		if (ret)
 			return ret;
 	}
diff --git a/odb/source-loose.c b/odb/source-loose.c
index 4b82c6f316512e..4e8b923498b5b2 100644
--- a/odb/source-loose.c
+++ b/odb/source-loose.c
@@ -2,6 +2,7 @@
 #include "abspath.h"
 #include "chdir-notify.h"
 #include "gettext.h"
+#include "hex.h"
 #include "loose.h"
 #include "object-file.h"
 #include "odb.h"
@@ -9,8 +10,198 @@
 #include "odb/source-loose.h"
 #include "odb/streaming.h"
 #include "oidtree.h"
+#include "repository.h"
 #include "strbuf.h"
 
+static int append_loose_object(const struct object_id *oid,
+			       const char *path UNUSED,
+			       void *data)
+{
+	oidtree_insert(data, oid, NULL);
+	return 0;
+}
+
+static struct oidtree *odb_source_loose_cache(struct odb_source_loose *loose,
+					      const struct object_id *oid)
+{
+	int subdir_nr = oid->hash[0];
+	struct strbuf buf = STRBUF_INIT;
+	size_t word_bits = bitsizeof(loose->subdir_seen[0]);
+	size_t word_index = subdir_nr / word_bits;
+	size_t mask = (size_t)1u << (subdir_nr % word_bits);
+	uint32_t *bitmap;
+
+	if (subdir_nr < 0 ||
+	    (size_t) subdir_nr >= bitsizeof(loose->subdir_seen))
+		BUG("subdir_nr out of range");
+
+	bitmap = &loose->subdir_seen[word_index];
+	if (*bitmap & mask)
+		return loose->cache;
+	if (!loose->cache) {
+		ALLOC_ARRAY(loose->cache, 1);
+		oidtree_init(loose->cache);
+	}
+	strbuf_addstr(&buf, loose->base.path);
+	for_each_file_in_obj_subdir(subdir_nr, &buf,
+				    loose->base.odb->repo->hash_algo,
+				    append_loose_object,
+				    NULL, NULL,
+				    loose->cache);
+	*bitmap |= mask;
+	strbuf_release(&buf);
+	return loose->cache;
+}
+
+static int quick_has_loose(struct odb_source_loose *loose,
+			   const struct object_id *oid)
+{
+	return !!oidtree_contains(odb_source_loose_cache(loose, oid), oid);
+}
+
+static int read_object_info_from_path(struct odb_source_loose *loose,
+				      const char *path,
+				      const struct object_id *oid,
+				      struct object_info *oi,
+				      enum object_info_flags flags)
+{
+	int ret;
+	int fd;
+	unsigned long mapsize;
+	void *map = NULL;
+	git_zstream stream, *stream_to_end = NULL;
+	char hdr[MAX_HEADER_LEN];
+	unsigned long size_scratch;
+	enum object_type type_scratch;
+	struct stat st;
+
+	/*
+	 * If we don't care about type or size, then we don't
+	 * need to look inside the object at all. Note that we
+	 * do not optimize out the stat call, even if the
+	 * caller doesn't care about the disk-size, since our
+	 * return value implicitly indicates whether the
+	 * object even exists.
+	 */
+	if (!oi || (!oi->typep && !oi->sizep && !oi->contentp)) {
+		struct stat st;
+
+		if ((!oi || (!oi->disk_sizep && !oi->mtimep)) && (flags & OBJECT_INFO_QUICK)) {
+			ret = quick_has_loose(loose, oid) ? 0 : -1;
+			goto out;
+		}
+
+		if (lstat(path, &st) < 0) {
+			ret = -1;
+			goto out;
+		}
+
+		if (oi) {
+			if (oi->disk_sizep)
+				*oi->disk_sizep = st.st_size;
+			if (oi->mtimep)
+				*oi->mtimep = st.st_mtime;
+		}
+
+		ret = 0;
+		goto out;
+	}
+
+	fd = git_open(path);
+	if (fd < 0) {
+		if (errno != ENOENT)
+			error_errno(_("unable to open loose object %s"), oid_to_hex(oid));
+		ret = -1;
+		goto out;
+	}
+
+	if (fstat(fd, &st)) {
+		close(fd);
+		ret = -1;
+		goto out;
+	}
+
+	mapsize = xsize_t(st.st_size);
+	if (!mapsize) {
+		close(fd);
+		ret = error(_("object file %s is empty"), path);
+		goto out;
+	}
+
+	map = xmmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, fd, 0);
+	close(fd);
+	if (!map) {
+		ret = -1;
+		goto out;
+	}
+
+	if (oi->disk_sizep)
+		*oi->disk_sizep = mapsize;
+	if (oi->mtimep)
+		*oi->mtimep = st.st_mtime;
+
+	stream_to_end = &stream;
+
+	switch (unpack_loose_header(&stream, map, mapsize, hdr, sizeof(hdr))) {
+	case ULHR_OK:
+		if (!oi->sizep)
+			oi->sizep = &size_scratch;
+		if (!oi->typep)
+			oi->typep = &type_scratch;
+
+		if (parse_loose_header(hdr, oi) < 0) {
+			ret = error(_("unable to parse %s header"), oid_to_hex(oid));
+			goto corrupt;
+		}
+
+		if (*oi->typep < 0)
+			die(_("invalid object type"));
+
+		if (oi->contentp) {
+			*oi->contentp = unpack_loose_rest(&stream, hdr, *oi->sizep, oid);
+			if (!*oi->contentp) {
+				ret = -1;
+				goto corrupt;
+			}
+		}
+
+		break;
+	case ULHR_BAD:
+		ret = error(_("unable to unpack %s header"),
+			    oid_to_hex(oid));
+		goto corrupt;
+	case ULHR_TOO_LONG:
+		ret = error(_("header for %s too long, exceeds %d bytes"),
+			    oid_to_hex(oid), MAX_HEADER_LEN);
+		goto corrupt;
+	}
+
+	ret = 0;
+
+corrupt:
+	if (ret && (flags & OBJECT_INFO_DIE_IF_CORRUPT))
+		die(_("loose object %s (stored in %s) is corrupt"),
+		    oid_to_hex(oid), path);
+
+out:
+	if (stream_to_end)
+		git_inflate_end(stream_to_end);
+	if (map)
+		munmap(map, mapsize);
+	if (oi) {
+		if (oi->sizep == &size_scratch)
+			oi->sizep = NULL;
+		if (oi->typep == &type_scratch)
+			oi->typep = NULL;
+		if (oi->delta_base_oid)
+			oidclr(oi->delta_base_oid, loose->base.odb->repo->hash_algo);
+		if (!ret)
+			oi->whence = OI_LOOSE;
+	}
+
+	return ret;
+}
+
 static int odb_source_loose_read_object_info(struct odb_source *source,
 					     const struct object_id *oid,
 					     struct object_info *oi,
@@ -218,6 +409,78 @@ static int odb_source_loose_read_object_stream(struct odb_read_stream **out,
 	return -1;
 }
 
+struct for_each_object_wrapper_data {
+	struct odb_source_loose *loose;
+	const struct object_info *request;
+	odb_for_each_object_cb cb;
+	void *cb_data;
+};
+
+static int for_each_object_wrapper_cb(const struct object_id *oid,
+				      const char *path,
+				      void *cb_data)
+{
+	struct for_each_object_wrapper_data *data = cb_data;
+
+	if (data->request) {
+		struct object_info oi = *data->request;
+
+		if (read_object_info_from_path(data->loose, path, oid, &oi, 0) < 0)
+			return -1;
+
+		return data->cb(oid, &oi, data->cb_data);
+	} else {
+		return data->cb(oid, NULL, data->cb_data);
+	}
+}
+
+static int for_each_prefixed_object_wrapper_cb(const struct object_id *oid,
+					       void *node_data UNUSED,
+					       void *cb_data)
+{
+	struct for_each_object_wrapper_data *data = cb_data;
+	if (data->request) {
+		struct object_info oi = *data->request;
+
+		if (odb_source_read_object_info(&data->loose->base,
+						oid, &oi, 0) < 0)
+			return -1;
+
+		return data->cb(oid, &oi, data->cb_data);
+	} else {
+		return data->cb(oid, NULL, data->cb_data);
+	}
+}
+
+static int odb_source_loose_for_each_object(struct odb_source *source,
+					    const struct object_info *request,
+					    odb_for_each_object_cb cb,
+					    void *cb_data,
+					    const struct odb_for_each_object_options *opts)
+{
+	struct odb_source_loose *loose = odb_source_loose_downcast(source);
+	struct for_each_object_wrapper_data data = {
+		.loose = loose,
+		.request = request,
+		.cb = cb,
+		.cb_data = cb_data,
+	};
+
+	/* There are no loose promisor objects, so we can return immediately. */
+	if ((opts->flags & ODB_FOR_EACH_OBJECT_PROMISOR_ONLY))
+		return 0;
+	if ((opts->flags & ODB_FOR_EACH_OBJECT_LOCAL_ONLY) && !source->local)
+		return 0;
+
+	if (opts->prefix)
+		return oidtree_each(odb_source_loose_cache(loose, opts->prefix),
+				    opts->prefix, opts->prefix_hex_len,
+				    for_each_prefixed_object_wrapper_cb, &data);
+
+	return for_each_loose_file_in_source(source, for_each_object_wrapper_cb,
+					     NULL, NULL, &data);
+}
+
 static void odb_source_loose_clear_cache(struct odb_source_loose *loose)
 {
 	oidtree_clear(loose->cache);
@@ -273,6 +536,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)
 	loose->base.reprepare = odb_source_loose_reprepare;
 	loose->base.read_object_info = odb_source_loose_read_object_info;
 	loose->base.read_object_stream = odb_source_loose_read_object_stream;
+	loose->base.for_each_object = odb_source_loose_for_each_object;
 
 	if (!is_absolute_path(loose->base.path))
 		chdir_notify_register(NULL, odb_source_loose_reparent, loose);

From 8a6da81cc113607bdc1ac08395f6e7121cd652e9 Mon Sep 17 00:00:00 2001
From: Patrick Steinhardt 
Date: Mon, 1 Jun 2026 10:20:32 +0200
Subject: [PATCH 072/106] odb/source-loose: wire up `find_abbrev_len()`
 callback

Move `odb_source_loose_find_abbrev_len()` and its associated helpers
from "object-file.c" into "odb/source-loose.c" and wire it up as the
`find_abbrev_len` callback of the loose source.

Signed-off-by: Patrick Steinhardt 
Signed-off-by: Junio C Hamano 
---
 object-file.c      | 39 ---------------------------------------
 object-file.h      | 12 ------------
 odb/source-files.c |  2 +-
 odb/source-loose.c | 40 ++++++++++++++++++++++++++++++++++++++++
 4 files changed, 41 insertions(+), 52 deletions(-)

diff --git a/object-file.c b/object-file.c
index 157ecad3ea204a..11957aa44f44fd 100644
--- a/object-file.c
+++ b/object-file.c
@@ -1662,45 +1662,6 @@ int odb_source_loose_count_objects(struct odb_source *source,
 	return ret;
 }
 
-struct find_abbrev_len_data {
-	const struct object_id *oid;
-	unsigned len;
-};
-
-static int find_abbrev_len_cb(const struct object_id *oid,
-			      struct object_info *oi UNUSED,
-			      void *cb_data)
-{
-	struct find_abbrev_len_data *data = cb_data;
-	unsigned len = oid_common_prefix_hexlen(oid, data->oid);
-	if (len != hash_algos[oid->algo].hexsz && len >= data->len)
-		data->len = len + 1;
-	return 0;
-}
-
-int odb_source_loose_find_abbrev_len(struct odb_source *source,
-				     const struct object_id *oid,
-				     unsigned min_len,
-				     unsigned *out)
-{
-	struct odb_source_files *files = odb_source_files_downcast(source);
-	struct odb_for_each_object_options opts = {
-		.prefix = oid,
-		.prefix_hex_len = min_len,
-	};
-	struct find_abbrev_len_data data = {
-		.oid = oid,
-		.len = min_len,
-	};
-	int ret;
-
-	ret = odb_source_for_each_object(&files->loose->base, NULL, find_abbrev_len_cb,
-					 &data, &opts);
-	*out = data.len;
-
-	return ret;
-}
-
 static int check_stream_oid(git_zstream *stream,
 			    const char *hdr,
 			    unsigned long size,
diff --git a/object-file.h b/object-file.h
index 9ee5649220931b..96760db0e1cb2b 100644
--- a/object-file.h
+++ b/object-file.h
@@ -110,18 +110,6 @@ int odb_source_loose_count_objects(struct odb_source *source,
 				   enum odb_count_objects_flags flags,
 				   unsigned long *out);
 
-/*
- * Find the shortest unique prefix for the given object ID, where `min_len` is
- * the minimum length that the prefix should have.
- *
- * Returns 0 on success, in which case the computed length will be written to
- * `out`. Otherwise, a negative error code is returned.
- */
-int odb_source_loose_find_abbrev_len(struct odb_source *source,
-				     const struct object_id *oid,
-				     unsigned min_len,
-				     unsigned *out);
-
 /**
  * format_object_header() is a thin wrapper around s xsnprintf() that
  * writes the initial " " part of the loose object
diff --git a/odb/source-files.c b/odb/source-files.c
index 676a641739bcbf..4a54b10e4af11d 100644
--- a/odb/source-files.c
+++ b/odb/source-files.c
@@ -136,7 +136,7 @@ static int odb_source_files_find_abbrev_len(struct odb_source *source,
 	if (ret < 0)
 		goto out;
 
-	ret = odb_source_loose_find_abbrev_len(source, oid, len, &len);
+	ret = odb_source_find_abbrev_len(&files->loose->base, oid, len, &len);
 	if (ret < 0)
 		goto out;
 
diff --git a/odb/source-loose.c b/odb/source-loose.c
index 4e8b923498b5b2..4b8d10bc870374 100644
--- a/odb/source-loose.c
+++ b/odb/source-loose.c
@@ -481,6 +481,45 @@ static int odb_source_loose_for_each_object(struct odb_source *source,
 					     NULL, NULL, &data);
 }
 
+struct find_abbrev_len_data {
+	const struct object_id *oid;
+	unsigned len;
+};
+
+static int find_abbrev_len_cb(const struct object_id *oid,
+			      struct object_info *oi UNUSED,
+			      void *cb_data)
+{
+	struct find_abbrev_len_data *data = cb_data;
+	unsigned len = oid_common_prefix_hexlen(oid, data->oid);
+	if (len != hash_algos[oid->algo].hexsz && len >= data->len)
+		data->len = len + 1;
+	return 0;
+}
+
+static int odb_source_loose_find_abbrev_len(struct odb_source *source,
+					    const struct object_id *oid,
+					    unsigned min_len,
+					    unsigned *out)
+{
+	struct odb_source_loose *loose = odb_source_loose_downcast(source);
+	struct odb_for_each_object_options opts = {
+		.prefix = oid,
+		.prefix_hex_len = min_len,
+	};
+	struct find_abbrev_len_data data = {
+		.oid = oid,
+		.len = min_len,
+	};
+	int ret;
+
+	ret = odb_source_for_each_object(&loose->base, NULL, find_abbrev_len_cb,
+					 &data, &opts);
+	*out = data.len;
+
+	return ret;
+}
+
 static void odb_source_loose_clear_cache(struct odb_source_loose *loose)
 {
 	oidtree_clear(loose->cache);
@@ -537,6 +576,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)
 	loose->base.read_object_info = odb_source_loose_read_object_info;
 	loose->base.read_object_stream = odb_source_loose_read_object_stream;
 	loose->base.for_each_object = odb_source_loose_for_each_object;
+	loose->base.find_abbrev_len = odb_source_loose_find_abbrev_len;
 
 	if (!is_absolute_path(loose->base.path))
 		chdir_notify_register(NULL, odb_source_loose_reparent, loose);

From 2ade08ac2978dc1c908602c2a4d653836ecb5acb Mon Sep 17 00:00:00 2001
From: Patrick Steinhardt 
Date: Mon, 1 Jun 2026 10:20:33 +0200
Subject: [PATCH 073/106] odb/source-loose: wire up `count_objects()` callback

Move `odb_source_loose_count_objects()` and its associated helpers from
"object-file.c" into "odb/source-loose.c" and wire it up as the
`count_objects()` callback of the loose source.

Signed-off-by: Patrick Steinhardt 
Signed-off-by: Junio C Hamano 
---
 builtin/gc.c       |  6 ++---
 object-file.c      | 60 ---------------------------------------------
 object-file.h      | 14 -----------
 odb/source-files.c |  2 +-
 odb/source-loose.c | 61 ++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 65 insertions(+), 78 deletions(-)

diff --git a/builtin/gc.c b/builtin/gc.c
index 84a66d32404e4d..c26c93ee0fe4a3 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -466,6 +466,7 @@ static int rerere_gc_condition(struct gc_config *cfg UNUSED)
 
 static int too_many_loose_objects(int limit)
 {
+	struct odb_source_files *files = odb_source_files_downcast(the_repository->objects->sources);
 	/*
 	 * This is weird, but stems from legacy behaviour: the GC auto
 	 * threshold was always essentially interpreted as if it was rounded up
@@ -474,9 +475,8 @@ static int too_many_loose_objects(int limit)
 	int auto_threshold = DIV_ROUND_UP(limit, 256) * 256;
 	unsigned long loose_count;
 
-	if (odb_source_loose_count_objects(the_repository->objects->sources,
-					   ODB_COUNT_OBJECTS_APPROXIMATE,
-					   &loose_count) < 0)
+	if (odb_source_count_objects(&files->loose->base, ODB_COUNT_OBJECTS_APPROXIMATE,
+				     &loose_count) < 0)
 		return 0;
 
 	return loose_count > auto_threshold;
diff --git a/object-file.c b/object-file.c
index 11957aa44f44fd..9b2044de3784e6 100644
--- a/object-file.c
+++ b/object-file.c
@@ -1602,66 +1602,6 @@ int for_each_loose_file_in_source(struct odb_source *source,
 	return r;
 }
 
-static int count_loose_object(const struct object_id *oid UNUSED,
-			      struct object_info *oi UNUSED,
-			      void *payload)
-{
-	unsigned long *count = payload;
-	(*count)++;
-	return 0;
-}
-
-int odb_source_loose_count_objects(struct odb_source *source,
-				   enum odb_count_objects_flags flags,
-				   unsigned long *out)
-{
-	struct odb_source_files *files = odb_source_files_downcast(source);
-	const unsigned hexsz = source->odb->repo->hash_algo->hexsz - 2;
-	char *path = NULL;
-	DIR *dir = NULL;
-	int ret;
-
-	if (flags & ODB_COUNT_OBJECTS_APPROXIMATE) {
-		unsigned long count = 0;
-		struct dirent *ent;
-
-		path = xstrfmt("%s/17", source->path);
-
-		dir = opendir(path);
-		if (!dir) {
-			if (errno == ENOENT) {
-				*out = 0;
-				ret = 0;
-				goto out;
-			}
-
-			ret = error_errno("cannot open object shard '%s'", path);
-			goto out;
-		}
-
-		while ((ent = readdir(dir)) != NULL) {
-			if (strspn(ent->d_name, "0123456789abcdef") != hexsz ||
-			    ent->d_name[hexsz] != '\0')
-				continue;
-			count++;
-		}
-
-		*out = count * 256;
-		ret = 0;
-	} else {
-		struct odb_for_each_object_options opts = { 0 };
-		*out = 0;
-		ret = odb_source_for_each_object(&files->loose->base, NULL, count_loose_object,
-						 out, &opts);
-	}
-
-out:
-	if (dir)
-		closedir(dir);
-	free(path);
-	return ret;
-}
-
 static int check_stream_oid(git_zstream *stream,
 			    const char *hdr,
 			    unsigned long size,
diff --git a/object-file.h b/object-file.h
index 96760db0e1cb2b..bc72d89f548915 100644
--- a/object-file.h
+++ b/object-file.h
@@ -96,20 +96,6 @@ int for_each_file_in_obj_subdir(unsigned int subdir_nr,
 				each_loose_subdir_fn subdir_cb,
 				void *data);
 
-/*
- * Count the number of loose objects in this source.
- *
- * The object count is approximated by opening a single sharding directory for
- * loose objects and scanning its contents. The result is then extrapolated by
- * 256. This should generally work as a reasonable estimate given that the
- * object hash is supposed to be indistinguishable from random.
- *
- * Returns 0 on success, a negative error code otherwise.
- */
-int odb_source_loose_count_objects(struct odb_source *source,
-				   enum odb_count_objects_flags flags,
-				   unsigned long *out);
-
 /**
  * format_object_header() is a thin wrapper around s xsnprintf() that
  * writes the initial " " part of the loose object
diff --git a/odb/source-files.c b/odb/source-files.c
index 4a54b10e4af11d..d5454e170dee66 100644
--- a/odb/source-files.c
+++ b/odb/source-files.c
@@ -109,7 +109,7 @@ static int odb_source_files_count_objects(struct odb_source *source,
 	if (!(flags & ODB_COUNT_OBJECTS_APPROXIMATE)) {
 		unsigned long loose_count;
 
-		ret = odb_source_loose_count_objects(source, flags, &loose_count);
+		ret = odb_source_count_objects(&files->loose->base, flags, &loose_count);
 		if (ret < 0)
 			goto out;
 
diff --git a/odb/source-loose.c b/odb/source-loose.c
index 4b8d10bc870374..27be066327a313 100644
--- a/odb/source-loose.c
+++ b/odb/source-loose.c
@@ -520,6 +520,66 @@ static int odb_source_loose_find_abbrev_len(struct odb_source *source,
 	return ret;
 }
 
+static int count_loose_object(const struct object_id *oid UNUSED,
+			      struct object_info *oi UNUSED,
+			      void *payload)
+{
+	unsigned long *count = payload;
+	(*count)++;
+	return 0;
+}
+
+static int odb_source_loose_count_objects(struct odb_source *source,
+					  enum odb_count_objects_flags flags,
+					  unsigned long *out)
+{
+	struct odb_source_loose *loose = odb_source_loose_downcast(source);
+	const unsigned hexsz = source->odb->repo->hash_algo->hexsz - 2;
+	char *path = NULL;
+	DIR *dir = NULL;
+	int ret;
+
+	if (flags & ODB_COUNT_OBJECTS_APPROXIMATE) {
+		unsigned long count = 0;
+		struct dirent *ent;
+
+		path = xstrfmt("%s/17", source->path);
+
+		dir = opendir(path);
+		if (!dir) {
+			if (errno == ENOENT) {
+				*out = 0;
+				ret = 0;
+				goto out;
+			}
+
+			ret = error_errno("cannot open object shard '%s'", path);
+			goto out;
+		}
+
+		while ((ent = readdir(dir)) != NULL) {
+			if (strspn(ent->d_name, "0123456789abcdef") != hexsz ||
+			    ent->d_name[hexsz] != '\0')
+				continue;
+			count++;
+		}
+
+		*out = count * 256;
+		ret = 0;
+	} else {
+		struct odb_for_each_object_options opts = { 0 };
+		*out = 0;
+		ret = odb_source_for_each_object(&loose->base, NULL, count_loose_object,
+						 out, &opts);
+	}
+
+out:
+	if (dir)
+		closedir(dir);
+	free(path);
+	return ret;
+}
+
 static void odb_source_loose_clear_cache(struct odb_source_loose *loose)
 {
 	oidtree_clear(loose->cache);
@@ -577,6 +637,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)
 	loose->base.read_object_stream = odb_source_loose_read_object_stream;
 	loose->base.for_each_object = odb_source_loose_for_each_object;
 	loose->base.find_abbrev_len = odb_source_loose_find_abbrev_len;
+	loose->base.count_objects = odb_source_loose_count_objects;
 
 	if (!is_absolute_path(loose->base.path))
 		chdir_notify_register(NULL, odb_source_loose_reparent, loose);

From 86f7ab5a1f12ecfdf51b6df0b9b014e2329944be Mon Sep 17 00:00:00 2001
From: Patrick Steinhardt 
Date: Mon, 1 Jun 2026 10:20:34 +0200
Subject: [PATCH 074/106] odb/source-loose: drop
 `odb_source_loose_has_object()`

The function `odb_source_loose_has_object()` checks whether a specific
object exists as a loose object on disk by using lstat(3p). This
interface is somewhat redundant, as we typically check for object
existence in a generic way via `odb_source_read_object_info()`.

In fact, these two calls are redundant in case the latter is called in a
specific way: when called without an object info request and without the
`OBJECT_INFO_QUICK` flag, then we will end up doing the same call to
lstat(3p) in `read_object_info_from_path()`.

Drop the function and adapt callers to instead use the generic
interface so that its calling conventions align with that of other
sources.

Signed-off-by: Patrick Steinhardt 
Signed-off-by: Junio C Hamano 
---
 builtin/pack-objects.c | 12 ++++++++----
 object-file.c          | 12 ++++--------
 object-file.h          |  8 --------
 3 files changed, 12 insertions(+), 20 deletions(-)

diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c
index 480cc0bd8c8d22..a6be3d659f8e36 100644
--- a/builtin/pack-objects.c
+++ b/builtin/pack-objects.c
@@ -1750,9 +1750,11 @@ static int want_object_in_pack_mtime(const struct object_id *oid,
 		 * skip the local object source.
 		 */
 		struct odb_source *source = the_repository->objects->sources->next;
-		for (; source; source = source->next)
-			if (odb_source_loose_has_object(source, oid))
+		for (; source; source = source->next) {
+			struct odb_source_files *files = odb_source_files_downcast(source);
+			if (!odb_source_read_object_info(&files->loose->base, oid, NULL, 0))
 				return 0;
+		}
 	}
 
 	/*
@@ -4135,9 +4137,11 @@ static void add_cruft_object_entry(const struct object_id *oid, enum object_type
 			struct odb_source *source = the_repository->objects->sources;
 			int found = 0;
 
-			for (; !found && source; source = source->next)
-				if (odb_source_loose_has_object(source, oid))
+			for (; !found && source; source = source->next) {
+				struct odb_source_files *files = odb_source_files_downcast(source);
+				if (!odb_source_read_object_info(&files->loose->base, oid, NULL, 0))
 					found = 1;
+			}
 
 			/*
 			 * If a traversed tree has a missing blob then we want
diff --git a/object-file.c b/object-file.c
index 9b2044de3784e6..c83136cf70024c 100644
--- a/object-file.c
+++ b/object-file.c
@@ -96,12 +96,6 @@ static int check_and_freshen_source(struct odb_source *source,
 	return check_and_freshen_file(path.buf, freshen);
 }
 
-int odb_source_loose_has_object(struct odb_source *source,
-				const struct object_id *oid)
-{
-	return check_and_freshen_source(source, oid, 0);
-}
-
 int format_object_header(char *str, size_t size, enum object_type type,
 			 size_t objsize)
 {
@@ -1000,9 +994,11 @@ int force_object_loose(struct odb_source *source,
 	int hdrlen;
 	int ret;
 
-	for (struct odb_source *s = source->odb->sources; s; s = s->next)
-		if (odb_source_loose_has_object(s, oid))
+	for (struct odb_source *s = source->odb->sources; s; s = s->next) {
+		struct odb_source_files *files = odb_source_files_downcast(s);
+		if (!odb_source_read_object_info(&files->loose->base, oid, NULL, 0))
 			return 0;
+	}
 
 	oi.typep = &type;
 	oi.sizep = &len;
diff --git a/object-file.h b/object-file.h
index bc72d89f548915..506ca6be40b749 100644
--- a/object-file.h
+++ b/object-file.h
@@ -23,14 +23,6 @@ int index_path(struct index_state *istate, struct object_id *oid, const char *pa
 struct object_info;
 struct odb_source;
 
-/*
- * Return true iff an object database source has a loose object
- * with the specified name.  This function does not respect replace
- * references.
- */
-int odb_source_loose_has_object(struct odb_source *source,
-				const struct object_id *oid);
-
 int odb_source_loose_freshen_object(struct odb_source *source,
 				    const struct object_id *oid);
 

From d8b9e8bb23ece128179ad54ed5ecbcd4bd809b1e Mon Sep 17 00:00:00 2001
From: Patrick Steinhardt 
Date: Mon, 1 Jun 2026 10:20:35 +0200
Subject: [PATCH 075/106] odb/source-loose: wire up `freshen_object()` callback

Move `odb_source_loose_freshen_object()` from "object-file.c" into
"odb/source-loose.c" and wire it up as the `freshen_object()` callback
of the loose source.

As part of the move, `check_and_freshen_source()` is inlined into the
callback function, as it has no other callers anymore.

Signed-off-by: Patrick Steinhardt 
Signed-off-by: Junio C Hamano 
---
 object-file.c      | 15 ---------------
 object-file.h      |  3 ---
 odb/source-files.c |  2 +-
 odb/source-loose.c |  9 +++++++++
 4 files changed, 10 insertions(+), 19 deletions(-)

diff --git a/object-file.c b/object-file.c
index c83136cf70024c..0689a4e67b156a 100644
--- a/object-file.c
+++ b/object-file.c
@@ -87,15 +87,6 @@ int check_and_freshen_file(const char *fn, int freshen)
 	return 1;
 }
 
-static int check_and_freshen_source(struct odb_source *source,
-				    const struct object_id *oid,
-				    int freshen)
-{
-	static struct strbuf path = STRBUF_INIT;
-	odb_loose_path(source, &path, oid);
-	return check_and_freshen_file(path.buf, freshen);
-}
-
 int format_object_header(char *str, size_t size, enum object_type type,
 			 size_t objsize)
 {
@@ -815,12 +806,6 @@ static int write_loose_object(struct odb_source *source,
 					  FOF_SKIP_COLLISION_CHECK);
 }
 
-int odb_source_loose_freshen_object(struct odb_source *source,
-				    const struct object_id *oid)
-{
-	return !!check_and_freshen_source(source, oid, 1);
-}
-
 int odb_source_loose_write_stream(struct odb_source *source,
 				  struct odb_write_stream *in_stream, size_t len,
 				  struct object_id *oid)
diff --git a/object-file.h b/object-file.h
index 506ca6be40b749..1d90df9d98b78e 100644
--- a/object-file.h
+++ b/object-file.h
@@ -23,9 +23,6 @@ int index_path(struct index_state *istate, struct object_id *oid, const char *pa
 struct object_info;
 struct odb_source;
 
-int odb_source_loose_freshen_object(struct odb_source *source,
-				    const struct object_id *oid);
-
 int odb_source_loose_write_object(struct odb_source *source,
 				  const void *buf, unsigned long len,
 				  enum object_type type, struct object_id *oid,
diff --git a/odb/source-files.c b/odb/source-files.c
index d5454e170dee66..ef548e6fe69cd0 100644
--- a/odb/source-files.c
+++ b/odb/source-files.c
@@ -152,7 +152,7 @@ static int odb_source_files_freshen_object(struct odb_source *source,
 {
 	struct odb_source_files *files = odb_source_files_downcast(source);
 	if (packfile_store_freshen_object(files->packed, oid) ||
-	    odb_source_loose_freshen_object(source, oid))
+	    odb_source_freshen_object(&files->loose->base, oid))
 		return 1;
 	return 0;
 }
diff --git a/odb/source-loose.c b/odb/source-loose.c
index 27be066327a313..e519365d23f680 100644
--- a/odb/source-loose.c
+++ b/odb/source-loose.c
@@ -580,6 +580,14 @@ static int odb_source_loose_count_objects(struct odb_source *source,
 	return ret;
 }
 
+static int odb_source_loose_freshen_object(struct odb_source *source,
+					   const struct object_id *oid)
+{
+	static struct strbuf path = STRBUF_INIT;
+	odb_loose_path(source, &path, oid);
+	return !!check_and_freshen_file(path.buf, 1);
+}
+
 static void odb_source_loose_clear_cache(struct odb_source_loose *loose)
 {
 	oidtree_clear(loose->cache);
@@ -638,6 +646,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)
 	loose->base.for_each_object = odb_source_loose_for_each_object;
 	loose->base.find_abbrev_len = odb_source_loose_find_abbrev_len;
 	loose->base.count_objects = odb_source_loose_count_objects;
+	loose->base.freshen_object = odb_source_loose_freshen_object;
 
 	if (!is_absolute_path(loose->base.path))
 		chdir_notify_register(NULL, odb_source_loose_reparent, loose);

From 87588db131a5c1c33471606860951c9959bbe6ae Mon Sep 17 00:00:00 2001
From: Patrick Steinhardt 
Date: Mon, 1 Jun 2026 10:20:36 +0200
Subject: [PATCH 076/106] loose: refactor object map to operate on `struct
 odb_source_loose`

While the loose object map functions in "loose.c" accept a generic
`struct odb_source *`, they always expect this to be the "files"
backend. Furthermore, the subsystem doesn't even care about the "files"
backend, but only uses it as a stepping stone to get to the "loose"
backend.

This assumption is implicit and thus not immediately obvious. Refactor
the interfaces to instead operate on a `struct odb_source_loose`
instead, which eliminates the implicit dependency and unnecessary detour
via the "files" source.

Signed-off-by: Patrick Steinhardt 
Signed-off-by: Junio C Hamano 
---
 loose.c       | 45 ++++++++++++++++++++++-----------------------
 loose.h       |  4 ++--
 object-file.c |  9 ++++++---
 3 files changed, 30 insertions(+), 28 deletions(-)

diff --git a/loose.c b/loose.c
index f7a3dd1a72f0fc..0b626c1b854642 100644
--- a/loose.c
+++ b/loose.c
@@ -46,38 +46,36 @@ static int insert_oid_pair(kh_oid_map_t *map, const struct object_id *key, const
 	return 1;
 }
 
-static int insert_loose_map(struct odb_source *source,
+static int insert_loose_map(struct odb_source_loose *loose,
 			    const struct object_id *oid,
 			    const struct object_id *compat_oid)
 {
-	struct odb_source_files *files = odb_source_files_downcast(source);
-	struct loose_object_map *map = files->loose->map;
+	struct loose_object_map *map = loose->map;
 	int inserted = 0;
 
 	inserted |= insert_oid_pair(map->to_compat, oid, compat_oid);
 	inserted |= insert_oid_pair(map->to_storage, compat_oid, oid);
 	if (inserted)
-		oidtree_insert(files->loose->cache, compat_oid, NULL);
+		oidtree_insert(loose->cache, compat_oid, NULL);
 
 	return inserted;
 }
 
-static int load_one_loose_object_map(struct repository *repo, struct odb_source *source)
+static int load_one_loose_object_map(struct repository *repo, struct odb_source_loose *loose)
 {
-	struct odb_source_files *files = odb_source_files_downcast(source);
 	struct strbuf buf = STRBUF_INIT, path = STRBUF_INIT;
 	FILE *fp;
 
-	if (!files->loose->map)
-		loose_object_map_init(&files->loose->map);
-	if (!files->loose->cache) {
-		ALLOC_ARRAY(files->loose->cache, 1);
-		oidtree_init(files->loose->cache);
+	if (!loose->map)
+		loose_object_map_init(&loose->map);
+	if (!loose->cache) {
+		ALLOC_ARRAY(loose->cache, 1);
+		oidtree_init(loose->cache);
 	}
 
-	insert_loose_map(source, repo->hash_algo->empty_tree, repo->compat_hash_algo->empty_tree);
-	insert_loose_map(source, repo->hash_algo->empty_blob, repo->compat_hash_algo->empty_blob);
-	insert_loose_map(source, repo->hash_algo->null_oid, repo->compat_hash_algo->null_oid);
+	insert_loose_map(loose, repo->hash_algo->empty_tree, repo->compat_hash_algo->empty_tree);
+	insert_loose_map(loose, repo->hash_algo->empty_blob, repo->compat_hash_algo->empty_blob);
+	insert_loose_map(loose, repo->hash_algo->null_oid, repo->compat_hash_algo->null_oid);
 
 	repo_common_path_replace(repo, &path, "objects/loose-object-idx");
 	fp = fopen(path.buf, "rb");
@@ -97,7 +95,7 @@ static int load_one_loose_object_map(struct repository *repo, struct odb_source
 		    parse_oid_hex_algop(p, &compat_oid, &p, repo->compat_hash_algo) ||
 		    p != buf.buf + buf.len)
 			goto err;
-		insert_loose_map(source, &oid, &compat_oid);
+		insert_loose_map(loose, &oid, &compat_oid);
 	}
 
 	strbuf_release(&buf);
@@ -119,7 +117,8 @@ int repo_read_loose_object_map(struct repository *repo)
 	odb_prepare_alternates(repo->objects);
 
 	for (source = repo->objects->sources; source; source = source->next) {
-		if (load_one_loose_object_map(repo, source) < 0) {
+		struct odb_source_files *files = odb_source_files_downcast(source);
+		if (load_one_loose_object_map(repo, files->loose) < 0) {
 			return -1;
 		}
 	}
@@ -171,7 +170,7 @@ int repo_write_loose_object_map(struct repository *repo)
 	return -1;
 }
 
-static int write_one_object(struct odb_source *source,
+static int write_one_object(struct odb_source_loose *loose,
 			    const struct object_id *oid,
 			    const struct object_id *compat_oid)
 {
@@ -180,7 +179,7 @@ static int write_one_object(struct odb_source *source,
 	struct stat st;
 	struct strbuf buf = STRBUF_INIT, path = STRBUF_INIT;
 
-	strbuf_addf(&path, "%s/loose-object-idx", source->path);
+	strbuf_addf(&path, "%s/loose-object-idx", loose->base.path);
 	hold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1);
 
 	fd = open(path.buf, O_WRONLY | O_CREAT | O_APPEND, 0666);
@@ -196,7 +195,7 @@ static int write_one_object(struct odb_source *source,
 		goto errout;
 	if (close(fd))
 		goto errout;
-	adjust_shared_perm(source->odb->repo, path.buf);
+	adjust_shared_perm(loose->base.odb->repo, path.buf);
 	rollback_lock_file(&lock);
 	strbuf_release(&buf);
 	strbuf_release(&path);
@@ -210,18 +209,18 @@ static int write_one_object(struct odb_source *source,
 	return -1;
 }
 
-int repo_add_loose_object_map(struct odb_source *source,
+int repo_add_loose_object_map(struct odb_source_loose *loose,
 			      const struct object_id *oid,
 			      const struct object_id *compat_oid)
 {
 	int inserted = 0;
 
-	if (!should_use_loose_object_map(source->odb->repo))
+	if (!should_use_loose_object_map(loose->base.odb->repo))
 		return 0;
 
-	inserted = insert_loose_map(source, oid, compat_oid);
+	inserted = insert_loose_map(loose, oid, compat_oid);
 	if (inserted)
-		return write_one_object(source, oid, compat_oid);
+		return write_one_object(loose, oid, compat_oid);
 	return 0;
 }
 
diff --git a/loose.h b/loose.h
index 6af1702973c058..6c9b3f4571602f 100644
--- a/loose.h
+++ b/loose.h
@@ -4,7 +4,7 @@
 #include "khash.h"
 
 struct repository;
-struct odb_source;
+struct odb_source_loose;
 
 struct loose_object_map {
 	kh_oid_map_t *to_compat;
@@ -17,7 +17,7 @@ int repo_loose_object_map_oid(struct repository *repo,
 			      const struct object_id *src,
 			      const struct git_hash_algo *dest_algo,
 			      struct object_id *dest);
-int repo_add_loose_object_map(struct odb_source *source,
+int repo_add_loose_object_map(struct odb_source_loose *loose,
 			      const struct object_id *oid,
 			      const struct object_id *compat_oid);
 int repo_read_loose_object_map(struct repository *repo);
diff --git a/object-file.c b/object-file.c
index 0689a4e67b156a..fe24f00d1b79bf 100644
--- a/object-file.c
+++ b/object-file.c
@@ -810,6 +810,7 @@ int odb_source_loose_write_stream(struct odb_source *source,
 				  struct odb_write_stream *in_stream, size_t len,
 				  struct object_id *oid)
 {
+	struct odb_source_files *files = odb_source_files_downcast(source);
 	const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;
 	struct object_id compat_oid;
 	int fd, ret, err = 0, flush = 0;
@@ -918,7 +919,7 @@ int odb_source_loose_write_stream(struct odb_source *source,
 	err = finalize_object_file_flags(source->odb->repo, tmp_file.buf, filename.buf,
 					 FOF_SKIP_COLLISION_CHECK);
 	if (!err && compat)
-		err = repo_add_loose_object_map(source, oid, &compat_oid);
+		err = repo_add_loose_object_map(files->loose, oid, &compat_oid);
 cleanup:
 	strbuf_release(&tmp_file);
 	strbuf_release(&filename);
@@ -931,6 +932,7 @@ int odb_source_loose_write_object(struct odb_source *source,
 				  struct object_id *compat_oid_in,
 				  enum odb_write_object_flags flags)
 {
+	struct odb_source_files *files = odb_source_files_downcast(source);
 	const struct git_hash_algo *algo = source->odb->repo->hash_algo;
 	const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;
 	struct object_id compat_oid;
@@ -962,13 +964,14 @@ int odb_source_loose_write_object(struct odb_source *source,
 	if (write_loose_object(source, oid, hdr, hdrlen, buf, len, 0, flags))
 		return -1;
 	if (compat)
-		return repo_add_loose_object_map(source, oid, &compat_oid);
+		return repo_add_loose_object_map(files->loose, oid, &compat_oid);
 	return 0;
 }
 
 int force_object_loose(struct odb_source *source,
 		       const struct object_id *oid, time_t mtime)
 {
+	struct odb_source_files *files = odb_source_files_downcast(source);
 	const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;
 	void *buf;
 	unsigned long len;
@@ -998,7 +1001,7 @@ int force_object_loose(struct odb_source *source,
 	hdrlen = format_object_header(hdr, sizeof(hdr), type, len);
 	ret = write_loose_object(source, oid, hdr, hdrlen, buf, len, mtime, 0);
 	if (!ret && compat)
-		ret = repo_add_loose_object_map(source, oid, &compat_oid);
+		ret = repo_add_loose_object_map(files->loose, oid, &compat_oid);
 	free(buf);
 
 	return ret;

From 04a6e84cbdbebadd01d939168f1c69680c174fce Mon Sep 17 00:00:00 2001
From: Patrick Steinhardt 
Date: Mon, 1 Jun 2026 10:20:37 +0200
Subject: [PATCH 077/106] odb/source-loose: wire up `write_object()` callback

Move `odb_source_loose_write_object()` from "object-file.c" into
"odb/source-loose.c" and wire it up as the `write_object()` callback of
the loose source.

As in preceding commits, this requires us to expose a couple of generic
functions from "object-file.c" as they are used in both subsystems now.

Signed-off-by: Patrick Steinhardt 
Signed-off-by: Junio C Hamano 
---
 object-file.c      | 58 +++++++---------------------------------------
 object-file.h      | 14 ++++++-----
 odb/source-files.c |  5 ++--
 odb/source-loose.c | 44 +++++++++++++++++++++++++++++++++++
 4 files changed, 63 insertions(+), 58 deletions(-)

diff --git a/object-file.c b/object-file.c
index fe24f00d1b79bf..7bb5b31bcad88b 100644
--- a/object-file.c
+++ b/object-file.c
@@ -326,10 +326,10 @@ static void hash_object_body(const struct git_hash_algo *algo, struct git_hash_c
 	git_hash_final_oid(oid, c);
 }
 
-static void write_object_file_prepare(const struct git_hash_algo *algo,
-				      const void *buf, unsigned long len,
-				      enum object_type type, struct object_id *oid,
-				      char *hdr, int *hdrlen)
+void write_object_file_prepare(const struct git_hash_algo *algo,
+			       const void *buf, unsigned long len,
+			       enum object_type type, struct object_id *oid,
+			       char *hdr, int *hdrlen)
 {
 	struct git_hash_ctx c;
 
@@ -746,10 +746,10 @@ static int end_loose_object_common(struct odb_source *source,
 	return Z_OK;
 }
 
-static int write_loose_object(struct odb_source *source,
-			      const struct object_id *oid, char *hdr,
-			      int hdrlen, const void *buf, unsigned long len,
-			      time_t mtime, unsigned flags)
+int write_loose_object(struct odb_source *source,
+		       const struct object_id *oid, char *hdr,
+		       int hdrlen, const void *buf, unsigned long len,
+		       time_t mtime, unsigned flags)
 {
 	int fd, ret;
 	unsigned char compressed[4096];
@@ -926,48 +926,6 @@ int odb_source_loose_write_stream(struct odb_source *source,
 	return err;
 }
 
-int odb_source_loose_write_object(struct odb_source *source,
-				  const void *buf, unsigned long len,
-				  enum object_type type, struct object_id *oid,
-				  struct object_id *compat_oid_in,
-				  enum odb_write_object_flags flags)
-{
-	struct odb_source_files *files = odb_source_files_downcast(source);
-	const struct git_hash_algo *algo = source->odb->repo->hash_algo;
-	const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;
-	struct object_id compat_oid;
-	char hdr[MAX_HEADER_LEN];
-	int hdrlen = sizeof(hdr);
-
-	/* Generate compat_oid */
-	if (compat) {
-		if (compat_oid_in)
-			oidcpy(&compat_oid, compat_oid_in);
-		else if (type == OBJ_BLOB)
-			hash_object_file(compat, buf, len, type, &compat_oid);
-		else {
-			struct strbuf converted = STRBUF_INIT;
-			convert_object_file(source->odb->repo, &converted, algo, compat,
-					    buf, len, type, 0);
-			hash_object_file(compat, converted.buf, converted.len,
-					 type, &compat_oid);
-			strbuf_release(&converted);
-		}
-	}
-
-	/* Normally if we have it in the pack then we do not bother writing
-	 * it out into .git/objects/??/?{38} file.
-	 */
-	write_object_file_prepare(algo, buf, len, type, oid, hdr, &hdrlen);
-	if (odb_freshen_object(source->odb, oid))
-		return 0;
-	if (write_loose_object(source, oid, hdr, hdrlen, buf, len, 0, flags))
-		return -1;
-	if (compat)
-		return repo_add_loose_object_map(files->loose, oid, &compat_oid);
-	return 0;
-}
-
 int force_object_loose(struct odb_source *source,
 		       const struct object_id *oid, time_t mtime)
 {
diff --git a/object-file.h b/object-file.h
index 1d90df9d98b78e..2b32592de1135b 100644
--- a/object-file.h
+++ b/object-file.h
@@ -23,12 +23,6 @@ int index_path(struct index_state *istate, struct object_id *oid, const char *pa
 struct object_info;
 struct odb_source;
 
-int odb_source_loose_write_object(struct odb_source *source,
-				  const void *buf, unsigned long len,
-				  enum object_type type, struct object_id *oid,
-				  struct object_id *compat_oid_in,
-				  enum odb_write_object_flags flags);
-
 int odb_source_loose_write_stream(struct odb_source *source,
 				  struct odb_write_stream *stream, size_t len,
 				  struct object_id *oid);
@@ -129,6 +123,14 @@ int finalize_object_file_flags(struct repository *repo,
 void hash_object_file(const struct git_hash_algo *algo, const void *buf,
 		      unsigned long len, enum object_type type,
 		      struct object_id *oid);
+void write_object_file_prepare(const struct git_hash_algo *algo,
+			       const void *buf, unsigned long len,
+			       enum object_type type, struct object_id *oid,
+			       char *hdr, int *hdrlen);
+int write_loose_object(struct odb_source *source,
+		       const struct object_id *oid, char *hdr,
+		       int hdrlen, const void *buf, unsigned long len,
+		       time_t mtime, unsigned flags);
 
 /* Helper to check and "touch" a file */
 int check_and_freshen_file(const char *fn, int freshen);
diff --git a/odb/source-files.c b/odb/source-files.c
index ef548e6fe69cd0..52ba04237acfd7 100644
--- a/odb/source-files.c
+++ b/odb/source-files.c
@@ -164,8 +164,9 @@ static int odb_source_files_write_object(struct odb_source *source,
 					 struct object_id *compat_oid,
 					 enum odb_write_object_flags flags)
 {
-	return odb_source_loose_write_object(source, buf, len, type,
-					     oid, compat_oid, flags);
+	struct odb_source_files *files = odb_source_files_downcast(source);
+	return odb_source_write_object(&files->loose->base, buf, len, type,
+				       oid, compat_oid, flags);
 }
 
 static int odb_source_files_write_object_stream(struct odb_source *source,
diff --git a/odb/source-loose.c b/odb/source-loose.c
index e519365d23f680..c91018109e5b68 100644
--- a/odb/source-loose.c
+++ b/odb/source-loose.c
@@ -5,6 +5,7 @@
 #include "hex.h"
 #include "loose.h"
 #include "object-file.h"
+#include "object-file-convert.h"
 #include "odb.h"
 #include "odb/source-files.h"
 #include "odb/source-loose.h"
@@ -588,6 +589,48 @@ static int odb_source_loose_freshen_object(struct odb_source *source,
 	return !!check_and_freshen_file(path.buf, 1);
 }
 
+static int odb_source_loose_write_object(struct odb_source *source,
+					 const void *buf, unsigned long len,
+					 enum object_type type, struct object_id *oid,
+					 struct object_id *compat_oid_in,
+					 enum odb_write_object_flags flags)
+{
+	struct odb_source_loose *loose = odb_source_loose_downcast(source);
+	const struct git_hash_algo *algo = source->odb->repo->hash_algo;
+	const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;
+	struct object_id compat_oid;
+	char hdr[MAX_HEADER_LEN];
+	int hdrlen = sizeof(hdr);
+
+	/* Generate compat_oid */
+	if (compat) {
+		if (compat_oid_in)
+			oidcpy(&compat_oid, compat_oid_in);
+		else if (type == OBJ_BLOB)
+			hash_object_file(compat, buf, len, type, &compat_oid);
+		else {
+			struct strbuf converted = STRBUF_INIT;
+			convert_object_file(source->odb->repo, &converted, algo, compat,
+					    buf, len, type, 0);
+			hash_object_file(compat, converted.buf, converted.len,
+					 type, &compat_oid);
+			strbuf_release(&converted);
+		}
+	}
+
+	/* Normally if we have it in the pack then we do not bother writing
+	 * it out into .git/objects/??/?{38} file.
+	 */
+	write_object_file_prepare(algo, buf, len, type, oid, hdr, &hdrlen);
+	if (odb_freshen_object(source->odb, oid))
+		return 0;
+	if (write_loose_object(source, oid, hdr, hdrlen, buf, len, 0, flags))
+		return -1;
+	if (compat)
+		return repo_add_loose_object_map(loose, oid, &compat_oid);
+	return 0;
+}
+
 static void odb_source_loose_clear_cache(struct odb_source_loose *loose)
 {
 	oidtree_clear(loose->cache);
@@ -647,6 +690,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)
 	loose->base.find_abbrev_len = odb_source_loose_find_abbrev_len;
 	loose->base.count_objects = odb_source_loose_count_objects;
 	loose->base.freshen_object = odb_source_loose_freshen_object;
+	loose->base.write_object = odb_source_loose_write_object;
 
 	if (!is_absolute_path(loose->base.path))
 		chdir_notify_register(NULL, odb_source_loose_reparent, loose);

From b9906a645c38ef77643d661ac9a5a6aa31fbeaf4 Mon Sep 17 00:00:00 2001
From: Patrick Steinhardt 
Date: Mon, 1 Jun 2026 10:20:38 +0200
Subject: [PATCH 078/106] object-file: refactor writing objects to use loose
 source

The "object-file" subsystem still hosts the majority of logic used to
write loose objects. Eventually, we'll want to move this logic into
"odb/source-loose.c", but this isn't yet easily possible because a lot
of the writing logic is still being shared with `force_object_loose()`.

We will eventually detangle this logic so that we can indeed move all of
it into the "loose" source. Meanwhile though, refactor the code so that
it operates on a `struct odb_source_loose` directly to already make the
dependency explicit.

Signed-off-by: Patrick Steinhardt 
Signed-off-by: Junio C Hamano 
---
 http-walker.c      |  3 +-
 http.c             |  6 ++--
 object-file.c      | 75 +++++++++++++++++++++++-----------------------
 object-file.h      |  6 ++--
 odb/source-files.c |  3 +-
 odb/source-loose.c |  9 +++---
 6 files changed, 53 insertions(+), 49 deletions(-)

diff --git a/http-walker.c b/http-walker.c
index 1b6d496548373e..435a7265408fa4 100644
--- a/http-walker.c
+++ b/http-walker.c
@@ -539,8 +539,9 @@ static int fetch_object(struct walker *walker, const struct object_id *oid)
 	} else if (!oideq(&obj_req->oid, &req->real_oid)) {
 		ret = error("File %s has bad hash", hex);
 	} else if (req->rename < 0) {
+		struct odb_source_files *files = odb_source_files_downcast(the_repository->objects->sources);
 		struct strbuf buf = STRBUF_INIT;
-		odb_loose_path(the_repository->objects->sources, &buf, &req->oid);
+		odb_loose_path(files->loose, &buf, &req->oid);
 		ret = error("unable to write sha1 filename %s", buf.buf);
 		strbuf_release(&buf);
 	}
diff --git a/http.c b/http.c
index ea9b16861bc3d4..3fcc0122337ba4 100644
--- a/http.c
+++ b/http.c
@@ -2826,6 +2826,7 @@ static size_t fwrite_sha1_file(char *ptr, size_t eltsize, size_t nmemb,
 struct http_object_request *new_http_object_request(const char *base_url,
 						    const struct object_id *oid)
 {
+	struct odb_source_files *files = odb_source_files_downcast(the_repository->objects->sources);
 	char *hex = oid_to_hex(oid);
 	struct strbuf filename = STRBUF_INIT;
 	struct strbuf prevfile = STRBUF_INIT;
@@ -2840,7 +2841,7 @@ struct http_object_request *new_http_object_request(const char *base_url,
 	oidcpy(&freq->oid, oid);
 	freq->localfile = -1;
 
-	odb_loose_path(the_repository->objects->sources, &filename, oid);
+	odb_loose_path(files->loose, &filename, oid);
 	strbuf_addf(&freq->tmpfile, "%s.temp", filename.buf);
 
 	strbuf_addf(&prevfile, "%s.prev", filename.buf);
@@ -2966,6 +2967,7 @@ void process_http_object_request(struct http_object_request *freq)
 
 int finish_http_object_request(struct http_object_request *freq)
 {
+	struct odb_source_files *files = odb_source_files_downcast(the_repository->objects->sources);
 	struct stat st;
 	struct strbuf filename = STRBUF_INIT;
 
@@ -2992,7 +2994,7 @@ int finish_http_object_request(struct http_object_request *freq)
 		unlink_or_warn(freq->tmpfile.buf);
 		return -1;
 	}
-	odb_loose_path(the_repository->objects->sources, &filename, &freq->oid);
+	odb_loose_path(files->loose, &filename, &freq->oid);
 	freq->rename = finalize_object_file(the_repository, freq->tmpfile.buf, filename.buf);
 	strbuf_release(&filename);
 
diff --git a/object-file.c b/object-file.c
index 7bb5b31bcad88b..bce941874eb994 100644
--- a/object-file.c
+++ b/object-file.c
@@ -54,14 +54,14 @@ static void fill_loose_path(struct strbuf *buf,
 	}
 }
 
-const char *odb_loose_path(struct odb_source *source,
+const char *odb_loose_path(struct odb_source_loose *loose,
 			   struct strbuf *buf,
 			   const struct object_id *oid)
 {
 	strbuf_reset(buf);
-	strbuf_addstr(buf, source->path);
+	strbuf_addstr(buf, loose->base.path);
 	strbuf_addch(buf, '/');
-	fill_loose_path(buf, oid, source->odb->repo->hash_algo);
+	fill_loose_path(buf, oid, loose->base.odb->repo->hash_algo);
 	return buf->buf;
 }
 
@@ -575,14 +575,14 @@ static void flush_loose_object_transaction(struct odb_transaction_files *transac
 }
 
 /* Finalize a file on disk, and close it. */
-static void close_loose_object(struct odb_source *source,
+static void close_loose_object(struct odb_source_loose *loose,
 			       int fd, const char *filename)
 {
-	if (source->will_destroy)
+	if (loose->base.will_destroy)
 		goto out;
 
 	if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))
-		fsync_loose_object_transaction(source->odb->transaction, fd, filename);
+		fsync_loose_object_transaction(loose->base.odb->transaction, fd, filename);
 	else if (fsync_object_files > 0)
 		fsync_or_die(fd, filename);
 	else
@@ -651,7 +651,7 @@ static int create_tmpfile(struct repository *repo,
  * Returns a "fd", which should later be provided to
  * end_loose_object_common().
  */
-static int start_loose_object_common(struct odb_source *source,
+static int start_loose_object_common(struct odb_source_loose *loose,
 				     struct strbuf *tmp_file,
 				     const char *filename, unsigned flags,
 				     git_zstream *stream,
@@ -659,18 +659,18 @@ static int start_loose_object_common(struct odb_source *source,
 				     struct git_hash_ctx *c, struct git_hash_ctx *compat_c,
 				     char *hdr, int hdrlen)
 {
-	const struct git_hash_algo *algo = source->odb->repo->hash_algo;
-	const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;
+	const struct git_hash_algo *algo = loose->base.odb->repo->hash_algo;
+	const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;
 	int fd;
 
-	fd = create_tmpfile(source->odb->repo, tmp_file, filename);
+	fd = create_tmpfile(loose->base.odb->repo, tmp_file, filename);
 	if (fd < 0) {
 		if (flags & ODB_WRITE_OBJECT_SILENT)
 			return -1;
 		else if (errno == EACCES)
 			return error(_("insufficient permission for adding "
 				       "an object to repository database %s"),
-				     source->path);
+				     loose->base.path);
 		else
 			return error_errno(
 				_("unable to create temporary file"));
@@ -700,14 +700,14 @@ static int start_loose_object_common(struct odb_source *source,
  * Common steps for the inner git_deflate() loop for writing loose
  * objects. Returns what git_deflate() returns.
  */
-static int write_loose_object_common(struct odb_source *source,
+static int write_loose_object_common(struct odb_source_loose *loose,
 				     struct git_hash_ctx *c, struct git_hash_ctx *compat_c,
 				     git_zstream *stream, const int flush,
 				     unsigned char *in0, const int fd,
 				     unsigned char *compressed,
 				     const size_t compressed_len)
 {
-	const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;
+	const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;
 	int ret;
 
 	ret = git_deflate(stream, flush ? Z_FINISH : 0);
@@ -728,12 +728,12 @@ static int write_loose_object_common(struct odb_source *source,
  * - End the compression of zlib stream.
  * - Get the calculated oid to "oid".
  */
-static int end_loose_object_common(struct odb_source *source,
+static int end_loose_object_common(struct odb_source_loose *loose,
 				   struct git_hash_ctx *c, struct git_hash_ctx *compat_c,
 				   git_zstream *stream, struct object_id *oid,
 				   struct object_id *compat_oid)
 {
-	const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;
+	const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;
 	int ret;
 
 	ret = git_deflate_end_gently(stream);
@@ -746,7 +746,7 @@ static int end_loose_object_common(struct odb_source *source,
 	return Z_OK;
 }
 
-int write_loose_object(struct odb_source *source,
+int write_loose_object(struct odb_source_loose *loose,
 		       const struct object_id *oid, char *hdr,
 		       int hdrlen, const void *buf, unsigned long len,
 		       time_t mtime, unsigned flags)
@@ -760,11 +760,11 @@ int write_loose_object(struct odb_source *source,
 	static struct strbuf filename = STRBUF_INIT;
 
 	if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))
-		prepare_loose_object_transaction(source->odb->transaction);
+		prepare_loose_object_transaction(loose->base.odb->transaction);
 
-	odb_loose_path(source, &filename, oid);
+	odb_loose_path(loose, &filename, oid);
 
-	fd = start_loose_object_common(source, &tmp_file, filename.buf, flags,
+	fd = start_loose_object_common(loose, &tmp_file, filename.buf, flags,
 				       &stream, compressed, sizeof(compressed),
 				       &c, NULL, hdr, hdrlen);
 	if (fd < 0)
@@ -776,14 +776,14 @@ int write_loose_object(struct odb_source *source,
 	do {
 		unsigned char *in0 = stream.next_in;
 
-		ret = write_loose_object_common(source, &c, NULL, &stream, 1, in0, fd,
+		ret = write_loose_object_common(loose, &c, NULL, &stream, 1, in0, fd,
 						compressed, sizeof(compressed));
 	} while (ret == Z_OK);
 
 	if (ret != Z_STREAM_END)
 		die(_("unable to deflate new object %s (%d)"), oid_to_hex(oid),
 		    ret);
-	ret = end_loose_object_common(source, &c, NULL, &stream, ¶no_oid, NULL);
+	ret = end_loose_object_common(loose, &c, NULL, &stream, ¶no_oid, NULL);
 	if (ret != Z_OK)
 		die(_("deflateEnd on object %s failed (%d)"), oid_to_hex(oid),
 		    ret);
@@ -791,7 +791,7 @@ int write_loose_object(struct odb_source *source,
 		die(_("confused by unstable object source data for %s"),
 		    oid_to_hex(oid));
 
-	close_loose_object(source, fd, tmp_file.buf);
+	close_loose_object(loose, fd, tmp_file.buf);
 
 	if (mtime) {
 		struct utimbuf utb;
@@ -802,16 +802,15 @@ int write_loose_object(struct odb_source *source,
 			warning_errno(_("failed utime() on %s"), tmp_file.buf);
 	}
 
-	return finalize_object_file_flags(source->odb->repo, tmp_file.buf, filename.buf,
+	return finalize_object_file_flags(loose->base.odb->repo, tmp_file.buf, filename.buf,
 					  FOF_SKIP_COLLISION_CHECK);
 }
 
-int odb_source_loose_write_stream(struct odb_source *source,
+int odb_source_loose_write_stream(struct odb_source_loose *loose,
 				  struct odb_write_stream *in_stream, size_t len,
 				  struct object_id *oid)
 {
-	struct odb_source_files *files = odb_source_files_downcast(source);
-	const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;
+	const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;
 	struct object_id compat_oid;
 	int fd, ret, err = 0, flush = 0;
 	unsigned char compressed[4096];
@@ -825,10 +824,10 @@ int odb_source_loose_write_stream(struct odb_source *source,
 	int hdrlen;
 
 	if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))
-		prepare_loose_object_transaction(source->odb->transaction);
+		prepare_loose_object_transaction(loose->base.odb->transaction);
 
 	/* Since oid is not determined, save tmp file to odb path. */
-	strbuf_addf(&filename, "%s/", source->path);
+	strbuf_addf(&filename, "%s/", loose->base.path);
 	hdrlen = format_object_header(hdr, sizeof(hdr), OBJ_BLOB, len);
 
 	/*
@@ -839,7 +838,7 @@ int odb_source_loose_write_stream(struct odb_source *source,
 	 *  - Setup zlib stream for compression.
 	 *  - Start to feed header to zlib stream.
 	 */
-	fd = start_loose_object_common(source, &tmp_file, filename.buf, 0,
+	fd = start_loose_object_common(loose, &tmp_file, filename.buf, 0,
 				       &stream, compressed, sizeof(compressed),
 				       &c, &compat_c, hdr, hdrlen);
 	if (fd < 0) {
@@ -867,7 +866,7 @@ int odb_source_loose_write_stream(struct odb_source *source,
 			if (in_stream->is_finished)
 				flush = 1;
 		}
-		ret = write_loose_object_common(source, &c, &compat_c, &stream, flush, in0, fd,
+		ret = write_loose_object_common(loose, &c, &compat_c, &stream, flush, in0, fd,
 						compressed, sizeof(compressed));
 		/*
 		 * Unlike write_loose_object(), we do not have the entire
@@ -890,16 +889,16 @@ int odb_source_loose_write_stream(struct odb_source *source,
 	 */
 	if (ret != Z_STREAM_END)
 		die(_("unable to stream deflate new object (%d)"), ret);
-	ret = end_loose_object_common(source, &c, &compat_c, &stream, oid, &compat_oid);
+	ret = end_loose_object_common(loose, &c, &compat_c, &stream, oid, &compat_oid);
 	if (ret != Z_OK)
 		die(_("deflateEnd on stream object failed (%d)"), ret);
-	close_loose_object(source, fd, tmp_file.buf);
+	close_loose_object(loose, fd, tmp_file.buf);
 
-	if (odb_freshen_object(source->odb, oid)) {
+	if (odb_freshen_object(loose->base.odb, oid)) {
 		unlink_or_warn(tmp_file.buf);
 		goto cleanup;
 	}
-	odb_loose_path(source, &filename, oid);
+	odb_loose_path(loose, &filename, oid);
 
 	/* We finally know the object path, and create the missing dir. */
 	dirlen = directory_size(filename.buf);
@@ -907,7 +906,7 @@ int odb_source_loose_write_stream(struct odb_source *source,
 		struct strbuf dir = STRBUF_INIT;
 		strbuf_add(&dir, filename.buf, dirlen);
 
-		if (safe_create_dir_in_gitdir(source->odb->repo, dir.buf) &&
+		if (safe_create_dir_in_gitdir(loose->base.odb->repo, dir.buf) &&
 		    errno != EEXIST) {
 			err = error_errno(_("unable to create directory %s"), dir.buf);
 			strbuf_release(&dir);
@@ -916,10 +915,10 @@ int odb_source_loose_write_stream(struct odb_source *source,
 		strbuf_release(&dir);
 	}
 
-	err = finalize_object_file_flags(source->odb->repo, tmp_file.buf, filename.buf,
+	err = finalize_object_file_flags(loose->base.odb->repo, tmp_file.buf, filename.buf,
 					 FOF_SKIP_COLLISION_CHECK);
 	if (!err && compat)
-		err = repo_add_loose_object_map(files->loose, oid, &compat_oid);
+		err = repo_add_loose_object_map(loose, oid, &compat_oid);
 cleanup:
 	strbuf_release(&tmp_file);
 	strbuf_release(&filename);
@@ -957,7 +956,7 @@ int force_object_loose(struct odb_source *source,
 				     oid_to_hex(oid), compat->name);
 	}
 	hdrlen = format_object_header(hdr, sizeof(hdr), type, len);
-	ret = write_loose_object(source, oid, hdr, hdrlen, buf, len, mtime, 0);
+	ret = write_loose_object(files->loose, oid, hdr, hdrlen, buf, len, mtime, 0);
 	if (!ret && compat)
 		ret = repo_add_loose_object_map(files->loose, oid, &compat_oid);
 	free(buf);
diff --git a/object-file.h b/object-file.h
index 2b32592de1135b..d30f1b10b2eb36 100644
--- a/object-file.h
+++ b/object-file.h
@@ -23,7 +23,7 @@ int index_path(struct index_state *istate, struct object_id *oid, const char *pa
 struct object_info;
 struct odb_source;
 
-int odb_source_loose_write_stream(struct odb_source *source,
+int odb_source_loose_write_stream(struct odb_source_loose *loose,
 				  struct odb_write_stream *stream, size_t len,
 				  struct object_id *oid);
 
@@ -31,7 +31,7 @@ int odb_source_loose_write_stream(struct odb_source *source,
  * Put in `buf` the name of the file in the local object database that
  * would be used to store a loose object with the specified oid.
  */
-const char *odb_loose_path(struct odb_source *source,
+const char *odb_loose_path(struct odb_source_loose *source,
 			   struct strbuf *buf,
 			   const struct object_id *oid);
 
@@ -127,7 +127,7 @@ void write_object_file_prepare(const struct git_hash_algo *algo,
 			       const void *buf, unsigned long len,
 			       enum object_type type, struct object_id *oid,
 			       char *hdr, int *hdrlen);
-int write_loose_object(struct odb_source *source,
+int write_loose_object(struct odb_source_loose *loose,
 		       const struct object_id *oid, char *hdr,
 		       int hdrlen, const void *buf, unsigned long len,
 		       time_t mtime, unsigned flags);
diff --git a/odb/source-files.c b/odb/source-files.c
index 52ba04237acfd7..2ba1def776e006 100644
--- a/odb/source-files.c
+++ b/odb/source-files.c
@@ -174,7 +174,8 @@ static int odb_source_files_write_object_stream(struct odb_source *source,
 						size_t len,
 						struct object_id *oid)
 {
-	return odb_source_loose_write_stream(source, stream, len, oid);
+	struct odb_source_files *files = odb_source_files_downcast(source);
+	return odb_source_loose_write_stream(files->loose, stream, len, oid);
 }
 
 static int odb_source_files_begin_transaction(struct odb_source *source,
diff --git a/odb/source-loose.c b/odb/source-loose.c
index c91018109e5b68..da8a60dba1c04c 100644
--- a/odb/source-loose.c
+++ b/odb/source-loose.c
@@ -220,7 +220,7 @@ static int odb_source_loose_read_object_info(struct odb_source *source,
 	if (flags & OBJECT_INFO_SECOND_READ)
 		return -1;
 
-	odb_loose_path(source, &buf, oid);
+	odb_loose_path(loose, &buf, oid);
 	return read_object_info_from_path(loose, buf.buf, oid, oi, flags);
 }
 
@@ -238,7 +238,7 @@ static int open_loose_object(struct odb_source_loose *loose,
 	static struct strbuf buf = STRBUF_INIT;
 	int fd;
 
-	*path = odb_loose_path(&loose->base, &buf, oid);
+	*path = odb_loose_path(loose, &buf, oid);
 	fd = git_open(*path);
 	if (fd >= 0)
 		return fd;
@@ -584,8 +584,9 @@ static int odb_source_loose_count_objects(struct odb_source *source,
 static int odb_source_loose_freshen_object(struct odb_source *source,
 					   const struct object_id *oid)
 {
+	struct odb_source_loose *loose = odb_source_loose_downcast(source);
 	static struct strbuf path = STRBUF_INIT;
-	odb_loose_path(source, &path, oid);
+	odb_loose_path(loose, &path, oid);
 	return !!check_and_freshen_file(path.buf, 1);
 }
 
@@ -624,7 +625,7 @@ static int odb_source_loose_write_object(struct odb_source *source,
 	write_object_file_prepare(algo, buf, len, type, oid, hdr, &hdrlen);
 	if (odb_freshen_object(source->odb, oid))
 		return 0;
-	if (write_loose_object(source, oid, hdr, hdrlen, buf, len, 0, flags))
+	if (write_loose_object(loose, oid, hdr, hdrlen, buf, len, 0, flags))
 		return -1;
 	if (compat)
 		return repo_add_loose_object_map(loose, oid, &compat_oid);

From e6a39bbe7a6bde5fb7de8d487e8f4ef928e6b751 Mon Sep 17 00:00:00 2001
From: Patrick Steinhardt 
Date: Mon, 1 Jun 2026 10:20:39 +0200
Subject: [PATCH 079/106] odb/source-loose: wire up `write_object_stream()`
 callback

Wire up the `write_object_stream()` callback.

Note that we don't move the implementation into "odb/source-loose.c".
This is because most of the logic to write loose objects is still
contained in "object-file.c", and detangling that requires us to do some
refactorings as explained in the preceding commit. So for now, the
implementation of writing an object stream is still located in
"object-file.c".

Signed-off-by: Patrick Steinhardt 
Signed-off-by: Junio C Hamano 
---
 object-file.h      | 12 +++++++++++-
 odb/source-files.c |  3 ++-
 odb/source-loose.c | 14 ++++++++++++++
 3 files changed, 27 insertions(+), 2 deletions(-)

diff --git a/object-file.h b/object-file.h
index d30f1b10b2eb36..528c4e6e697f87 100644
--- a/object-file.h
+++ b/object-file.h
@@ -23,7 +23,17 @@ int index_path(struct index_state *istate, struct object_id *oid, const char *pa
 struct object_info;
 struct odb_source;
 
-int odb_source_loose_write_stream(struct odb_source_loose *loose,
+/*
+ * Write the given stream into the loose object source. The only difference
+ * from the generic implementation of this function is that we don't perform an
+ * object existence check here.
+ *
+ * TODO: We should stop exposing this function altogether and move it into
+ * "odb/source-loose.c". This requires a couple of refactorings though to make
+ * `force_object_loose()` generic and is thus postponed to a later point in
+ * time.
+ */
+int odb_source_loose_write_stream(struct odb_source_loose *source,
 				  struct odb_write_stream *stream, size_t len,
 				  struct object_id *oid);
 
diff --git a/odb/source-files.c b/odb/source-files.c
index 2ba1def776e006..83f8066c67dd3c 100644
--- a/odb/source-files.c
+++ b/odb/source-files.c
@@ -7,6 +7,7 @@
 #include "odb.h"
 #include "odb/source.h"
 #include "odb/source-files.h"
+#include "odb/source-loose.h"
 #include "packfile.h"
 #include "strbuf.h"
 #include "write-or-die.h"
@@ -175,7 +176,7 @@ static int odb_source_files_write_object_stream(struct odb_source *source,
 						struct object_id *oid)
 {
 	struct odb_source_files *files = odb_source_files_downcast(source);
-	return odb_source_loose_write_stream(files->loose, stream, len, oid);
+	return odb_source_write_object_stream(&files->loose->base, stream, len, oid);
 }
 
 static int odb_source_files_begin_transaction(struct odb_source *source,
diff --git a/odb/source-loose.c b/odb/source-loose.c
index da8a60dba1c04c..e52fc289a24102 100644
--- a/odb/source-loose.c
+++ b/odb/source-loose.c
@@ -632,6 +632,19 @@ static int odb_source_loose_write_object(struct odb_source *source,
 	return 0;
 }
 
+static int odb_source_loose_write_object_stream(struct odb_source *source,
+						struct odb_write_stream *in_stream,
+						size_t len,
+						struct object_id *oid)
+{
+	/*
+	 * TODO: the implementation should be moved here, see the comment on
+	 * the called function in "object-file.h".
+	 */
+	struct odb_source_loose *loose = odb_source_loose_downcast(source);
+	return odb_source_loose_write_stream(loose, in_stream, len, oid);
+}
+
 static void odb_source_loose_clear_cache(struct odb_source_loose *loose)
 {
 	oidtree_clear(loose->cache);
@@ -692,6 +705,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)
 	loose->base.count_objects = odb_source_loose_count_objects;
 	loose->base.freshen_object = odb_source_loose_freshen_object;
 	loose->base.write_object = odb_source_loose_write_object;
+	loose->base.write_object_stream = odb_source_loose_write_object_stream;
 
 	if (!is_absolute_path(loose->base.path))
 		chdir_notify_register(NULL, odb_source_loose_reparent, loose);

From 87af3bb434b86805f69fae40c966d92db1bd2eae Mon Sep 17 00:00:00 2001
From: Patrick Steinhardt 
Date: Mon, 1 Jun 2026 10:20:40 +0200
Subject: [PATCH 080/106] odb/source-loose: stub out remaining callbacks

Stub out remaining callback functions for the "loose" backend.

Note that we also stub out transactions for loose objects. In fact, we
already have the infrastructure in place for those, and we could in
theory implement those, as well. But there are separate efforts ongoing
to polish up transactional interfaces, and doing so now would likely
result in some messiness. This omission will thus be worked on in a
subsequent patch series, once the dust has settled.

Signed-off-by: Patrick Steinhardt 
Signed-off-by: Junio C Hamano 
---
 odb/source-loose.c | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/odb/source-loose.c b/odb/source-loose.c
index e52fc289a24102..e1749413184160 100644
--- a/odb/source-loose.c
+++ b/odb/source-loose.c
@@ -645,6 +645,25 @@ static int odb_source_loose_write_object_stream(struct odb_source *source,
 	return odb_source_loose_write_stream(loose, in_stream, len, oid);
 }
 
+static int odb_source_loose_begin_transaction(struct odb_source *source UNUSED,
+					      struct odb_transaction **out UNUSED)
+{
+	/* TODO: this is a known omission that we'll want to address eventually. */
+	return error("loose source does not support transactions");
+}
+
+static int odb_source_loose_read_alternates(struct odb_source *source UNUSED,
+					    struct strvec *out UNUSED)
+{
+	return 0;
+}
+
+static int odb_source_loose_write_alternate(struct odb_source *source UNUSED,
+					    const char *alternate UNUSED)
+{
+	return error("loose source does not support alternates");
+}
+
 static void odb_source_loose_clear_cache(struct odb_source_loose *loose)
 {
 	oidtree_clear(loose->cache);
@@ -706,6 +725,9 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)
 	loose->base.freshen_object = odb_source_loose_freshen_object;
 	loose->base.write_object = odb_source_loose_write_object;
 	loose->base.write_object_stream = odb_source_loose_write_object_stream;
+	loose->base.begin_transaction = odb_source_loose_begin_transaction;
+	loose->base.read_alternates = odb_source_loose_read_alternates;
+	loose->base.write_alternate = odb_source_loose_write_alternate;
 
 	if (!is_absolute_path(loose->base.path))
 		chdir_notify_register(NULL, odb_source_loose_reparent, loose);

From ef4778bcba323ab38d442811f851af092760b6b5 Mon Sep 17 00:00:00 2001
From: Patrick Steinhardt 
Date: Mon, 1 Jun 2026 10:20:41 +0200
Subject: [PATCH 081/106] odb/source-loose: drop pointer to the "files" source

Now that all callbacks of the loose source operate on `struct
odb_source_loose` directly we no longer have to reach into the "files"
source at all.

Drop this field and update `odb_source_loose_new()` to instead accept
all parameters required to initialize itself. This ensures that the
"loose" backend is a fully standalone source.

Signed-off-by: Patrick Steinhardt 
Signed-off-by: Junio C Hamano 
---
 odb/source-files.c | 2 +-
 odb/source-loose.c | 8 ++++----
 odb/source-loose.h | 7 ++++---
 3 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/odb/source-files.c b/odb/source-files.c
index 83f8066c67dd3c..5bdd0429225397 100644
--- a/odb/source-files.c
+++ b/odb/source-files.c
@@ -268,7 +268,7 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb,
 
 	CALLOC_ARRAY(files, 1);
 	odb_source_init(&files->base, odb, ODB_SOURCE_FILES, path, local);
-	files->loose = odb_source_loose_new(files);
+	files->loose = odb_source_loose_new(odb, path, local);
 	files->packed = packfile_store_new(&files->base);
 
 	files->base.free = odb_source_files_free;
diff --git a/odb/source-loose.c b/odb/source-loose.c
index e1749413184160..7d7ea2fb842537 100644
--- a/odb/source-loose.c
+++ b/odb/source-loose.c
@@ -705,14 +705,14 @@ static void odb_source_loose_free(struct odb_source *source)
 	free(loose);
 }
 
-struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)
+struct odb_source_loose *odb_source_loose_new(struct object_database *odb,
+					      const char *path,
+					      bool local)
 {
 	struct odb_source_loose *loose;
 
 	CALLOC_ARRAY(loose, 1);
-	odb_source_init(&loose->base, files->base.odb, ODB_SOURCE_LOOSE,
-			files->base.path, files->base.local);
-	loose->files = files;
+	odb_source_init(&loose->base, odb, ODB_SOURCE_LOOSE, path, local);
 
 	loose->base.free = odb_source_loose_free;
 	loose->base.close = odb_source_loose_close;
diff --git a/odb/source-loose.h b/odb/source-loose.h
index 4dd4fd6ce30a7e..6070aaf3ce6ab2 100644
--- a/odb/source-loose.h
+++ b/odb/source-loose.h
@@ -9,11 +9,10 @@ struct oidtree;
 
 /*
  * An object database source that stores its objects in loose format, one
- * file per object. This source is part of the files source.
+ * file per object.
  */
 struct odb_source_loose {
 	struct odb_source base;
-	struct odb_source_files *files;
 
 	/*
 	 * Used to store the results of readdir(3) calls when we are OK
@@ -31,7 +30,9 @@ struct odb_source_loose {
 	struct loose_object_map *map;
 };
 
-struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files);
+struct odb_source_loose *odb_source_loose_new(struct object_database *odb,
+					      const char *path,
+					      bool local);
 
 /*
  * Cast the given object database source to the loose backend. This will cause

From 96ee7f1650e6096561599f069d18c052412d7506 Mon Sep 17 00:00:00 2001
From: LorenzoPegorari 
Date: Mon, 1 Jun 2026 15:52:01 +0200
Subject: [PATCH 082/106] http: cleanup function fetch_and_setup_pack_index()

Cleanup the function `fetch_and_setup_pack_index()` by removing the
useless call to the function `unlink()`.

This is not necessary anymore since 63aca3f7f1 (dumb-http: store
downloaded pack idx as tempfile, 2024-10-25), when `fetch_pack_index()`
started registering its return value (in this case `tmp_idx`) as a
tempfile to be deleted at process exit.

Signed-off-by: LorenzoPegorari 
Signed-off-by: Junio C Hamano 
---
 http.c | 2 --
 1 file changed, 2 deletions(-)

diff --git a/http.c b/http.c
index ea9b16861bc3d4..55dd856a279a23 100644
--- a/http.c
+++ b/http.c
@@ -2609,9 +2609,7 @@ static int fetch_and_setup_pack_index(struct packfile_list *packs,
 
 	new_pack = parse_pack_index(the_repository, sha1, tmp_idx);
 	if (!new_pack) {
-		unlink(tmp_idx);
 		free(tmp_idx);
-
 		return -1; /* parse_pack_index() already issued error message */
 	}
 

From 18decad922884a69ea39c0332f7a94ce82cf99cc Mon Sep 17 00:00:00 2001
From: LorenzoPegorari 
Date: Mon, 1 Jun 2026 15:52:12 +0200
Subject: [PATCH 083/106] http: fix memory leak in fetch_and_setup_pack_index()

Inside the function `fetch_and_setup_pack_index()`, when the pack
obtained using `parse_pack_index()` fails to be verified by
`verify_pack_index()`, the function returns without closing and freeing
said pack.

Fix this by calling `close_pack_index()` to munmap the index file for
the leaking pack (which might have been mmapped by `fetch_pack_index()`
or `verify_pack_index()`), and then free it, when the verification
fails.

Signed-off-by: LorenzoPegorari 
Signed-off-by: Junio C Hamano 
---
 http.c | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/http.c b/http.c
index 55dd856a279a23..d50a34e4460a90 100644
--- a/http.c
+++ b/http.c
@@ -2614,11 +2614,13 @@ static int fetch_and_setup_pack_index(struct packfile_list *packs,
 	}
 
 	ret = verify_pack_index(new_pack);
-	if (!ret)
-		close_pack_index(new_pack);
+
+	close_pack_index(new_pack);
 	free(tmp_idx);
-	if (ret)
+	if (ret) {
+		free(new_pack);
 		return -1;
+	}
 
 	packfile_list_prepend(packs, new_pack);
 	return 0;

From 1891707d1b8bb0ac3c47343e881fcf28ec69457a Mon Sep 17 00:00:00 2001
From: Jacob Keller 
Date: Mon, 1 Jun 2026 16:36:08 -0700
Subject: [PATCH 084/106] describe: fix --exclude, --match with --contains and
 --all

git describe --contains acts as a wrapper around git name-rev. When
operating with --contains and --all, the --match and --exclude patterns
are not properly forwarded to name-rev as --exclude and --refs options.

This results in the command silently discarding match and exclude
requests from the user when operating in --all mode.

We could check and die() if the user provides --contains, --all, and
--match/--exclude. However, its also straight forward to just pass the
filters down to git name-rev.

Notice that the documentation for --match and --exclude mention the
--all mode. It explains that they operate on refs with the prefix
refs/tags, and additionally refs/heads and refs/remotes when using
--all.

Fix the describe logic to pass the patterns down with the appropriate
prefixes when --all is provided. This fixes the support to match the
documented behavior.

Add tests to check that this works as expected.

Reported-by: Tuomas Ahola 
Signed-off-by: Jacob Keller 
Signed-off-by: Junio C Hamano 
---
 builtin/describe.c  | 18 +++++++++++++++---
 t/t6120-describe.sh | 22 ++++++++++++++++++++++
 2 files changed, 37 insertions(+), 3 deletions(-)

diff --git a/builtin/describe.c b/builtin/describe.c
index bffeed13a3cb14..62800ef15ed915 100644
--- a/builtin/describe.c
+++ b/builtin/describe.c
@@ -712,13 +712,25 @@ int cmd_describe(int argc,
 			     NULL);
 		if (always)
 			strvec_push(&args, "--always");
-		if (!all) {
+		if (!all)
 			strvec_push(&args, "--tags");
+
+		for_each_string_list_item(item, &patterns)
+			strvec_pushf(&args, "--refs=refs/tags/%s", item->string);
+		for_each_string_list_item(item, &exclude_patterns)
+			strvec_pushf(&args, "--exclude=refs/tags/%s", item->string);
+
+		if (all) {
 			for_each_string_list_item(item, &patterns)
-				strvec_pushf(&args, "--refs=refs/tags/%s", item->string);
+				strvec_pushf(&args, "--refs=refs/heads/%s", item->string);
 			for_each_string_list_item(item, &exclude_patterns)
-				strvec_pushf(&args, "--exclude=refs/tags/%s", item->string);
+				strvec_pushf(&args, "--exclude=refs/heads/%s", item->string);
+			for_each_string_list_item(item, &patterns)
+				strvec_pushf(&args, "--refs=refs/remotes/%s", item->string);
+			for_each_string_list_item(item, &exclude_patterns)
+				strvec_pushf(&args, "--exclude=refs/remotes/%s", item->string);
 		}
+
 		if (argc)
 			strvec_pushv(&args, argv);
 		else
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 2c70cc561ad5f6..e5bcf537602a21 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -345,6 +345,28 @@ test_expect_success 'describe --contains and --no-match' '
 	test_cmp expect actual
 '
 
+test_expect_success 'describe --contains --all --match no matching commit' '
+	echo "tags/A^0" >expect &&
+	tagged_commit=$(git rev-parse "refs/tags/A^0") &&
+	test_must_fail git describe --contains --all --match="B" $tagged_commit
+'
+
+check_describe "tags/A^0" --contains --all --match="A" $(git rev-parse "refs/tags/A^0")
+
+check_describe "branch_A" --contains --all --match="branch*" $(git rev-parse "refs/tags/A^0")
+
+check_describe "branch_C~1" --contains --all --match="branch*" --exclude="branch_A" $(git rev-parse "refs/tags/A^0")
+
+check_describe "branch_A" --contains --all \
+	--exclude="A" --exclude="c" --exclude="test*" --exclude="origin/remote_branch_A" \
+	$(git rev-parse "refs/tags/A^0")
+
+check_describe "remotes/origin/remote_branch_A" --contains --all --match="origin/remote*" $(git rev-parse "refs/tags/A^0")
+
+check_describe "remotes/origin/remote_branch_C~1" --contains --all \
+	--match="origin/remote*" --exclude="origin/remote_branch_A" \
+	$(git rev-parse "refs/tags/A^0")
+
 test_expect_success 'setup and absorb a submodule' '
 	test_create_repo sub1 &&
 	test_commit -C sub1 initial &&

From 5cd4d0d8500c6ef1b102f5cb35187a91c299f013 Mon Sep 17 00:00:00 2001
From: Harald Nordgren 
Date: Tue, 2 Jun 2026 07:37:58 +0000
Subject: [PATCH 085/106] config.mak.uname: avoid macOS linker warning on Xcode
 16.3+

Building on macOS with Xcode 16.3 or newer emits:

    ld: warning: reducing alignment of section __DATA,__common
    from 0x8000 to 0x4000 because it exceeds segment maximum
    alignment

Pass -fno-common when "ld -v" reports ld-1167 or newer, so tentative
definitions of large arrays go into BSS instead of __DATA,__common.

Signed-off-by: Harald Nordgren 
Signed-off-by: Junio C Hamano 
---
 config.mak.uname | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/config.mak.uname b/config.mak.uname
index 3c35ae33a3c0c0..32b58e7a95091e 100644
--- a/config.mak.uname
+++ b/config.mak.uname
@@ -160,6 +160,12 @@ ifeq ($(uname_S),Darwin)
 		NEEDS_GOOD_LIBICONV = UnfortunatelyYes
         endif
 
+	# Silence Xcode 16.3+ linker warning about __DATA,__common alignment.
+	LD_MAJOR_VERSION = $(shell ld -v 2>&1 | sed -n 's/.*PROJECT:ld-\([0-9]*\).*/\1/p')
+        ifeq ($(shell test -n "$(LD_MAJOR_VERSION)" && test "$(LD_MAJOR_VERSION)" -ge 1167 && echo 1),1)
+		BASIC_CFLAGS += -fno-common
+        endif
+
 	# The builtin FSMonitor on MacOS builds upon Simple-IPC.  Both require
 	# Unix domain sockets and PThreads.
         ifndef NO_PTHREADS

From 4018dc29eea31e4273c0f1b02effe6ee852f3898 Mon Sep 17 00:00:00 2001
From: Luna Schwalbe 
Date: Tue, 2 Jun 2026 10:17:36 +0200
Subject: [PATCH 086/106] doc: document and test `@` prefix for raw timestamps

The Git internal date format ` `
fails to parse when the timestamp is less than 100,000,000 (fewer than
9 digits). This happens to avoid potential ambiguity with other date
formats such as `YYYYMMDD`, especially when used with approxidate.

To force the parser to interpret the value as a raw timestamp, it must
be prefixed with `@` (e.g., `@0 +0000`). This behavior was introduced
in 2c733fb24c10a9d7aacc51f956bf9b7881980870 (parse_date(): '@' prefix
forces git-timestamp, 2012-02-02) but was never documented.

Document the `@` prefix in `Documentation/date-formats.adoc` to make
this behavior explicit. Also add test cases to `t/t0006-date.sh` to
verify and demonstrate the difference between prefixed and unprefixed
small timestamps (e.g., `@2000` vs `2000`).

Signed-off-by: Luna Schwalbe 
Co-authored-by: Junio C Hamano 
Signed-off-by: Junio C Hamano 
---
 Documentation/date-formats.adoc |  5 +++++
 t/t0006-date.sh                 | 11 +++++++++++
 2 files changed, 16 insertions(+)

diff --git a/Documentation/date-formats.adoc b/Documentation/date-formats.adoc
index e24517c496fce4..330424b2baccda 100644
--- a/Documentation/date-formats.adoc
+++ b/Documentation/date-formats.adoc
@@ -9,6 +9,11 @@ Git internal format::
 	`` is the number of seconds since the UNIX epoch.
 	`` is a positive or negative offset from UTC.
 	For example CET (which is 1 hour ahead of UTC) is `+0100`.
++
+It is safer to prepend the `` with `@` (e.g.,
+`@0 +0000`), which forces Git to interpret it as a raw timestamp. This
+is required for values less than 100,000,000 (which have fewer than 9
+digits) to avoid confusion with other date formats like `YYYYMMDD`.
 
 RFC 2822::
 	The standard date format as described by RFC 2822, for example
diff --git a/t/t0006-date.sh b/t/t0006-date.sh
index 53ced36df448f1..8b4e1870bf118d 100755
--- a/t/t0006-date.sh
+++ b/t/t0006-date.sh
@@ -138,6 +138,13 @@ check_parse '1969-12-31 23:59:59 Z' bad
 check_parse '1969-12-31 23:59:59 +11' bad
 check_parse '1969-12-31 23:59:59 -11' bad
 
+# pathologically small timestamps requiring `@` prefix
+check_parse '@0 +0000' '1970-01-01 00:00:00 +0000'
+check_parse '@99999999 +0000' '1973-03-03 09:46:39 +0000'
+check_parse '99999999 +0000' bad
+check_parse '@100000000 +0000' '1973-03-03 09:46:40 +0000'
+check_parse '100000000 +0000' '1973-03-03 09:46:40 +0000'
+
 REQUIRE_64BIT_TIME=HAVE_64BIT_TIME
 check_parse '2099-12-31 23:59:59' '2099-12-31 23:59:59 +0000'
 check_parse '2099-12-31 23:59:59 +00' '2099-12-31 23:59:59 +0000'
@@ -195,6 +202,10 @@ check_approxidate '6AM, June 7, 2009' '2009-06-07 06:00:00'
 check_approxidate '2008-12-01' '2008-12-01 19:20:00'
 check_approxidate '2009-12-01' '2009-12-01 19:20:00'
 
+# ambiguous raw timestamp
+check_approxidate '2000 +0000' '2000-08-30 19:20:00'
+check_approxidate '@2000 +0000' '1970-01-01 00:33:20'
+
 check_date_format_human() {
 	t=$(($GIT_TEST_DATE_NOW - $1))
 	echo "$t -> $2" >expect

From b8b38eee85ba9c7af95f5ad6eca6b36d18140866 Mon Sep 17 00:00:00 2001
From: Andrew Kreimer 
Date: Sun, 31 May 2026 21:43:58 +0300
Subject: [PATCH 087/106] doc: fix typos via codespell

There are some typos in the documentation, comments, etc.
Fix them via codespell.

Signed-off-by: Andrew Kreimer 
---
 Documentation/SubmittingPatches            |  2 +-
 Documentation/git-sparse-checkout.adoc     |  2 +-
 Documentation/technical/build-systems.adoc |  6 +++---
 builtin/pack-objects.c                     |  2 +-
 commit-graph.h                             |  2 +-
 compat/precompose_utf8.c                   |  2 +-
 hook.h                                     |  2 +-
 meson_options.txt                          |  2 +-
 midx-write.c                               |  2 +-
 odb/source.h                               |  2 +-
 packfile.h                                 |  2 +-
 path.h                                     |  2 +-
 reftable/system.h                          |  2 +-
 t/README                                   |  2 +-
 t/chainlint.pl                             |  2 +-
 t/chainlint/chain-break-false.expect       |  2 +-
 t/chainlint/chain-break-false.test         |  2 +-
 t/t1700-split-index.sh                     |  2 +-
 t/t3909-stash-pathspec-file.sh             |  6 +++---
 t/t4052-stat-output.sh                     |  2 +-
 t/t4067-diff-partial-clone.sh              |  2 +-
 t/t9150/svk-merge.dump                     | 10 +++++-----
 t/t9151/svn-mergeinfo.dump                 | 18 +++++++++---------
 t/unit-tests/clar/README.md                |  2 +-
 24 files changed, 40 insertions(+), 40 deletions(-)

diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches
index d570184ec84998..35b4952c8a8d27 100644
--- a/Documentation/SubmittingPatches
+++ b/Documentation/SubmittingPatches
@@ -92,7 +92,7 @@ input and avoids unnecessary churn from many rapid iterations.
   topic are appropriate, so such an incremental updates are limited to
   small corrections and polishing.  After a topic cooks for some time
   (like 7 calendar days) in 'next' without needing further tweaks on
-  top, it gets merged to the 'master' branch and wait to become part
+  top, it gets merged to the 'master' branch and waits to become part
   of the next major release.
 
 In the following sections, many techniques and conventions are listed
diff --git a/Documentation/git-sparse-checkout.adoc b/Documentation/git-sparse-checkout.adoc
index 0d1618f161ed63..e286584c67f98f 100644
--- a/Documentation/git-sparse-checkout.adoc
+++ b/Documentation/git-sparse-checkout.adoc
@@ -134,7 +134,7 @@ the `clean.requireForce` config option is set to `false`.
 +
 The `--dry-run` option will list the directories that would be removed
 without deleting them. Running in this mode can be helpful to predict the
-behavior of the clean comand or to determine which kinds of files are left
+behavior of the clean command or to determine which kinds of files are left
 in the sparse directories.
 +
 The `--verbose` option will list every file within the directories that
diff --git a/Documentation/technical/build-systems.adoc b/Documentation/technical/build-systems.adoc
index 3c5237b9fd4727..ca5b5d96f149ba 100644
--- a/Documentation/technical/build-systems.adoc
+++ b/Documentation/technical/build-systems.adoc
@@ -47,7 +47,7 @@ Auto-detection of the following items is considered to be important:
 
   - Check for the existence of headers.
   - Check for the existence of libraries.
-  - Check for the existence of exectuables.
+  - Check for the existence of executables.
   - Check for the runtime behavior of specific functions.
   - Check for specific link order requirements when multiple libraries are
     involved.
@@ -106,7 +106,7 @@ by the build system:
 
   - C: the primary compiled language used by Git, must be supported. Relevant
     toolchains are GCC, Clang and MSVC.
-  - Rust: candidate as a second compiled lanugage, should be supported. Relevant
+  - Rust: candidate as a second compiled language, should be supported. Relevant
     toolchains is the LLVM-based rustc.
 
 Built-in support for the respective languages is preferred over support that
@@ -142,7 +142,7 @@ The following list of build systems are considered:
 
 === GNU Make
 
-- Platform support: ubitquitous on all platforms, but not well-integrated into Windows.
+- Platform support: ubiquitous on all platforms, but not well-integrated into Windows.
 - Auto-detection: no built-in support for auto-detection of features.
 - Ease of use: easy to use, but discovering available options is hard. Makefile
   rules can quickly get out of hand once reaching a certain scope.
diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c
index dd2480a73d2edf..806068907e79b9 100644
--- a/builtin/pack-objects.c
+++ b/builtin/pack-objects.c
@@ -1341,7 +1341,7 @@ static void write_pack_file(void)
 			 * length of them as buffer length.
 			 *
 			 * Note that we need to subtract one though to
-			 * accomodate for the sideband byte.
+			 * accommodate for the sideband byte.
 			 */
 			struct hashfd_options opts = {
 				.progress = progress_state,
diff --git a/commit-graph.h b/commit-graph.h
index f6a54336415453..13ca4ff010fa18 100644
--- a/commit-graph.h
+++ b/commit-graph.h
@@ -18,7 +18,7 @@
  * This method is only used to enhance coverage of the commit-graph
  * feature in the test suite with the GIT_TEST_COMMIT_GRAPH and
  * GIT_TEST_COMMIT_GRAPH_CHANGED_PATHS environment variables. Do not
- * call this method oustide of a builtin, and only if you know what
+ * call this method outside of a builtin, and only if you know what
  * you are doing!
  */
 void git_test_write_commit_graph_or_die(struct odb_source *source);
diff --git a/compat/precompose_utf8.c b/compat/precompose_utf8.c
index 43b3be011439ef..6e709bd1384cab 100644
--- a/compat/precompose_utf8.c
+++ b/compat/precompose_utf8.c
@@ -85,7 +85,7 @@ const char *precompose_string_if_needed(const char *in)
 		out = reencode_string_iconv(in, inlen, ic_prec, 0, &outlen);
 		if (out) {
 			if (outlen == inlen && !memcmp(in, out, outlen))
-				free(out); /* no need to return indentical */
+				free(out); /* no need to return identical */
 			else
 				in = out;
 		}
diff --git a/hook.h b/hook.h
index 5c5628dd1f822c..5f0c3f19bb9cc8 100644
--- a/hook.h
+++ b/hook.h
@@ -116,7 +116,7 @@ struct run_hooks_opt {
 	 * While the callback allows piecemeal writing, it can also be
 	 * used for smaller inputs, where it gets called only once.
 	 *
-	 * Add hook callback initalization context to `feed_pipe_ctx`.
+	 * Add hook callback initialization context to `feed_pipe_ctx`.
 	 * Add hook callback internal state to `feed_pipe_cb_data`.
 	 *
 	 */
diff --git a/meson_options.txt b/meson_options.txt
index 659cbb218f46e0..1ed228d42ad7cd 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -106,7 +106,7 @@ option('highlight_bin', type: 'string', value: 'highlight')
 
 # Documentation.
 option('docs', type: 'array', choices: ['man', 'html'], value: [],
-  description: 'Which documenattion formats to build and install.')
+  description: 'Which documentation formats to build and install.')
 option('default_help_format', type: 'combo', choices: ['man', 'html', 'platform'], value: 'platform',
   description: 'Default format used when executing git-help(1).')
 option('docs_backend', type: 'combo', choices: ['asciidoc', 'asciidoctor', 'auto'], value: 'auto',
diff --git a/midx-write.c b/midx-write.c
index a25cab75abad11..5a9756043650a9 100644
--- a/midx-write.c
+++ b/midx-write.c
@@ -1438,7 +1438,7 @@ static int write_midx_internal(struct write_midx_opts *opts)
 
 		/*
 		 * Attempt opening the pack index to populate num_objects.
-		 * Ignore failiures as they can be expected and are not
+		 * Ignore failures as they can be expected and are not
 		 * fatal during this selection time.
 		 */
 		open_pack_index(oldest);
diff --git a/odb/source.h b/odb/source.h
index f706e0608a4855..4958a503cfe2b9 100644
--- a/odb/source.h
+++ b/odb/source.h
@@ -338,7 +338,7 @@ static inline int odb_source_read_object_stream(struct odb_read_stream **out,
  * are only iterated over once.
  *
  * The optional `request` structure serves as a template for retrieving the
- * object info for each indvidual iterated object and will be populated as if
+ * object info for each individual iterated object and will be populated as if
  * `odb_source_read_object_info()` was called on the object. It will not be
  * modified, the callback will instead be invoked with a separate `struct
  * object_info` for every object. Object info will not be read when passing a
diff --git a/packfile.h b/packfile.h
index 9b647da7dda7c1..6dea707ba42af1 100644
--- a/packfile.h
+++ b/packfile.h
@@ -124,7 +124,7 @@ struct packfile_store {
 	 * that packs that contain a lot of accessed objects will be located
 	 * towards the front.
 	 *
-	 * This is usually desireable, but there are exceptions. One exception
+	 * This is usually desirable, but there are exceptions. One exception
 	 * is when the looking up multiple objects in a loop for each packfile.
 	 * In that case, we may easily end up with an infinite loop as the
 	 * packfiles get reordered to the front repeatedly.
diff --git a/path.h b/path.h
index 0434ba5e07e806..4c2958a9037179 100644
--- a/path.h
+++ b/path.h
@@ -217,7 +217,7 @@ void safe_create_dir(struct repository *repo, const char *dir, int share);
  *
  *   - It always adjusts shared permissions.
  *
- * Returns a negative erorr code on error, 0 on success.
+ * Returns a negative error code on error, 0 on success.
  */
 int safe_create_dir_in_gitdir(struct repository *repo, const char *path);
 
diff --git a/reftable/system.h b/reftable/system.h
index c0e2cbe0ffb90c..628232a46f31f6 100644
--- a/reftable/system.h
+++ b/reftable/system.h
@@ -84,7 +84,7 @@ struct reftable_flock {
  * to acquire the lock. If `timeout_ms` is 0 we don't wait, if it is negative
  * we block indefinitely.
  *
- * Retrun 0 on success, a reftable error code on error. Specifically,
+ * Return 0 on success, a reftable error code on error. Specifically,
  * `REFTABLE_LOCK_ERROR` should be returned in case the target path is already
  * locked.
  */
diff --git a/t/README b/t/README
index adbbd9acf4ab27..085921be4b6c2a 100644
--- a/t/README
+++ b/t/README
@@ -972,7 +972,7 @@ see test-lib-functions.sh for the full list and their options.
  - test_lazy_prereq