* Re: [PATCH RFC 0/2] Move libgit.a sources into separate "lib/" directory
From: Patrick Steinhardt @ 2026-04-22 6:39 UTC (permalink / raw)
To: Derrick Stolee; +Cc: git
In-Reply-To: <4199e58b-d0fc-4240-9717-16c89ae73322@gmail.com>
On Tue, Apr 21, 2026 at 10:13:31AM -0400, Derrick Stolee wrote:
> On 4/21/2026 1:55 AM, Patrick Steinhardt wrote:
> > On Mon, Apr 20, 2026 at 08:03:44AM -0400, Derrick Stolee wrote:
>
> Thanks for your thoughts on the earlier parts of my message. I think
> you're moving in the right direction and I don't have further comments.
Thanks a lot for your feedback!
Patrick
^ permalink raw reply
* Re: [PATCH] generate-configlist: collapse depfile for older Ninja
From: Patrick Steinhardt @ 2026-04-22 6:36 UTC (permalink / raw)
To: Toon Claes; +Cc: git, D. Ben Knoble
In-Reply-To: <20260421-toon-fix-almalinux8-v1-1-aec1d54addde@iotcl.com>
On Tue, Apr 21, 2026 at 09:17:25PM +0200, Toon Claes wrote:
> For the Makefile build system this works great. And since
> ebeea3c471 (build: regenerate config-list.h when Documentation changes,
> 2026-02-24) this script is also called from the Meson build system.
> Nevertheless, on AlmaLinux 8 the following build failure is seen:
>
> ninja: error: dependency cycle: config-list.h -> config-list.h
>
> This version of this distro uses Ninja 1.8.2 and it seems to have some
> issues with the format of the `config-list.h.d` file.
Right, I can reproduce the issue with that version indeed:
$ meson setup -Ddocs=man build
$ ninja -C build
... successful build ...
$ ninja -C build
ninja: error: dependency cycle: config-list.h -> config-list.h
> Ninja versions before 1.10.0 do not reset the depfile parser state on
> newlines. This causes issues when the depfile has one dependency per
> line, like we have in `config-list.h.d`:
>
> config-list.h: Documentation/config.adoc
> config-list.h: Documentation/config/add.adoc
>
> The parser only recognizes the first "config-list.h:" as a target. On
> subsequent lines it is still in dependency-parsing mode, so the repeated
> output name is recorded as an input. This causes the error mentioned
> above.
>
> The bug in Ninja is fixed in 1.10, with commit
> ninja-build/ninja@1daa7470ab7e (depfile_parser: remove restriction on
> multiple outputs, 2019-11-20).
Good find.
> To be compatible with older versions of Ninja, add a fourth optional
> argument to `generate-configlist.sh` that can be empty or "collapse".
> When this argument is "collapse", there is a post-processing step on
> `config-list.h.d` to put the dependencies for `config-list.h` on a
> single line, like:
>
> config-list.h: Documentation/config.adoc Documentation/config/add.adoc ...
>
> This works around the bug in older versions of Ninja.
Do we really have to make the logic conditional? I would expect that
this works just fine for newer versions of Ninja and for Make, so it
feels rather pointless to me to have two modes to worry about.
So wouldn't the below simplified version be sufficient?
Thanks!
Patrick
diff --git a/tools/generate-configlist.sh b/tools/generate-configlist.sh
index e28054f9e0..f5f42492c6 100755
--- a/tools/generate-configlist.sh
+++ b/tools/generate-configlist.sh
@@ -44,7 +44,9 @@ then
{
printf '%s\n' "$SOURCE_DIR"/Documentation/*config.adoc \
"$SOURCE_DIR"/Documentation/config/*.adoc |
- sed -e 's/[# ]/\\&/g' -e "s/^/$QUOTED_OUTPUT: /"
+ sed -e 's/[# ]/\\&/g' |
+ tr '\n' ' ' |
+ sed -e "s/^/$QUOTED_OUTPUT: /" -e 's/ $/\n/'
printf '%s:\n' "$SOURCE_DIR"/Documentation/*config.adoc \
"$SOURCE_DIR"/Documentation/config/*.adoc |
sed -e 's/[# ]/\\&/g'
^ permalink raw reply related
* Re: [BUG] git-credential-libsecret writes secret to stdout on store
From: Mantas Mikulėnas @ 2026-04-22 5:49 UTC (permalink / raw)
To: Lutz-Christian Quander; +Cc: git
In-Reply-To: <60cf5f7c-9ccb-4dfe-82e4-9b6e54b3c2c0@wateringcan.de>
(Re-adding list to recipients.)
On 21/04/2026 14.47, Lutz-Christian Quander wrote:
> Thanks — fair correction. I tested `git credential approve` with the
> libsecret helper and it does not leak: git discards the helper's
> stdout on `store`, so the documented user-facing interface is safe.
> The leak only manifests when the helper binary is invoked directly.
>
> That narrows the argument, but I'd still submit that the fix is
> worth landing for two reasons:
>
> 1. gitcredentials(7) specifies that helpers should use stderr (not
> stdout) for messages on `store`/`erase`, and that helper stdout
> is ignored on those operations. The current unconditional
> `credential_write()` violates that contract regardless of how the
> helper is invoked -- it just happens to be harmless when git is
> the caller because git discards the stream.
The API contract isn't violated IMO, as documenting "output is ignored"
gives permission to produce output, even if that output is unnecessary.
(that is, from my reading, it implies that output *will* go to /dev/null
– and running the helper manually is what really violates the API
contract from the other side by not ignoring the helper's output...)
I agree that the unconditional credential_write() is a bit weird upon a
closer look – it and the entire main() was just copied as-is from the
older "gnomekeyring" helper, under assumption that that was "the
expected way" the credential_*() functions were to be used.
>
> 2. The direct-invocation pattern shows up widely in distro docs,
> StackOverflow answers, and automation scripts -- empirically the
> "internal protocol" boundary is porous. Fixing the helper is one
> line; documenting the internal boundary across the ecosystem is
> not.
>
> If the preferred answer is instead "users should only use
> `git credential approve`", that would also work for me, but it may
> deserve a note in gitcredentials(7) to steer people away from the
> direct pattern -- the current docs don't actively discourage it.
I think it should be fixed to remove the useless output, especially if
the command is as widely documented as you say (although I'm actually
surprised that any CI environments even have a libsecret backend running
*in the first place*; I would have assumed that they would use
git-credential-cache or something instead). You should send a patch.
At the same time, I also think it's a bit too much 'self-inflicted' of a
security issue to be CVE-worthy, so to speak... I mean, I don't like
automatically blaming the user for holding it wrong, but in this
particular case, I'd like to think that one would test the command on
their own machine first and see how it behaves before putting it in a
script.
>
> Happy with whichever direction you prefer.
>
> Best regards
>
> Lutz-Christian Quander
>
> p.s.
>
> Thank you for your very quick reponse and your Open Source work
>
>
> Am 21.04.26 um 13:37 schrieb Mantas Mikulėnas:
>> On 21/04/2026 14.03, Lutz-Christian Quander wrote:
>>> The documented pattern for seeding credentials non-interactively is:
>>>
>>> printf "protocol=...\nhost=...\nusername=...\npassword=...\n\n"
>>> | git-credential-<helper> store
>>>
>>> Running this at a terminal prints the secret into scrollback.
>>> Running it in a shell script whose stdout goes to a log file
>>> persists the secret in that log. Running it in CI captures the
>>> secret in the pipeline artefact. Every real-world use of the
>>> documented pattern is affected.
>>>
>>> Severity is moderate: the leak requires the user to run a legitimate
>>> command -- no attacker-controlled input path -- but the leak happens
>>> on the "correct" documented workflow, silently, with exit code 0.
>>
>> Is it actually the correct documented workflow? I couldn't find it in
>> the Git docs. My understanding was that writing to "git credential
>> approve" was the sole user interface, while "git-credential-<helper>
>> store" was the internal interface between the git-credential builtin
>> and the helper.
>>
^ permalink raw reply
* Re: [PATCH v2 0/9] pack-bitmap: fix various pseudo-merge bugs
From: Elijah Newren @ 2026-04-22 1:37 UTC (permalink / raw)
To: Taylor Blau; +Cc: git, Junio C Hamano, Jeff King
In-Reply-To: <cover.1776801694.git.me@ttaylorr.com>
On Tue, Apr 21, 2026 at 1:01 PM Taylor Blau <me@ttaylorr.com> wrote:
>
> [Note to the maintainer: this series has been rebased onto the current
> tip of master, which is 94f057755b7 (Git 2.54, 2026-04-19) at the time
> of writing.]
>
> This is a small reroll of my series to fix several bugs in the
> pseudo-merge bitmap implementation. The main changes since last time
> are:
>
> - Fixed a use-after-realloc bug in the test helper introduced in the
> first commit.
>
> - Swapped the order of initializing and cleaning up repositories in the
> new test scripts.
>
> - Updated bitmapPseudoMerge.<name>.sampleRate's documentation to
> describe the range as (0,1], and added a new commit fixing a broken
> example in gitpacking(7).
Thanks for fixing these.
> Range-diff against v1:
[...]
> 4: af9f651269d ! 4: 07f70a07c20 pack-bitmap: fix inverted binary search in `pseudo_merge_at()`
> @@ t/t5333-pseudo-merge-bitmaps.sh: test_expect_success 'apply pseudo-merges during
>
> -test_expect_failure 'apply pseudo-merges from multiple groups during fill-in' '
> +test_expect_success 'apply pseudo-merges from multiple groups during fill-in' '
> - git init pseudo-merge-fill-in-multi &&
> test_when_finished "rm -fr pseudo-merge-fill-in-multi" &&
> + git init pseudo-merge-fill-in-multi &&
Here you fixed the order, but...
> ++ git init pseudo-merge-fill-in-multi &&
...then you immediately run git init a second time? I'm guessing this
was a stray edit made while trying to fix the order; could we get rid
of the duplicate?
> (
> + cd pseudo-merge-fill-in-multi &&
> +
Looks like you addressed all the feedback so far from v1. There does
appear to be a new accidental double-init that I noted above in patch
4, but I didn't spot any other issues.
^ permalink raw reply
* Re: [PATCH 2/2] status: improve rebase todo list parsing
From: Elijah Newren @ 2026-04-22 0:32 UTC (permalink / raw)
To: Phillip Wood; +Cc: git, Phillip Wood
In-Reply-To: <d20dc1f6550078883995ae963b91faaa00984c6e.1776697483.git.phillip.wood@dunelm.org.uk>
On Mon, Apr 20, 2026 at 8:25 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
>
> From: Phillip Wood <phillip.wood@dunelm.org.uk>
>
> When there is rebase in progress "git status" displays the last couple
> of completed and the next couple of pending commands from the todo
> list. When it does this is tries to abbreviate the object ids of
is tries => it tries ?
[...]
> @@ -1363,6 +1363,51 @@ static int split_commit_in_progress(struct wt_status *s)
> free(rebase_orig_head);
>
> return split_in_progress;
> +}
> +
> +static void abbrev_oid_in_line(struct repository *r,
> + struct strbuf *line, char **pp)
> +{
> + char *p = *pp;
> + char *end_of_object_name, saved;
> + const char *abbrev;
> + struct object_id oid;
> + bool have_oid;
I'll put "thinking out loud" text in square brackets below...
> +
> + p += strspn(p, " \t");
> + end_of_object_name = p + strcspn(p, " \t");
[Advances p after whitespace, marks the end of the object with the
next whitespace after that.]
> + /*
> + * The for "merge" and "reset" the object name may be a label or
The for => For ?
> + * ref rather than a hex object id. Only abbreviate the object
> + * name if it is a hex object id.
> + */
> + for (const char *q = p; q < end_of_object_name; q++) {
> + if (!isxdigit(*q))
> + goto out;
> + }
> + saved = *end_of_object_name;
> + *end_of_object_name = '\0';
> + have_oid = !repo_get_oid(r, p, &oid);
> + *end_of_object_name = saved;
[Tries to resolve the token, doing NUL-termination and restore dance.]
> + if (!have_oid)
> + goto out; /* object name was a label */
> + abbrev = repo_find_unique_abbrev(r, &oid, DEFAULT_ABBREV);
> + if (!starts_with(p, abbrev))
> + goto out; /* object name was a refname containing only xdigits */
[Ensures what we have is an oid rather than a branch name that can be
resolved to an oid]
> + p += strlen(abbrev);
> + strbuf_remove(line, p - line->buf, end_of_object_name - p);
> + end_of_object_name = p;
[Splice out a bunch of characters in the middle?]
> +out:
> + *pp = end_of_object_name;
> +}
I had a hard time following the logic in the function and trying to
figure out what it was doing. I went line by line but had no mental
model to follow. When I got to the comment that is now above
format_todo_line(), I suddenly understood, but without it, all the
code was hard to follow. Maybe a small comment at the beginning of
the function along the lines of
/*
* If the whitespace-delimited token starting at or just after *pp is a
* full hex object id that resolves uniquely, rewrite it in place to
* its default abbreviation, shrinking `line` accordingly. On return
* *pp points one past the (possibly abbreviated) token. Leaves both
* `line` and *pp-advanced-past-the-token unchanged in all other cases
* (non-hex token, unresolvable, or a refname that happens to consist
* only of hex digits).
*/
? (Assuming I'm understanding correctly, of course.)
> +
> +static void skip_dash_c(char **pp) {
Move the brace to the next line?
> + char *p = *pp;
> +
> + p += strspn(p, " \t");
> + /* The (void) cast is required to silence -Wunused_value */
-Wunused_value => -Wunused-value ?
> + (void)(skip_prefix(p, "-C", &p) || skip_prefix(p, "-c", &p));
> + *pp = p;
> }
>
> /*
> @@ -1371,29 +1416,57 @@ static int split_commit_in_progress(struct wt_status *s)
> * into
> * "pick d6a2f03 some message"
> *
> - * The function assumes that the line does not contain useless spaces
> - * before or after the command.
> + * Returns false on comment lines, true otherwise
> */
> -static void abbrev_oid_in_line(struct repository *r, struct strbuf *line)
> +static bool format_todo_line(struct repository *r, struct strbuf *line)
> {
> - struct string_list split = STRING_LIST_INIT_DUP;
> - struct object_id oid;
> -
> - if (starts_with(line->buf, "exec ") ||
> - starts_with(line->buf, "x ") ||
> - starts_with(line->buf, "label ") ||
> - starts_with(line->buf, "l "))
> - return;
> -
> - if ((2 <= string_list_split(&split, line->buf, " ", 2)) &&
> - !repo_get_oid(r, split.items[1].string, &oid)) {
> - strbuf_reset(line);
> - strbuf_addf(line, "%s ", split.items[0].string);
> - strbuf_add_unique_abbrev(line, &oid, DEFAULT_ABBREV);
> - for (size_t i = 2; i < split.nr; i++)
> - strbuf_addf(line, " %s", split.items[i].string);
> + enum todo_command cmd;
> + char *p = line->buf;
> +
> + if (!sequencer_parse_todo_command((const char**)&p, &cmd))
> + return true; /* keep invalid lines */
> +
> + switch (cmd) {
> + case TODO_COMMENT:
> + return false;
> +
> + case TODO_MERGE:
> + skip_dash_c(&p);
> + while (true) {
> + p += strspn(p, " \t");
> + if (!p[0] || (p[0] == '#' && (!p[1] || isspace(p[1]))))
> + break;
> + abbrev_oid_in_line(r, line, &p);
> + }
> + break;
> +
> + case TODO_FIXUP:
> + skip_dash_c(&p);
> + /* fallthrough */
> + case TODO_DROP:
> + case TODO_EDIT:
> + case TODO_PICK:
> + case TODO_RESET:
> + case TODO_REVERT:
> + case TODO_REWORD:
> + case TODO_SQUASH:
> + abbrev_oid_in_line(r, line, &p);
> + break;
> +
> + /*
> + * Avoid "default" and instead list all the other commands so
> + * that -Wswitch warns if a new command is added without handling
> + * it in this function.
> + */
Nice. :-)
> + case TODO_BREAK:
> + case TODO_EXEC:
> + case TODO_LABEL:
> + case TODO_NOOP:
> + case TODO_UPDATE_REF:
> + break;
> }
> - string_list_clear(&split, 0);
> +
> + return true;
> }
>
> static int read_rebase_todolist(struct repository *r, const char *fname, struct string_list *lines)
> @@ -1411,13 +1484,9 @@ static int read_rebase_todolist(struct repository *r, const char *fname, struct
> repo_git_path_replace(r, &buf, "%s", fname));
> }
> while (!strbuf_getline_lf(&buf, f)) {
> - if (starts_with(buf.buf, comment_line_str))
> - continue;
> strbuf_trim(&buf);
> - if (!buf.len)
> - continue;
> - abbrev_oid_in_line(r, &buf);
> - string_list_append(lines, buf.buf);
> + if (format_todo_line(r, &buf))
> + string_list_append(lines, buf.buf);
> }
> fclose(f);
>
> --
> 2.54.0.rc1.174.gd833f386ac5.dirty
Other than the minor comments above, this looks like a nice cleanup.
^ permalink raw reply
* Re: [PATCH 1/2] sequencer: factor out parsing of todo commands
From: Elijah Newren @ 2026-04-22 0:32 UTC (permalink / raw)
To: Phillip Wood; +Cc: git, Phillip Wood
In-Reply-To: <3d5135a719221031e50ad8067ff42740a3bbce0c.1776697483.git.phillip.wood@dunelm.org.uk>
On Mon, Apr 20, 2026 at 8:41 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
>
> From: Phillip Wood <phillip.wood@dunelm.org.uk>
>
> Move the code that parses todo commands into a separate function so that
> it can be shared with "git status" in the next commit.
>
> Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>
> ---
> sequencer.c | 45 ++++++++++++++++++++++++++++++---------------
> sequencer.h | 1 +
> 2 files changed, 31 insertions(+), 15 deletions(-)
>
> diff --git a/sequencer.c b/sequencer.c
> index b7d8dca47f..b8e860434a 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -2625,6 +2625,27 @@ static int is_command(enum todo_command command, const char **bol)
> return 1;
> }
> return 0;
> +}
> +
> +bool sequencer_parse_todo_command(const char **p, enum todo_command *cmd)
> +{
> + const char *s = *p;
> +
> + for (int i = 0; i < TODO_COMMENT; i++)
> + if (is_command(i, p)) {
> + *cmd = i;
> + return true;
> + }
> +
> + if (starts_with(s, comment_line_str)) {
> + *cmd = TODO_COMMENT;
> + return true;
> + } else if (s[0] == '\n' || (s[0] == '\r' && s[1] == '\n') || !s[0]) {
> + *cmd = TODO_COMMENT;
> + return true;
> + }
> +
> + return false;
> }
Since *p will be advanced by this function, perhaps it's worth a quick
comment before the function explaining how it advances?
>
> static int check_label_or_ref_arg(enum todo_command command, const char *arg)
> @@ -2716,29 +2737,23 @@ static int parse_insn_line(struct repository *r, struct replay_opts *opts,
> {
> struct object_id commit_oid;
> char *end_of_object_name;
> - int i, saved, status, padding;
> + int saved, status, padding;
>
> item->flags = 0;
>
> /* left-trim */
> bol += strspn(bol, " \t");
>
> - if (bol == eol || *bol == '\r' || starts_with_mem(bol, eol - bol, comment_line_str)) {
> - item->command = TODO_COMMENT;
> - item->commit = NULL;
> - item->arg_offset = bol - buf;
> - item->arg_len = eol - bol;
> - return 0;
> - }
> -
> - for (i = 0; i < TODO_COMMENT; i++)
> - if (is_command(i, &bol)) {
> - item->command = i;
> - break;
> - }
> - if (i >= TODO_COMMENT)
> + if (!sequencer_parse_todo_command(&bol, &item->command))
> return error(_("invalid command '%.*s'"),
> (int)strcspn(bol, " \t\r\n"), bol);
> +
> + if (item->command == TODO_COMMENT) {
> + item->commit = NULL;
> + item->arg_offset = bol - buf;
> + item->arg_len = eol - bol;
> + return 0;
> + }
The commit message says it's just moving code, but are there some
subtle changes? It switches the order of parsing commands and
comments, which I think doesn't matter. I think the previous code
would treat a bare carriage return not immediately followed by a
newline as the start of a comment, whereas the new code only handles
carriage return immediately followed by a newline as the start of a
comment. I don't think that matters in practice, and was potentially
buggy before, but feels like the commit message isn't quite telling
the whole story. Maybe it's worth calling out the minor but harmless
tweaks in the commit message?
> /* Eat up extra spaces/ tabs before object name */
> padding = strspn(bol, " \t");
> diff --git a/sequencer.h b/sequencer.h
> index a6fa670c7c..20f6fac48a 100644
> --- a/sequencer.h
> +++ b/sequencer.h
> @@ -262,6 +262,7 @@ int read_author_script(const char *path, char **name, char **email, char **date,
> int write_basic_state(struct replay_opts *opts, const char *head_name,
> struct commit *onto, const struct object_id *orig_head);
> void sequencer_post_commit_cleanup(struct repository *r, int verbose);
> +bool sequencer_parse_todo_command(const char **p, enum todo_command *cmd);
> int sequencer_get_last_command(struct repository* r,
> enum replay_action *action);
> int sequencer_determine_whence(struct repository *r, enum commit_whence *whence);
> --
> 2.54.0.rc1.174.gd833f386ac5.dirty
Otherwise, looks good.
^ permalink raw reply
* Re: [PATCH v2 0/2] revision.c: implement --reverse=before for walks
From: Mirko Faina @ 2026-04-22 0:30 UTC (permalink / raw)
To: git
Cc: Junio C Hamano, Jeff King, Jean-Noël Avila,
Patrick Steinhardt, Tian Yuchen, Ben Knoble
In-Reply-To: <20260422002840.303477-4-mroik@delayed.space>
Sorry, I forgot to thread it
^ permalink raw reply
* [PATCH v2 1/2] revision.c: implement --reverse=before for walks
From: Mirko Faina @ 2026-04-22 0:28 UTC (permalink / raw)
To: git
Cc: Mirko Faina, Junio C Hamano, Jeff King, Jean-Noël Avila,
Patrick Steinhardt, Tian Yuchen, Ben Knoble
In-Reply-To: <20260418164736.2367523-2-mroik@delayed.space>
In a revision walk `--reverse` can only be applied after any commit
limiting option. This makes getting a limited amount of commits from the
tail impossible. E.g.
git log --reverse --max-count=3
Some would expect this to give back the first 3 commits of the project.
Instead it returns the last 3 but in reversed order.
Teach `get_revision()` to accpet an argument `(after|before)` from the
CLI, and apply the reversal before or after the commit limiting options
based on this argument. If no argument is provided default to the
current behaviour, applying `--reverse` after the commit limiting
options.
Signed-off-by: Mirko Faina <mroik@delayed.space>
---
Documentation/rev-list-options.adoc | 14 ++++--
revision.c | 31 ++++++++++++--
revision.h | 8 +++-
t/t4202-log.sh | 66 +++++++++++++++++++++++++++++
4 files changed, 111 insertions(+), 8 deletions(-)
diff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc
index 2d195a1474..7244e85108 100644
--- a/Documentation/rev-list-options.adoc
+++ b/Documentation/rev-list-options.adoc
@@ -914,10 +914,16 @@ With `--topo-order`, they would show 8 6 5 3 7 4 2 1 (or 8 7 4 2 6 5
avoid showing the commits from two parallel development track mixed
together.
-`--reverse`::
- Output the commits chosen to be shown (see 'Commit Limiting'
- section above) in reverse order. Cannot be combined with
- `--walk-reflogs`.
+`--[no-]reverse[=(after|before)]`::
+ Accepts `after` or `before`. Cannot be combined with
+ `--walk-reflogs`. If `after`, output the commits chosen to be
+ shown (see 'Commit Limiting' section above) in reverse order. If
+ `before`, reverse the commits before filtering with `Commit
+ Limiting` options. This option can be used multiple times, last
+ one is applied. When the argument for `--reverse` is omitted, if
+ the current state is in no reverse, it defaults to `after`. If
+ it is in any reversed state, it restores the original ordering
+ by removing the reverse state.
endif::git-shortlog[]
ifndef::git-shortlog[]
diff --git a/revision.c b/revision.c
index 599b3a66c3..d581f5e38e 100644
--- a/revision.c
+++ b/revision.c
@@ -2686,7 +2686,16 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
git_log_output_encoding = xstrdup("");
return argcount;
} else if (!strcmp(arg, "--reverse")) {
- revs->reverse ^= 1;
+ revs->reverse = !revs->reverse;
+ } else if (skip_prefix(arg, "--reverse=", &optarg)) {
+ if (!strcmp(optarg, "after"))
+ revs->reverse = REVERSE_AFTER;
+ else if(!strcmp(optarg, "before"))
+ revs->reverse = REVERSE_BEFORE;
+ else
+ die(_("unknown value for --reverse: %s"), optarg);
+ } else if (!strcmp(arg, "--no-reverse")) {
+ revs->reverse = NO_REVERSE;
} else if (!strcmp(arg, "--children")) {
revs->children.name = "children";
revs->limited = 1;
@@ -4525,19 +4534,35 @@ struct commit *get_revision(struct rev_info *revs)
{
struct commit *c;
struct commit_list *reversed;
+ int max_count = revs->max_count;
+
+ if (revs->reverse && !revs->reverse_output_stage) {
+ if (revs->reverse == 3) {
+ BUG("allowed values for reverse are 0, 1 and 2");
+ revs->reverse = 1;
+ }
+
+ if (revs->reverse == REVERSE_BEFORE)
+ revs->max_count = -1;
- if (revs->reverse) {
reversed = NULL;
while ((c = get_revision_internal(revs)))
commit_list_insert(c, &reversed);
commit_list_free(revs->commits);
revs->commits = reversed;
- revs->reverse = 0;
revs->reverse_output_stage = 1;
+
+ if (revs->reverse == REVERSE_BEFORE)
+ revs->max_count = max_count;
}
if (revs->reverse_output_stage) {
+ if (revs->reverse == REVERSE_BEFORE && revs->max_count == 0)
+ return NULL;
+
c = pop_commit(&revs->commits);
+ if (revs->reverse == REVERSE_BEFORE)
+ revs->max_count--;
if (revs->track_linear)
revs->linear = !!(c && c->object.flags & TRACK_LINEAR);
return c;
diff --git a/revision.h b/revision.h
index 584f1338b5..02881577dc 100644
--- a/revision.h
+++ b/revision.h
@@ -121,6 +121,12 @@ struct ref_exclusions {
struct oidset;
struct topo_walk_info;
+enum rev_reverse {
+ NO_REVERSE = 0,
+ REVERSE_AFTER = 1,
+ REVERSE_BEFORE = 2,
+};
+
struct rev_info {
/* Starting list */
struct commit_list *commits;
@@ -167,6 +173,7 @@ struct rev_info {
ignore_missing_links:1;
/* Traversal flags */
+ enum rev_reverse reverse:2;
unsigned int dense:1,
prune:1,
no_walk:1,
@@ -196,7 +203,6 @@ struct rev_info {
rewrite_parents:1,
print_parents:1,
show_decorations:1,
- reverse:1,
reverse_output_stage:1,
cherry_pick:1,
cherry_mark:1,
diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index 05cee9e41b..3bfe2c99b8 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -1882,6 +1882,72 @@ test_expect_success 'log --graph with --name-status' '
test_cmp_graph --name-status tangle..reach
'
+cat >expect <<-\EOF
+c3f451c Merge tag 'reach'
+046b221 to remove
+EOF
+
+test_expect_success 'log --reverse --oneline --max-count=2' '
+ test_when_finished git reset --hard HEAD~1 &&
+ touch to_remove &&
+ git add to_remove &&
+ git commit -m "to remove" &&
+ git log --reverse --oneline --max-count=2 >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'log --reverse --reverse --reverse --oneline --max-count=2' '
+ test_when_finished git reset --hard HEAD~1 &&
+ touch to_remove &&
+ git add to_remove &&
+ git commit -m "to remove" &&
+ git log --reverse --reverse --reverse --oneline --max-count=2 >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'log --reverse=after --oneline --max-count=2' '
+ test_when_finished git reset --hard HEAD~1 &&
+ touch to_remove &&
+ git add to_remove &&
+ git commit -m "to remove" &&
+ git log --reverse=after --oneline --max-count=2 >actual &&
+ test_cmp expect actual
+'
+
+cat >expect <<-\EOF
+3a2fdcb initial
+f7dab8e second
+EOF
+
+test_expect_success 'log --reverse=before --oneline --max-count=2' '
+ test_when_finished rm actual &&
+ git log --reverse=before --oneline --max-count=2 >actual &&
+ test_cmp expect actual
+'
+
+cat >expect <<-\EOF
+046b221 to remove
+c3f451c Merge tag 'reach'
+EOF
+
+test_expect_success 'log --reverse --reverse --oneline --max-count=2' '
+ test_when_finished git reset --hard HEAD~1 &&
+ touch to_remove &&
+ git add to_remove &&
+ git commit -m "to remove" &&
+ git log --reverse --reverse --oneline --max-count=2 >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'log --reverse --no-reverse --oneline --max-count=2' '
+ test_when_finished git reset --hard HEAD~1 &&
+ touch to_remove &&
+ git add to_remove &&
+ git commit -m "to remove" &&
+ git log --reverse --no-reverse --oneline --max-count=2 >actual &&
+ test_cmp expect actual
+'
+
cat >expect <<-\EOF
* reach
|
--
2.54.0
^ permalink raw reply related
* [PATCH v2 2/2] revision.c: reduce memory usage on reverse before
From: Mirko Faina @ 2026-04-22 0:28 UTC (permalink / raw)
To: git
Cc: Mirko Faina, Junio C Hamano, Jeff King, Jean-Noël Avila,
Patrick Steinhardt, Tian Yuchen, Ben Knoble
In-Reply-To: <20260418164736.2367523-2-mroik@delayed.space>
Due to the nature of --reverse=before we have to walk all of the history
and store each non-filtered processed commit, this can be expensive on
memory for projects with a long history. When --max-count is being used
we don't really have to keep every processed commit, we can discard
older commits (as in have been processed before than the ones we're now
considering, from a chronological commit order they are the newer
commits) as we surpass the --max-count limit.
Teach get_revision() to keep only the newer commits as we walk a
revision with --reverse=before and --max-count=<k>. We do this through a
simple queue. With N nodes and K as the --max-count argument, assuming K
< N, we go from a space complexity of O(N) to O(K). When it comes down
to time complexity, the queue has an ammortized time of O(1) for pops,
so the complexity remains O(N).
Signed-off-by: Mirko Faina <mroik@delayed.space>
---
revision.c | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 52 insertions(+), 2 deletions(-)
diff --git a/revision.c b/revision.c
index d581f5e38e..4730f21ea6 100644
--- a/revision.c
+++ b/revision.c
@@ -4530,6 +4530,52 @@ static struct commit *get_revision_internal(struct rev_info *revs)
return c;
}
+static void retrieve_with_window(struct rev_info *revs, int max_count,
+ struct commit_list **reversed)
+{
+ struct commit *c;
+ struct commit_list *into_queue = NULL;
+ struct commit_list *outo_queue = NULL;
+ int into_count = 0;
+ int outo_count = 0;
+
+ while ((c = get_revision_internal(revs))) {
+ commit_list_insert(c, &into_queue);
+ into_count++;
+ if (into_count + outo_count > max_count) {
+ if (!outo_count) {
+ while (into_count) {
+ c = pop_commit(&into_queue);
+ into_count--;
+ commit_list_insert(c, &outo_queue);
+ outo_count++;
+ }
+ }
+ pop_commit(&outo_queue);
+ outo_count--;
+ }
+ }
+
+ while (outo_count) {
+ c = pop_commit(&outo_queue);
+ outo_count--;
+ commit_list_insert(c, reversed);
+ }
+
+ while (into_count) {
+ c = pop_commit(&into_queue);
+ into_count--;
+ commit_list_insert(c, &outo_queue);
+ outo_count++;
+ }
+
+ while (outo_count) {
+ c = pop_commit(&outo_queue);
+ outo_count--;
+ commit_list_insert(c, reversed);
+ }
+}
+
struct commit *get_revision(struct rev_info *revs)
{
struct commit *c;
@@ -4546,8 +4592,12 @@ struct commit *get_revision(struct rev_info *revs)
revs->max_count = -1;
reversed = NULL;
- while ((c = get_revision_internal(revs)))
- commit_list_insert(c, &reversed);
+ if (revs->reverse == REVERSE_BEFORE && max_count != -1) {
+ retrieve_with_window(revs, max_count, &reversed);
+ } else {
+ while ((c = get_revision_internal(revs)))
+ commit_list_insert(c, &reversed);
+ }
commit_list_free(revs->commits);
revs->commits = reversed;
revs->reverse_output_stage = 1;
--
2.54.0
^ permalink raw reply related
* [PATCH v2 0/2] revision.c: implement --reverse=before for walks
From: Mirko Faina @ 2026-04-22 0:28 UTC (permalink / raw)
To: git
Cc: Mirko Faina, Junio C Hamano, Jeff King, Jean-Noël Avila,
Patrick Steinhardt, Tian Yuchen, Ben Knoble
In-Reply-To: <20260418164736.2367523-2-mroik@delayed.space>
Since v1 I've:
* removed the non stuck form for the --reverse option
* --reverse with no argument now flips "no reverse -> reverse
after", "reverse after -> no reverse" and "reverse before ->
no reverse"
* implemented a window to reduce memory usage when
--reverse=before with --max-count=<n>
* updated the docs to highlight the peculiarities of --reverse
when it's specified multiple times
[1/2] revision.c: implement --reverse=before for walks (Mirko Faina)
[2/2] revision.c: reduce memory usage on reverse before (Mirko Faina)
Documentation/rev-list-options.adoc | 14 +++--
revision.c | 85 +++++++++++++++++++++++++++--
revision.h | 8 ++-
t/t4202-log.sh | 66 ++++++++++++++++++++++
4 files changed, 163 insertions(+), 10 deletions(-)
base-commit: e8955061076952cc5eab0300424fc48b601fe12d
--
2.54.0
^ permalink raw reply
* Re: [BUG] v2.45+: git commit -S invalidates signature for non-UTF-8 messages
From: brian m. carlson @ 2026-04-21 22:13 UTC (permalink / raw)
To: Kushal Das; +Cc: git
In-Reply-To: <c0df6dbd-47f9-4a2d-b68d-cb0c1e19ca5a@sunet.se>
[-- Attachment #1: Type: text/plain, Size: 382 bytes --]
On 2026-04-21 at 07:39:11, Kushal Das wrote:
> I am also wondering in the test harness for git signing, if you want to
> include other tools than gnupg for testing.
The signing code is abstract at that point in the code, so it should
work identically with SSH or X.509 and I don't think a separate test is
necessary.
--
brian m. carlson (they/them)
Toronto, Ontario, CA
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 325 bytes --]
^ permalink raw reply
* Re: [PATCH v2 2/3] builtin/log: prefetch necessary blobs for `git cherry`
From: Elijah Newren @ 2026-04-21 21:28 UTC (permalink / raw)
To: phillip.wood; +Cc: Elijah Newren via GitGitGadget, git
In-Reply-To: <a010a4ad-403a-4b6f-9a92-a33323eca0f2@gmail.com>
Hi Phillip,
On Sun, Apr 19, 2026 at 7:04 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
>
> Hi Elijah
>
> On 18/04/2026 01:32, Elijah Newren via GitGitGadget wrote:
> > From: Elijah Newren <newren@gmail.com>
> >
> > In partial clones, `git cherry` fetches necessary blobs on-demand one
> > at a time, which can be very slow. We would like to prefetch all
> > necessary blobs upfront. To do so, we need to be able to first figure
> > out which blobs are needed.
>
> "git rebase" without "--reapply-cherry-picks" suffers from this problem
> as well as it does the equivalent of "git log --cherry-pick". Is there
> any way to share prefetch_cherry_blobs() with the cherry-pick detection
> in revision.c?
Yes, you're right; git rebase without --reapply-cherry-picks and git
log --cherry-pick both go through cherry_pick_list() in revision.c,
which has exactly the same shape as the patch-ids loop in
cmd_cherry(): build a hashmap of one side via add_commit_patch_id(),
then look up the other side via patch_id_iter_first(). The on-demand
blob fetches come from the same patch_id_neq() callback.
After poking around, I think the approximate scope of the fix would
be: Move collect_diff_blob_oids(), always_match(), and
prefetch_cherry_blobs() from builtin/log.c to patch-ids.c and expose
the last one in patch-ids.h. In cherry_pick_list(), between the
add_commit_patch_id loop and the comparison loop, build a temporary
list of just the lookup-side commits (filtering by
SYMMETRIC_LEFT/BOUNDARY as the existing loop already does) and call
prefetch_cherry_blobs() on it.
That said, I'd rather leave this out of the current series. The bigger
picture is that I have reservations about expanding partial-clone
support further into this area. git cherry, git log --cherry-pick, and
the default cherry-pick detection in git rebase all exist to answer
"has this patch already landed upstream?" -- a question that, in
repositories large enough to need partial clones, I feel is rarely
worth the cost of computing patch-ids across arbitrary amounts of
history. The honest guidance I would probably give for users on a
large repo is "pass --reapply-cherry-picks (with rebase) and skip this
entirely" or to narrow the range under consideration. The omission of
a --no-reapply-cherry-picks option in git-replay wasn't a lack of
effort or oversight, but a deliberate choice where I'd rather hold off
(possibly indefinitely) on implementing it. So I'm a bit reluctant to
make the performance hazard less visible without also asking whether
we should even be doing that piece of the operation.
I only implemented the git cherry fix because of a specific customer
situation where the operation was already baked into tooling, and
prefetching at least makes the worst case tolerable. I don't want to
hold myself to doing the same for the cherry_pick_list() path, but I'm
fairly confident the code here can be re-used for those other cases
and I'd help review a patch from anyone who wants to carry it forward.
Anyway, you are making the right connection, it's just that my
personal answer is to let some other interested individual do it.
^ permalink raw reply
* Re: [PATCH v2 15/16] repack: introduce `--write-midx=incremental`
From: Taylor Blau @ 2026-04-21 21:02 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <6119f15d3e8619f75c7dfb2e5dcf67e9f5fafc41.1776803827.git.me@ttaylorr.com>
On Tue, Apr 21, 2026 at 04:37:57PM -0400, Taylor Blau wrote:
> ---
> Documentation/config/repack.adoc | 18 ++
> Documentation/git-repack.adoc | 39 ++-
> builtin/repack.c | 49 ++-
> midx.c | 31 ++
> midx.h | 3 +
> repack-geometry.c | 13 +-
> repack-midx.c | 5 +
> repack.c | 56 +++-
> repack.h | 10 +-
> t/meson.build | 1 +
> t/t7705-repack-incremental-midx.sh | 500 +++++++++++++++++++++++++++++
> 11 files changed, 701 insertions(+), 24 deletions(-)
> create mode 100755 t/t7705-repack-incremental-midx.sh
I squashed a new test into this commit, but meant to squash it into the
latter. That test depends on functionality that is not yet introduced
until the subsequent commit, hence t7705 fails on this commit.
Since the maintainer is offline currently, I'll avoid re-submitting for
now since this won't get queued anyways. Once there is some review, I'll
re-submit the series with the test squashed into the right commit,
alongside any changes from that review.
Sorry about that.
Thanks,
Taylor
^ permalink raw reply
* [PATCH v2 16/16] repack: allow `--write-midx=incremental` without `--geometric`
From: Taylor Blau @ 2026-04-21 20:38 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1776803827.git.me@ttaylorr.com>
Previously, `--write-midx=incremental` required `--geometric` and would
die() without it. Relax this restriction so that incremental MIDX
repacking can be used independently.
Without `--geometric`, the behavior is append-only: a single new MIDX
layer is created containing whatever packs were written by the repack
and appended to the existing chain (or a new chain is started). Existing
layers are preserved as-is with no compaction or merging.
Implement this via a new repack_make_midx_append_plan() that builds a
plan consisting of a WRITE step for the freshly written packs followed
by COPY steps for every existing MIDX layer. The existing compaction
plan (repack_make_midx_compaction_plan) is used only when `--geometric`
is active.
Update the documentation to describe the behavior with and without
`--geometric`, and replace the test that enforced the old restriction
with one exercising append-only incremental MIDX repacking.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
Documentation/git-repack.adoc | 19 +++++----
builtin/repack.c | 3 --
repack-midx.c | 64 ++++++++++++++++++++++++++++--
t/t7705-repack-incremental-midx.sh | 35 +++++++++++++---
4 files changed, 103 insertions(+), 18 deletions(-)
diff --git a/Documentation/git-repack.adoc b/Documentation/git-repack.adoc
index 27a99cc46f4..72c42015e23 100644
--- a/Documentation/git-repack.adoc
+++ b/Documentation/git-repack.adoc
@@ -263,14 +263,19 @@ linkgit:git-multi-pack-index[1]).
`incremental`;;
Write an incremental MIDX chain instead of a single
- flat MIDX. This mode requires `--geometric`.
+ flat MIDX.
+
-The incremental mode maintains a chain of MIDX layers that is compacted
-over time using a geometric merging strategy. Each repack creates a new
-tip layer containing the newly written pack(s). Adjacent layers are then
-merged whenever the newer layer's object count exceeds
-`1/repack.midxSplitFactor` of the next deeper layer's count. Layers
-that do not meet this condition are retained as-is.
+Without `--geometric`, a new MIDX layer is appended to the existing
+chain (or a new chain is started) containing whatever packs were written
+by the repack. Existing layers are preserved as-is.
++
+When combined with `--geometric`, the incremental mode maintains a chain
+of MIDX layers that is compacted over time using a geometric merging
+strategy. Each repack creates a new tip layer containing the newly
+written pack(s). Adjacent layers are then merged whenever the newer
+layer's object count exceeds `1/repack.midxSplitFactor` of the next
+deeper layer's count. Layers that do not meet this condition are
+retained as-is.
+
The result is that newer (tip) layers tend to contain many small packs
with relatively few objects, while older (deeper) layers contain fewer,
diff --git a/builtin/repack.c b/builtin/repack.c
index 5ffa18e085e..1524a9c13ad 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -263,9 +263,6 @@ int cmd_repack(int argc,
if (pack_everything & PACK_CRUFT)
pack_everything |= ALL_INTO_ONE;
- if (write_midx == REPACK_WRITE_MIDX_INCREMENTAL && !geometry.split_factor)
- die(_("--write-midx=incremental requires --geometric"));
-
if (write_bitmaps < 0) {
if (write_midx == REPACK_WRITE_MIDX_NONE &&
(!(pack_everything & ALL_INTO_ONE) || !is_bare_repository()))
diff --git a/repack-midx.c b/repack-midx.c
index 6a0358096ec..b1c429b8cee 100644
--- a/repack-midx.c
+++ b/repack-midx.c
@@ -555,6 +555,60 @@ static void midx_compaction_step_release(struct midx_compaction_step *step)
free(step->csum);
}
+/*
+ * Build an append-only MIDX plan: a single WRITE step for the freshly
+ * written packs, plus COPY steps for every existing layer. No
+ * compaction or merging is performed.
+ */
+static void repack_make_midx_append_plan(struct repack_write_midx_opts *opts,
+ struct midx_compaction_step **steps_p,
+ size_t *steps_nr_p)
+{
+ struct multi_pack_index *m;
+ struct midx_compaction_step *steps = NULL;
+ struct midx_compaction_step *step;
+ size_t steps_nr = 0, steps_alloc = 0;
+
+ odb_reprepare(opts->existing->repo->objects);
+ m = get_multi_pack_index(opts->existing->source);
+
+ if (opts->names->nr) {
+ struct strbuf buf = STRBUF_INIT;
+ uint32_t i;
+
+ ALLOC_GROW(steps, st_add(steps_nr, 1), steps_alloc);
+
+ step = &steps[steps_nr++];
+ memset(step, 0, sizeof(*step));
+
+ step->type = MIDX_COMPACTION_STEP_WRITE;
+ string_list_init_dup(&step->u.write);
+
+ for (i = 0; i < opts->names->nr; i++) {
+ strbuf_reset(&buf);
+ strbuf_addf(&buf, "pack-%s.idx",
+ opts->names->items[i].string);
+ string_list_append(&step->u.write, buf.buf);
+ }
+
+ strbuf_release(&buf);
+ }
+
+ for (; m; m = m->base_midx) {
+ ALLOC_GROW(steps, st_add(steps_nr, 1), steps_alloc);
+
+ step = &steps[steps_nr++];
+ memset(step, 0, sizeof(*step));
+
+ step->type = MIDX_COMPACTION_STEP_COPY;
+ step->u.copy = m;
+ step->objects_nr = m->num_objects;
+ }
+
+ *steps_p = steps;
+ *steps_nr_p = steps_nr;
+}
+
static int repack_make_midx_compaction_plan(struct repack_write_midx_opts *opts,
struct midx_compaction_step **steps_p,
size_t *steps_nr_p)
@@ -911,9 +965,13 @@ static int write_midx_incremental(struct repack_write_midx_opts *opts)
goto done;
}
- if (repack_make_midx_compaction_plan(opts, &steps, &steps_nr) < 0) {
- ret = error(_("unable to generate compaction plan"));
- goto done;
+ if (opts->geometry->split_factor) {
+ if (repack_make_midx_compaction_plan(opts, &steps, &steps_nr) < 0) {
+ ret = error(_("unable to generate compaction plan"));
+ goto done;
+ }
+ } else {
+ repack_make_midx_append_plan(opts, &steps, &steps_nr);
}
for (i = 0; i < steps_nr; i++) {
diff --git a/t/t7705-repack-incremental-midx.sh b/t/t7705-repack-incremental-midx.sh
index af1a1483b8c..25a8c40e8ee 100755
--- a/t/t7705-repack-incremental-midx.sh
+++ b/t/t7705-repack-incremental-midx.sh
@@ -63,10 +63,36 @@ create_layers () {
done
}
-test_expect_success '--write-midx=incremental requires --geometric' '
- test_must_fail git repack --write-midx=incremental 2>err &&
+test_expect_success '--write-midx=incremental without --geometric' '
+ git init incremental-without-geometric &&
+ (
+ cd incremental-without-geometric &&
- test_grep -- "--write-midx=incremental requires --geometric" err
+ git config maintenance.auto false &&
+
+ test_commit first &&
+ git repack -d &&
+
+ test_commit second &&
+ git repack --write-midx=incremental &&
+
+ git multi-pack-index verify &&
+ test_line_count = 1 $midx_chain &&
+ cp $midx_chain $midx_chain.before &&
+
+ # A second repack appends a new layer without
+ # disturbing the existing one.
+ test_commit third &&
+ git repack --write-midx=incremental &&
+
+ git multi-pack-index verify &&
+ test_line_count = 2 $midx_chain &&
+ head -n 1 $midx_chain.before >expect &&
+ head -n 1 $midx_chain >actual &&
+ test_cmp expect actual &&
+
+ git fsck
+ )
'
test_expect_success 'below layer threshold, tip packs excluded' '
@@ -334,8 +360,7 @@ test_expect_success 'kept packs are excluded from repack' '
# entirely, so no rollup occurs as there is only one
# non-kept pack. A new MIDX layer is written containing
# that pack.
- git repack --geometric=2 -d --write-midx=incremental \
- --write-bitmap-index &&
+ git repack --geometric=2 -d --write-midx=incremental &&
test-tool read-midx $objdir >actual &&
grep "^pack-.*\.idx$" actual >actual.packs &&
--
2.54.0.9.gb905fd5d0ae
^ permalink raw reply related
* [PATCH v2 15/16] repack: introduce `--write-midx=incremental`
From: Taylor Blau @ 2026-04-21 20:37 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1776803827.git.me@ttaylorr.com>
Expose the incremental MIDX repacking mode (implemented in an earlier
commit) via a new --write-midx=incremental option for `git repack`.
Add "incremental" as a recognized argument to the --write-midx
OPT_CALLBACK, mapping it to REPACK_WRITE_MIDX_INCREMENTAL. When this
mode is active and --geometric is in use, set the midx_layer_threshold
on the pack geometry so that only packs in sufficiently large tip layers
are considered for repacking.
Two new configuration options control the compaction behavior:
- repack.midxSplitFactor (default: 2): the factor used in the
geometric merging condition for MIDX layers.
- repack.midxNewLayerThreshold (default: 8): the minimum number of
packs in the tip MIDX layer before its packs are considered as
candidates for geometric repacking.
Add tests exercising the new mode across a variety of scenarios
including basic geometric violations, multi-round chain integrity,
branching and merging histories, cross-layer object uniqueness, and
threshold-based compaction.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
Documentation/config/repack.adoc | 18 ++
Documentation/git-repack.adoc | 39 ++-
builtin/repack.c | 49 ++-
midx.c | 31 ++
midx.h | 3 +
repack-geometry.c | 13 +-
repack-midx.c | 5 +
repack.c | 56 +++-
repack.h | 10 +-
t/meson.build | 1 +
t/t7705-repack-incremental-midx.sh | 500 +++++++++++++++++++++++++++++
11 files changed, 701 insertions(+), 24 deletions(-)
create mode 100755 t/t7705-repack-incremental-midx.sh
diff --git a/Documentation/config/repack.adoc b/Documentation/config/repack.adoc
index e9e78dcb198..4c22a499f62 100644
--- a/Documentation/config/repack.adoc
+++ b/Documentation/config/repack.adoc
@@ -46,3 +46,21 @@ repack.midxMustContainCruft::
`--write-midx`. When false, cruft packs are only included in the MIDX
when necessary (e.g., because they might be required to form a
reachability closure with MIDX bitmaps). Defaults to true.
+
+repack.midxSplitFactor::
+ The factor used in the geometric merging condition when
+ compacting incremental MIDX layers during `git repack` when
+ invoked with the `--write-midx=incremental` option.
++
+Adjacent layers are merged when the accumulated object count of the
+newer layer exceeds `1/<N>` of the object count of the next deeper
+layer. Must be at least 2. Defaults to 2.
+
+repack.midxNewLayerThreshold::
+ The minimum number of packs in the tip MIDX layer before those
+ packs are considered as candidates for geometric repacking
+ during `git repack --write-midx=incremental`.
++
+When the tip layer has fewer packs than this threshold, those packs are
+excluded from the geometric repack entirely, and are thus left
+unmodified. Must be at least 1. Defaults to 8.
diff --git a/Documentation/git-repack.adoc b/Documentation/git-repack.adoc
index 673ce910837..27a99cc46f4 100644
--- a/Documentation/git-repack.adoc
+++ b/Documentation/git-repack.adoc
@@ -11,7 +11,7 @@ SYNOPSIS
[verse]
'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]
[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]
- [--write-midx] [--name-hash-version=<n>] [--path-walk]
+ [--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]
DESCRIPTION
-----------
@@ -250,9 +250,42 @@ pack as the preferred pack for object selection by the MIDX (see
linkgit:git-multi-pack-index[1]).
-m::
---write-midx::
+--write-midx[=<mode>]::
Write a multi-pack index (see linkgit:git-multi-pack-index[1])
- containing the non-redundant packs.
+ containing the non-redundant packs. The following modes are
+ available:
++
+--
+ `default`;;
+ Write a single MIDX covering all packs. This is the
+ default when `--write-midx` is given without an
+ explicit mode.
+
+ `incremental`;;
+ Write an incremental MIDX chain instead of a single
+ flat MIDX. This mode requires `--geometric`.
++
+The incremental mode maintains a chain of MIDX layers that is compacted
+over time using a geometric merging strategy. Each repack creates a new
+tip layer containing the newly written pack(s). Adjacent layers are then
+merged whenever the newer layer's object count exceeds
+`1/repack.midxSplitFactor` of the next deeper layer's count. Layers
+that do not meet this condition are retained as-is.
++
+The result is that newer (tip) layers tend to contain many small packs
+with relatively few objects, while older (deeper) layers contain fewer,
+larger packs covering more objects. Because compaction is driven by the
+tip of the chain, newer layers are also rewritten more frequently than
+older ones, which are only touched when enough objects have accumulated
+to justify merging into them. This keeps the total number of layers
+logarithmic relative to the total number of objects.
++
+Only packs in the tip MIDX layer are considered as candidates for the
+geometric repack; packs in deeper layers are left untouched. If the tip
+layer contains fewer packs than `repack.midxNewLayerThreshold`, those
+packs are excluded from the geometry entirely, and a new layer is
+created for any new pack(s) without disturbing the existing chain.
+--
--name-hash-version=<n>::
Provide this argument to the underlying `git pack-objects` process.
diff --git a/builtin/repack.c b/builtin/repack.c
index 75c57736780..5ffa18e085e 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -33,7 +33,7 @@ static int midx_must_contain_cruft = 1;
static const char *const git_repack_usage[] = {
N_("git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\n"
"[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]\n"
- "[--write-midx] [--name-hash-version=<n>] [--path-walk]"),
+ "[--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]"),
NULL
};
@@ -48,6 +48,8 @@ static const char incremental_bitmap_conflict_error[] = N_(
struct repack_config_ctx {
struct pack_objects_args *po_args;
struct pack_objects_args *cruft_po_args;
+ int midx_split_factor;
+ int midx_new_layer_threshold;
};
static int repack_config(const char *var, const char *value,
@@ -97,6 +99,16 @@ static int repack_config(const char *var, const char *value,
midx_must_contain_cruft = git_config_bool(var, value);
return 0;
}
+ if (!strcmp(var, "repack.midxsplitfactor")) {
+ repack_ctx->midx_split_factor = git_config_int(var, value,
+ ctx->kvi);
+ return 0;
+ }
+ if (!strcmp(var, "repack.midxnewlayerthreshold")) {
+ repack_ctx->midx_new_layer_threshold = git_config_int(var, value,
+ ctx->kvi);
+ return 0;
+ }
return git_default_config(var, value, ctx, cb);
}
@@ -112,6 +124,8 @@ static int option_parse_write_midx(const struct option *opt, const char *arg,
if (!arg || !*arg)
*cfg = REPACK_WRITE_MIDX_DEFAULT;
+ else if (!strcmp(arg, "incremental"))
+ *cfg = REPACK_WRITE_MIDX_INCREMENTAL;
else
return error(_("unknown value for %s: %s"), opt->long_name, arg);
@@ -226,6 +240,8 @@ int cmd_repack(int argc,
memset(&config_ctx, 0, sizeof(config_ctx));
config_ctx.po_args = &po_args;
config_ctx.cruft_po_args = &cruft_po_args;
+ config_ctx.midx_split_factor = DEFAULT_MIDX_SPLIT_FACTOR;
+ config_ctx.midx_new_layer_threshold = DEFAULT_MIDX_NEW_LAYER_THRESHOLD;
repo_config(repo, repack_config, &config_ctx);
@@ -247,6 +263,9 @@ int cmd_repack(int argc,
if (pack_everything & PACK_CRUFT)
pack_everything |= ALL_INTO_ONE;
+ if (write_midx == REPACK_WRITE_MIDX_INCREMENTAL && !geometry.split_factor)
+ die(_("--write-midx=incremental requires --geometric"));
+
if (write_bitmaps < 0) {
if (write_midx == REPACK_WRITE_MIDX_NONE &&
(!(pack_everything & ALL_INTO_ONE) || !is_bare_repository()))
@@ -273,6 +292,13 @@ int cmd_repack(int argc,
write_bitmaps = 0;
}
+ if (config_ctx.midx_split_factor < 2)
+ die(_("invalid value for %s: %d"), "--midx-split-factor",
+ config_ctx.midx_split_factor);
+ if (config_ctx.midx_new_layer_threshold < 1)
+ die(_("invalid value for %s: %d"), "--midx-new-layer-threshold",
+ config_ctx.midx_new_layer_threshold);
+
if (write_midx != REPACK_WRITE_MIDX_NONE && write_bitmaps) {
struct strbuf path = STRBUF_INIT;
@@ -296,6 +322,10 @@ int cmd_repack(int argc,
if (geometry.split_factor) {
if (pack_everything)
die(_("options '%s' and '%s' cannot be used together"), "--geometric", "-A/-a");
+ if (write_midx == REPACK_WRITE_MIDX_INCREMENTAL) {
+ geometry.midx_layer_threshold = config_ctx.midx_new_layer_threshold;
+ geometry.midx_layer_threshold_set = true;
+ }
pack_geometry_init(&geometry, &existing, &po_args);
pack_geometry_split(&geometry);
}
@@ -545,8 +575,11 @@ int cmd_repack(int argc,
packtmp);
/* End of pack replacement. */
- if (delete_redundant && pack_everything & ALL_INTO_ONE)
+ if (delete_redundant && pack_everything & ALL_INTO_ONE) {
+ if (write_midx == REPACK_WRITE_MIDX_INCREMENTAL)
+ existing_packs_retain_midx_packs(&existing);
existing_packs_mark_for_deletion(&existing, &names);
+ }
if (write_midx != REPACK_WRITE_MIDX_NONE) {
struct repack_write_midx_opts opts = {
@@ -558,8 +591,8 @@ int cmd_repack(int argc,
.show_progress = show_progress,
.write_bitmaps = write_bitmaps > 0,
.midx_must_contain_cruft = midx_must_contain_cruft,
- .midx_split_factor = DEFAULT_MIDX_SPLIT_FACTOR,
- .midx_new_layer_threshold = DEFAULT_MIDX_NEW_LAYER_THRESHOLD,
+ .midx_split_factor = config_ctx.midx_split_factor,
+ .midx_new_layer_threshold = config_ctx.midx_new_layer_threshold,
.mode = write_midx,
};
@@ -572,11 +605,15 @@ int cmd_repack(int argc,
if (delete_redundant) {
int opts = 0;
- existing_packs_remove_redundant(&existing, packdir);
+ bool wrote_incremental_midx = write_midx == REPACK_WRITE_MIDX_INCREMENTAL;
+
+ existing_packs_remove_redundant(&existing, packdir,
+ wrote_incremental_midx);
if (geometry.split_factor)
pack_geometry_remove_redundant(&geometry, &names,
- &existing, packdir);
+ &existing, packdir,
+ wrote_incremental_midx);
if (show_progress)
opts |= PRUNE_PACKED_VERBOSE;
prune_packed_objects(opts);
diff --git a/midx.c b/midx.c
index dc86c8e7fee..cd31fa20788 100644
--- a/midx.c
+++ b/midx.c
@@ -850,6 +850,37 @@ void clear_midx_file(struct repository *r)
strbuf_release(&midx);
}
+void clear_incremental_midx_files(struct repository *r,
+ const struct strvec *keep_hashes)
+{
+ struct strbuf chain = STRBUF_INIT;
+
+ get_midx_chain_filename(r->objects->sources, &chain);
+
+ if (r->objects) {
+ struct odb_source *source = r->objects->sources;
+ for (source = r->objects->sources; source; source = source->next) {
+ struct odb_source_files *files = odb_source_files_downcast(source);
+ if (files->packed->midx)
+ close_midx(files->packed->midx);
+ files->packed->midx = NULL;
+ }
+ }
+
+ if (!keep_hashes && remove_path(chain.buf))
+ die(_("failed to clear multi-pack-index chain at %s"),
+ chain.buf);
+
+ clear_incremental_midx_files_ext(r->objects->sources, MIDX_EXT_BITMAP,
+ keep_hashes);
+ clear_incremental_midx_files_ext(r->objects->sources, MIDX_EXT_REV,
+ keep_hashes);
+ clear_incremental_midx_files_ext(r->objects->sources, MIDX_EXT_MIDX,
+ keep_hashes);
+
+ strbuf_release(&chain);
+}
+
static int verify_midx_error;
__attribute__((format (printf, 1, 2)))
diff --git a/midx.h b/midx.h
index 3ee12dd08ec..63853a03a47 100644
--- a/midx.h
+++ b/midx.h
@@ -9,6 +9,7 @@ struct repository;
struct bitmapped_pack;
struct git_hash_algo;
struct odb_source;
+struct strvec;
#define MIDX_SIGNATURE 0x4d494458 /* "MIDX" */
#define MIDX_VERSION_V1 1
@@ -143,6 +144,8 @@ int write_midx_file_compact(struct odb_source *source,
const char *incremental_base,
unsigned flags);
void clear_midx_file(struct repository *r);
+void clear_incremental_midx_files(struct repository *r,
+ const struct strvec *keep_hashes);
int verify_midx_file(struct odb_source *source, unsigned flags);
int expire_midx_packs(struct odb_source *source, unsigned flags);
int midx_repack(struct odb_source *source, size_t batch_size, unsigned flags);
diff --git a/repack-geometry.c b/repack-geometry.c
index 2408b8a3cc2..2064683dcfe 100644
--- a/repack-geometry.c
+++ b/repack-geometry.c
@@ -249,7 +249,8 @@ static void remove_redundant_packs(struct packed_git **pack,
uint32_t pack_nr,
struct string_list *names,
struct existing_packs *existing,
- const char *packdir)
+ const char *packdir,
+ bool wrote_incremental_midx)
{
const struct git_hash_algo *algop = existing->repo->hash_algo;
struct strbuf buf = STRBUF_INIT;
@@ -269,7 +270,8 @@ static void remove_redundant_packs(struct packed_git **pack,
(string_list_has_string(&existing->kept_packs, buf.buf)))
continue;
- repack_remove_redundant_pack(existing->repo, packdir, buf.buf);
+ repack_remove_redundant_pack(existing->repo, packdir, buf.buf,
+ wrote_incremental_midx);
}
strbuf_release(&buf);
@@ -278,12 +280,13 @@ static void remove_redundant_packs(struct packed_git **pack,
void pack_geometry_remove_redundant(struct pack_geometry *geometry,
struct string_list *names,
struct existing_packs *existing,
- const char *packdir)
+ const char *packdir,
+ bool wrote_incremental_midx)
{
remove_redundant_packs(geometry->pack, geometry->split,
- names, existing, packdir);
+ names, existing, packdir, wrote_incremental_midx);
remove_redundant_packs(geometry->promisor_pack, geometry->promisor_split,
- names, existing, packdir);
+ names, existing, packdir, wrote_incremental_midx);
}
void pack_geometry_release(struct pack_geometry *geometry)
diff --git a/repack-midx.c b/repack-midx.c
index 8f3720772b8..6a0358096ec 100644
--- a/repack-midx.c
+++ b/repack-midx.c
@@ -894,6 +894,7 @@ static int write_midx_incremental(struct repack_write_midx_opts *opts)
struct midx_compaction_step *steps = NULL;
struct strbuf lock_name = STRBUF_INIT;
struct lock_file lf;
+ struct strvec keep_hashes = STRVEC_INIT;
size_t steps_nr = 0;
size_t i;
int ret = 0;
@@ -939,11 +940,15 @@ static int write_midx_incremental(struct repack_write_midx_opts *opts)
BUG("missing result for compaction step %"PRIuMAX,
(uintmax_t)i);
fprintf(get_lock_file_fp(&lf), "%s\n", step->csum);
+ strvec_push(&keep_hashes, step->csum);
}
commit_lock_file(&lf);
+ clear_incremental_midx_files(opts->existing->repo, &keep_hashes);
+
done:
+ strvec_clear(&keep_hashes);
strbuf_release(&lock_name);
for (i = 0; i < steps_nr; i++)
midx_compaction_step_release(&steps[i]);
diff --git a/repack.c b/repack.c
index 2ee6b51420a..571dabb665e 100644
--- a/repack.c
+++ b/repack.c
@@ -55,14 +55,18 @@ void pack_objects_args_release(struct pack_objects_args *args)
}
void repack_remove_redundant_pack(struct repository *repo, const char *dir_name,
- const char *base_name)
+ const char *base_name,
+ bool wrote_incremental_midx)
{
struct strbuf buf = STRBUF_INIT;
struct odb_source *source = repo->objects->sources;
struct multi_pack_index *m = get_multi_pack_index(source);
strbuf_addf(&buf, "%s.pack", base_name);
- if (m && source->local && midx_contains_pack(m, buf.buf))
+ if (m && source->local && midx_contains_pack(m, buf.buf)) {
clear_midx_file(repo);
+ if (!wrote_incremental_midx)
+ clear_incremental_midx_files(repo, NULL);
+ }
strbuf_insertf(&buf, 0, "%s/", dir_name);
unlink_pack_path(buf.buf, 1);
strbuf_release(&buf);
@@ -250,25 +254,63 @@ void existing_packs_mark_for_deletion(struct existing_packs *existing,
&existing->cruft_packs);
}
+/*
+ * Mark every pack that is referenced by the existing MIDX chain as
+ * retained, so that a subsequent call to
+ * existing_packs_mark_for_deletion() will not mark them for deletion.
+ *
+ * This is used when writing an incremental MIDX layer on top of an
+ * existing chain: retained layers continue to reference the same
+ * packs on disk, so those packs must not be unlinked even if the
+ * freshly-written pack supersedes them.
+ */
+void existing_packs_retain_midx_packs(struct existing_packs *existing)
+{
+ struct string_list_item *item;
+ struct strbuf buf = STRBUF_INIT;
+
+ for_each_string_list_item(item, &existing->midx_packs) {
+ struct string_list_item *found;
+
+ strbuf_reset(&buf);
+ strbuf_addstr(&buf, item->string);
+ strbuf_strip_suffix(&buf, ".pack");
+ strbuf_strip_suffix(&buf, ".idx");
+
+ found = string_list_lookup(&existing->non_kept_packs, buf.buf);
+ if (found)
+ existing_packs_mark_retained(found);
+
+ found = string_list_lookup(&existing->cruft_packs, buf.buf);
+ if (found)
+ existing_packs_mark_retained(found);
+ }
+
+ strbuf_release(&buf);
+}
+
static void remove_redundant_packs_1(struct repository *repo,
struct string_list *packs,
- const char *packdir)
+ const char *packdir,
+ bool wrote_incremental_midx)
{
struct string_list_item *item;
for_each_string_list_item(item, packs) {
if (!existing_pack_is_marked_for_deletion(item))
continue;
- repack_remove_redundant_pack(repo, packdir, item->string);
+ repack_remove_redundant_pack(repo, packdir, item->string,
+ wrote_incremental_midx);
}
}
void existing_packs_remove_redundant(struct existing_packs *existing,
- const char *packdir)
+ const char *packdir,
+ bool wrote_incremental_midx)
{
remove_redundant_packs_1(existing->repo, &existing->non_kept_packs,
- packdir);
+ packdir, wrote_incremental_midx);
remove_redundant_packs_1(existing->repo, &existing->cruft_packs,
- packdir);
+ packdir, wrote_incremental_midx);
}
void existing_packs_release(struct existing_packs *existing)
diff --git a/repack.h b/repack.h
index 831ccfb1c6c..f9fbc895f02 100644
--- a/repack.h
+++ b/repack.h
@@ -34,7 +34,8 @@ void prepare_pack_objects(struct child_process *cmd,
void pack_objects_args_release(struct pack_objects_args *args);
void repack_remove_redundant_pack(struct repository *repo, const char *dir_name,
- const char *base_name);
+ const char *base_name,
+ bool wrote_incremental_midx);
struct write_pack_opts {
struct pack_objects_args *po_args;
@@ -83,8 +84,10 @@ void existing_packs_retain_cruft(struct existing_packs *existing,
struct packed_git *cruft);
void existing_packs_mark_for_deletion(struct existing_packs *existing,
struct string_list *names);
+void existing_packs_retain_midx_packs(struct existing_packs *existing);
void existing_packs_remove_redundant(struct existing_packs *existing,
- const char *packdir);
+ const char *packdir,
+ bool wrote_incremental_midx);
void existing_packs_release(struct existing_packs *existing);
struct generated_pack;
@@ -129,7 +132,8 @@ struct packed_git *pack_geometry_preferred_pack(struct pack_geometry *geometry);
void pack_geometry_remove_redundant(struct pack_geometry *geometry,
struct string_list *names,
struct existing_packs *existing,
- const char *packdir);
+ const char *packdir,
+ bool wrote_incremental_midx);
void pack_geometry_release(struct pack_geometry *geometry);
struct tempfile;
diff --git a/t/meson.build b/t/meson.build
index 7528e5cda5f..25f0d823d8e 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -951,6 +951,7 @@ integration_tests = [
't7702-repack-cyclic-alternate.sh',
't7703-repack-geometric.sh',
't7704-repack-cruft.sh',
+ 't7705-repack-incremental-midx.sh',
't7800-difftool.sh',
't7810-grep.sh',
't7811-grep-open.sh',
diff --git a/t/t7705-repack-incremental-midx.sh b/t/t7705-repack-incremental-midx.sh
new file mode 100755
index 00000000000..af1a1483b8c
--- /dev/null
+++ b/t/t7705-repack-incremental-midx.sh
@@ -0,0 +1,500 @@
+#!/bin/sh
+
+test_description='git repack --write-midx=incremental'
+
+. ./test-lib.sh
+
+GIT_TEST_MULTI_PACK_INDEX=0
+GIT_TEST_MULTI_PACK_INDEX_WRITE_BITMAP=0
+GIT_TEST_MULTI_PACK_INDEX_WRITE_INCREMENTAL=0
+
+objdir=.git/objects
+packdir=$objdir/pack
+midxdir=$packdir/multi-pack-index.d
+midx_chain=$midxdir/multi-pack-index-chain
+
+# incrementally_repack N
+#
+# Make "N" new commits, each stored in their own pack, and then repacked
+# with the --write-midx=incremental strategy.
+incrementally_repack () {
+ for i in $(test_seq 1 "$1")
+ do
+ test_commit "$i" &&
+
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index &&
+ git multi-pack-index verify || return 1
+ done
+}
+
+# Create packs with geometrically increasing sizes so that they
+# satisfy the geometric progression and survive a --geometric=2
+# repack without being rolled up. Creates 3 packs containing 1,
+# 2, and 6 commits (3, 6, and 18 objects) respectively.
+create_geometric_packs () {
+ test_commit "small" &&
+ git repack -d &&
+
+ test_commit_bulk --message="medium" 2 &&
+ test_commit_bulk --message="large" 6 &&
+
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index
+}
+
+# create_layer <test_commit_bulk args>
+#
+# Creates a new MIDX layer with the contents of "test_commit_bulk $@".
+create_layer () {
+ test_commit_bulk "$@" &&
+
+ git multi-pack-index write --incremental --bitmap
+}
+
+# create_layers
+#
+# Reads lines of "<message> <nr>" from stdin and creates a new MIDX
+# layer for each line. See create_layer above for more.
+create_layers () {
+ while read msg nr
+ do
+ create_layer --message="$msg" "$nr" || return 1
+ done
+}
+
+test_expect_success '--write-midx=incremental requires --geometric' '
+ test_must_fail git repack --write-midx=incremental 2>err &&
+
+ test_grep -- "--write-midx=incremental requires --geometric" err
+'
+
+test_expect_success 'below layer threshold, tip packs excluded' '
+ git init below-layer-threshold-tip-packs-excluded &&
+ (
+ cd below-layer-threshold-tip-packs-excluded &&
+
+ git config maintenance.auto false &&
+ git config repack.midxnewlayerthreshold 4 &&
+ git config repack.midxsplitfactor 2 &&
+
+ # Create 3 packs forming a geometric progression by
+ # object count such that they are unmodified by the
+ # initial repack. The MIDX chain thusly contains a
+ # single layer with three packs.
+ create_geometric_packs &&
+ ls $packdir/pack-*.idx | sort >packs.before &&
+ test_line_count = 1 $midx_chain &&
+ cp $midx_chain $midx_chain.before &&
+
+ # Repack a new commit. Since the layer threshold is
+ # unmet, a new MIDX layer is added on top of the
+ # existing one.
+ test_commit extra &&
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index &&
+ git multi-pack-index verify &&
+
+ ls $packdir/pack-*.idx | sort >packs.after &&
+ comm -13 packs.before packs.after >packs.new &&
+ test_line_count = 1 packs.new &&
+
+ test_line_count = 2 "$midx_chain" &&
+ head -n 1 "$midx_chain.before" >expect &&
+ head -n 1 "$midx_chain" >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'above layer threshold, tip packs repacked' '
+ git init above-layer-threshold-tip-packs-repacked &&
+ (
+ cd above-layer-threshold-tip-packs-repacked &&
+
+ git config maintenance.auto false &&
+ git config repack.midxnewlayerthreshold 2 &&
+ git config repack.midxsplitfactor 2 &&
+
+ # Same setup, but with the layer threshold set to 2.
+ # Since the tip MIDX layer meets that threshold, its
+ # packs are considered repack candidates.
+ create_geometric_packs &&
+ cp $midx_chain $midx_chain.before &&
+
+ # Perturb the existing progression such that it is
+ # rolled up into a single new pack, invalidating the
+ # existing MIDX layer and replacing it with a new one.
+ test_commit extra &&
+ git repack -d &&
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index &&
+
+ ! test_cmp $midx_chain.before $midx_chain &&
+ test_line_count = 1 $midx_chain &&
+
+ git multi-pack-index verify
+ )
+'
+
+test_expect_success 'above layer threshold, tip layer preserved' '
+ git init above-layer-threshold-tip-layer-preserved &&
+ (
+ cd above-layer-threshold-tip-layer-preserved &&
+
+ git config maintenance.auto false &&
+ git config repack.midxnewlayerthreshold 2 &&
+ git config repack.midxsplitfactor 2 &&
+
+ test_commit_bulk --message="medium" 2 &&
+ test_commit_bulk --message="large" 6 &&
+
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index &&
+
+ test_line_count = 1 "$midx_chain" &&
+ ls $packdir/pack-*.idx | sort >packs.before &&
+ cp $midx_chain $midx_chain.before &&
+
+ # Create objects to form a pack satisfying the geometric
+ # progression (thus preserving the tip layer), but not
+ # so large that it meets the layer merging condition.
+ test_commit_bulk --message="small" 1 &&
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index &&
+
+ ls $packdir/pack-*.idx | sort >packs.after &&
+ comm -13 packs.before packs.after >packs.new &&
+
+ test_line_count = 1 packs.new &&
+ test_line_count = 3 packs.after &&
+ test_line_count = 2 "$midx_chain" &&
+ head -n 1 "$midx_chain.before" >expect &&
+ head -n 1 "$midx_chain" >actual &&
+ test_cmp expect actual &&
+
+ git multi-pack-index verify
+ )
+'
+
+test_expect_success 'above layer threshold, tip packs preserved' '
+ git init above-layer-threshold-tip-packs-preserved &&
+ (
+ cd above-layer-threshold-tip-packs-preserved &&
+
+ git config maintenance.auto false &&
+ git config repack.midxnewlayerthreshold 2 &&
+ git config repack.midxsplitfactor 2 &&
+
+ create_geometric_packs &&
+ ls $packdir/pack-*.idx | sort >packs.before &&
+ cp $midx_chain $midx_chain.before &&
+
+ # Same setup as above, but this time the new objects do
+ # not satisfy the new layer merging condition, resulting
+ # in a new tip layer.
+ test_commit_bulk --message="huge" 18 &&
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index &&
+
+ ls $packdir/pack-*.idx | sort >packs.after &&
+ comm -13 packs.before packs.after >packs.new &&
+
+ ! test_cmp $midx_chain.before $midx_chain &&
+ test_line_count = 1 $midx_chain &&
+ test_line_count = 1 packs.new &&
+
+ git multi-pack-index verify
+ )
+'
+
+test_expect_success 'new tip absorbs multiple layers' '
+ git init new-tip-absorbs-multiple-layers &&
+ (
+ cd new-tip-absorbs-multiple-layers &&
+
+ git config maintenance.auto false &&
+ git config repack.midxnewlayerthreshold 1 &&
+ git config repack.midxsplitfactor 2 &&
+
+ # Build a 4-layer chain where each layer is too small to
+ # absorb the one below it. The sizes must satisfy L(n) <
+ # L(n-1)/2 for each adjacent pair:
+ #
+ # L0 (oldest): 75 obj (25 commits)
+ # L1: 21 obj (7 commits, 21 < 75/2)
+ # L2: 9 obj (3 commits, 9 < 21/2)
+ # L3 (tip): 3 obj (1 commit, 3 < 9/2)
+ create_layers <<-\EOF &&
+ L0 25
+ L1 7
+ L2 3
+ L3 1
+ EOF
+
+ test_line_count = 4 "$midx_chain" &&
+ cp $midx_chain $midx_chain.before &&
+
+ # Now add a new commit. The merging condition is
+ # satisfied between L3-L1, but violated at L0, which is
+ # too large relative to the accumulated size.
+ #
+ # As a result, the chain shrinks from 4 to 2 layers.
+ test_commit new &&
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index &&
+
+ ! test_cmp $midx_chain.before $midx_chain &&
+ test_line_count = 2 "$midx_chain" &&
+ git multi-pack-index verify
+ )
+'
+
+test_expect_success 'compaction of older layers' '
+ git init compaction-of-older-layers &&
+ (
+ cd compaction-of-older-layers &&
+
+ git config maintenance.auto false &&
+ git config repack.midxnewlayerthreshold 1 &&
+ git config repack.midxsplitfactor 2 &&
+
+ # Build a chain with two small layers at the bottom
+ # and a larger barrier layer on top, producing a
+ # chain that violates the compaction invariant, since
+ # the two small layers would normally have been merged.
+ create_layers <<-\EOF &&
+ one 2
+ two 4
+ barrier 54
+ EOF
+
+ cp $midx_chain $midx_chain.before &&
+
+ # Running an incremental repack compacts the two
+ # small layers at the bottom of the chain as a
+ # separate step in the compaction plan.
+ test_commit another &&
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index &&
+
+ test_line_count = 2 "$midx_chain" &&
+ git multi-pack-index verify
+ )
+'
+
+test_expect_success 'geometric rollup with surviving tip packs' '
+ git init geometric-rollup-with-surviving-tip-packs &&
+ (
+ cd geometric-rollup-with-surviving-tip-packs &&
+
+ git config maintenance.auto false &&
+ git config repack.midxnewlayerthreshold 1 &&
+ git config repack.midxsplitfactor 2 &&
+
+ # Create a pack large enough to anchor the geometric
+ # progression when small packs are added alongside it.
+ create_layer --message="big" 5 &&
+
+ test_line_count = 1 "$midx_chain" &&
+ cp $midx_chain $midx_chain.before &&
+
+ # Repack a small number of objects such that the
+ # progression is unbothered. Note that the existing pack
+ # is considered a repack candidate as the new layer
+ # threshold is set to 1.
+ test_commit small-1 &&
+ git repack -d &&
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index &&
+
+ ! test_cmp $midx_chain.before $midx_chain &&
+ cp $midx_chain $midx_chain.before
+ )
+'
+
+test_expect_success 'kept packs are excluded from repack' '
+ git init kept-packs-excluded-from-repack &&
+ (
+ cd kept-packs-excluded-from-repack &&
+
+ git config maintenance.auto false &&
+ git config repack.midxnewlayerthreshold 1 &&
+ git config repack.midxsplitfactor 2 &&
+
+ # Create two equal-sized packs, marking one as kept.
+ for i in A B
+ do
+ test_commit "$i" && git repack -d || return 1
+ done &&
+
+ keep=$(ls $packdir/pack-*.idx | head -n 1) &&
+ touch "${keep%.idx}.keep" &&
+
+ # The kept pack is excluded as a repacking candidate
+ # entirely, so no rollup occurs as there is only one
+ # non-kept pack. A new MIDX layer is written containing
+ # that pack.
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index &&
+
+ test-tool read-midx $objdir >actual &&
+ grep "^pack-.*\.idx$" actual >actual.packs &&
+ test_line_count = 1 actual.packs &&
+ test_grep ! "$keep" actual.packs &&
+
+ git multi-pack-index verify &&
+
+ # All objects (from both kept and non-kept packs)
+ # must still be accessible.
+ git fsck
+ )
+'
+
+test_expect_success 'incremental MIDX with --max-pack-size' '
+ git init incremental-midx-with--max-pack-size &&
+ (
+ cd incremental-midx-with--max-pack-size &&
+
+ git config maintenance.auto false &&
+ git config repack.midxnewlayerthreshold 1 &&
+ git config repack.midxsplitfactor 2 &&
+
+ create_layer --message="base" 1 &&
+
+ # Now add enough data that a small --max-pack-size will
+ # cause pack-objects to split its output. Create objects
+ # large enough to fill multiple packs.
+ test-tool genrandom foo 1M >big1 &&
+ test-tool genrandom bar 1M >big2 &&
+ git add big1 big2 &&
+ test_tick &&
+ git commit -a -m "big blobs" &&
+ git repack -d &&
+
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index --max-pack-size=1M &&
+
+ test_line_count = 1 "$midx_chain" &&
+ test-tool read-midx $objdir >actual &&
+ grep "^pack-.*\.idx$" actual >actual.packs &&
+ test_line_count -gt 1 actual.packs &&
+
+ git multi-pack-index verify
+ )
+'
+
+test_expect_success 'noop repack preserves valid MIDX chain' '
+ git init noop-repack-preserves-valid-midx-chain &&
+ (
+ cd noop-repack-preserves-valid-midx-chain &&
+
+ git config maintenance.auto false &&
+ git config repack.midxnewlayerthreshold 1 &&
+ git config repack.midxsplitfactor 2 &&
+
+ create_layer --message="base" 1 &&
+
+ git multi-pack-index verify &&
+ cp $midx_chain $midx_chain.before &&
+
+ # Running again with no new objects should not break
+ # the MIDX chain. It produces "Nothing new to pack."
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index &&
+
+ test_cmp $midx_chain.before $midx_chain &&
+
+ git multi-pack-index verify &&
+ git fsck
+ )
+'
+
+test_expect_success 'repack -ad removes stale incremental chain' '
+ git init repack--ad-removes-stale-incremental-chain &&
+ (
+ cd repack--ad-removes-stale-incremental-chain &&
+
+ git config maintenance.auto false &&
+ git config repack.midxnewlayerthreshold 1 &&
+ git config repack.midxsplitfactor 2 &&
+
+ create_layers <<-\EOF &&
+ one 1
+ two 1
+ EOF
+
+ test_path_is_file $midx_chain &&
+ test_line_count = 2 $midx_chain &&
+
+ git repack -ad &&
+
+ test_path_is_missing $packdir/multi-pack-index &&
+ test_dir_is_empty $midxdir
+ )
+'
+
+test_expect_success 'repack -ad --write-midx=incremental is safe' '
+ git init ad-incremental-midx &&
+ (
+ cd ad-incremental-midx &&
+
+ git config maintenance.auto false &&
+
+ # Build a MIDX chain with multiple layers referencing
+ # distinct packs.
+ test_commit first &&
+ git repack -d &&
+
+ test_commit second &&
+ git repack -d --write-midx=incremental &&
+
+ git multi-pack-index verify &&
+ test_line_count = 1 $midx_chain &&
+
+ # Now do a full -ad repack. The new pack contains all
+ # objects, but any retained MIDX layers still reference
+ # the now-deleted packs.
+ test_commit third &&
+ git repack -ad --write-midx=incremental &&
+
+ git multi-pack-index verify &&
+ git fsck &&
+ git rev-list --all --objects >/dev/null
+ )
+'
+
+test_expect_success 'repack rejects invalid midxSplitFactor' '
+ test_when_finished "rm -fr bad-split-factor" &&
+ git init bad-split-factor &&
+ (
+ cd bad-split-factor &&
+ test_commit base &&
+
+ for v in 0 1 -1
+ do
+ test_must_fail git -c repack.midxSplitFactor=$v \
+ repack -d --geometric=2 --write-midx=incremental 2>err &&
+ test_grep "invalid value for --midx-split-factor" err ||
+ return 1
+ done
+ )
+'
+
+test_expect_success 'repack rejects invalid midxNewLayerThreshold' '
+ test_when_finished "rm -fr bad-layer-threshold" &&
+ git init bad-layer-threshold &&
+ (
+ cd bad-layer-threshold &&
+ test_commit base &&
+
+ for v in 0 -1
+ do
+ test_must_fail git -c repack.midxNewLayerThreshold=$v \
+ repack -d --geometric=2 --write-midx=incremental 2>err &&
+ test_grep "invalid value for --midx-new-layer-threshold" err ||
+ return 1
+ done
+ )
+'
+
+test_done
--
2.54.0.9.gb905fd5d0ae
^ permalink raw reply related
* [PATCH v2 14/16] repack: implement incremental MIDX repacking
From: Taylor Blau @ 2026-04-21 20:37 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1776803827.git.me@ttaylorr.com>
Implement the `write_midx_incremental()` function, which builds and
maintains an incremental MIDX chain as part of the geometric repacking
process.
Unlike the default mode which writes a single flat MIDX, the incremental
mode constructs a compaction plan that determines which MIDX layers to
write, compact, or copy, and then executes each step using `git
multi-pack-index` subcommands with the --checksum-only flag.
The repacking strategy works as follows:
* Acquire the lock guarding the multi-pack-index-chain.
* A new MIDX layer is always written containing the newly created
pack(s). If the tip MIDX layer was rewritten during geometric
repacking, any surviving packs from that layer are also included.
* Starting from the new layer, adjacent MIDX layers are merged together
as long as the accumulated object count exceeds half the object count
of the next deeper layer (controlled by 'repack.midxSplitFactor').
* Remaining layers in the chain are evaluated pairwise and either
compacted or copied as-is, following the same merging condition.
* Write the contents of the new multi-pack-index chain, atomically move
it into place, and then release the lock.
* Delete any now-unused MIDX layers.
After writing the new layer, the strategy is evaluated among the
existing MIDX layers in order from oldest to newest. Each step that
writes a new MIDX layer uses "--checksum-only" to avoid updating the
multi-pack-index-chain file. After all steps are complete, the new chain
file is written and then atomically moved into place.
At present, this functionality is exposed behind a new enum value,
`REPACK_WRITE_MIDX_INCREMENTAL`, but has no external callers. A
subsequent commit will expose this mode via `git repack
--write-midx=incremental`.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
builtin/repack.c | 5 +
repack-midx.c | 578 ++++++++++++++++++++++++++++++++++++++++++++++-
repack.h | 3 +
3 files changed, 584 insertions(+), 2 deletions(-)
diff --git a/builtin/repack.c b/builtin/repack.c
index 5d366340c34..75c57736780 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -42,6 +42,9 @@ static const char incremental_bitmap_conflict_error[] = N_(
"--no-write-bitmap-index or disable the pack.writeBitmaps configuration."
);
+#define DEFAULT_MIDX_SPLIT_FACTOR 2
+#define DEFAULT_MIDX_NEW_LAYER_THRESHOLD 8
+
struct repack_config_ctx {
struct pack_objects_args *po_args;
struct pack_objects_args *cruft_po_args;
@@ -555,6 +558,8 @@ int cmd_repack(int argc,
.show_progress = show_progress,
.write_bitmaps = write_bitmaps > 0,
.midx_must_contain_cruft = midx_must_contain_cruft,
+ .midx_split_factor = DEFAULT_MIDX_SPLIT_FACTOR,
+ .midx_new_layer_threshold = DEFAULT_MIDX_NEW_LAYER_THRESHOLD,
.mode = write_midx,
};
diff --git a/repack-midx.c b/repack-midx.c
index 4a568a2a9b8..8f3720772b8 100644
--- a/repack-midx.c
+++ b/repack-midx.c
@@ -2,12 +2,16 @@
#include "repack.h"
#include "hash.h"
#include "hex.h"
+#include "lockfile.h"
+#include "midx.h"
#include "odb.h"
#include "oidset.h"
#include "pack-bitmap.h"
+#include "path.h"
#include "refs.h"
#include "run-command.h"
#include "tempfile.h"
+#include "trace2.h"
struct midx_snapshot_ref_data {
struct repository *repo;
@@ -293,13 +297,16 @@ static void repack_prepare_midx_command(struct child_process *cmd,
}
static int repack_fill_midx_stdin_packs(struct child_process *cmd,
- struct string_list *include)
+ struct string_list *include,
+ struct string_list *out)
{
struct string_list_item *item;
FILE *in;
int ret;
cmd->in = -1;
+ if (out)
+ cmd->out = -1;
strvec_push(&cmd->args, "--stdin-packs");
@@ -312,6 +319,17 @@ static int repack_fill_midx_stdin_packs(struct child_process *cmd,
fprintf(in, "%s\n", item->string);
fclose(in);
+ if (out) {
+ struct strbuf buf = STRBUF_INIT;
+ FILE *outf = xfdopen(cmd->out, "r");
+
+ while (strbuf_getline(&buf, outf) != EOF)
+ string_list_append(out, buf.buf);
+ strbuf_release(&buf);
+
+ fclose(outf);
+ }
+
return finish_command(cmd);
}
@@ -369,7 +387,7 @@ static int write_midx_included_packs(struct repack_write_midx_opts *opts)
strvec_pushf(&cmd.args, "--refs-snapshot=%s",
opts->refs_snapshot);
- ret = repack_fill_midx_stdin_packs(&cmd, &include);
+ ret = repack_fill_midx_stdin_packs(&cmd, &include, NULL);
done:
if (!ret && opts->write_bitmaps)
remove_redundant_bitmaps(&include, opts->packdir);
@@ -379,6 +397,560 @@ static int write_midx_included_packs(struct repack_write_midx_opts *opts)
return ret;
}
+struct midx_compaction_step {
+ union {
+ struct multi_pack_index *copy;
+ struct string_list write;
+ struct {
+ struct multi_pack_index *from;
+ struct multi_pack_index *to;
+ } compact;
+ } u;
+
+ uint32_t objects_nr;
+ char *csum;
+
+ enum {
+ MIDX_COMPACTION_STEP_UNKNOWN,
+ MIDX_COMPACTION_STEP_COPY,
+ MIDX_COMPACTION_STEP_WRITE,
+ MIDX_COMPACTION_STEP_COMPACT,
+ } type;
+};
+
+static const char *midx_compaction_step_base(const struct midx_compaction_step *step)
+{
+ switch (step->type) {
+ case MIDX_COMPACTION_STEP_UNKNOWN:
+ BUG("cannot use UNKNOWN step as a base");
+ case MIDX_COMPACTION_STEP_COPY:
+ return midx_get_checksum_hex(step->u.copy);
+ case MIDX_COMPACTION_STEP_WRITE:
+ BUG("cannot use WRITE step as a base");
+ case MIDX_COMPACTION_STEP_COMPACT:
+ return midx_get_checksum_hex(step->u.compact.to);
+ default:
+ BUG("unhandled midx compaction step type %d", step->type);
+ }
+}
+
+static int midx_compaction_step_exec_copy(struct midx_compaction_step *step)
+{
+ step->csum = xstrdup(midx_get_checksum_hex(step->u.copy));
+ return 0;
+}
+
+static int midx_compaction_step_exec_write(struct midx_compaction_step *step,
+ struct repack_write_midx_opts *opts,
+ const char *base)
+{
+ struct child_process cmd = CHILD_PROCESS_INIT;
+ struct string_list hash = STRING_LIST_INIT_DUP;
+ struct string_list_item *item;
+ const char *preferred_pack = NULL;
+ int ret = 0;
+
+ if (!step->u.write.nr) {
+ ret = error(_("no packs to write MIDX during compaction"));
+ goto out;
+ }
+
+ for_each_string_list_item(item, &step->u.write) {
+ if (item->util)
+ preferred_pack = item->string;
+ }
+
+ repack_prepare_midx_command(&cmd, opts, "write");
+ strvec_pushl(&cmd.args, "--incremental", "--no-write-chain-file", NULL);
+ strvec_pushf(&cmd.args, "--base=%s", base ? base : "none");
+
+ if (preferred_pack) {
+ struct strbuf buf = STRBUF_INIT;
+
+ strbuf_addstr(&buf, preferred_pack);
+ strbuf_strip_suffix(&buf, ".idx");
+ strbuf_addstr(&buf, ".pack");
+
+ strvec_pushf(&cmd.args, "--preferred-pack=%s", buf.buf);
+
+ strbuf_release(&buf);
+ }
+
+ ret = repack_fill_midx_stdin_packs(&cmd, &step->u.write, &hash);
+ if (hash.nr != 1) {
+ ret = error(_("expected exactly one line during MIDX write, "
+ "got: %"PRIuMAX),
+ (uintmax_t)hash.nr);
+ goto out;
+ }
+
+ step->csum = xstrdup(hash.items[0].string);
+
+out:
+ string_list_clear(&hash, 0);
+
+ return ret;
+}
+
+static int midx_compaction_step_exec_compact(struct midx_compaction_step *step,
+ struct repack_write_midx_opts *opts)
+{
+ struct child_process cmd = CHILD_PROCESS_INIT;
+ struct strbuf buf = STRBUF_INIT;
+ FILE *out = NULL;
+ int ret;
+
+ repack_prepare_midx_command(&cmd, opts, "compact");
+ strvec_pushl(&cmd.args, "--incremental", "--no-write-chain-file",
+ midx_get_checksum_hex(step->u.compact.from),
+ midx_get_checksum_hex(step->u.compact.to), NULL);
+
+ cmd.out = -1;
+
+ ret = start_command(&cmd);
+ if (ret)
+ goto out;
+
+ out = xfdopen(cmd.out, "r");
+ while (strbuf_getline_lf(&buf, out) != EOF) {
+ if (step->csum) {
+ ret = error(_("unexpected MIDX output: '%s'"), buf.buf);
+ goto out;
+ }
+ step->csum = strbuf_detach(&buf, NULL);
+ }
+
+ ret = finish_command(&cmd);
+
+out:
+ if (out)
+ fclose(out);
+ strbuf_release(&buf);
+
+ return ret;
+}
+
+static int midx_compaction_step_exec(struct midx_compaction_step *step,
+ struct repack_write_midx_opts *opts,
+ const char *base)
+{
+ switch (step->type) {
+ case MIDX_COMPACTION_STEP_UNKNOWN:
+ BUG("cannot execute UNKNOWN midx compaction step");
+ case MIDX_COMPACTION_STEP_COPY:
+ return midx_compaction_step_exec_copy(step);
+ case MIDX_COMPACTION_STEP_WRITE:
+ return midx_compaction_step_exec_write(step, opts, base);
+ case MIDX_COMPACTION_STEP_COMPACT:
+ return midx_compaction_step_exec_compact(step, opts);
+ default:
+ BUG("unhandled midx compaction step type %d", step->type);
+ }
+}
+
+static void midx_compaction_step_release(struct midx_compaction_step *step)
+{
+ if (step->type == MIDX_COMPACTION_STEP_WRITE)
+ string_list_clear(&step->u.write, 0);
+ free(step->csum);
+}
+
+static int repack_make_midx_compaction_plan(struct repack_write_midx_opts *opts,
+ struct midx_compaction_step **steps_p,
+ size_t *steps_nr_p)
+{
+ struct multi_pack_index *m;
+ struct midx_compaction_step *steps = NULL;
+ struct midx_compaction_step step = { 0 };
+ struct strbuf buf = STRBUF_INIT;
+ size_t steps_nr = 0, steps_alloc = 0;
+ uint32_t i;
+ int ret = 0;
+
+ trace2_region_enter("repack", "make_midx_compaction_plan",
+ opts->existing->repo);
+
+ odb_reprepare(opts->existing->repo->objects);
+ m = get_multi_pack_index(opts->existing->source);
+
+ for (i = 0; m && i < m->num_packs + m->num_packs_in_base; i++) {
+ if (prepare_midx_pack(m, i)) {
+ ret = error(_("could not load pack %"PRIu32" from MIDX"),
+ i);
+ goto out;
+ }
+ }
+
+ trace2_region_enter("repack", "steps:write", opts->existing->repo);
+
+ /*
+ * The first MIDX in the resulting chain is always going to be
+ * new.
+ *
+ * At a minimum, it will include all of the newly written packs.
+ * If there is an existing MIDX whose tip layer contains packs
+ * that were repacked, it will also include any of its packs
+ * which were *not* rolled up as part of the geometric repack
+ * (if any), and the previous tip will be replaced.
+ *
+ * It may grow to include the packs from zero or more MIDXs from
+ * the old chain, beginning either at the old tip (if the MIDX
+ * was *not* rewritten) or the old tip's base MIDX layer
+ * (otherwise).
+ */
+ step.type = MIDX_COMPACTION_STEP_WRITE;
+ string_list_init_dup(&step.u.write);
+
+ for (i = 0; i < opts->names->nr; i++) {
+ strbuf_reset(&buf);
+ strbuf_addf(&buf, "pack-%s.idx", opts->names->items[i].string);
+ string_list_append(&step.u.write, buf.buf);
+
+ trace2_data_string("repack", opts->existing->repo,
+ "include:fresh",
+ step.u.write.items[step.u.write.nr - 1].string);
+ }
+ for (i = 0; i < opts->geometry->split; i++) {
+ struct packed_git *p = opts->geometry->pack[i];
+ if (unsigned_add_overflows(step.objects_nr, p->num_objects)) {
+ ret = error(_("too many objects in MIDX compaction step"));
+ goto out;
+ }
+
+ step.objects_nr += p->num_objects;
+ }
+ trace2_data_intmax("repack", opts->existing->repo,
+ "include:fresh:objects_nr",
+ (uintmax_t)step.objects_nr);
+
+ /*
+ * Now handle any existing packs which were *not* rewritten.
+ *
+ * The list of packs in opts->geometry only contains MIDX'd
+ * packs from the newest layer when that layer has more than
+ * 'repack.midxNewLayerThreshold' number of packs.
+ *
+ * If the MIDX tip was rewritten (that is, one or more of those
+ * packs appear below the split line), then add all packs above
+ * the split line to the new layer, as the old one is no longer
+ * usable.
+ *
+ * If the MIDX tip was not rewritten (that is, all MIDX'd packs
+ * from the youngest layer appear below the split line, or were
+ * not included in the geometric repack at all because there
+ * were too few of them), ignore them since we'll retain the
+ * existing layer as-is.
+ */
+ for (i = opts->geometry->split; i < opts->geometry->pack_nr; i++) {
+ struct packed_git *p = opts->geometry->pack[i];
+ struct string_list_item *item;
+
+ strbuf_reset(&buf);
+ strbuf_addstr(&buf, pack_basename(p));
+ strbuf_strip_suffix(&buf, ".pack");
+ strbuf_addstr(&buf, ".idx");
+
+ if (p->multi_pack_index &&
+ !opts->geometry->midx_tip_rewritten) {
+ trace2_data_string("repack", opts->existing->repo,
+ "exclude:unmodified", buf.buf);
+ continue;
+ }
+
+ trace2_data_string("repack", opts->existing->repo,
+ "include:unmodified", buf.buf);
+ trace2_data_string("repack", opts->existing->repo,
+ "include:unmodified:midx",
+ p->multi_pack_index ? "true" : "false");
+
+ item = string_list_append(&step.u.write, buf.buf);
+ if (p->multi_pack_index || i == opts->geometry->pack_nr - 1)
+ item->util = (void *)1; /* mark as preferred */
+
+ if (unsigned_add_overflows(step.objects_nr, p->num_objects)) {
+ ret = error(_("too many objects in MIDX compaction step"));
+ goto out;
+ }
+
+ step.objects_nr += p->num_objects;
+ }
+ trace2_data_intmax("repack", opts->existing->repo,
+ "include:unmodified:objects_nr",
+ (uintmax_t)step.objects_nr);
+
+ /*
+ * If the MIDX tip was rewritten, then we no longer consider it
+ * a candidate for compaction, since it will not exist in the
+ * MIDX chain being built.
+ */
+ if (opts->geometry->midx_tip_rewritten)
+ m = m->base_midx;
+
+ trace2_data_string("repack", opts->existing->repo, "midx:rewrote-tip",
+ opts->geometry->midx_tip_rewritten ? "true" : "false");
+
+ trace2_region_enter("repack", "compact", opts->existing->repo);
+
+ /*
+ * Compact additional MIDX layers into this proposed one until
+ * the merging condition is violated.
+ */
+ while (m) {
+ uint32_t preferred_pack_idx;
+
+ trace2_data_string("repack", opts->existing->repo,
+ "candidate", midx_get_checksum_hex(m));
+
+ if (step.objects_nr < m->num_objects / opts->midx_split_factor) {
+ /*
+ * Stop compacting MIDX layer as soon as the
+ * merged size is less than half the size of the
+ * next layer in the chain.
+ */
+ trace2_data_string("repack", opts->existing->repo,
+ "compact", "violated");
+ trace2_data_intmax("repack", opts->existing->repo,
+ "objects_nr",
+ (uintmax_t)step.objects_nr);
+ trace2_data_intmax("repack", opts->existing->repo,
+ "next_objects_nr",
+ (uintmax_t)m->num_objects);
+ trace2_data_intmax("repack", opts->existing->repo,
+ "split_factor",
+ (uintmax_t)opts->midx_split_factor);
+
+ break;
+ }
+
+ if (midx_preferred_pack(m, &preferred_pack_idx) < 0) {
+ ret = error(_("could not find preferred pack for MIDX "
+ "%s"), midx_get_checksum_hex(m));
+ goto out;
+ }
+
+ for (i = 0; i < m->num_packs; i++) {
+ struct string_list_item *item;
+ uint32_t pack_int_id = i + m->num_packs_in_base;
+ struct packed_git *p = nth_midxed_pack(m, pack_int_id);
+
+ strbuf_reset(&buf);
+ strbuf_addstr(&buf, pack_basename(p));
+ strbuf_strip_suffix(&buf, ".pack");
+ strbuf_addstr(&buf, ".idx");
+
+ trace2_data_string("repack", opts->existing->repo,
+ "midx:pack", buf.buf);
+
+ item = string_list_append(&step.u.write, buf.buf);
+ if (pack_int_id == preferred_pack_idx)
+ item->util = (void *)1; /* mark as preferred */
+ }
+
+ if (unsigned_add_overflows(step.objects_nr, m->num_objects)) {
+ ret = error(_("too many objects in MIDX compaction step"));
+ goto out;
+ }
+ step.objects_nr += m->num_objects;
+
+ m = m->base_midx;
+ }
+
+ if (step.u.write.nr > 0) {
+ /*
+ * As long as there is at least one new pack to write
+ * (and thus the MIDX is non-empty), add it to the plan.
+ */
+ ALLOC_GROW(steps, steps_nr + 1, steps_alloc);
+ steps[steps_nr++] = step;
+ }
+
+ trace2_data_intmax("repack", opts->existing->repo,
+ "step:objects_nr", (uintmax_t)step.objects_nr);
+ trace2_data_intmax("repack", opts->existing->repo,
+ "step:packs_nr", (uintmax_t)step.u.write.nr);
+
+ trace2_region_leave("repack", "compact", opts->existing->repo);
+ trace2_region_leave("repack", "steps:write", opts->existing->repo);
+
+ trace2_region_enter("repack", "steps:rest", opts->existing->repo);
+
+ /*
+ * Then start over, repeat, and either compact or keep as-is
+ * each MIDX layer until we have exhausted the chain.
+ *
+ * Finally, evaluate the remainder of the chain (if any) and
+ * either compact a sequence of adjacent layers, or keep
+ * individual layers as-is according to the same merging
+ * condition as above.
+ */
+ while (m) {
+ struct multi_pack_index *next = m;
+
+ ALLOC_GROW(steps, steps_nr + 1, steps_alloc);
+
+ memset(&step, 0, sizeof(step));
+ step.type = MIDX_COMPACTION_STEP_UNKNOWN;
+
+ trace2_region_enter("repack", "step", opts->existing->repo);
+
+ trace2_data_string("repack", opts->existing->repo,
+ "from", midx_get_checksum_hex(m));
+
+ while (next) {
+ uint32_t proposed_objects_nr;
+ if (unsigned_add_overflows(step.objects_nr, next->num_objects)) {
+ ret = error(_("too many objects in MIDX compaction step"));
+ trace2_region_leave("repack", "step", opts->existing->repo);
+ goto out;
+ }
+
+ proposed_objects_nr = step.objects_nr + next->num_objects;
+
+ trace2_data_string("repack", opts->existing->repo,
+ "proposed",
+ midx_get_checksum_hex(next));
+ trace2_data_intmax("repack", opts->existing->repo,
+ "proposed:objects_nr",
+ (uintmax_t)next->num_objects);
+
+ if (!next->base_midx) {
+ /*
+ * If we are at the end of the MIDX
+ * chain, there is nothing to compact,
+ * so mark it and stop.
+ */
+ step.objects_nr = proposed_objects_nr;
+ break;
+ }
+
+ if (proposed_objects_nr < next->base_midx->num_objects / opts->midx_split_factor) {
+ /*
+ * If there is a MIDX following this
+ * one, but our accumulated size is less
+ * than half of its size, compacting
+ * them would violate the merging
+ * condition, so stop here.
+ */
+
+ trace2_data_string("repack", opts->existing->repo,
+ "compact:violated:at",
+ midx_get_checksum_hex(next->base_midx));
+ trace2_data_intmax("repack", opts->existing->repo,
+ "compact:violated:at:objects_nr",
+ (uintmax_t)next->base_midx->num_objects);
+ break;
+ }
+
+ /*
+ * Otherwise, it is OK to compact the next layer
+ * into this one. Do so, and then continue
+ * through the remainder of the chain.
+ */
+ step.objects_nr = proposed_objects_nr;
+ trace2_data_intmax("repack", opts->existing->repo,
+ "step:objects_nr",
+ (uintmax_t)step.objects_nr);
+ next = next->base_midx;
+ }
+
+ if (m == next) {
+ step.type = MIDX_COMPACTION_STEP_COPY;
+ step.u.copy = m;
+
+ trace2_data_string("repack", opts->existing->repo,
+ "type", "copy");
+ } else {
+ step.type = MIDX_COMPACTION_STEP_COMPACT;
+ step.u.compact.from = next;
+ step.u.compact.to = m;
+
+ trace2_data_string("repack", opts->existing->repo,
+ "to", midx_get_checksum_hex(m));
+ trace2_data_string("repack", opts->existing->repo,
+ "type", "compact");
+ }
+
+ m = next->base_midx;
+ steps[steps_nr++] = step;
+ trace2_region_leave("repack", "step", opts->existing->repo);
+ }
+
+ trace2_region_leave("repack", "steps:rest", opts->existing->repo);
+
+out:
+ *steps_p = steps;
+ *steps_nr_p = steps_nr;
+
+ strbuf_release(&buf);
+
+ trace2_region_leave("repack", "make_midx_compaction_plan",
+ opts->existing->repo);
+
+ return ret;
+}
+
+static int write_midx_incremental(struct repack_write_midx_opts *opts)
+{
+ struct midx_compaction_step *steps = NULL;
+ struct strbuf lock_name = STRBUF_INIT;
+ struct lock_file lf;
+ size_t steps_nr = 0;
+ size_t i;
+ int ret = 0;
+
+ get_midx_chain_filename(opts->existing->source, &lock_name);
+ if (safe_create_leading_directories(opts->existing->repo,
+ lock_name.buf))
+ die_errno(_("unable to create leading directories of %s"),
+ lock_name.buf);
+ hold_lock_file_for_update(&lf, lock_name.buf, LOCK_DIE_ON_ERROR);
+
+ if (!fdopen_lock_file(&lf, "w")) {
+ ret = error_errno(_("unable to open multi-pack-index chain file"));
+ goto done;
+ }
+
+ if (repack_make_midx_compaction_plan(opts, &steps, &steps_nr) < 0) {
+ ret = error(_("unable to generate compaction plan"));
+ goto done;
+ }
+
+ for (i = 0; i < steps_nr; i++) {
+ struct midx_compaction_step *step = &steps[i];
+ char *base = NULL;
+
+ if (i + 1 < steps_nr)
+ base = xstrdup(midx_compaction_step_base(&steps[i + 1]));
+
+ if (midx_compaction_step_exec(step, opts, base) < 0) {
+ ret = error(_("unable to execute compaction step %"PRIuMAX),
+ (uintmax_t)i);
+ free(base);
+ goto done;
+ }
+
+ free(base);
+ }
+
+ i = steps_nr;
+ while (i--) {
+ struct midx_compaction_step *step = &steps[i];
+ if (!step->csum)
+ BUG("missing result for compaction step %"PRIuMAX,
+ (uintmax_t)i);
+ fprintf(get_lock_file_fp(&lf), "%s\n", step->csum);
+ }
+
+ commit_lock_file(&lf);
+
+done:
+ strbuf_release(&lock_name);
+ for (i = 0; i < steps_nr; i++)
+ midx_compaction_step_release(&steps[i]);
+ free(steps);
+ return ret;
+}
+
int repack_write_midx(struct repack_write_midx_opts *opts)
{
switch (opts->mode) {
@@ -386,6 +958,8 @@ int repack_write_midx(struct repack_write_midx_opts *opts)
BUG("write_midx mode is NONE?");
case REPACK_WRITE_MIDX_DEFAULT:
return write_midx_included_packs(opts);
+ case REPACK_WRITE_MIDX_INCREMENTAL:
+ return write_midx_incremental(opts);
default:
BUG("unhandled write_midx mode: %d", opts->mode);
}
diff --git a/repack.h b/repack.h
index 81907fcce7f..831ccfb1c6c 100644
--- a/repack.h
+++ b/repack.h
@@ -137,6 +137,7 @@ struct tempfile;
enum repack_write_midx_mode {
REPACK_WRITE_MIDX_NONE,
REPACK_WRITE_MIDX_DEFAULT,
+ REPACK_WRITE_MIDX_INCREMENTAL,
};
struct repack_write_midx_opts {
@@ -148,6 +149,8 @@ struct repack_write_midx_opts {
int show_progress;
int write_bitmaps;
int midx_must_contain_cruft;
+ int midx_split_factor;
+ int midx_new_layer_threshold;
enum repack_write_midx_mode mode;
};
--
2.54.0.9.gb905fd5d0ae
^ permalink raw reply related
* [PATCH v2 13/16] packfile: ensure `close_pack_revindex()` frees in-memory revindex
From: Taylor Blau @ 2026-04-21 20:37 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1776803827.git.me@ttaylorr.com>
The following commit will introduce a case where we write a MIDX bitmap
over packs that do not themselves have on-disk *.rev files.
This case is supported within Git, and we will simply fall back to
generating the revindex in memory. But we don't ever release that
memory, causing a leak that is exposed by a test introduced in the
following commit.
(As far as I could find, we never free()'d memory allocated as a
byproduct of creating an in-memory revindex, likely because that code
predates the leak-checking niceties we have in the test suite now.)
Rectify this by calling `FREE_AND_NULL()` on the `p->revindex` field
when calling `close_pack_revindex()`.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
packfile.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/packfile.c b/packfile.c
index b012d648ada..a1e88fdb223 100644
--- a/packfile.c
+++ b/packfile.c
@@ -420,6 +420,8 @@ void close_pack_index(struct packed_git *p)
static void close_pack_revindex(struct packed_git *p)
{
+ FREE_AND_NULL(p->revindex);
+
if (!p->revindex_map)
return;
--
2.54.0.9.gb905fd5d0ae
^ permalink raw reply related
* [PATCH v2 12/16] builtin/repack.c: convert `--write-midx` to an `OPT_CALLBACK`
From: Taylor Blau @ 2026-04-21 20:37 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1776803827.git.me@ttaylorr.com>
Change the --write-midx (-m) flag from an OPT_BOOL to an OPT_CALLBACK
that accepts an optional mode argument. Introduce an enum with
REPACK_WRITE_MIDX_NONE and REPACK_WRITE_MIDX_DEFAULT to distinguish
between the two states, and update all existing boolean checks
accordingly.
For now, passing no argument (or just `-m`) selects the default mode,
preserving existing behavior. A subsequent commit will add a new mode
for writing incremental MIDXs.
Extract repack_write_midx() as a dispatcher that selects the
appropriate MIDX-writing implementation based on the mode.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
builtin/repack.c | 50 ++++++++++++++++++++++++++++++++++++------------
repack-midx.c | 14 +++++++++++++-
repack.h | 8 +++++++-
3 files changed, 58 insertions(+), 14 deletions(-)
diff --git a/builtin/repack.c b/builtin/repack.c
index 24be147d39a..5d366340c34 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -97,6 +97,24 @@ static int repack_config(const char *var, const char *value,
return git_default_config(var, value, ctx, cb);
}
+static int option_parse_write_midx(const struct option *opt, const char *arg,
+ int unset)
+{
+ enum repack_write_midx_mode *cfg = opt->value;
+
+ if (unset) {
+ *cfg = REPACK_WRITE_MIDX_NONE;
+ return 0;
+ }
+
+ if (!arg || !*arg)
+ *cfg = REPACK_WRITE_MIDX_DEFAULT;
+ else
+ return error(_("unknown value for %s: %s"), opt->long_name, arg);
+
+ return 0;
+}
+
int cmd_repack(int argc,
const char **argv,
const char *prefix,
@@ -119,7 +137,7 @@ int cmd_repack(int argc,
struct string_list keep_pack_list = STRING_LIST_INIT_NODUP;
struct pack_objects_args po_args = PACK_OBJECTS_ARGS_INIT;
struct pack_objects_args cruft_po_args = PACK_OBJECTS_ARGS_INIT;
- int write_midx = 0;
+ enum repack_write_midx_mode write_midx = REPACK_WRITE_MIDX_NONE;
const char *cruft_expiration = NULL;
const char *expire_to = NULL;
const char *filter_to = NULL;
@@ -185,8 +203,14 @@ int cmd_repack(int argc,
N_("do not repack this pack")),
OPT_INTEGER('g', "geometric", &geometry.split_factor,
N_("find a geometric progression with factor <N>")),
- OPT_BOOL('m', "write-midx", &write_midx,
- N_("write a multi-pack index of the resulting packs")),
+ OPT_CALLBACK_F(0, "write-midx", &write_midx,
+ N_("mode"),
+ N_("write a multi-pack index of the resulting packs"),
+ PARSE_OPT_OPTARG, option_parse_write_midx),
+ OPT_SET_INT_F('m', NULL, &write_midx,
+ N_("write a multi-pack index of the resulting packs"),
+ REPACK_WRITE_MIDX_DEFAULT,
+ PARSE_OPT_HIDDEN),
OPT_STRING(0, "expire-to", &expire_to, N_("dir"),
N_("pack prefix to store a pack containing pruned objects")),
OPT_STRING(0, "filter-to", &filter_to, N_("dir"),
@@ -221,14 +245,16 @@ int cmd_repack(int argc,
pack_everything |= ALL_INTO_ONE;
if (write_bitmaps < 0) {
- if (!write_midx &&
+ if (write_midx == REPACK_WRITE_MIDX_NONE &&
(!(pack_everything & ALL_INTO_ONE) || !is_bare_repository()))
write_bitmaps = 0;
}
if (po_args.pack_kept_objects < 0)
- po_args.pack_kept_objects = write_bitmaps > 0 && !write_midx;
+ po_args.pack_kept_objects = write_bitmaps > 0 &&
+ write_midx == REPACK_WRITE_MIDX_NONE;
- if (write_bitmaps && !(pack_everything & ALL_INTO_ONE) && !write_midx)
+ if (write_bitmaps && !(pack_everything & ALL_INTO_ONE) &&
+ write_midx == REPACK_WRITE_MIDX_NONE)
die(_(incremental_bitmap_conflict_error));
if (write_bitmaps && po_args.local &&
@@ -244,7 +270,7 @@ int cmd_repack(int argc,
write_bitmaps = 0;
}
- if (write_midx && write_bitmaps) {
+ if (write_midx != REPACK_WRITE_MIDX_NONE && write_bitmaps) {
struct strbuf path = STRBUF_INIT;
strbuf_addf(&path, "%s/%s_XXXXXX",
@@ -297,7 +323,7 @@ int cmd_repack(int argc,
}
if (repo_has_promisor_remote(repo))
strvec_push(&cmd.args, "--exclude-promisor-objects");
- if (!write_midx) {
+ if (write_midx == REPACK_WRITE_MIDX_NONE) {
if (write_bitmaps > 0)
strvec_push(&cmd.args, "--write-bitmap-index");
else if (write_bitmaps < 0)
@@ -519,7 +545,7 @@ int cmd_repack(int argc,
if (delete_redundant && pack_everything & ALL_INTO_ONE)
existing_packs_mark_for_deletion(&existing, &names);
- if (write_midx) {
+ if (write_midx != REPACK_WRITE_MIDX_NONE) {
struct repack_write_midx_opts opts = {
.existing = &existing,
.geometry = &geometry,
@@ -528,11 +554,11 @@ int cmd_repack(int argc,
.packdir = packdir,
.show_progress = show_progress,
.write_bitmaps = write_bitmaps > 0,
- .midx_must_contain_cruft = midx_must_contain_cruft
+ .midx_must_contain_cruft = midx_must_contain_cruft,
+ .mode = write_midx,
};
- ret = write_midx_included_packs(&opts);
-
+ ret = repack_write_midx(&opts);
if (ret)
goto cleanup;
}
diff --git a/repack-midx.c b/repack-midx.c
index 78f069c2151..4a568a2a9b8 100644
--- a/repack-midx.c
+++ b/repack-midx.c
@@ -315,7 +315,7 @@ static int repack_fill_midx_stdin_packs(struct child_process *cmd,
return finish_command(cmd);
}
-int write_midx_included_packs(struct repack_write_midx_opts *opts)
+static int write_midx_included_packs(struct repack_write_midx_opts *opts)
{
struct child_process cmd = CHILD_PROCESS_INIT;
struct string_list include = STRING_LIST_INIT_DUP;
@@ -378,3 +378,15 @@ int write_midx_included_packs(struct repack_write_midx_opts *opts)
return ret;
}
+
+int repack_write_midx(struct repack_write_midx_opts *opts)
+{
+ switch (opts->mode) {
+ case REPACK_WRITE_MIDX_NONE:
+ BUG("write_midx mode is NONE?");
+ case REPACK_WRITE_MIDX_DEFAULT:
+ return write_midx_included_packs(opts);
+ default:
+ BUG("unhandled write_midx mode: %d", opts->mode);
+ }
+}
diff --git a/repack.h b/repack.h
index 77d24ee45fb..81907fcce7f 100644
--- a/repack.h
+++ b/repack.h
@@ -134,6 +134,11 @@ void pack_geometry_release(struct pack_geometry *geometry);
struct tempfile;
+enum repack_write_midx_mode {
+ REPACK_WRITE_MIDX_NONE,
+ REPACK_WRITE_MIDX_DEFAULT,
+};
+
struct repack_write_midx_opts {
struct existing_packs *existing;
struct pack_geometry *geometry;
@@ -143,10 +148,11 @@ struct repack_write_midx_opts {
int show_progress;
int write_bitmaps;
int midx_must_contain_cruft;
+ enum repack_write_midx_mode mode;
};
void midx_snapshot_refs(struct repository *repo, struct tempfile *f);
-int write_midx_included_packs(struct repack_write_midx_opts *opts);
+int repack_write_midx(struct repack_write_midx_opts *opts);
int write_filtered_pack(const struct write_pack_opts *opts,
struct existing_packs *existing,
--
2.54.0.9.gb905fd5d0ae
^ permalink raw reply related
* [PATCH v2 11/16] repack-geometry: prepare for incremental MIDX repacking
From: Taylor Blau @ 2026-04-21 20:37 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1776803827.git.me@ttaylorr.com>
Teach `pack_geometry_init()` to optionally restrict the set of
repacking candidates to only packs in the tip MIDX layer when a
`midx_layer_threshold` is configured. If the tip layer has fewer packs
than the threshold, those packs are excluded entirely; otherwise only
packs in that layer participate in the geometric repack.
Also track whether any tip-layer packs were included in the rollup
(`midx_tip_rewritten`), which a subsequent commit will use to decide
how to update the MIDX chain after repacking.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
repack-geometry.c | 35 +++++++++++++++++++++++++++++++++++
repack.h | 4 ++++
2 files changed, 39 insertions(+)
diff --git a/repack-geometry.c b/repack-geometry.c
index 7cebd0cb45f..2408b8a3cc2 100644
--- a/repack-geometry.c
+++ b/repack-geometry.c
@@ -4,6 +4,7 @@
#include "repack.h"
#include "repository.h"
#include "hex.h"
+#include "midx.h"
#include "packfile.h"
static uint32_t pack_geometry_weight(struct packed_git *p)
@@ -31,8 +32,28 @@ void pack_geometry_init(struct pack_geometry *geometry,
{
struct packed_git *p;
struct strbuf buf = STRBUF_INIT;
+ struct multi_pack_index *m = get_multi_pack_index(existing->source);
repo_for_each_pack(existing->repo, p) {
+ if (geometry->midx_layer_threshold_set && m &&
+ p->multi_pack_index) {
+ /*
+ * When writing MIDX layers incrementally,
+ * ignore packs unless they are in the most
+ * recent MIDX layer *and* there are at least
+ * 'midx_layer_threshold' packs in that layer.
+ *
+ * Otherwise 'p' is either in an older layer, or
+ * the youngest layer does not have enough packs
+ * to consider its packs as candidates for
+ * repacking. In either of those cases we want
+ * to ignore the pack.
+ */
+ if (m->num_packs < geometry->midx_layer_threshold ||
+ !midx_layer_contains_pack(m, pack_basename(p)))
+ continue;
+ }
+
if (args->local && !p->pack_local)
/*
* When asked to only repack local packfiles we skip
@@ -173,6 +194,20 @@ void pack_geometry_split(struct pack_geometry *geometry)
geometry->promisor_split = compute_pack_geometry_split(geometry->promisor_pack,
geometry->promisor_pack_nr,
geometry->split_factor);
+ for (uint32_t i = 0; i < geometry->split; i++) {
+ struct packed_git *p = geometry->pack[i];
+ /*
+ * During incremental MIDX/bitmap repacking, any packs
+ * included in the rollup are either (a) not MIDX'd, or
+ * (b) contained in the tip layer iff it has at least
+ * the threshold number of packs.
+ *
+ * In the latter case, we can safely conclude that the
+ * tip of the MIDX chain will be rewritten.
+ */
+ if (p->multi_pack_index)
+ geometry->midx_tip_rewritten = true;
+ }
}
struct packed_git *pack_geometry_preferred_pack(struct pack_geometry *geometry)
diff --git a/repack.h b/repack.h
index c0e9f0ca647..77d24ee45fb 100644
--- a/repack.h
+++ b/repack.h
@@ -108,6 +108,10 @@ struct pack_geometry {
uint32_t promisor_pack_nr, promisor_pack_alloc;
uint32_t promisor_split;
+ uint32_t midx_layer_threshold;
+ bool midx_layer_threshold_set;
+ bool midx_tip_rewritten;
+
int split_factor;
};
--
2.54.0.9.gb905fd5d0ae
^ permalink raw reply related
* [PATCH v2 10/16] repack-midx: extract `repack_fill_midx_stdin_packs()`
From: Taylor Blau @ 2026-04-21 20:37 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1776803827.git.me@ttaylorr.com>
The function `write_midx_included_packs()` manages the lifecycle of
writing packs to stdin when running `git multi-pack-index write` as a
child process.
Extract a standalone `repack_fill_midx_stdin_packs()` helper, which
handles `--stdin-packs` argument setup, starting the command, writing
pack names to its standard input, and finishing the command.
This simplifies `write_midx_included_packs()` and prepares for a
subsequent commit where the same helper is called with `cmd->out = -1`
to capture the MIDX's checksum from the command's standard output,
which is needed when writing MIDX layers with `--checksum-only`.
No functional changes are included in this patch.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
repack-midx.c | 38 ++++++++++++++++++++++++--------------
1 file changed, 24 insertions(+), 14 deletions(-)
diff --git a/repack-midx.c b/repack-midx.c
index 83151d4734a..78f069c2151 100644
--- a/repack-midx.c
+++ b/repack-midx.c
@@ -292,23 +292,42 @@ static void repack_prepare_midx_command(struct child_process *cmd,
strvec_push(&cmd->args, "--bitmap");
}
+static int repack_fill_midx_stdin_packs(struct child_process *cmd,
+ struct string_list *include)
+{
+ struct string_list_item *item;
+ FILE *in;
+ int ret;
+
+ cmd->in = -1;
+
+ strvec_push(&cmd->args, "--stdin-packs");
+
+ ret = start_command(cmd);
+ if (ret)
+ return ret;
+
+ in = xfdopen(cmd->in, "w");
+ for_each_string_list_item(item, include)
+ fprintf(in, "%s\n", item->string);
+ fclose(in);
+
+ return finish_command(cmd);
+}
+
int write_midx_included_packs(struct repack_write_midx_opts *opts)
{
struct child_process cmd = CHILD_PROCESS_INIT;
struct string_list include = STRING_LIST_INIT_DUP;
struct string_list_item *item;
struct packed_git *preferred = pack_geometry_preferred_pack(opts->geometry);
- FILE *in;
int ret = 0;
midx_included_packs(&include, opts);
if (!include.nr)
goto done;
- cmd.in = -1;
-
repack_prepare_midx_command(&cmd, opts, "write");
- strvec_push(&cmd.args, "--stdin-packs");
if (preferred)
strvec_pushf(&cmd.args, "--preferred-pack=%s",
@@ -350,16 +369,7 @@ int write_midx_included_packs(struct repack_write_midx_opts *opts)
strvec_pushf(&cmd.args, "--refs-snapshot=%s",
opts->refs_snapshot);
- ret = start_command(&cmd);
- if (ret)
- goto done;
-
- in = xfdopen(cmd.in, "w");
- for_each_string_list_item(item, &include)
- fprintf(in, "%s\n", item->string);
- fclose(in);
-
- ret = finish_command(&cmd);
+ ret = repack_fill_midx_stdin_packs(&cmd, &include);
done:
if (!ret && opts->write_bitmaps)
remove_redundant_bitmaps(&include, opts->packdir);
--
2.54.0.9.gb905fd5d0ae
^ permalink raw reply related
* [PATCH v2 09/16] repack-midx: factor out `repack_prepare_midx_command()`
From: Taylor Blau @ 2026-04-21 20:37 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1776803827.git.me@ttaylorr.com>
The `write_midx_included_packs()` function assembles and executes a
`git multi-pack-index write` command, constructing the argument list
inline.
Future commits will introduce additional callers that need to construct
similar `git multi-pack-index` commands (for both `write` and `compact`
subcommands), so extract the common portions of the command setup into a
reusable `repack_prepare_midx_command()` helper.
The extracted helper sets `git_cmd`, pushes the `multi-pack-index`
subcommand and verb, and handles `--progress`/`--no-progress` and
`--bitmap` flags. The remaining arguments that are specific to the
`write` subcommand (such as `--stdin-packs`) are left to the caller.
No functional changes are included in this patch.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
repack-midx.c | 30 +++++++++++++++++++-----------
1 file changed, 19 insertions(+), 11 deletions(-)
diff --git a/repack-midx.c b/repack-midx.c
index 0682b80c427..83151d4734a 100644
--- a/repack-midx.c
+++ b/repack-midx.c
@@ -275,6 +275,23 @@ static void remove_redundant_bitmaps(struct string_list *include,
strbuf_release(&path);
}
+static void repack_prepare_midx_command(struct child_process *cmd,
+ struct repack_write_midx_opts *opts,
+ const char *verb)
+{
+ cmd->git_cmd = 1;
+
+ strvec_pushl(&cmd->args, "multi-pack-index", verb, NULL);
+
+ if (opts->show_progress)
+ strvec_push(&cmd->args, "--progress");
+ else
+ strvec_push(&cmd->args, "--no-progress");
+
+ if (opts->write_bitmaps)
+ strvec_push(&cmd->args, "--bitmap");
+}
+
int write_midx_included_packs(struct repack_write_midx_opts *opts)
{
struct child_process cmd = CHILD_PROCESS_INIT;
@@ -289,18 +306,9 @@ int write_midx_included_packs(struct repack_write_midx_opts *opts)
goto done;
cmd.in = -1;
- cmd.git_cmd = 1;
- strvec_push(&cmd.args, "multi-pack-index");
- strvec_pushl(&cmd.args, "write", "--stdin-packs", NULL);
-
- if (opts->show_progress)
- strvec_push(&cmd.args, "--progress");
- else
- strvec_push(&cmd.args, "--no-progress");
-
- if (opts->write_bitmaps)
- strvec_push(&cmd.args, "--bitmap");
+ repack_prepare_midx_command(&cmd, opts, "write");
+ strvec_push(&cmd.args, "--stdin-packs");
if (preferred)
strvec_pushf(&cmd.args, "--preferred-pack=%s",
--
2.54.0.9.gb905fd5d0ae
^ permalink raw reply related
* [PATCH v2 08/16] midx: expose `midx_layer_contains_pack()`
From: Taylor Blau @ 2026-04-21 20:37 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1776803827.git.me@ttaylorr.com>
Rename the function `midx_contains_pack_1()` to instead be called
`midx_layer_contains_pack()` and make it accessible. Unlike
`midx_contains_pack()` (which recurses through the entire chain), this
function checks only a single MIDX layer.
This will be used by a subsequent commit to determine whether a given
pack belongs to the tip MIDX layer specifically, rather than to any
layer in the chain.
No functional changes are present in this commit.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
midx.c | 6 +++---
midx.h | 2 ++
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/midx.c b/midx.c
index bcb8c999015..dc86c8e7fee 100644
--- a/midx.c
+++ b/midx.c
@@ -667,8 +667,8 @@ static int midx_pack_names_cmp(const void *a, const void *b, void *m_)
m->pack_names[*(const size_t *)b]);
}
-static int midx_contains_pack_1(struct multi_pack_index *m,
- const char *idx_or_pack_name)
+int midx_layer_contains_pack(struct multi_pack_index *m,
+ const char *idx_or_pack_name)
{
uint32_t first = 0, last = m->num_packs;
@@ -709,7 +709,7 @@ static int midx_contains_pack_1(struct multi_pack_index *m,
int midx_contains_pack(struct multi_pack_index *m, const char *idx_or_pack_name)
{
for (; m; m = m->base_midx)
- if (midx_contains_pack_1(m, idx_or_pack_name))
+ if (midx_layer_contains_pack(m, idx_or_pack_name))
return 1;
return 0;
}
diff --git a/midx.h b/midx.h
index 77dd66de02b..3ee12dd08ec 100644
--- a/midx.h
+++ b/midx.h
@@ -119,6 +119,8 @@ struct object_id *nth_midxed_object_oid(struct object_id *oid,
int fill_midx_entry(struct multi_pack_index *m, const struct object_id *oid, struct pack_entry *e);
int midx_contains_pack(struct multi_pack_index *m,
const char *idx_or_pack_name);
+int midx_layer_contains_pack(struct multi_pack_index *m,
+ const char *idx_or_pack_name);
int midx_preferred_pack(struct multi_pack_index *m, uint32_t *pack_int_id);
int prepare_multi_pack_index_one(struct odb_source *source);
--
2.54.0.9.gb905fd5d0ae
^ permalink raw reply related
* [PATCH v2 07/16] repack: track the ODB source via existing_packs
From: Taylor Blau @ 2026-04-21 20:37 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1776803827.git.me@ttaylorr.com>
Store the ODB source in the `existing_packs` struct and use that in
place of the raw `repo->objects->sources` access within `cmd_repack()`.
The source used is still assigned from the first source in the list, so
there are no functional changes in this commit. The changes instead
serve two purposes (one immediate, one not):
- The incremental MIDX-based repacking machinery will need to know what
source is being used to read the existing MIDX/chain (should one
exist).
- In the future, if "git repack" is taught how to operate on other
object sources, this field will serve as the authoritative value for
that source.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
builtin/repack.c | 5 ++---
repack.c | 2 ++
repack.h | 1 +
3 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/builtin/repack.c b/builtin/repack.c
index 4c5a82c2c8d..24be147d39a 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -417,7 +417,7 @@ int cmd_repack(int argc,
* midx_has_unknown_packs() will make the decision for
* us.
*/
- if (!get_multi_pack_index(repo->objects->sources))
+ if (!get_multi_pack_index(existing.source))
midx_must_contain_cruft = 1;
}
@@ -564,8 +564,7 @@ int cmd_repack(int argc,
unsigned flags = 0;
if (git_env_bool(GIT_TEST_MULTI_PACK_INDEX_WRITE_INCREMENTAL, 0))
flags |= MIDX_WRITE_INCREMENTAL;
- write_midx_file(repo->objects->sources,
- NULL, NULL, flags);
+ write_midx_file(existing.source, NULL, NULL, flags);
}
cleanup:
diff --git a/repack.c b/repack.c
index 596841027af..2ee6b51420a 100644
--- a/repack.c
+++ b/repack.c
@@ -154,6 +154,8 @@ void existing_packs_collect(struct existing_packs *existing,
string_list_append(&existing->non_kept_packs, buf.buf);
}
+ existing->source = existing->repo->objects->sources;
+
string_list_sort(&existing->kept_packs);
string_list_sort(&existing->non_kept_packs);
string_list_sort(&existing->cruft_packs);
diff --git a/repack.h b/repack.h
index bc9f2e1a5de..c0e9f0ca647 100644
--- a/repack.h
+++ b/repack.h
@@ -56,6 +56,7 @@ struct packed_git;
struct existing_packs {
struct repository *repo;
+ struct odb_source *source;
struct string_list kept_packs;
struct string_list non_kept_packs;
struct string_list cruft_packs;
--
2.54.0.9.gb905fd5d0ae
^ permalink raw reply related
* [PATCH v2 06/16] midx: support custom `--base` for incremental MIDX writes
From: Taylor Blau @ 2026-04-21 20:37 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1776803827.git.me@ttaylorr.com>
Both `compact` and `write --incremental` fix the base of the resulting
MIDX layer: `compact` always places the compacted result on top of
"from's" immediate parent in the chain, and `write --incremental` always
appends a new layer to the existing tip. In both cases the base is not
configurable.
Future callers need additional flexibility. For instance, the incremental
MIDX-based repacking code may wish to write a layer based on some
intermediate ancestor rather than the current tip, or produce a root
layer when replacing the bottommost entries in the chain.
Introduce a new `--base` option for both subcommands to specify the
checksum of the MIDX layer to use as the base. The given checksum must
refer to a valid layer in the MIDX chain that is an ancestor of the
topmost layer being written or compacted.
The special value "none" is accepted to produce a root layer with no
parent. This will be needed when the incremental repacking machinery
determines that the bottommost layers of the chain should be replaced.
If no `--base` is given, behavior is unchanged: `compact` uses "from's"
immediate parent in the chain, and `write` appends to the existing tip.
For the `write` subcommand, `--base` requires `--no-write-chain-file`. A plain
`write --incremental` appends a new layer to the live chain tip with no
mechanism to atomically replace it; overriding the base would produce a
layer that does not extend the tip, breaking chain invariants. With
`--no-write-chain-file` the chain is left unmodified and the caller is
responsible for assembling a valid chain.
For `compact`, no such restriction applies. The compaction operation
atomically replaces the compacted range in the chain file, so writing
the result on top of any valid ancestor preserves chain invariants.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
Documentation/git-multi-pack-index.adoc | 17 +++++-
builtin/multi-pack-index.c | 24 ++++++--
midx-write.c | 34 ++++++++++-
midx.h | 5 +-
t/t5334-incremental-multi-pack-index.sh | 30 ++++++++++
t/t5335-compact-multi-pack-index.sh | 77 +++++++++++++++++++++++++
6 files changed, 178 insertions(+), 9 deletions(-)
diff --git a/Documentation/git-multi-pack-index.adoc b/Documentation/git-multi-pack-index.adoc
index c26196815e2..c6d23aeeb9a 100644
--- a/Documentation/git-multi-pack-index.adoc
+++ b/Documentation/git-multi-pack-index.adoc
@@ -12,8 +12,10 @@ SYNOPSIS
'git multi-pack-index' [<options>] write [--preferred-pack=<pack>]
[--[no-]bitmap] [--[no-]incremental] [--[no-]stdin-packs]
[--refs-snapshot=<path>] [--[no-]write-chain-file]
+ [--base=<checksum>]
'git multi-pack-index' [<options>] compact [--[no-]incremental]
- [--[no-]bitmap] [--[no-]write-chain-file] <from> <to>
+ [--[no-]bitmap] [--base=<checksum>] [--[no-]write-chain-file]
+ <from> <to>
'git multi-pack-index' [<options>] verify
'git multi-pack-index' [<options>] expire
'git multi-pack-index' [<options>] repack [--batch-size=<size>]
@@ -90,6 +92,13 @@ marker).
The checksum of the new layer is printed to standard
output, allowing the caller to assemble and write the
chain itself. Requires `--incremental`.
+
+ --base=<checksum>::
+ Specify the checksum of an existing MIDX layer to use
+ as the base when writing a new incremental layer.
+ The special value `none` indicates that the new layer
+ should have no base (i.e., it becomes a root layer).
+ Requires `--no-write-chain-file`.
--
compact::
@@ -110,6 +119,12 @@ compact::
MIDX layer but do not update the multi-pack-index-chain
file. The checksum of the new layer is printed to
standard output. Requires `--incremental`.
+
+ --base=<checksum>::
+ Specify the checksum of an existing MIDX layer to use
+ as the base for the compacted result, instead of using
+ the immediate parent of `<from>`. The special value
+ `none` indicates that the result should have no base.
--
+
Note that the compact command requires writing a version-2 midx that
diff --git a/builtin/multi-pack-index.c b/builtin/multi-pack-index.c
index f861b4b8394..00ffb36394d 100644
--- a/builtin/multi-pack-index.c
+++ b/builtin/multi-pack-index.c
@@ -16,11 +16,13 @@
#define BUILTIN_MIDX_WRITE_USAGE \
N_("git multi-pack-index [<options>] write [--preferred-pack=<pack>]\n" \
" [--[no-]bitmap] [--[no-]incremental] [--[no-]stdin-packs]\n" \
- " [--refs-snapshot=<path>] [--[no-]write-chain-file]")
+ " [--refs-snapshot=<path>] [--[no-]write-chain-file]\n" \
+ " [--base=<checksum>]")
#define BUILTIN_MIDX_COMPACT_USAGE \
N_("git multi-pack-index [<options>] compact [--[no-]incremental]\n" \
- " [--[no-]bitmap] [--[no-]write-chain-file] <from> <to>")
+ " [--[no-]bitmap] [--base=<checksum>] [--[no-]write-chain-file]\n" \
+ " <from> <to>")
#define BUILTIN_MIDX_VERIFY_USAGE \
N_("git multi-pack-index [<options>] verify")
@@ -63,6 +65,7 @@ static char const * const builtin_multi_pack_index_usage[] = {
static struct opts_multi_pack_index {
char *object_dir;
const char *preferred_pack;
+ const char *incremental_base;
char *refs_snapshot;
unsigned long batch_size;
unsigned flags;
@@ -151,6 +154,8 @@ static int cmd_multi_pack_index_write(int argc, const char **argv,
N_("pack for reuse when computing a multi-pack bitmap")),
OPT_BIT(0, "bitmap", &opts.flags, N_("write multi-pack bitmap"),
MIDX_WRITE_BITMAP | MIDX_WRITE_REV_INDEX),
+ OPT_STRING(0, "base", &opts.incremental_base, N_("checksum"),
+ N_("base MIDX for incremental writes")),
OPT_BIT(0, "incremental", &opts.flags,
N_("write a new incremental MIDX"), MIDX_WRITE_INCREMENTAL),
OPT_NEGBIT(0, "write-chain-file", &opts.flags,
@@ -190,6 +195,13 @@ static int cmd_multi_pack_index_write(int argc, const char **argv,
options);
}
+ if (opts.incremental_base &&
+ !(opts.flags & MIDX_WRITE_NO_CHAIN)) {
+ error(_("cannot use --base without --no-write-chain-file"));
+ usage_with_options(builtin_multi_pack_index_write_usage,
+ options);
+ }
+
source = handle_object_dir_option(repo);
FREE_AND_NULL(options);
@@ -201,7 +213,8 @@ static int cmd_multi_pack_index_write(int argc, const char **argv,
ret = write_midx_file_only(source, &packs,
opts.preferred_pack,
- opts.refs_snapshot, opts.flags);
+ opts.refs_snapshot,
+ opts.incremental_base, opts.flags);
string_list_clear(&packs, 0);
free(opts.refs_snapshot);
@@ -229,6 +242,8 @@ static int cmd_multi_pack_index_compact(int argc, const char **argv,
struct option *options;
static struct option builtin_multi_pack_index_compact_options[] = {
+ OPT_STRING(0, "base", &opts.incremental_base, N_("checksum"),
+ N_("base MIDX for incremental writes")),
OPT_BIT(0, "bitmap", &opts.flags, N_("write multi-pack bitmap"),
MIDX_WRITE_BITMAP | MIDX_WRITE_REV_INDEX),
OPT_BIT(0, "incremental", &opts.flags,
@@ -290,7 +305,8 @@ static int cmd_multi_pack_index_compact(int argc, const char **argv,
die(_("MIDX %s must be an ancestor of %s"), argv[0], argv[1]);
}
- ret = write_midx_file_compact(source, from_midx, to_midx, opts.flags);
+ ret = write_midx_file_compact(source, from_midx, to_midx,
+ opts.incremental_base, opts.flags);
return ret;
}
diff --git a/midx-write.c b/midx-write.c
index 38c898e5ff5..561e9eedc0e 100644
--- a/midx-write.c
+++ b/midx-write.c
@@ -1247,6 +1247,7 @@ struct write_midx_opts {
const char *preferred_pack_name;
const char *refs_snapshot;
+ const char *incremental_base;
unsigned flags;
};
@@ -1330,11 +1331,32 @@ static int write_midx_internal(struct write_midx_opts *opts)
/*
* If compacting MIDX layer(s) in the range [from, to], then the
- * compacted MIDX will share the same base MIDX as 'from'.
+ * compacted MIDX will share the same base MIDX as 'from',
+ * unless a custom --base is specified (see below).
*/
if (ctx.compact)
ctx.base_midx = ctx.compact_from->base_midx;
+ if (opts->incremental_base) {
+ if (!strcmp(opts->incremental_base, "none")) {
+ ctx.base_midx = NULL;
+ } else {
+ while (ctx.base_midx) {
+ const char *cmp = midx_get_checksum_hex(ctx.base_midx);
+ if (!strcmp(opts->incremental_base, cmp))
+ break;
+
+ ctx.base_midx = ctx.base_midx->base_midx;
+ }
+
+ if (!ctx.base_midx) {
+ error(_("could not find base MIDX '%s'"),
+ opts->incremental_base);
+ goto cleanup;
+ }
+ }
+ }
+
ctx.nr = 0;
ctx.alloc = ctx.m ? ctx.m->num_packs + ctx.m->num_packs_in_base : 16;
ctx.info = NULL;
@@ -1827,7 +1849,8 @@ static int write_midx_internal(struct write_midx_opts *opts)
int write_midx_file(struct odb_source *source,
const char *preferred_pack_name,
- const char *refs_snapshot, unsigned flags)
+ const char *refs_snapshot,
+ unsigned flags)
{
struct write_midx_opts opts = {
.source = source,
@@ -1842,13 +1865,16 @@ int write_midx_file(struct odb_source *source,
int write_midx_file_only(struct odb_source *source,
struct string_list *packs_to_include,
const char *preferred_pack_name,
- const char *refs_snapshot, unsigned flags)
+ const char *refs_snapshot,
+ const char *incremental_base,
+ unsigned flags)
{
struct write_midx_opts opts = {
.source = source,
.packs_to_include = packs_to_include,
.preferred_pack_name = preferred_pack_name,
.refs_snapshot = refs_snapshot,
+ .incremental_base = incremental_base,
.flags = flags,
};
@@ -1858,12 +1884,14 @@ int write_midx_file_only(struct odb_source *source,
int write_midx_file_compact(struct odb_source *source,
struct multi_pack_index *from,
struct multi_pack_index *to,
+ const char *incremental_base,
unsigned flags)
{
struct write_midx_opts opts = {
.source = source,
.compact_from = from,
.compact_to = to,
+ .incremental_base = incremental_base,
.flags = flags | MIDX_WRITE_COMPACT,
};
diff --git a/midx.h b/midx.h
index 5b193882dcf..77dd66de02b 100644
--- a/midx.h
+++ b/midx.h
@@ -132,10 +132,13 @@ int write_midx_file(struct odb_source *source,
int write_midx_file_only(struct odb_source *source,
struct string_list *packs_to_include,
const char *preferred_pack_name,
- const char *refs_snapshot, unsigned flags);
+ const char *refs_snapshot,
+ const char *incremental_base,
+ unsigned flags);
int write_midx_file_compact(struct odb_source *source,
struct multi_pack_index *from,
struct multi_pack_index *to,
+ const char *incremental_base,
unsigned flags);
void clear_midx_file(struct repository *r);
int verify_midx_file(struct odb_source *source, unsigned flags);
diff --git a/t/t5334-incremental-multi-pack-index.sh b/t/t5334-incremental-multi-pack-index.sh
index 66d6894761b..68a103d13d2 100755
--- a/t/t5334-incremental-multi-pack-index.sh
+++ b/t/t5334-incremental-multi-pack-index.sh
@@ -113,6 +113,36 @@ test_expect_success 'write non-incremental MIDX layer with --no-write-chain-file
test_grep "cannot use --no-write-chain-file without --incremental" err
'
+test_expect_success 'write MIDX layer with --base without --no-write-chain-file' '
+ test_must_fail git multi-pack-index write --bitmap --incremental \
+ --base=none 2>err &&
+ test_grep "cannot use --base without --no-write-chain-file" err
+'
+
+test_expect_success 'write MIDX layer with --base=none and --no-write-chain-file' '
+ test_commit base-none &&
+ git repack -d &&
+
+ cp "$midx_chain" "$midx_chain.bak" &&
+ layer="$(git multi-pack-index write --bitmap --incremental \
+ --no-write-chain-file --base=none)" &&
+
+ test_cmp "$midx_chain.bak" "$midx_chain" &&
+ test_path_is_file "$midxdir/multi-pack-index-$layer.midx"
+'
+
+test_expect_success 'write MIDX layer with --base=<hash> and --no-write-chain-file' '
+ test_commit base-hash &&
+ git repack -d &&
+
+ cp "$midx_chain" "$midx_chain.bak" &&
+ layer="$(git multi-pack-index write --bitmap --incremental \
+ --no-write-chain-file --base="$(nth_line 1 "$midx_chain")")" &&
+
+ test_cmp "$midx_chain.bak" "$midx_chain" &&
+ test_path_is_file "$midxdir/multi-pack-index-$layer.midx"
+'
+
for reuse in false single multi
do
test_expect_success "full clone (pack.allowPackReuse=$reuse)" '
diff --git a/t/t5335-compact-multi-pack-index.sh b/t/t5335-compact-multi-pack-index.sh
index 1a65d48b62b..ec1dafe89fc 100755
--- a/t/t5335-compact-multi-pack-index.sh
+++ b/t/t5335-compact-multi-pack-index.sh
@@ -304,6 +304,7 @@ test_expect_success 'MIDX compaction with --no-write-chain-file' '
layer="$(git multi-pack-index compact --incremental \
--no-write-chain-file \
+ --base="$(nth_line 1 "$midx_chain")" \
"$(nth_line 2 "$midx_chain")" \
"$(nth_line 3 "$midx_chain")")" &&
@@ -326,4 +327,80 @@ test_expect_success 'MIDX compaction with --no-write-chain-file' '
)
'
+test_expect_success 'MIDX compaction with --base' '
+ git init midx-compact-with--base &&
+ (
+ cd midx-compact-with--base &&
+
+ git config maintenance.auto false &&
+
+ write_packs A B C D &&
+
+ test_line_count = 4 "$midx_chain" &&
+
+ cp "$midx_chain" "$midx_chain.bak" &&
+
+ git multi-pack-index compact --incremental \
+ --base="$(nth_line 1 "$midx_chain")" \
+ "$(nth_line 3 "$midx_chain")" \
+ "$(nth_line 4 "$midx_chain")" &&
+ test_line_count = 2 $midx_chain &&
+
+ nth_line 1 "$midx_chain.bak" >expect &&
+ nth_line 1 "$midx_chain" >actual &&
+
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'MIDX compaction with --base=none' '
+ git init midx-compact-base-none &&
+ (
+ cd midx-compact-base-none &&
+
+ git config maintenance.auto false &&
+
+ write_packs A B C D &&
+
+ test_line_count = 4 $midx_chain &&
+
+ cp "$midx_chain" "$midx_chain".bak &&
+
+ # Compact the two bottommost layers (A and B) into a new
+ # root layer with no parent.
+ git multi-pack-index compact --incremental \
+ --base=none \
+ "$(nth_line 1 "$midx_chain")" \
+ "$(nth_line 2 "$midx_chain")" &&
+
+ test_line_count = 3 $midx_chain &&
+
+ # The upper layers (C and D) should be preserved
+ # unchanged.
+ nth_line 3 "$midx_chain.bak" >expect &&
+ nth_line 4 "$midx_chain.bak" >>expect &&
+ nth_line 2 "$midx_chain" >actual &&
+ nth_line 3 "$midx_chain" >>actual &&
+
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'MIDX compaction with bogus --base checksum' '
+ git init midx-compact-bogus-base &&
+ (
+ cd midx-compact-bogus-base &&
+
+ git config maintenance.auto false &&
+
+ write_packs A B C &&
+
+ test_must_fail git multi-pack-index compact --incremental \
+ --base=deadbeef \
+ "$(nth_line 2 "$midx_chain")" \
+ "$(nth_line 3 "$midx_chain")" 2>err &&
+ test_grep "could not find base MIDX" err
+ )
+'
+
test_done
--
2.54.0.9.gb905fd5d0ae
^ permalink raw reply related
* [PATCH v2 05/16] midx: introduce `--no-write-chain-file` for incremental MIDX writes
From: Taylor Blau @ 2026-04-21 20:37 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1776803827.git.me@ttaylorr.com>
When writing an incremental MIDX layer, the MIDX machinery writes the
new layer into the multi-pack-index.d directory and then updates the
multi-pack-index-chain file to include the freshly written layer.
Future callers however may not wish to immediately update the MIDX chain
itself, preferring instead to write out new layer(s) themselves before
atomically updating the chain. Concretely, the new incremental
MIDX-based repacking strategy will want to do exactly this (that is,
assemble the new MIDX chain itself before writing a new chain file and
atomically linking it into place).
Introduce a `--no-write-chain-file` flag that:
* writes the new MIDX layer into the multi-pack-index.d directory
* prints its checksum
* does not update the multi-pack-index-chain file.
The MIDX chain file (and thus, the lock protecting it) remain untouched,
allowing callers to assemble the chain themselves. This flag requires
`--incremental`, since the notion of a separate layer only makes sense
for incremental MIDXs.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
Documentation/git-multi-pack-index.adoc | 17 ++++++++--
builtin/multi-pack-index.c | 28 +++++++++++++++--
midx-write.c | 42 ++++++++++++++++---------
midx.h | 1 +
t/t5334-incremental-multi-pack-index.sh | 17 ++++++++++
t/t5335-compact-multi-pack-index.sh | 36 +++++++++++++++++++++
6 files changed, 123 insertions(+), 18 deletions(-)
diff --git a/Documentation/git-multi-pack-index.adoc b/Documentation/git-multi-pack-index.adoc
index 3a5aa227784..c26196815e2 100644
--- a/Documentation/git-multi-pack-index.adoc
+++ b/Documentation/git-multi-pack-index.adoc
@@ -11,9 +11,9 @@ SYNOPSIS
[verse]
'git multi-pack-index' [<options>] write [--preferred-pack=<pack>]
[--[no-]bitmap] [--[no-]incremental] [--[no-]stdin-packs]
- [--refs-snapshot=<path>]
+ [--refs-snapshot=<path>] [--[no-]write-chain-file]
'git multi-pack-index' [<options>] compact [--[no-]incremental]
- [--[no-]bitmap] <from> <to>
+ [--[no-]bitmap] [--[no-]write-chain-file] <from> <to>
'git multi-pack-index' [<options>] verify
'git multi-pack-index' [<options>] expire
'git multi-pack-index' [<options>] repack [--batch-size=<size>]
@@ -83,6 +83,13 @@ marker).
and packs not present in an existing MIDX layer.
Migrates non-incremental MIDXs to incremental ones when
necessary.
+
+ --[no-]write-chain-file::
+ When used with `--incremental`, write a new MIDX layer
+ but do not update the multi-pack-index-chain file.
+ The checksum of the new layer is printed to standard
+ output, allowing the caller to assemble and write the
+ chain itself. Requires `--incremental`.
--
compact::
@@ -97,6 +104,12 @@ compact::
--[no-]bitmap::
Control whether or not a multi-pack bitmap is written.
+
+ --[no-]write-chain-file::
+ When used with `--incremental`, write a new compacted
+ MIDX layer but do not update the multi-pack-index-chain
+ file. The checksum of the new layer is printed to
+ standard output. Requires `--incremental`.
--
+
Note that the compact command requires writing a version-2 midx that
diff --git a/builtin/multi-pack-index.c b/builtin/multi-pack-index.c
index 0f72d96c02d..f861b4b8394 100644
--- a/builtin/multi-pack-index.c
+++ b/builtin/multi-pack-index.c
@@ -16,11 +16,11 @@
#define BUILTIN_MIDX_WRITE_USAGE \
N_("git multi-pack-index [<options>] write [--preferred-pack=<pack>]\n" \
" [--[no-]bitmap] [--[no-]incremental] [--[no-]stdin-packs]\n" \
- " [--refs-snapshot=<path>]")
+ " [--refs-snapshot=<path>] [--[no-]write-chain-file]")
#define BUILTIN_MIDX_COMPACT_USAGE \
N_("git multi-pack-index [<options>] compact [--[no-]incremental]\n" \
- " [--[no-]bitmap] <from> <to>")
+ " [--[no-]bitmap] [--[no-]write-chain-file] <from> <to>")
#define BUILTIN_MIDX_VERIFY_USAGE \
N_("git multi-pack-index [<options>] verify")
@@ -153,6 +153,9 @@ static int cmd_multi_pack_index_write(int argc, const char **argv,
MIDX_WRITE_BITMAP | MIDX_WRITE_REV_INDEX),
OPT_BIT(0, "incremental", &opts.flags,
N_("write a new incremental MIDX"), MIDX_WRITE_INCREMENTAL),
+ OPT_NEGBIT(0, "write-chain-file", &opts.flags,
+ N_("write the multi-pack-index chain file"),
+ MIDX_WRITE_NO_CHAIN),
OPT_BOOL(0, "stdin-packs", &opts.stdin_packs,
N_("write multi-pack index containing only given indexes")),
OPT_FILENAME(0, "refs-snapshot", &opts.refs_snapshot,
@@ -178,6 +181,15 @@ static int cmd_multi_pack_index_write(int argc, const char **argv,
if (argc)
usage_with_options(builtin_multi_pack_index_write_usage,
options);
+
+ if (opts.flags & MIDX_WRITE_NO_CHAIN &&
+ !(opts.flags & MIDX_WRITE_INCREMENTAL)) {
+ error(_("cannot use %s without %s"),
+ "--no-write-chain-file", "--incremental");
+ usage_with_options(builtin_multi_pack_index_write_usage,
+ options);
+ }
+
source = handle_object_dir_option(repo);
FREE_AND_NULL(options);
@@ -221,6 +233,9 @@ static int cmd_multi_pack_index_compact(int argc, const char **argv,
MIDX_WRITE_BITMAP | MIDX_WRITE_REV_INDEX),
OPT_BIT(0, "incremental", &opts.flags,
N_("write a new incremental MIDX"), MIDX_WRITE_INCREMENTAL),
+ OPT_NEGBIT(0, "write-chain-file", &opts.flags,
+ N_("write the multi-pack-index chain file"),
+ MIDX_WRITE_NO_CHAIN),
OPT_END(),
};
@@ -239,6 +254,15 @@ static int cmd_multi_pack_index_compact(int argc, const char **argv,
if (argc != 2)
usage_with_options(builtin_multi_pack_index_compact_usage,
options);
+
+ if (opts.flags & MIDX_WRITE_NO_CHAIN &&
+ !(opts.flags & MIDX_WRITE_INCREMENTAL)) {
+ error(_("cannot use %s without %s"),
+ "--no-write-chain-file", "--incremental");
+ usage_with_options(builtin_multi_pack_index_compact_usage,
+ options);
+ }
+
source = handle_object_dir_option(the_repository);
FREE_AND_NULL(options);
diff --git a/midx-write.c b/midx-write.c
index 5d9409a9741..38c898e5ff5 100644
--- a/midx-write.c
+++ b/midx-write.c
@@ -1257,7 +1257,7 @@ static int write_midx_internal(struct write_midx_opts *opts)
unsigned char midx_hash[GIT_MAX_RAWSZ];
uint32_t start_pack;
struct hashfile *f = NULL;
- struct lock_file lk;
+ struct lock_file lk = LOCK_INIT;
struct tempfile *incr;
struct write_midx_context ctx = {
.preferred_pack_idx = NO_PREFERRED_PACK,
@@ -1601,11 +1601,14 @@ static int write_midx_internal(struct write_midx_opts *opts)
}
if (ctx.incremental) {
- struct strbuf lock_name = STRBUF_INIT;
+ if (!(opts->flags & MIDX_WRITE_NO_CHAIN)) {
+ struct strbuf lock_name = STRBUF_INIT;
- get_midx_chain_filename(opts->source, &lock_name);
- hold_lock_file_for_update(&lk, lock_name.buf, LOCK_DIE_ON_ERROR);
- strbuf_release(&lock_name);
+ get_midx_chain_filename(opts->source, &lock_name);
+ hold_lock_file_for_update(&lk, lock_name.buf,
+ LOCK_DIE_ON_ERROR);
+ strbuf_release(&lock_name);
+ }
incr = mks_tempfile_m(midx_name.buf, 0444);
if (!incr) {
@@ -1707,16 +1710,23 @@ static int write_midx_internal(struct write_midx_opts *opts)
if (ctx.num_multi_pack_indexes_before == UINT32_MAX)
die(_("too many multi-pack-indexes"));
+ if (!is_lock_file_locked(&lk))
+ printf("%s\n", hash_to_hex_algop(midx_hash, r->hash_algo));
+ else if (opts->flags & MIDX_WRITE_NO_CHAIN)
+ BUG("lockfile held with MIDX_WRITE_NO_CHAIN set?");
+
if (ctx.incremental) {
- FILE *chainf = fdopen_lock_file(&lk, "w");
struct strbuf final_midx_name = STRBUF_INIT;
struct multi_pack_index *m = ctx.base_midx;
struct multi_pack_index **layers = NULL;
size_t layers_nr = 0, layers_alloc = 0;
- if (!chainf) {
- error_errno(_("unable to open multi-pack-index chain file"));
- goto cleanup;
+ if (is_lock_file_locked(&lk)){
+ FILE *chainf = fdopen_lock_file(&lk, "w");
+ if (!chainf) {
+ error_errno(_("unable to open multi-pack-index chain file"));
+ goto cleanup;
+ }
}
if (link_midx_to_chain(ctx.base_midx) < 0)
@@ -1773,8 +1783,10 @@ static int write_midx_internal(struct write_midx_opts *opts)
free(layers);
- for (size_t i = 0; i < keep_hashes.nr; i++)
- fprintf(get_lock_file_fp(&lk), "%s\n", keep_hashes.v[i]);
+ if (is_lock_file_locked(&lk))
+ for (size_t i = 0; i < keep_hashes.nr; i++)
+ fprintf(get_lock_file_fp(&lk), "%s\n",
+ keep_hashes.v[i]);
} else {
strvec_push(&keep_hashes,
hash_to_hex_algop(midx_hash, r->hash_algo));
@@ -1783,10 +1795,12 @@ static int write_midx_internal(struct write_midx_opts *opts)
if (ctx.m || ctx.base_midx)
odb_close(ctx.repo->objects);
- if (commit_lock_file(&lk) < 0)
- die_errno(_("could not write multi-pack-index"));
+ if (is_lock_file_locked(&lk)) {
+ if (commit_lock_file(&lk) < 0)
+ die_errno(_("could not write multi-pack-index"));
- clear_midx_files(opts->source, &keep_hashes, ctx.incremental);
+ clear_midx_files(opts->source, &keep_hashes, ctx.incremental);
+ }
result = 0;
cleanup:
diff --git a/midx.h b/midx.h
index 08f3728e520..5b193882dcf 100644
--- a/midx.h
+++ b/midx.h
@@ -83,6 +83,7 @@ struct multi_pack_index {
#define MIDX_WRITE_BITMAP_LOOKUP_TABLE (1 << 4)
#define MIDX_WRITE_INCREMENTAL (1 << 5)
#define MIDX_WRITE_COMPACT (1 << 6)
+#define MIDX_WRITE_NO_CHAIN (1 << 7)
#define MIDX_EXT_REV "rev"
#define MIDX_EXT_BITMAP "bitmap"
diff --git a/t/t5334-incremental-multi-pack-index.sh b/t/t5334-incremental-multi-pack-index.sh
index c9f5b4e87aa..66d6894761b 100755
--- a/t/t5334-incremental-multi-pack-index.sh
+++ b/t/t5334-incremental-multi-pack-index.sh
@@ -96,6 +96,23 @@ test_expect_success 'show object from second pack' '
git cat-file -p 2.2
'
+test_expect_success 'write MIDX layer with --no-write-chain-file' '
+ test_commit no-write-chain-file &&
+ git repack -d &&
+
+ cp "$midx_chain" "$midx_chain.bak" &&
+ layer="$(git multi-pack-index write --bitmap --incremental \
+ --no-write-chain-file)" &&
+
+ test_cmp "$midx_chain.bak" "$midx_chain" &&
+ test_path_is_file "$midxdir/multi-pack-index-$layer.midx"
+'
+
+test_expect_success 'write non-incremental MIDX layer with --no-write-chain-file' '
+ test_must_fail git multi-pack-index write --bitmap --no-write-chain-file 2>err &&
+ test_grep "cannot use --no-write-chain-file without --incremental" err
+'
+
for reuse in false single multi
do
test_expect_success "full clone (pack.allowPackReuse=$reuse)" '
diff --git a/t/t5335-compact-multi-pack-index.sh b/t/t5335-compact-multi-pack-index.sh
index 40f3844282f..1a65d48b62b 100755
--- a/t/t5335-compact-multi-pack-index.sh
+++ b/t/t5335-compact-multi-pack-index.sh
@@ -290,4 +290,40 @@ test_expect_success 'MIDX compaction with bitmaps (non-trivial)' '
)
'
+test_expect_success 'MIDX compaction with --no-write-chain-file' '
+ git init midx-compact-with--no-write-chain-file &&
+ (
+ cd midx-compact-with--no-write-chain-file &&
+
+ git config maintenance.auto false &&
+
+ write_packs A B C D &&
+
+ test_line_count = 4 $midx_chain &&
+ cp "$midx_chain" "$midx_chain".bak &&
+
+ layer="$(git multi-pack-index compact --incremental \
+ --no-write-chain-file \
+ "$(nth_line 2 "$midx_chain")" \
+ "$(nth_line 3 "$midx_chain")")" &&
+
+ test_cmp "$midx_chain.bak" "$midx_chain" &&
+
+ # After writing the new layer, insert it into the chain
+ # manually. This is done in order to make $layer visible
+ # to the read-midx test helper below, and matches what
+ # the MIDX command would do without --no-write-chain-file.
+ {
+ nth_line 1 "$midx_chain.bak" &&
+ echo $layer &&
+ nth_line 4 "$midx_chain.bak"
+ } >$midx_chain &&
+
+ test-tool read-midx $objdir $layer >midx.data &&
+ grep "^pack-B-.*\.idx" midx.data &&
+ grep "^pack-C-.*\.idx" midx.data
+
+ )
+'
+
test_done
--
2.54.0.9.gb905fd5d0ae
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox