* Re: [PATCH 1/2] t3404: add failing branch symref test
From: Phillip Wood @ 2026-06-01 13:52 UTC (permalink / raw)
To: Son Luong Ngoc via GitGitGadget, git; +Cc: Son Luong Ngoc
In-Reply-To: <a550923440a233daea0b9819e05d6c380de00d09.1779946921.git.gitgitgadget@gmail.com>
On 28/05/2026 06:42, Son Luong Ngoc via GitGitGadget wrote:
> From: Son Luong Ngoc <sluongng@gmail.com>
>
> rebase --update-refs queues local branch decorations by their literal
> refnames. When a branch such as refs/heads/main is a symbolic ref to
> the current branch, the normal rebase path first updates the current
> branch and the queued symref update later tries to update the same
> referent with the old value it recorded before the rebase.
>
> Add a known-breakage test that exercises this case so that the fix can
> flip it to test_expect_success. The expected behavior is that the branch
> symref keeps pointing at the rebased current branch.
Thanks for adding a test, I'd find it easier to review this series if
the test was added in the same patch as the fix which is our usual practice.
> +test_expect_failure '--update-refs skips branch symrefs to current branch' '
> + test_when_finished "
> + test_might_fail git rebase --abort &&
> + git checkout primary &&
> + test_might_fail git symbolic-ref -d refs/heads/update-refs-symref-alias &&
> + test_might_fail git branch -D update-refs-symref update-refs-symref-base
> + " &&
> + git checkout -B update-refs-symref-base primary &&
> + test_commit --no-tag update-refs-symref-base symref-base.t &&
> + git checkout -B update-refs-symref &&
> + test_commit --no-tag update-refs-symref-topic symref-topic.t &&
> + git checkout update-refs-symref-base &&
> + test_commit --no-tag update-refs-symref-newbase symref-newbase.t &&
> + git checkout update-refs-symref &&
> + git symbolic-ref refs/heads/update-refs-symref-alias refs/heads/update-refs-symref &&
I think we want to test a symref that does not match HEAD as well.
Rather than adding a new test, can we instead add a couple of symref
branches to the test "--update-refs updates refs correctly"?
Thanks
Phillip
> +
> + git rebase --update-refs update-refs-symref-base 2>err &&
> +
> + test_cmp_rev update-refs-symref-base update-refs-symref^ &&
> + test_cmp_rev refs/heads/update-refs-symref refs/heads/update-refs-symref-alias &&
> + test_write_lines refs/heads/update-refs-symref >expect &&
> + git symbolic-ref refs/heads/update-refs-symref-alias >actual &&
> + test_cmp expect actual
> +'
> +
> test_expect_success '--update-refs updates refs correctly' '
> git checkout -B update-refs no-conflict-branch &&
> git branch -f base HEAD~4 &&
^ permalink raw reply
* Re: [PATCH 0/5] Duplicate entry hardening
From: Patrick Steinhardt @ 2026-06-01 13:54 UTC (permalink / raw)
To: Junio C Hamano; +Cc: Elijah Newren via GitGitGadget, git, Elijah Newren
In-Reply-To: <xmqqpl2a4f09.fsf@gitster.g>
On Mon, Jun 01, 2026 at 09:33:10PM +0900, Junio C Hamano wrote:
> "Elijah Newren via GitGitGadget" <gitgitgadget@gmail.com> writes:
>
> > We had some corrupt trees with duplicate entries in real world repositories,
> > which triggered an assertion failure in merge-ort. Further, the corrupt tree
> > creation in the third party tool would have been avoided had verify_cache()
> > correctly checked for D/F conflicts. Provide fixes for both issues,
> > including 3 preparatory changes for the merge-ort fix.
> >
> > Elijah Newren (5):
> > merge-ort: propagate callback errors from traverse_trees_wrapper()
> > merge-ort: drop unnecessary show_all_errors from collect_merge_info()
> > merge-ort: free diff pairs queue in clear_or_reinit_internal_opts()
> > merge-ort: abort merge when trees have duplicate entries
> > cache-tree: fix verify_cache() to catch non-adjacent D/F conflicts
>
> This is a fix to an important corner of our system, but somehow left
> in "Needs review" state for much longer than I would have liked, so
> even though I am officially on vacation ;-), I took some time to
> read these through (by the way it was a pleasant read, thank you).
Honestly, I always shy away from the merge-related subsystems. It has a
lot of subtleties that I don't have any experience with, so I never
really consider my input to be helpful here.
> I wonder if we create a rule like
>
> Those of you who have more than 30 commits in our project are
> expected to review one topic (or more) from other contributors
> for every three patches you send and ask for reviews by others.
Heh, that would make me condense patch series into fewer patches ;)
> it would help balance the patch vs review ratio, perhaps?
It's a good question. I typically try to aim for reviewing series on the
mailing list at least every second day, and I always encourage other
folks in my team to do the same. But recently I (well, rather we)
haven't really been able to due to the current situation at GitLab,
which forces us to put almost all of our focus towards a different
project for a while.
Overall I agree that everyone who is a core contributor should also make
reviews part of their regular worflow. At least for corporate
contributors that might also make it easier to communicate this to their
respective employers. Regardless of that, my expectation is that there
will be times where it works well, and other times where it works less
well.
Patrick
^ permalink raw reply
* Re: [PATCH v3 1/8] environment: move "trust_ctime" into `struct repo_config_values`
From: Bello Olamide @ 2026-06-01 14:01 UTC (permalink / raw)
To: Tian Yuchen
Cc: git, Phillip Wood, Junio C Hamano, Christain Couder,
Usman Akinyemi, Kaartic Sivaraam, Taylor Blau
In-Reply-To: <08efcc49-0db8-49f6-8971-633aa55eb66c@malon.dev>
On Thu, May 21, 2026, 5:37 PM Tian Yuchen <cat@malon.dev> wrote:
>
> Hi Bello!
>
> On 4/24/26 00:54, Olamide Caleb Bello wrote:
>
> The code itself looks great to me, but I have some reservations about
> the description here (in terms of why trust_ctime is eagerly parsed):
>
> > `core.trustctime` is parsed eagerly
> > because it is used in low‑level stat‑matching functions
> > (`match_stat_data()`), where a lazy parse could cause unexpected
> > fatal errors and complicate libification efforts.
>
> It's true that if we use repo_config_get_bool() to parse trust_ctime,
> following the call stack downwards, there is a die() call. The terminate
> condition is that the configuration does not exist or contains invalid
> characters.
>
> But I think there is another factor: match_stat_data() is called on a
> hot path. The following code is implemented in read-cache.c,
> refresh_index() function:
>
> for (i = 0; i < istate->cache_nr; i++) {
> ...
> new_entry = refresh_cache_ent(istate, ce, options,
> &cache_errno, &changed,
> &t2_did_lstat, &t2_did_scan);
> t2_sum_lstat += t2_did_lstat;
> t2_sum_scan += t2_did_scan;
> if (new_entry == ce)
> ...
>
> The call chain: refresh_index() -> refresh_cache_ent() ->
> ie_match_stat() -> ce_match_stat_basic() -> *match_stat_data()*
>
> Therefore, if the variable is lazily parsed, this means there will be a
> performance regression whenever the index status needs to be checked,
> e.g. 'git status'.
>
> So, I guess it would be better to extend a bit:
>
> '...where a lazy parse could cause unexpected fatal, and result in a
> performance regression...'
noted...
>
> Thanks, yuchen
Thank you, Yuchen.
^ permalink raw reply
* Re: [PATCH 2/2] rebase: skip branch symref aliases
From: Phillip Wood @ 2026-06-01 14:10 UTC (permalink / raw)
To: Son Luong Ngoc via GitGitGadget, git; +Cc: Son Luong Ngoc
In-Reply-To: <0ab0a717441e9fc7c494da194065a948a35a7f01.1779946921.git.gitgitgadget@gmail.com>
On 28/05/2026 06:42, Son Luong Ngoc via GitGitGadget wrote:
> From: Son Luong Ngoc <sluongng@gmail.com>
>
> rebase --update-refs records local branch decorations before replaying
> commits. If a decoration is a symbolic branch such as refs/heads/main
> pointing at refs/heads/master, updating it later dereferences back to
> master and can fail because the normal rebase path already moved that
> branch.
Good explanation, thanks for working on this.
> Resolve local branch symref decorations to their referents before
s/referents/targets/ ?
> queuing update-ref commands, and skip duplicates. This keeps branch
> aliases from scheduling a second update for the same underlying branch
> while still using the existing old-OID check for the single queued
> update.
That's not quite what the patch does though - it only checks that the
target of the symref differs from the target of HEAD. If a symref points
to another branch we still try to update it when we should skip it.
> Signed-off-by: Son Luong Ngoc <sluongng@gmail.com>
> ---
> sequencer.c | 63 +++++++++++++++++++++++++++++------
> t/t3404-rebase-interactive.sh | 2 +-
> 2 files changed, 53 insertions(+), 12 deletions(-)
>
> diff --git a/sequencer.c b/sequencer.c
> index 1ee4b2875b..4a83d1337c 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -6445,15 +6445,22 @@ static int add_decorations_to_list(const struct commit *commit,
> struct todo_add_branch_context *ctx)
> {
> const struct name_decoration *decoration = get_name_decoration(&commit->object);
> - const char *head_ref = refs_resolve_ref_unsafe(get_main_ref_store(the_repository),
> - "HEAD",
> + struct ref_store *refs = get_main_ref_store(the_repository);
> + const char *head_ref = refs_resolve_ref_unsafe(refs, "HEAD",
> RESOLVE_REF_READING,
> - NULL,
> - NULL);
> + NULL, NULL);
> + char *resolved_head_ref = refs_resolve_refdup(refs, "HEAD",
> + RESOLVE_REF_READING,
> + NULL, NULL);
We need to use refs_resolve_refdup() instead of
refs_resolve_ref_unsafe() so that the return value is not overwritten by
the later calls to refs_resolve_ref_unsafe() that are added below. But
that is the only change that is needed - we do not need to add a new
variable, we just replace refs_resolve_ref_unsafe() with
refs_resole_refdup() and free "head_ref" before we return.
> + struct strbuf update_ref = STRBUF_INIT;
>
> while (decoration) {
> struct todo_item *item;
> const char *path;
> + const char *ref = decoration->name;
> + const char *resolved_ref;
> + int is_symref = 0;
> + int flags = 0;
> size_t base_offset = ctx->buf->len;
>
> /*
> @@ -6461,12 +6468,44 @@ static int add_decorations_to_list(const struct commit *commit,
> * updated by the default rebase behavior.
> * Exclude it from the list of refs to update,
> * as well as any non-branch decorations.
> + *
> + * Resolve branch symrefs after checking for the current HEAD so
> + * that aliases do not schedule duplicate updates for their
> + * referents.
> + *
> * Non-branch decorations may be present if the pretty format
> * includes "%d", which would have loaded all refs
> * into the global decoration table.
> */
> - if ((head_ref && !strcmp(head_ref, decoration->name)) ||
> - (decoration->type != DECORATION_REF_LOCAL)) {
> + if (decoration->type != DECORATION_REF_LOCAL) {
> + decoration = decoration->next;
> + continue;
> + }
> +
> + if (head_ref && !strcmp(head_ref, ref)) {
> + decoration = decoration->next;
> + continue;
> + }
This is just rewriting the existing if statement which has nothing to do
with the stated aim of this patch - lets leave it as it was.
> +
> + strbuf_reset(&update_ref);
> + resolved_ref = refs_resolve_ref_unsafe(refs, ref,
> + RESOLVE_REF_READING |
> + RESOLVE_REF_NO_RECURSE,
Why are we passing RESOLVE_REF_NO_RECURSE here? I'd have thought we want
to resolve the whole chain of symbolic refs to find out which ref is
actually going to be updated.
> + NULL, &flags);
> + if ((flags & REF_ISSYMREF) && resolved_ref) {
I think it is generally safer to check the return value before using any
of the "out" parameters from a function call. In this case the function
unconditionally clears flags at the beginning so it is safe.
> + if (!starts_with(resolved_ref, "refs/heads/")) {
> + decoration = decoration->next;
> + continue;
This is the opposite of what I was expecting - if the decoration is a
symref that resolves to a branch then that branch will also be in the
list of decorations and so will be updated. If the decoration is a
symref that resolves outside "refs/heads/" then we want to add the
decoration to the list of refs to update to keep the current behavior.
If we do that then we skip all symbolic refs that point to another
branch, instead of just skipping those that match HEAD and we don't need
any of the changes below here.
Thanks
Phillip
> + }
> +
> + strbuf_addstr(&update_ref, resolved_ref);
> + ref = update_ref.buf;
> + is_symref = 1;
> + }
> +
> + if ((is_symref && resolved_head_ref &&
> + !strcmp(resolved_head_ref, ref)) ||
> + string_list_has_string(&ctx->refs_to_oids, ref)) {
> decoration = decoration->next;
> continue;
> }
> @@ -6478,19 +6517,19 @@ static int add_decorations_to_list(const struct commit *commit,
> memset(item, 0, sizeof(*item));
>
> /* If the branch is checked out, then leave a comment instead. */
> - if ((path = branch_checked_out(decoration->name))) {
> + if ((path = branch_checked_out(ref))) {
> item->command = TODO_COMMENT;
> strbuf_commented_addf(ctx->buf, comment_line_str,
> "Ref %s checked out at '%s'\n",
> - decoration->name, path);
> + ref, path);
> } else {
> struct string_list_item *sti;
> item->command = TODO_UPDATE_REF;
> - strbuf_addf(ctx->buf, "%s\n", decoration->name);
> + strbuf_addf(ctx->buf, "%s\n", ref);
>
> sti = string_list_insert(&ctx->refs_to_oids,
> - decoration->name);
> - sti->util = init_update_ref_record(decoration->name);
> + ref);
> + sti->util = init_update_ref_record(ref);
> }
>
> item->offset_in_buf = base_offset;
> @@ -6501,6 +6540,8 @@ static int add_decorations_to_list(const struct commit *commit,
> decoration = decoration->next;
> }
>
> + strbuf_release(&update_ref);
> + free(resolved_head_ref);
> return 0;
> }
>
> diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh
> index 42ba8cc313..29447c0fc3 100755
> --- a/t/t3404-rebase-interactive.sh
> +++ b/t/t3404-rebase-interactive.sh
> @@ -1978,7 +1978,7 @@ test_expect_success '--update-refs ignores non-branch decorations' '
> test_cmp expect actual
> '
>
> -test_expect_failure '--update-refs skips branch symrefs to current branch' '
> +test_expect_success '--update-refs skips branch symrefs to current branch' '
> test_when_finished "
> test_might_fail git rebase --abort &&
> git checkout primary &&
^ permalink raw reply
* Re: [PATCH 2/2] builtin/init-db: deprecate alias for git-init(1)
From: Patrick Steinhardt @ 2026-06-01 14:20 UTC (permalink / raw)
To: phillip.wood; +Cc: Kristoffer Haugsbakk, git
In-Reply-To: <042e66b5-122b-4c86-a9a9-f75f763666a7@gmail.com>
On Mon, Jun 01, 2026 at 02:48:05PM +0100, Phillip Wood wrote:
>
>
> On 01/06/2026 13:10, Patrick Steinhardt wrote:
> > On Mon, Jun 01, 2026 at 11:31:46AM +0200, Kristoffer Haugsbakk wrote:
> > > On Mon, Jun 1, 2026, at 09:56, Patrick Steinhardt wrote:
> > > > diff --git a/git.c b/git.c
> > > > index a72394b599..6bf6a60360 100644
> > > > --- a/git.c
> > > > +++ b/git.c
> > > > @@ -591,7 +591,9 @@ static struct cmd_struct commands[] = {
> > > > { "hook", cmd_hook, RUN_SETUP_GENTLY },
> > > > { "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
> > > > { "init", cmd_init },
> > > > +#ifndef WITH_BREAKING_CHANGES
> > > > { "init-db", cmd_init },
> > >
> > > This can be marked as deprecated.
> > >
> > > { "init-db", cmd_init, DEPRECATED },
> >
> > Ah, indeed! Added locally now, thanks.
>
> Deprecating this command seems very sensible to me. As well as marking it
> deprecated, do we want to print a warning when it is run? I imagine anyone
> who has this command in their muscle memory is unlikely to be reading the
> man page on a regular basis so wont see the warning there.
I was wondering whether we want to call `you_still_use_that()` here. I
found it to be a bit heavy-handed as it's so trivial to replace with
git-init(1), but on the other hand it's a trivial thing to do.
Patrick
^ permalink raw reply
* Re: [PATCH v2 2/2] status: improve rebase todo list parsing
From: Phillip Wood @ 2026-06-01 15:20 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git, Elijah Newren, Patrick Steinhardt
In-Reply-To: <xmqqbjdwcsno.fsf@gitster.g>
Hi Junio
On 31/05/2026 01:46, Junio C Hamano wrote:
> Phillip Wood <phillip.wood123@gmail.com> writes:
>
>> +static void abbrev_oid_in_line(struct repository *r,
>> + struct strbuf *line, char **pp)
>> +{
>> ...
>> + have_oid = !repo_get_oid(r, p, &oid);
>> + *end_of_object_name = saved;
>> + if (!have_oid)
>> + goto out; /* object name was a label */
>
> Can there be a label "deadbeef123" that is unrelated to an object whose
> object name happens to abbreviate to "deadbeef123"?
In theory yes, but I had assumed it was so unlikely to happen that we
could ignore it. If we want to be more careful then we could add a "bool
maybe_label" argument for commands that accept a label or a revision and
check if "refs/rewritten/$object_name" exists before trying repo_get_oid().
>> + 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;
>
> What does this loop do? A "merge" command may look like "merge
> [[-C|-c] <commit>] <label>", and we give each whitespace-separated
> token to abbrev_oid_in_line()? Would "<label>" that is ambiguous
> cause an issue? You may want to limit the scope of what the loop
> does a bit, e.g., massage only the token after -C/-c, or something?
The parents can be a label or any revision so we want to abbreviate the
parent if it is a hex object id. The same is true for "reset" below.
Thanks
Phillip
>
>> + case TODO_FIXUP:
>> + skip_dash_c(&p);
>> + /* fallthrough */
>> + case TODO_DROP:
>> + case TODO_EDIT:
>> + case TODO_PICK:
>> + case TODO_RESET:
>
> Doesn't RESET also take a <label>? And if it happens to be the same
> as an abbreviated object name, e.g., "deadbeef123", of an unrelated
> object, would wt-status say "reset deadbeef1", causing a mismatch?
> If this is indeed an issue, would moving this to the "no-op" section
> below, next to TODO_LABEL, solve it?
>
>> + 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 (which is included in -Wall) warns if a new
>> + * command is added without handling it in this function.
>> + */
>> + case TODO_BREAK:
>> + case TODO_EXEC:
>> + case TODO_LABEL:
>> + case TODO_NOOP:
>> + case TODO_UPDATE_REF:
>> + break;
>> }
>> - string_list_clear(&split, 0);
>> +
>> + return true;
>> }
>
^ permalink raw reply
* [GSoC][PATCH 0/4] teach git repo info to handle path keys
From: K Jayatheerth @ 2026-06-01 15:19 UTC (permalink / raw)
To: git
Cc: jltobler, lucasseikioshiro, gitster, phillip.wood, sandals,
kumarayushjha123, a3205153416, K Jayatheerth
Hi!
The first and second patches are self-explanatory, so I will
focus more on the third and fourth patches, which introduce the
path-related fields to `git repo info`.
In the last discussion [1] we had on the mailing list about paths
in repo info, we didn't reach a definitive conclusion, but
adding both options made the most sense based on the feedback.
So in patches 3 and 4, we add both `path.<field>.absolute` and
`path.<field>.relative` for `gitdir` and `commondir`. Initially,
it was proposed by Ayush to use `path.absolute.<field>`, but
this would break the lexicographical order of the internal field
array. I tweaked it to place the variant at the end as a suffix instead.
There are still a few open questions that should be addressed
by the community. I am tagging members who were involved in the
previous discussions:
Justin Tobler, Lucas Seiki Oshiro, Junio, Phillip Wood,
brian m. carlson, and Ayush Jha.
Apologies if I missed anyone; I included everyone who reviewed
or participated in the discussions of Eslam's and Lucas's
patches.
Questions:
1. Should there still be a --path-format flag?
2. Should we consider a default option?
Currently we have path.gitdir.absolute; should we consider
an option where a plain path.gitdir returns some default?
If yes:
2.1 Should we keep the default the same as rev-parse? Or
should either relative or absolute be the default?
2.2 When printing using --all, should the default be
printed, or should we print both absolute and
relative?
3. Is printing both absolute and relative in a single call
using --all acceptable?
If no:
3.1 What's a better approach?
I have discussed these changes with both Justin and Lucas
internally. This series is presented to gather opinions from the
wider community before moving forward.
K Jayatheerth (4):
path: add strbuf_add_path for formatting paths
rev-parse: use strbuf_add_path for path formatting
repo: add path.gitdir with absolute and relative suffix formatting
repo: add path.commondir with absolute and relative suffix formatting
Documentation/git-repo.adoc | 15 ++++++
builtin/repo.c | 50 ++++++++++++++++++
builtin/rev-parse.c | 100 ++++++++----------------------------
path.c | 58 +++++++++++++++++++++
path.h | 16 ++++++
t/t1900-repo-info.sh | 32 ++++++++++++
6 files changed, 192 insertions(+), 79 deletions(-)
--
2.54.0
^ permalink raw reply
* [GSoC][PATCH 1/4] path: add strbuf_add_path for formatting paths
From: K Jayatheerth @ 2026-06-01 15:19 UTC (permalink / raw)
To: git
Cc: jltobler, lucasseikioshiro, gitster, phillip.wood, sandals,
kumarayushjha123, a3205153416, K Jayatheerth
In-Reply-To: <20260601151950.30686-1-jayatheerthkulkarni2005@gmail.com>
The `print_path()` function in `builtin/rev-parse.c` contains
logic for formatting paths as either absolute or relative based on user
preferences and default behaviors. However, this logic is currently
locked inside `rev-parse` and writes directly to stdout using `puts()`.
To allow other builtins (such as the new `git repo` command) to utilize
this same path-formatting logic, extract the core algorithm into a new
string-builder function, `strbuf_add_path()`, in `path.c`.
Additionally, extract the associated enums (`format_type` and
`default_type`), and prefix them with `path_` (e.g., `path_format_type`)
to safely expose them in `path.h` without polluting the global namespace.
Signed-off-by: K Jayatheerth <jayatheerthkulkarni2005@gmail.com>
Mentored-by: Justin Tobler <jltobler@gmail.com>
Mentored-by: Lucas Seiki Oshiro <lucasseikioshiro@gmail.com>
---
path.c | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
path.h | 16 ++++++++++++++++
2 files changed, 74 insertions(+)
diff --git a/path.c b/path.c
index d7e17bf174..914812320f 100644
--- a/path.c
+++ b/path.c
@@ -1579,6 +1579,64 @@ char *xdg_cache_home(const char *filename)
return NULL;
}
+void strbuf_add_path(struct strbuf *sb, const char *path, const char *prefix,
+ enum path_format_type format, enum path_default_type def)
+{
+ char *cwd = NULL;
+
+ /*
+ * We don't ever produce a relative path if prefix is NULL, so set the
+ * prefix to the current directory so that we can produce a relative
+ * path whenever possible. If we're using RELATIVE_IF_SHARED mode, then
+ * we want an absolute path unless the two share a common prefix, so don't
+ * set it in that case, since doing so causes a relative path to always
+ * be produced if possible.
+ */
+ if (!prefix && (format != PATH_FORMAT_DEFAULT || def != PATH_DEFAULT_RELATIVE_IF_SHARED))
+ prefix = cwd = xgetcwd();
+
+ if (format == PATH_FORMAT_DEFAULT && def == PATH_DEFAULT_UNMODIFIED) {
+ /* Case 1: Return the path exactly as-is without modifications */
+ strbuf_addstr(sb, path);
+ } else if (format == PATH_FORMAT_RELATIVE ||
+ (format == PATH_FORMAT_DEFAULT && def == PATH_DEFAULT_RELATIVE)) {
+ /*
+ * Case 2: Explicitly or implicitly relative.
+ * inside relative_path(), both targets must be absolute paths
+ * to compute a reliable relative tracking offset.
+ */
+ struct strbuf buf = STRBUF_INIT, realbuf = STRBUF_INIT, prefixbuf = STRBUF_INIT;
+
+ if (!is_absolute_path(path)) {
+ strbuf_realpath_forgiving(&realbuf, path, 1);
+ path = realbuf.buf;
+ }
+ if (!is_absolute_path(prefix)) {
+ strbuf_realpath_forgiving(&prefixbuf, prefix, 1);
+ prefix = prefixbuf.buf;
+ }
+
+ strbuf_addstr(sb, relative_path(path, prefix, &buf));
+
+ strbuf_release(&buf);
+ strbuf_release(&realbuf);
+ strbuf_release(&prefixbuf);
+ } else if (format == PATH_FORMAT_DEFAULT && def == PATH_DEFAULT_RELATIVE_IF_SHARED) {
+ /* Case 3: Relative format if they share a common root pathway */
+ struct strbuf buf = STRBUF_INIT;
+ strbuf_addstr(sb, relative_path(path, prefix, &buf));
+ strbuf_release(&buf);
+ } else {
+ /* Case 4: Forced absolute / canonical format optimization */
+ struct strbuf buf = STRBUF_INIT;
+ strbuf_realpath_forgiving(&buf, path, 1);
+ strbuf_addbuf(sb, &buf);
+ strbuf_release(&buf);
+ }
+
+ free(cwd);
+}
+
REPO_GIT_PATH_FUNC(squash_msg, "SQUASH_MSG")
REPO_GIT_PATH_FUNC(merge_msg, "MERGE_MSG")
REPO_GIT_PATH_FUNC(merge_rr, "MERGE_RR")
diff --git a/path.h b/path.h
index 0434ba5e07..b9b626ce4a 100644
--- a/path.h
+++ b/path.h
@@ -262,6 +262,22 @@ enum scld_error safe_create_leading_directories_no_share(char *path);
int safe_create_file_with_leading_directories(struct repository *repo,
const char *path);
+enum path_format_type {
+ PATH_FORMAT_DEFAULT,
+ PATH_FORMAT_RELATIVE,
+ PATH_FORMAT_CANONICAL
+};
+
+enum path_default_type {
+ PATH_DEFAULT_RELATIVE,
+ PATH_DEFAULT_RELATIVE_IF_SHARED,
+ PATH_DEFAULT_CANONICAL,
+ PATH_DEFAULT_UNMODIFIED
+};
+
+void strbuf_add_path(struct strbuf *buf, const char *path, const char *prefix,
+ enum path_format_type format, enum path_default_type def);
+
# ifdef USE_THE_REPOSITORY_VARIABLE
# include "strbuf.h"
# include "repository.h"
--
2.54.0
^ permalink raw reply related
* [GSoC][PATCH 2/4] rev-parse: use strbuf_add_path for path formatting
From: K Jayatheerth @ 2026-06-01 15:19 UTC (permalink / raw)
To: git
Cc: jltobler, lucasseikioshiro, gitster, phillip.wood, sandals,
kumarayushjha123, a3205153416, K Jayatheerth
In-Reply-To: <20260601151950.30686-1-jayatheerthkulkarni2005@gmail.com>
Now that the core path-formatting logic has been abstracted into
strbuf_add_path() inside path.c, remove the duplicate localized
implementation from builtin/rev-parse.c.
Drop the local format_type and default_type enums from the builtin, and
update print_path() to act as a light wrapper around the new shared
strbuf engine. Update cmd_rev_parse() to use the new path_ format and
default enum types exposed via path.h.
Signed-off-by: K Jayatheerth <jayatheerthkulkarni2005@gmail.com>
Mentored-by: Justin Tobler <jltobler@gmail.com>
Mentored-by: Lucas Seiki Oshiro <lucasseikioshiro@gmail.com>
---
builtin/rev-parse.c | 100 ++++++++++----------------------------------
1 file changed, 21 insertions(+), 79 deletions(-)
diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c
index 218b5f34d6..812cfd55ad 100644
--- a/builtin/rev-parse.c
+++ b/builtin/rev-parse.c
@@ -632,73 +632,15 @@ static void handle_ref_opt(const char *pattern, const char *prefix)
clear_ref_exclusions(&ref_excludes);
}
-enum format_type {
- /* We would like a relative path. */
- FORMAT_RELATIVE,
- /* We would like a canonical absolute path. */
- FORMAT_CANONICAL,
- /* We would like the default behavior. */
- FORMAT_DEFAULT,
-};
-
-enum default_type {
- /* Our default is a relative path. */
- DEFAULT_RELATIVE,
- /* Our default is a relative path if there's a shared root. */
- DEFAULT_RELATIVE_IF_SHARED,
- /* Our default is a canonical absolute path. */
- DEFAULT_CANONICAL,
- /* Our default is not to modify the item. */
- DEFAULT_UNMODIFIED,
-};
-
-static void print_path(const char *path, const char *prefix, enum format_type format, enum default_type def)
+static void print_path(const char *path, const char *prefix,
+ enum path_format_type format, enum path_default_type def)
{
- char *cwd = NULL;
- /*
- * We don't ever produce a relative path if prefix is NULL, so set the
- * prefix to the current directory so that we can produce a relative
- * path whenever possible. If we're using RELATIVE_IF_SHARED mode, then
- * we want an absolute path unless the two share a common prefix, so don't
- * set it in that case, since doing so causes a relative path to always
- * be produced if possible.
- */
- if (!prefix && (format != FORMAT_DEFAULT || def != DEFAULT_RELATIVE_IF_SHARED))
- prefix = cwd = xgetcwd();
- if (format == FORMAT_DEFAULT && def == DEFAULT_UNMODIFIED) {
- puts(path);
- } else if (format == FORMAT_RELATIVE ||
- (format == FORMAT_DEFAULT && def == DEFAULT_RELATIVE)) {
- /*
- * In order for relative_path to work as expected, we need to
- * make sure that both paths are absolute paths. If we don't,
- * we can end up with an unexpected absolute path that the user
- * didn't want.
- */
- struct strbuf buf = STRBUF_INIT, realbuf = STRBUF_INIT, prefixbuf = STRBUF_INIT;
- if (!is_absolute_path(path)) {
- strbuf_realpath_forgiving(&realbuf, path, 1);
- path = realbuf.buf;
- }
- if (!is_absolute_path(prefix)) {
- strbuf_realpath_forgiving(&prefixbuf, prefix, 1);
- prefix = prefixbuf.buf;
- }
- puts(relative_path(path, prefix, &buf));
- strbuf_release(&buf);
- strbuf_release(&realbuf);
- strbuf_release(&prefixbuf);
- } else if (format == FORMAT_DEFAULT && def == DEFAULT_RELATIVE_IF_SHARED) {
- struct strbuf buf = STRBUF_INIT;
- puts(relative_path(path, prefix, &buf));
- strbuf_release(&buf);
- } else {
- struct strbuf buf = STRBUF_INIT;
- strbuf_realpath_forgiving(&buf, path, 1);
- puts(buf.buf);
- strbuf_release(&buf);
- }
- free(cwd);
+ struct strbuf sb = STRBUF_INIT;
+
+ strbuf_add_path(&sb, path, prefix, format, def);
+ puts(sb.buf);
+
+ strbuf_release(&sb);
}
int cmd_rev_parse(int argc,
@@ -717,7 +659,7 @@ int cmd_rev_parse(int argc,
const char *name = NULL;
struct strbuf buf = STRBUF_INIT;
int seen_end_of_options = 0;
- enum format_type format = FORMAT_DEFAULT;
+ enum path_format_type format = PATH_FORMAT_DEFAULT;
show_usage_if_asked(argc, argv, builtin_rev_parse_usage);
@@ -798,7 +740,7 @@ int cmd_rev_parse(int argc,
print_path(repo_git_path_replace(the_repository, &buf,
"%s", argv[i + 1]), prefix,
format,
- DEFAULT_RELATIVE_IF_SHARED);
+ PATH_DEFAULT_RELATIVE_IF_SHARED);
i++;
continue;
}
@@ -820,9 +762,9 @@ int cmd_rev_parse(int argc,
if (!arg)
die(_("--path-format requires an argument"));
if (!strcmp(arg, "absolute")) {
- format = FORMAT_CANONICAL;
+ format = PATH_FORMAT_CANONICAL;
} else if (!strcmp(arg, "relative")) {
- format = FORMAT_RELATIVE;
+ format = PATH_FORMAT_RELATIVE;
} else {
die(_("unknown argument to --path-format: %s"), arg);
}
@@ -985,7 +927,7 @@ int cmd_rev_parse(int argc,
if (!strcmp(arg, "--show-toplevel")) {
const char *work_tree = repo_get_work_tree(the_repository);
if (work_tree)
- print_path(work_tree, prefix, format, DEFAULT_UNMODIFIED);
+ print_path(work_tree, prefix, format, PATH_DEFAULT_UNMODIFIED);
else
die(_("this operation must be run in a work tree"));
continue;
@@ -993,7 +935,7 @@ int cmd_rev_parse(int argc,
if (!strcmp(arg, "--show-superproject-working-tree")) {
struct strbuf superproject = STRBUF_INIT;
if (get_superproject_working_tree(&superproject))
- print_path(superproject.buf, prefix, format, DEFAULT_UNMODIFIED);
+ print_path(superproject.buf, prefix, format, PATH_DEFAULT_UNMODIFIED);
strbuf_release(&superproject);
continue;
}
@@ -1028,18 +970,18 @@ int cmd_rev_parse(int argc,
const char *gitdir = getenv(GIT_DIR_ENVIRONMENT);
char *cwd;
int len;
- enum format_type wanted = format;
+ enum path_format_type wanted = format;
if (arg[2] == 'g') { /* --git-dir */
if (gitdir) {
- print_path(gitdir, prefix, format, DEFAULT_UNMODIFIED);
+ print_path(gitdir, prefix, format, PATH_DEFAULT_UNMODIFIED);
continue;
}
if (!prefix) {
- print_path(".git", prefix, format, DEFAULT_UNMODIFIED);
+ print_path(".git", prefix, format, PATH_DEFAULT_UNMODIFIED);
continue;
}
} else { /* --absolute-git-dir */
- wanted = FORMAT_CANONICAL;
+ wanted = PATH_FORMAT_CANONICAL;
if (!gitdir && !prefix)
gitdir = ".git";
if (gitdir) {
@@ -1055,11 +997,11 @@ int cmd_rev_parse(int argc,
strbuf_reset(&buf);
strbuf_addf(&buf, "%s%s.git", cwd, len && cwd[len-1] != '/' ? "/" : "");
free(cwd);
- print_path(buf.buf, prefix, wanted, DEFAULT_CANONICAL);
+ print_path(buf.buf, prefix, wanted, PATH_DEFAULT_CANONICAL);
continue;
}
if (!strcmp(arg, "--git-common-dir")) {
- print_path(repo_get_common_dir(the_repository), prefix, format, DEFAULT_RELATIVE_IF_SHARED);
+ print_path(repo_get_common_dir(the_repository), prefix, format, PATH_DEFAULT_RELATIVE_IF_SHARED);
continue;
}
if (!strcmp(arg, "--is-inside-git-dir")) {
@@ -1089,7 +1031,7 @@ int cmd_rev_parse(int argc,
if (the_repository->index->split_index) {
const struct object_id *oid = &the_repository->index->split_index->base_oid;
const char *path = repo_git_path_replace(the_repository, &buf, "sharedindex.%s", oid_to_hex(oid));
- print_path(path, prefix, format, DEFAULT_RELATIVE);
+ print_path(path, prefix, format, PATH_DEFAULT_RELATIVE);
}
continue;
}
--
2.54.0
^ permalink raw reply related
* [GSoC][PATCH 3/4] repo: add path.gitdir with absolute and relative suffix formatting
From: K Jayatheerth @ 2026-06-01 15:19 UTC (permalink / raw)
To: git
Cc: jltobler, lucasseikioshiro, gitster, phillip.wood, sandals,
kumarayushjha123, a3205153416, K Jayatheerth
In-Reply-To: <20260601151950.30686-1-jayatheerthkulkarni2005@gmail.com>
Introduce path-related metadata fields to `git repo info` by adding
explicit `path.gitdir.absolute` and `path.gitdir.relative` keys. This
replaces dynamic prefix parsing machinery with individual, predictable
lexicographically-sorted keys that map directly to dedicated formatting
callbacks.
To calculate paths relative to the current working directory, update
`builtin/repo.c` to include `setup.h` and supply `startup_info->prefix`
to the path-formatting engine. Both explicit variants automatically
populate bulk dumps via `--all` and output predictably under `--keys`.
Update `t/t1900-repo-info.sh` to use a modernized, function-based loop
helper (`test_repo_info_path`) and `test_grep` to cleanly assert separate
path variation lookups.
Signed-off-by: K Jayatheerth <jayatheerthkulkarni2005@gmail.com>
Mentored-by: Justin Tobler <jltobler@gmail.com>
Mentored-by: Lucas Seiki Oshiro <lucasseikioshiro@gmail.com>
---
Documentation/git-repo.adoc | 6 ++++++
builtin/repo.c | 26 ++++++++++++++++++++++++++
t/t1900-repo-info.sh | 31 +++++++++++++++++++++++++++++++
3 files changed, 63 insertions(+)
diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index 42262c1983..a0dca7ce88 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -104,6 +104,12 @@ values that they return:
`object.format`::
The object format (hash algorithm) used in the repository.
+`path.gitdir.absolute`::
+ The canonical absolute path to the Git repository directory (the `.git` directory).
+
+`path.gitdir.relative`::
+ The path to the Git repository directory relative to the current working directory.
+
`references.format`::
The reference storage format. The valid values are:
+
diff --git a/builtin/repo.c b/builtin/repo.c
index 71a5c1c29c..c141ef892a 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -7,12 +7,14 @@
#include "hex.h"
#include "odb.h"
#include "parse-options.h"
+#include "path.h"
#include "path-walk.h"
#include "progress.h"
#include "quote.h"
#include "ref-filter.h"
#include "refs.h"
#include "revision.h"
+#include "setup.h"
#include "strbuf.h"
#include "string-list.h"
#include "shallow.h"
@@ -75,6 +77,28 @@ static int get_object_format(struct repository *repo, struct strbuf *buf)
return 0;
}
+static int get_path_gitdir_absolute(struct repository *repo, struct strbuf *buf)
+{
+ const char *git_dir = repo_get_git_dir(repo);
+
+ if (!git_dir)
+ return error(_("unable to get git directory"));
+
+ strbuf_add_path(buf, git_dir, startup_info->prefix, PATH_FORMAT_CANONICAL, PATH_DEFAULT_UNMODIFIED);
+ return 0;
+}
+
+static int get_path_gitdir_relative(struct repository *repo, struct strbuf *buf)
+{
+ const char *git_dir = repo_get_git_dir(repo);
+
+ if (!git_dir)
+ return error(_("unable to get git directory"));
+
+ strbuf_add_path(buf, git_dir, startup_info->prefix, PATH_FORMAT_RELATIVE, PATH_DEFAULT_UNMODIFIED);
+ return 0;
+}
+
static int get_references_format(struct repository *repo, struct strbuf *buf)
{
strbuf_addstr(buf,
@@ -87,6 +111,8 @@ static const struct repo_info_field repo_info_field[] = {
{ "layout.bare", get_layout_bare },
{ "layout.shallow", get_layout_shallow },
{ "object.format", get_object_format },
+ { "path.gitdir.absolute", get_path_gitdir_absolute },
+ { "path.gitdir.relative", get_path_gitdir_relative },
{ "references.format", get_references_format },
};
diff --git a/t/t1900-repo-info.sh b/t/t1900-repo-info.sh
index 39bb77dda0..7c7dfbb052 100755
--- a/t/t1900-repo-info.sh
+++ b/t/t1900-repo-info.sh
@@ -155,4 +155,35 @@ test_expect_success 'git repo info -h shows only repo info usage' '
test_grep ! "git repo structure" actual
'
+test_repo_info_path () {
+ field_name=$1
+ expect_relative=$2
+
+ test_expect_success "query individual key: path.$field_name.absolute" '
+ (
+ cd test-repo/sub &&
+ expect_absolute=$(cd .. && pwd)/.git &&
+ echo "path.$field_name.absolute=$expect_absolute" >expect &&
+ git repo info path.$field_name.absolute >actual &&
+ test_cmp expect actual
+ )
+ '
+
+ test_expect_success "query individual key: path.$field_name.relative" '
+ (
+ cd test-repo/sub &&
+ echo "path.$field_name.relative=$expect_relative" >expect &&
+ git repo info path.$field_name.relative >actual &&
+ test_cmp expect actual
+ )
+ '
+}
+
+test_expect_success 'setup test repository layout for path fields' '
+ git init test-repo &&
+ mkdir -p test-repo/sub
+'
+
+test_repo_info_path 'gitdir' '../.git'
+
test_done
--
2.54.0
^ permalink raw reply related
* [GSoC][PATCH 4/4] repo: add path.commondir with absolute and relative suffix formatting
From: K Jayatheerth @ 2026-06-01 15:19 UTC (permalink / raw)
To: git
Cc: jltobler, lucasseikioshiro, gitster, phillip.wood, sandals,
kumarayushjha123, a3205153416, K Jayatheerth
In-Reply-To: <20260601151950.30686-1-jayatheerthkulkarni2005@gmail.com>
Introduce `path.commondir.absolute` and `path.commondir.relative` keys
to `git repo info`. These track the repository's common directory path,
extending the path metadata engine alongside the existing `gitdir` fields.
Update `repo_info_field` to store the new keys in proper lexicographical
order to protect binary search operations, and expand the test matrix in
`t/t1900-repo-info.sh` to validate separate queries, bulk dumps, and
key listings.
Signed-off-by: K Jayatheerth <jayatheerthkulkarni2005@gmail.com>
Mentored-by: Justin Tobler <jltobler@gmail.com>
Mentored-by: Lucas Seiki Oshiro <lucasseikioshiro@gmail.com>
---
Documentation/git-repo.adoc | 9 +++++++++
builtin/repo.c | 24 ++++++++++++++++++++++++
t/t1900-repo-info.sh | 1 +
3 files changed, 34 insertions(+)
diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index a0dca7ce88..ed7d80c690 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -104,6 +104,15 @@ values that they return:
`object.format`::
The object format (hash algorithm) used in the repository.
+`path.commondir.absolute`::
+ The canonical absolute path to the Git repository's common
+ directory (the shared `.git` directory containing objects,
+ refs, and global configuration).
+
+`path.commondir.relative`::
+ The path to the Git repository's common directory relative to
+ the current working directory.
+
`path.gitdir.absolute`::
The canonical absolute path to the Git repository directory (the `.git` directory).
diff --git a/builtin/repo.c b/builtin/repo.c
index c141ef892a..be24a5a8e8 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -77,6 +77,28 @@ static int get_object_format(struct repository *repo, struct strbuf *buf)
return 0;
}
+static int get_path_commondir_absolute(struct repository *repo, struct strbuf *buf)
+{
+ const char *common_dir = repo_get_common_dir(repo);
+
+ if (!common_dir)
+ return error(_("unable to get common directory"));
+
+ strbuf_add_path(buf, common_dir, startup_info->prefix, PATH_FORMAT_CANONICAL, PATH_DEFAULT_UNMODIFIED);
+ return 0;
+}
+
+static int get_path_commondir_relative(struct repository *repo, struct strbuf *buf)
+{
+ const char *common_dir = repo_get_common_dir(repo);
+
+ if (!common_dir)
+ return error(_("unable to get common directory"));
+
+ strbuf_add_path(buf, common_dir, startup_info->prefix, PATH_FORMAT_RELATIVE, PATH_DEFAULT_UNMODIFIED);
+ return 0;
+}
+
static int get_path_gitdir_absolute(struct repository *repo, struct strbuf *buf)
{
const char *git_dir = repo_get_git_dir(repo);
@@ -111,6 +133,8 @@ static const struct repo_info_field repo_info_field[] = {
{ "layout.bare", get_layout_bare },
{ "layout.shallow", get_layout_shallow },
{ "object.format", get_object_format },
+ { "path.commondir.absolute", get_path_commondir_absolute },
+ { "path.commondir.relative", get_path_commondir_relative },
{ "path.gitdir.absolute", get_path_gitdir_absolute },
{ "path.gitdir.relative", get_path_gitdir_relative },
{ "references.format", get_references_format },
diff --git a/t/t1900-repo-info.sh b/t/t1900-repo-info.sh
index 7c7dfbb052..dd2706e1f7 100755
--- a/t/t1900-repo-info.sh
+++ b/t/t1900-repo-info.sh
@@ -184,6 +184,7 @@ test_expect_success 'setup test repository layout for path fields' '
mkdir -p test-repo/sub
'
+test_repo_info_path 'commondir' '../.git'
test_repo_info_path 'gitdir' '../.git'
test_done
--
2.54.0
^ permalink raw reply related
* [PATCH 0/2] builtin/history: introduce "drop" subcommand
From: Patrick Steinhardt @ 2026-06-01 15:36 UTC (permalink / raw)
To: git
Hi,
this small patch series introduces the new "drop" subcommand for
git-history(1). As a reader might guess, the command does exactly that:
given a commit, it will drop that commit from the commit history and
replay descendant branches on top of it.
Thanks!
Patrick
---
Patrick Steinhardt (2):
builtin/history: split handling of ref updates into two phases
builtin/history: implement "drop" subcommand
Documentation/git-history.adoc | 38 ++-
builtin/history.c | 333 +++++++++++++++++++++++---
t/meson.build | 1 +
t/t3454-history-drop.sh | 513 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 846 insertions(+), 39 deletions(-)
---
base-commit: 1666c1265231b0bc5f613fbbf3f0a9896cdef76e
change-id: 20260601-b4-pks-history-drop-28f6c6399e7b
^ permalink raw reply
* [PATCH 1/2] builtin/history: split handling of ref updates into two phases
From: Patrick Steinhardt @ 2026-06-01 15:36 UTC (permalink / raw)
To: git
In-Reply-To: <20260601-b4-pks-history-drop-v1-0-643e32340d55@pks.im>
The function `handle_reference_updates()` is used by git-history(1) to
update all references that refer to commits that have been rewritten. As
such, it performs two steps:
- It gathers the references that need to be updated in the first
place.
- It prepares and commits the reference transaction.
In a subsequent commit we'll want to handle those two steps separately.
Prepare for this by splitting up the function into two.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
builtin/history.c | 102 ++++++++++++++++++++++++++++++++++--------------------
1 file changed, 64 insertions(+), 38 deletions(-)
diff --git a/builtin/history.c b/builtin/history.c
index 0fc06fb204..4fadf38c32 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -333,21 +333,17 @@ static int handle_ref_update(struct ref_transaction *transaction,
NULL, NULL, 0, reflog_msg, err);
}
-static int handle_reference_updates(struct rev_info *revs,
- enum ref_action action,
- struct commit *original,
- struct commit *rewritten,
- const char *reflog_msg,
- int dry_run,
- enum replay_empty_commit_action empty)
+static int compute_pending_ref_updates(struct rev_info *revs,
+ enum ref_action action,
+ struct commit *original,
+ struct commit *rewritten,
+ enum replay_empty_commit_action empty,
+ struct replay_result *result)
{
const struct name_decoration *decoration;
struct replay_revisions_options opts = {
.empty = empty,
};
- struct replay_result result = { 0 };
- struct ref_transaction *transaction = NULL;
- struct strbuf err = STRBUF_INIT;
char hex[GIT_MAX_HEXSZ + 1];
bool detached_head;
int head_flags = 0;
@@ -359,34 +355,13 @@ static int handle_reference_updates(struct rev_info *revs,
opts.onto = oid_to_hex_r(hex, &rewritten->object.oid);
- ret = replay_revisions(revs, &opts, &result);
+ ret = replay_revisions(revs, &opts, result);
if (ret)
- goto out;
+ return ret;
if (action != REF_ACTION_BRANCHES && action != REF_ACTION_HEAD)
BUG("unsupported ref action %d", action);
- if (!dry_run) {
- transaction = ref_store_transaction_begin(get_main_ref_store(revs->repo), 0, &err);
- if (!transaction) {
- ret = error(_("failed to begin ref transaction: %s"), err.buf);
- goto out;
- }
- }
-
- for (size_t i = 0; i < result.updates_nr; i++) {
- ret = handle_ref_update(transaction,
- result.updates[i].refname,
- &result.updates[i].new_oid,
- &result.updates[i].old_oid,
- reflog_msg, &err);
- if (ret) {
- ret = error(_("failed to update ref '%s': %s"),
- result.updates[i].refname, err.buf);
- goto out;
- }
- }
-
/*
* `replay_revisions()` only updates references that are
* ancestors of `rewritten`, so we need to manually
@@ -414,14 +389,43 @@ static int handle_reference_updates(struct rev_info *revs,
!detached_head)
continue;
+ ALLOC_GROW(result->updates, result->updates_nr + 1, result->updates_alloc);
+ result->updates[result->updates_nr].refname = xstrdup(decoration->name);
+ result->updates[result->updates_nr].old_oid = original->object.oid;
+ result->updates[result->updates_nr].new_oid = rewritten->object.oid;
+ result->updates_nr++;
+ }
+
+ return 0;
+}
+
+static int apply_pending_ref_updates(struct repository *repo,
+ const struct replay_result *result,
+ const char *reflog_msg,
+ int dry_run)
+{
+ struct ref_transaction *transaction = NULL;
+ struct strbuf err = STRBUF_INIT;
+ int ret;
+
+ if (!dry_run) {
+ transaction = ref_store_transaction_begin(get_main_ref_store(repo),
+ 0, &err);
+ if (!transaction) {
+ ret = error(_("failed to begin ref transaction: %s"), err.buf);
+ goto out;
+ }
+ }
+
+ for (size_t i = 0; i < result->updates_nr; i++) {
ret = handle_ref_update(transaction,
- decoration->name,
- &rewritten->object.oid,
- &original->object.oid,
+ result->updates[i].refname,
+ &result->updates[i].new_oid,
+ &result->updates[i].old_oid,
reflog_msg, &err);
if (ret) {
ret = error(_("failed to update ref '%s': %s"),
- decoration->name, err.buf);
+ result->updates[i].refname, err.buf);
goto out;
}
}
@@ -435,11 +439,33 @@ static int handle_reference_updates(struct rev_info *revs,
out:
ref_transaction_free(transaction);
- replay_result_release(&result);
strbuf_release(&err);
return ret;
}
+static int handle_reference_updates(struct rev_info *revs,
+ enum ref_action action,
+ struct commit *original,
+ struct commit *rewritten,
+ const char *reflog_msg,
+ int dry_run,
+ enum replay_empty_commit_action empty)
+{
+ struct replay_result result = { 0 };
+ int ret;
+
+ ret = compute_pending_ref_updates(revs, action, original, rewritten,
+ empty, &result);
+ if (ret)
+ goto out;
+
+ ret = apply_pending_ref_updates(revs->repo, &result, reflog_msg, dry_run);
+
+out:
+ replay_result_release(&result);
+ return ret;
+}
+
static int commit_became_empty(struct repository *repo,
struct commit *original,
struct tree *result)
--
2.54.0.926.g75ba10bac6.dirty
^ permalink raw reply related
* [PATCH 2/2] builtin/history: implement "drop" subcommand
From: Patrick Steinhardt @ 2026-06-01 15:36 UTC (permalink / raw)
To: git
In-Reply-To: <20260601-b4-pks-history-drop-v1-0-643e32340d55@pks.im>
A common operation when editing the commit history is to drop a specific
commit from the history entirely, but this operation is not currently
covered by git-history(1).
A couple of noteworthy bits:
- This is the first git-history(1) command that will ultimately result
in changes to both the index and the working tree. We thus have to
add logic to merge resulting changes into those.
- It is still not possible to replay merge commits, so this limitation
is inherited for the new "drop" command.
- For now we refuse to drop root commits. While we _can_ indeed drop
root commits in the general case, there are edge cases where the
resulting history would become completely empty. This is thus left
to a subsequent patch series.
Other than that, most of the logic is rather straight-forward as we can
continue to build on the preexisting logic in git-history(1) for most of
the part.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 38 ++-
builtin/history.c | 231 +++++++++++++++++++
t/meson.build | 1 +
t/t3454-history-drop.sh | 513 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 782 insertions(+), 1 deletion(-)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 2ba8121795..4eac732fd2 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -8,6 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history
SYNOPSIS
--------
[synopsis]
+git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]
git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]
git history reword <commit> [--dry-run] [--update-refs=(branches|head)]
git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]
@@ -51,13 +52,28 @@ be stateful operations. The limitation can be lifted once (if) Git learns about
first-class conflicts.
When using `fixup` with `--empty=drop`, dropping the root commit is not yet
-supported.
+supported. Likewise, `drop` cannot remove the root commit or a merge commit.
COMMANDS
--------
The following commands are available to rewrite history in different ways:
+`drop <commit>`::
+ Remove the specified commit from the history. All descendants of the
+ commit are replayed directly onto its parent.
++
+The root commit cannot be dropped as that may lead to edge cases where refs
+end up with no commits anymore. Merge commits cannot be dropped either; see
+LIMITATIONS.
++
+If `HEAD` points at a commit that is to be rewritten, the index and working
+tree are updated to match the new `HEAD`. The command aborts before any
+references are updated in case local modifications would be overwritten.
++
+If replaying any descendant would result in a conflict, the command aborts
+with an error.
+
`fixup <commit>`::
Apply the currently staged changes to the specified commit. This is
similar in nature to `git commit --fixup=<commit>` followed by `git
@@ -170,6 +186,26 @@ The staged addition of `unrelated.txt` has been incorporated into the `first`
commit. All descendant commits have been replayed on top of the rewritten
history.
+Drop a commit
+~~~~~~~~~~~~~
+
+----------
+$ git log --oneline
+abc1234 (HEAD -> main) third
+def5678 second
+ghi9012 first
+
+$ git history drop def5678
+
+$ git log --oneline
+jkl3456 (HEAD -> main) third
+ghi9012 first
+----------
+
+The `second` commit has been removed from the history, and `third` has been
+replayed directly on top of `first`. All branches that pointed at the dropped
+commit have been moved to its parent.
+
Split a commit
~~~~~~~~~~~~~~
diff --git a/builtin/history.c b/builtin/history.c
index 4fadf38c32..12c27defbb 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -21,9 +21,12 @@
#include "sequencer.h"
#include "strvec.h"
#include "tree.h"
+#include "tree-walk.h"
#include "unpack-trees.h"
#include "wt-status.h"
+#define GIT_HISTORY_DROP_USAGE \
+ N_("git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]")
#define GIT_HISTORY_FIXUP_USAGE \
N_("git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]")
#define GIT_HISTORY_REWORD_USAGE \
@@ -1001,12 +1004,239 @@ static int cmd_history_split(int argc,
return ret;
}
+static int update_worktree(struct repository *repo,
+ const struct commit *old_head,
+ const struct commit *new_head,
+ bool dry_run)
+{
+ struct index_state index = INDEX_STATE_INIT(repo);
+ struct unpack_trees_options opts = { 0 };
+ struct lock_file lock = LOCK_INIT;
+ struct tree_desc desc[2] = { 0 };
+ char *desc_buf[2] = { 0 };
+ int ret;
+
+ if (!dry_run &&
+ repo_hold_locked_index(repo, &lock, LOCK_REPORT_ON_ERROR) < 0)
+ return -1;
+
+ if (read_index_from(&index, repo->index_file, repo->gitdir) < 0) {
+ ret = error(_("unable to read index"));
+ goto out;
+ }
+
+ setup_unpack_trees_porcelain(&opts, "history drop");
+ opts.head_idx = 1;
+ opts.src_index = &index;
+ opts.dst_index = &index;
+ opts.fn = twoway_merge;
+ opts.merge = 1;
+ opts.update = !dry_run;
+ opts.dry_run = dry_run;
+ opts.preserve_ignored = 0;
+ init_checkout_metadata(&opts.meta, NULL, &new_head->object.oid, NULL);
+
+ desc_buf[0] = fill_tree_descriptor(repo, &desc[0], &old_head->object.oid);
+ desc_buf[1] = fill_tree_descriptor(repo, &desc[1], &new_head->object.oid);
+
+ if (unpack_trees(2, desc, &opts)) {
+ ret = -1;
+ goto out;
+ }
+
+ if (!dry_run) {
+ cache_tree_free(&index.cache_tree);
+
+ if (write_locked_index(&index, &lock, COMMIT_LOCK)) {
+ ret = error(_("could not write index"));
+ goto out;
+ }
+ }
+
+ ret = 0;
+
+out:
+ clear_unpack_trees_porcelain(&opts);
+ rollback_lock_file(&lock);
+ release_index(&index);
+ free(desc_buf[0]);
+ free(desc_buf[1]);
+ return ret;
+}
+
+static int find_head_tree_change(struct repository *repo,
+ const struct replay_result *result,
+ struct commit **old_head,
+ struct commit **new_head,
+ bool *changed)
+{
+ const struct replay_ref_update *head_update = NULL;
+ struct commit *old_head_commit, *new_head_commit;
+ struct tree *old_head_tree, *new_head_tree;
+ const char *head_target;
+ int head_flags;
+
+ *changed = false;
+
+ head_target = refs_resolve_ref_unsafe(get_main_ref_store(repo),
+ "HEAD", RESOLVE_REF_NO_RECURSE,
+ NULL, &head_flags);
+ if (!head_target)
+ return error(_("cannot look up HEAD"));
+ if (!(head_flags & REF_ISSYMREF))
+ head_target = "HEAD";
+
+ for (size_t i = 0; i < result->updates_nr; i++) {
+ if (!strcmp(result->updates[i].refname, head_target)) {
+ head_update = &result->updates[i];
+ break;
+ }
+ }
+
+ if (!head_update)
+ return 0;
+
+ old_head_commit = lookup_commit_reference(repo, &head_update->old_oid);
+ new_head_commit = lookup_commit_reference(repo, &head_update->new_oid);
+ if (!old_head_commit || !new_head_commit)
+ return error(_("cannot resolve HEAD commit"));
+
+ old_head_tree = repo_get_commit_tree(repo, old_head_commit);
+ new_head_tree = repo_get_commit_tree(repo, new_head_commit);
+ if (!old_head_tree || !new_head_tree)
+ return error(_("cannot resolve tree for HEAD"));
+
+ if (oideq(&old_head_tree->object.oid, &new_head_tree->object.oid))
+ return 0;
+
+ *old_head = old_head_commit;
+ *new_head = new_head_commit;
+ *changed = true;
+
+ return 0;
+}
+
+static int cmd_history_drop(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ GIT_HISTORY_DROP_USAGE,
+ NULL,
+ };
+ enum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;
+ enum ref_action action = REF_ACTION_DEFAULT;
+ int dry_run = 0;
+ struct option options[] = {
+ OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
+ N_("control which refs should be updated"),
+ PARSE_OPT_NONEG, parse_ref_action),
+ OPT_BOOL('n', "dry-run", &dry_run,
+ N_("perform a dry-run without updating any refs")),
+ OPT_CALLBACK_F(0, "empty", &empty, "(drop|keep|abort)",
+ N_("how to handle descendants that become empty"),
+ PARSE_OPT_NONEG, parse_opt_empty),
+ OPT_END(),
+ };
+ struct strbuf reflog_msg = STRBUF_INIT;
+ struct commit *original, *rewritten;
+ struct rev_info revs = { 0 };
+ struct replay_result result = { 0 };
+ struct commit *old_head, *new_head;
+ bool head_moves = false;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc != 1) {
+ ret = error(_("command expects a single revision"));
+ goto out;
+ }
+ repo_config(repo, git_default_config, NULL);
+
+ if (action == REF_ACTION_DEFAULT)
+ action = REF_ACTION_BRANCHES;
+
+ original = lookup_commit_reference_by_name(argv[0]);
+ if (!original) {
+ ret = error(_("commit cannot be found: %s"), argv[0]);
+ goto out;
+ }
+
+ if (!original->parents) {
+ ret = error(_("cannot drop root commit %s: "
+ "it has no parent to replay onto"),
+ argv[0]);
+ goto out;
+ } else if (original->parents->next) {
+ ret = error(_("cannot drop merge commit"));
+ goto out;
+ }
+
+ ret = setup_revwalk(repo, action, original, &revs);
+ if (ret)
+ goto out;
+
+ rewritten = original->parents->item;
+
+ ret = compute_pending_ref_updates(&revs, action, original, rewritten,
+ empty, &result);
+ if (ret) {
+ ret = error(_("failed replaying descendants"));
+ goto out;
+ }
+
+ /*
+ * If HEAD will move as a result of the rewrite then we'll have to
+ * merge in the changes into the worktree and index. This merge can of
+ * course conflict, which will cause the whole operation to abort.
+ *
+ * If we had already updated the refs at that point then we'd have an
+ * inconsistent repository state. So we first perform a dry-run merge
+ * here before updating refs.
+ */
+ if (!dry_run && !is_bare_repository()) {
+ ret = find_head_tree_change(repo, &result, &old_head,
+ &new_head, &head_moves);
+ if (ret < 0)
+ goto out;
+
+ if (head_moves && update_worktree(repo, old_head, new_head, true) < 0) {
+ ret = error(_("dropping this commit would "
+ "overwrite local changes; aborting"));
+ goto out;
+ }
+ }
+
+ strbuf_addf(&reflog_msg, "drop: dropping %s", argv[0]);
+ ret = apply_pending_ref_updates(repo, &result, reflog_msg.buf, dry_run);
+ if (ret < 0) {
+ ret = error(_("failed to update references"));
+ goto out;
+ }
+
+ if (head_moves && update_worktree(repo, old_head, new_head, false) < 0) {
+ ret = error(_("failed to update working tree; "
+ "run `git checkout HEAD` to sync"));
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ replay_result_release(&result);
+ strbuf_release(&reflog_msg);
+ release_revisions(&revs);
+ return ret;
+}
+
int cmd_history(int argc,
const char **argv,
const char *prefix,
struct repository *repo)
{
const char * const usage[] = {
+ GIT_HISTORY_DROP_USAGE,
GIT_HISTORY_FIXUP_USAGE,
GIT_HISTORY_REWORD_USAGE,
GIT_HISTORY_SPLIT_USAGE,
@@ -1014,6 +1244,7 @@ int cmd_history(int argc,
};
parse_opt_subcommand_fn *fn = NULL;
struct option options[] = {
+ OPT_SUBCOMMAND("drop", &fn, cmd_history_drop),
OPT_SUBCOMMAND("fixup", &fn, cmd_history_fixup),
OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
OPT_SUBCOMMAND("split", &fn, cmd_history_split),
diff --git a/t/meson.build b/t/meson.build
index 2af8d01279..d5e71056b2 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -399,6 +399,7 @@ integration_tests = [
't3451-history-reword.sh',
't3452-history-split.sh',
't3453-history-fixup.sh',
+ 't3454-history-drop.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3454-history-drop.sh b/t/t3454-history-drop.sh
new file mode 100755
index 0000000000..b320ff09b3
--- /dev/null
+++ b/t/t3454-history-drop.sh
@@ -0,0 +1,513 @@
+#!/bin/sh
+
+test_description='tests for git-history drop subcommand'
+
+. ./test-lib.sh
+. "$TEST_DIRECTORY/lib-log-graph.sh"
+
+expect_graph () {
+ cat >expect &&
+ lib_test_cmp_graph --graph --format=%s "$@"
+}
+
+expect_log () {
+ git log --format="%s" "$@" >actual &&
+ cat >expect &&
+ test_cmp expect actual
+}
+
+test_expect_success 'errors on missing commit argument' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ test_must_fail git history drop 2>err &&
+ test_grep "command expects a single revision" err
+ )
+'
+
+test_expect_success 'errors on too many arguments' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ test_must_fail git history drop HEAD HEAD 2>err &&
+ test_grep "command expects a single revision" err
+ )
+'
+
+test_expect_success 'errors on unknown revision' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ test_must_fail git history drop does-not-exist 2>err &&
+ test_grep "commit cannot be found: does-not-exist" err
+ )
+'
+
+test_expect_success 'errors with invalid --empty= value' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ test_commit -C repo initial &&
+ test_commit -C repo second &&
+ test_must_fail git -C repo history drop --empty=bogus HEAD 2>err &&
+ test_grep "unrecognized.*--empty.*bogus" err
+'
+
+test_expect_success 'drops a commit in the middle and replays descendants' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git symbolic-ref HEAD >expect &&
+ git history drop HEAD~ &&
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ expect_log <<-\EOF &&
+ third
+ first
+ EOF
+
+ test_must_fail git show HEAD:second.t &&
+ test_path_is_missing second.t &&
+
+ git reflog >reflog &&
+ test_grep "drop: dropping HEAD~" reflog
+ )
+'
+
+test_expect_success 'drops the HEAD commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+
+ git history drop HEAD &&
+
+ expect_log <<-\EOF
+ first
+ EOF
+ )
+'
+
+test_expect_success 'drops a commit on detached HEAD' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ git checkout --detach HEAD &&
+
+ git history drop HEAD~ &&
+
+ expect_log <<-\EOF
+ third
+ first
+ EOF
+ )
+'
+
+# Note: in this case it would actually be fine to drop the root commit, as we
+# do have a descendant commit, and no reference points to the root commit
+# directly. So this is something that we may relax eventually.
+test_expect_success 'refuses to drop the root commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+
+ test_must_fail git history drop HEAD~ 2>err &&
+ test_grep "cannot drop root commit" err
+ )
+'
+
+# In contrast to the above case, we actually don't want to drop the root commit
+# here as that would cause us to end up with an empty commit graph.
+test_expect_success 'refuses to drop the root commit when branch becomes empty' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+
+ test_must_fail git history drop HEAD 2>err &&
+ test_grep "cannot drop root commit" err
+ )
+'
+
+test_expect_success 'refuses to drop a merge commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ git switch - &&
+ git merge theirs &&
+
+ test_must_fail git history drop HEAD 2>err &&
+ test_grep "cannot drop merge commit" err
+ )
+'
+
+test_expect_success 'refuses when descendants contain a merge commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ test_commit middle &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ git switch - &&
+ git merge theirs &&
+
+ test_must_fail git history drop middle 2>err &&
+ test_grep "replaying merge commits is not supported yet" err
+ )
+'
+
+test_expect_success 'works in a bare repository' '
+ test_when_finished "rm -rf repo repo.git" &&
+
+ git init repo &&
+ test_commit -C repo first &&
+ test_commit -C repo second &&
+ test_commit -C repo third &&
+
+ git clone --bare repo repo.git &&
+ (
+ cd repo.git &&
+
+ git history drop HEAD~ &&
+ expect_log <<-\EOF
+ third
+ first
+ EOF
+ )
+'
+
+test_expect_success 'updates branches on other lines of descent' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ test_commit target &&
+ git branch theirs &&
+ test_commit ours &&
+ git switch theirs &&
+ test_commit theirs &&
+
+ expect_graph --branches <<-\EOF &&
+ * theirs
+ | * ours
+ |/
+ * target
+ * base
+ EOF
+
+ git history drop target &&
+
+ expect_graph --branches <<-\EOF
+ * ours
+ | * theirs
+ |/
+ * base
+ EOF
+ )
+'
+
+test_expect_success 'moves branch pointing at dropped commit to its parent' '
+ test_when_finished "rm -rf repo" &&
+ git init repo --initial-branch=main &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ git branch points-at-second &&
+ test_commit third &&
+
+ git rev-parse first >expect &&
+ git history drop second &&
+ git rev-parse points-at-second >actual &&
+ test_cmp expect actual &&
+
+ expect_log --format="%s %D" --branches <<-\EOF
+ third HEAD -> main
+ first tag: first, points-at-second
+ EOF
+ )
+'
+
+test_expect_success '--dry-run prints ref updates without modifying repo' '
+ test_when_finished "rm -rf repo" &&
+ git init repo --initial-branch=main &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit middle &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+
+ git refs list >refs-expect &&
+ git history drop --dry-run main~ >updates &&
+ git refs list >refs-actual &&
+ test_cmp refs-expect refs-actual &&
+ test_grep "update refs/heads/main" updates &&
+
+ git update-ref --stdin <updates &&
+ expect_log main <<-\EOF
+ ours
+ base
+ EOF
+ )
+'
+
+test_expect_success '--update-refs=head updates only HEAD' '
+ test_when_finished "rm -rf repo" &&
+ git init repo --initial-branch=main &&
+ (
+ cd repo &&
+ test_commit base &&
+ test_commit target &&
+ git branch theirs &&
+ test_commit ours &&
+ git switch theirs &&
+ test_commit theirs &&
+
+ # When told to update HEAD only, the command refuses to
+ # rewrite commits that are not an ancestor of HEAD.
+ test_must_fail git history drop --update-refs=head main 2>err &&
+ test_grep "rewritten commit must be an ancestor of HEAD" err &&
+
+ expect_graph --branches <<-\EOF &&
+ * theirs
+ | * ours
+ |/
+ * target
+ * base
+ EOF
+
+ git switch main &&
+ git history drop --update-refs=head target &&
+
+ expect_graph --branches <<-\EOF
+ * ours
+ | * theirs
+ | * target
+ |/
+ * base
+ EOF
+ )
+'
+
+test_expect_success 'conflict with replayed commit aborts cleanly' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ test_commit conflict-a file &&
+ test_commit conflict-b file &&
+
+ git refs list >refs-expect &&
+ test_must_fail git history drop HEAD~ 2>err &&
+ test_grep "failed replaying descendants" err &&
+ git refs list >refs-actual &&
+ test_cmp refs-expect refs-actual
+ )
+'
+
+# Build a history where a descendant of the drop target reverts the change
+# introduced by the drop target. After dropping, the descendant's diff applies
+# against a tree that already lacks the change, so it becomes empty.
+setup_empty_descendant_repo () {
+ git init "$1" &&
+ (
+ cd "$1" &&
+ echo C1 >file &&
+ git add file &&
+ git commit -m "base" &&
+ git tag base &&
+ echo C2 >file &&
+ git add file &&
+ git commit -m "drop-me" &&
+ git tag drop-me &&
+ test_commit middle &&
+ echo C1 >file &&
+ git add file &&
+ git commit -m "revert-drop-me" &&
+ git tag revert-drop-me
+ )
+}
+
+test_expect_success '--empty=drop drops descendants that become empty' '
+ test_when_finished "rm -rf repo" &&
+ setup_empty_descendant_repo repo &&
+ (
+ cd repo &&
+
+ git history drop --empty=drop drop-me &&
+
+ expect_log <<-\EOF
+ middle
+ base
+ EOF
+ )
+'
+
+test_expect_success '--empty=keep keeps descendants that become empty' '
+ test_when_finished "rm -rf repo" &&
+ setup_empty_descendant_repo repo &&
+ (
+ cd repo &&
+
+ git history drop --empty=keep drop-me &&
+
+ expect_log <<-\EOF &&
+ revert-drop-me
+ middle
+ base
+ EOF
+ git diff HEAD~ HEAD >diff &&
+ test_must_be_empty diff
+ )
+'
+
+test_expect_success '--empty=abort errors out when a descendant becomes empty' '
+ test_when_finished "rm -rf repo" &&
+ setup_empty_descendant_repo repo &&
+ (
+ cd repo &&
+
+ test_must_fail git history drop --empty=abort drop-me 2>err &&
+ test_grep "became empty after replay" err
+ )
+'
+
+test_expect_success 'updates index and worktree when HEAD moves' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git history drop second &&
+
+ # Worktree should no longer contain second.t.
+ test_path_is_missing second.t &&
+ test_path_is_file first.t &&
+ test_path_is_file third.t &&
+
+ # Index and worktree should both match the new HEAD.
+ git status --porcelain --untracked-files=no >status &&
+ test_must_be_empty status
+ )
+'
+
+test_expect_success 'updates worktree when dropping HEAD itself' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+
+ git history drop HEAD &&
+
+ test_path_is_missing second.t &&
+ test_path_is_file first.t &&
+
+ git status --porcelain --untracked-files=no >status &&
+ test_must_be_empty status
+ )
+'
+
+test_expect_success 'preserves unrelated unstaged modifications' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ echo first-content >unrelated.txt &&
+ git add unrelated.txt &&
+ git commit -m "add unrelated" &&
+ test_commit second &&
+ test_commit third &&
+
+ echo locally-modified >unrelated.txt &&
+
+ git diff >diff-expect &&
+ git history drop second &&
+ git diff >diff-actual &&
+ test_cmp diff-expect diff-actual &&
+ test_path_is_missing second.t
+ )
+'
+
+test_expect_success 'preserves unrelated staged changes' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ echo first-content >unrelated.txt &&
+ git add unrelated.txt &&
+ git commit -m "add unrelated" &&
+ test_commit second &&
+ test_commit third &&
+
+ echo staged-change >unrelated.txt &&
+ git add unrelated.txt &&
+
+ git diff --cached >diff-expect &&
+ git history drop second &&
+ git diff --cached >diff-actual &&
+ test_cmp diff-expect diff-actual &&
+ test_path_is_missing second.t
+ )
+'
+
+test_expect_success 'aborts when local modifications would be overwritten' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ test_commit conflict &&
+
+ echo local-edit >conflict.t &&
+ git diff >diff-expect &&
+ test_must_fail git history drop HEAD 2>err &&
+ test_grep "would overwrite local changes" err &&
+ git diff >diff-actual &&
+ test_cmp diff-expect diff-actual
+ )
+'
+
+test_done
--
2.54.0.926.g75ba10bac6.dirty
^ permalink raw reply related
* [PATCH v4 0/8] repo_config_values: migrate more globals
From: Olamide Caleb Bello @ 2026-06-01 15:42 UTC (permalink / raw)
To: git
Cc: phillip.wood123, christian.couder, usmanakinyemi202,
kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260423160832.114816-1-belkid98@gmail.com>
Changes since version 3:
- Reword commit subjects for consistency (changed `env` to `environment`)
- Updated commit message in (move "trust_ctime" into `struct repo_config_values`)
Olamide Caleb Bello (8):
environment: move "trust_ctime" into `struct repo_config_values`
environment: move "check_stat" into `struct repo_config_values`
environment: move `zlib_compression_level` into `struct
repo_config_values`
environment: move "pack_compression_level" into `struct
repo_config_values`
environment: move "precomposed_unicode" into `struct
repo_config_values`
environment: move "core_sparse_checkout_cone" into `struct
repo_config_values`
environment: move "sparse_expect_files_outside_of_patterns" into
`repo_config_values`
environment: move "warn_on_object_refname_ambiguity" into `struct
repo_config_values`
builtin/cat-file.c | 7 ++++---
builtin/fast-import.c | 8 +++++---
builtin/index-pack.c | 3 ++-
builtin/mv.c | 2 +-
builtin/pack-objects.c | 24 +++++++++++++----------
builtin/sparse-checkout.c | 37 +++++++++++++++++++++---------------
compat/precompose_utf8.c | 20 +++++++++++++-------
diff.c | 3 ++-
dir.c | 3 ++-
entry.c | 3 ++-
environment.c | 40 +++++++++++++++++++++------------------
environment.h | 19 ++++++++++---------
http-push.c | 3 ++-
object-file.c | 6 ++++--
object-name.c | 3 ++-
revision.c | 7 ++++---
sparse-index.c | 4 ++--
statinfo.c | 12 +++++++-----
submodule.c | 7 ++++---
upload-pack.c | 3 ++-
20 files changed, 126 insertions(+), 88 deletions(-)
--
2.53.0.155.g9f36b15afa
^ permalink raw reply
* [PATCH v4 1/8] environment: move "trust_ctime" into `struct repo_config_values`
From: Olamide Caleb Bello @ 2026-06-01 15:42 UTC (permalink / raw)
To: git
Cc: phillip.wood123, christian.couder, usmanakinyemi202,
kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260601154211.82370-1-belkid98@gmail.com>
The `core.trustctime` configuration is currently stored in the global
variable `trust_ctime`, which makes it shared across repository
instances in a single process.
Store it instead in `repo_config_values`, where eagerly‑parsed
repository configuration lives. `core.trustctime` is parsed eagerly
because it is used in low‑level stat‑matching functions
(`match_stat_data()`), where a lazy parse could cause unexpected
fatal errors, result in a performance regression and complicate
libification efforts. This preserves that behavior while tying the
value to the repository from which it was read, avoiding cross‑repository
state leakage and continuing the effort to reduce reliance on global
configuration state.
Update all references to use repo_config_values().
Mentored-by: Christian Couder <christian.couder@gmail.com>
Signed-off-by: Olamide Caleb Bello <belkid98@gmail.com>
---
environment.c | 4 ++--
environment.h | 2 +-
statinfo.c | 6 ++++--
3 files changed, 7 insertions(+), 5 deletions(-)
diff --git a/environment.c b/environment.c
index fc3ed8bb1c..0a9067729e 100644
--- a/environment.c
+++ b/environment.c
@@ -42,7 +42,6 @@ static int pack_compression_seen;
static int zlib_compression_seen;
int trust_executable_bit = 1;
-int trust_ctime = 1;
int check_stat = 1;
int has_symlinks = 1;
int minimum_abbrev = 4, default_abbrev = -1;
@@ -309,7 +308,7 @@ int git_default_core_config(const char *var, const char *value,
return 0;
}
if (!strcmp(var, "core.trustctime")) {
- trust_ctime = git_config_bool(var, value);
+ cfg->trust_ctime = git_config_bool(var, value);
return 0;
}
if (!strcmp(var, "core.checkstat")) {
@@ -721,4 +720,5 @@ void repo_config_values_init(struct repo_config_values *cfg)
cfg->attributes_file = NULL;
cfg->apply_sparse_checkout = 0;
cfg->branch_track = BRANCH_TRACK_REMOTE;
+ cfg->trust_ctime = 1;
}
diff --git a/environment.h b/environment.h
index 123a71cdc8..64d537686e 100644
--- a/environment.h
+++ b/environment.h
@@ -91,6 +91,7 @@ struct repo_config_values {
/* section "core" config values */
char *attributes_file;
int apply_sparse_checkout;
+ int trust_ctime;
/* section "branch" config values */
enum branch_track branch_track;
@@ -161,7 +162,6 @@ extern char *git_work_tree_cfg;
/* Environment bits from configuration mechanism */
extern int trust_executable_bit;
-extern int trust_ctime;
extern int check_stat;
extern int has_symlinks;
extern int minimum_abbrev, default_abbrev;
diff --git a/statinfo.c b/statinfo.c
index 30a164b0e6..4fc12053f4 100644
--- a/statinfo.c
+++ b/statinfo.c
@@ -3,6 +3,7 @@
#include "git-compat-util.h"
#include "environment.h"
#include "statinfo.h"
+#include "repository.h"
/*
* Munge st_size into an unsigned int.
@@ -63,17 +64,18 @@ void fake_lstat_data(const struct stat_data *sd, struct stat *st)
int match_stat_data(const struct stat_data *sd, struct stat *st)
{
int changed = 0;
+ struct repo_config_values *cfg = repo_config_values(the_repository);
if (sd->sd_mtime.sec != (unsigned int)st->st_mtime)
changed |= MTIME_CHANGED;
- if (trust_ctime && check_stat &&
+ if (cfg->trust_ctime && check_stat &&
sd->sd_ctime.sec != (unsigned int)st->st_ctime)
changed |= CTIME_CHANGED;
#ifdef USE_NSEC
if (check_stat && sd->sd_mtime.nsec != ST_MTIME_NSEC(*st))
changed |= MTIME_CHANGED;
- if (trust_ctime && check_stat &&
+ if (cfg->trust_ctime && check_stat &&
sd->sd_ctime.nsec != ST_CTIME_NSEC(*st))
changed |= CTIME_CHANGED;
#endif
--
2.53.0.155.g9f36b15afa
^ permalink raw reply related
* [PATCH v4 2/8] environment: move "check_stat" into `struct repo_config_values`
From: Olamide Caleb Bello @ 2026-06-01 15:42 UTC (permalink / raw)
To: git
Cc: phillip.wood123, christian.couder, usmanakinyemi202,
kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260601154211.82370-1-belkid98@gmail.com>
The `core.checkstat` configuration is currently stored in the global
variable `check_stat`, which makes it shared across repository
instances within a single process.
Store it instead in `repo_config_values`, where eagerly‑parsed
repository configuration lives. `core.checkstat` is parsed eagerly
because it controls how `match_stat_data()` and related functions
decide file freshness; a lazy parse could lead to unexpected
behavior or complicate libification. This preserves the existing
eager‑parsing behavior while tying the value to the repository it
was read from, avoiding cross‑repository state leakage, and
continuing the effort to reduce reliance on global configuration
state.
Update all references to use `repo_config_values()`.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Usman Akinyemi <usmanakinyemi202@gmail.com>
Signed-off-by: Olamide Caleb Bello <belkid98@gmail.com>
---
entry.c | 3 ++-
environment.c | 6 +++---
environment.h | 2 +-
statinfo.c | 10 +++++-----
4 files changed, 11 insertions(+), 10 deletions(-)
diff --git a/entry.c b/entry.c
index 7817aee362..c55e867d8a 100644
--- a/entry.c
+++ b/entry.c
@@ -443,7 +443,8 @@ static int check_path(const char *path, int len, struct stat *st, int skiplen)
static void mark_colliding_entries(const struct checkout *state,
struct cache_entry *ce, struct stat *st)
{
- int trust_ino = check_stat;
+ struct repo_config_values *cfg = repo_config_values(the_repository);
+ int trust_ino = cfg->check_stat;
#if defined(GIT_WINDOWS_NATIVE) || defined(__CYGWIN__)
trust_ino = 0;
diff --git a/environment.c b/environment.c
index 0a9067729e..8542ac3141 100644
--- a/environment.c
+++ b/environment.c
@@ -42,7 +42,6 @@ static int pack_compression_seen;
static int zlib_compression_seen;
int trust_executable_bit = 1;
-int check_stat = 1;
int has_symlinks = 1;
int minimum_abbrev = 4, default_abbrev = -1;
int ignore_case;
@@ -315,9 +314,9 @@ int git_default_core_config(const char *var, const char *value,
if (!value)
return config_error_nonbool(var);
if (!strcasecmp(value, "default"))
- check_stat = 1;
+ cfg->check_stat = 1;
else if (!strcasecmp(value, "minimal"))
- check_stat = 0;
+ cfg->check_stat = 0;
else
return error(_("invalid value for '%s': '%s'"),
var, value);
@@ -721,4 +720,5 @@ void repo_config_values_init(struct repo_config_values *cfg)
cfg->apply_sparse_checkout = 0;
cfg->branch_track = BRANCH_TRACK_REMOTE;
cfg->trust_ctime = 1;
+ cfg->check_stat = 1;
}
diff --git a/environment.h b/environment.h
index 64d537686e..1d3e2e4f23 100644
--- a/environment.h
+++ b/environment.h
@@ -92,6 +92,7 @@ struct repo_config_values {
char *attributes_file;
int apply_sparse_checkout;
int trust_ctime;
+ int check_stat;
/* section "branch" config values */
enum branch_track branch_track;
@@ -162,7 +163,6 @@ extern char *git_work_tree_cfg;
/* Environment bits from configuration mechanism */
extern int trust_executable_bit;
-extern int check_stat;
extern int has_symlinks;
extern int minimum_abbrev, default_abbrev;
extern int ignore_case;
diff --git a/statinfo.c b/statinfo.c
index 4fc12053f4..5e00af127d 100644
--- a/statinfo.c
+++ b/statinfo.c
@@ -68,19 +68,19 @@ int match_stat_data(const struct stat_data *sd, struct stat *st)
if (sd->sd_mtime.sec != (unsigned int)st->st_mtime)
changed |= MTIME_CHANGED;
- if (cfg->trust_ctime && check_stat &&
+ if (cfg->trust_ctime && cfg->check_stat &&
sd->sd_ctime.sec != (unsigned int)st->st_ctime)
changed |= CTIME_CHANGED;
#ifdef USE_NSEC
- if (check_stat && sd->sd_mtime.nsec != ST_MTIME_NSEC(*st))
+ if (cfg->check_stat && sd->sd_mtime.nsec != ST_MTIME_NSEC(*st))
changed |= MTIME_CHANGED;
- if (cfg->trust_ctime && check_stat &&
+ if (cfg->trust_ctime && cfg->check_stat &&
sd->sd_ctime.nsec != ST_CTIME_NSEC(*st))
changed |= CTIME_CHANGED;
#endif
- if (check_stat) {
+ if (cfg->check_stat) {
if (sd->sd_uid != (unsigned int) st->st_uid ||
sd->sd_gid != (unsigned int) st->st_gid)
changed |= OWNER_CHANGED;
@@ -94,7 +94,7 @@ int match_stat_data(const struct stat_data *sd, struct stat *st)
* clients will have different views of what "device"
* the filesystem is on
*/
- if (check_stat && sd->sd_dev != (unsigned int) st->st_dev)
+ if (cfg->check_stat && sd->sd_dev != (unsigned int) st->st_dev)
changed |= INODE_CHANGED;
#endif
--
2.53.0.155.g9f36b15afa
^ permalink raw reply related
* [PATCH v4 3/8] environment: move `zlib_compression_level` into `struct repo_config_values`
From: Olamide Caleb Bello @ 2026-06-01 15:42 UTC (permalink / raw)
To: git
Cc: phillip.wood123, christian.couder, usmanakinyemi202,
kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260601154211.82370-1-belkid98@gmail.com>
The `zlib_compression_level` configuration is currently stored in the
global variable `zlib_compression_level`, which makes it shared across
repository instances within a single process.
Store it instead in `repo_config_values`, where eagerly‑parsed
repository configuration lives. `zlib_compression_level` is parsed
eagerly because it determines compression behaviour for objects and
packs – core operations where a lazy parse could lead to unpredictable
results and hinder libification. This preserves the existing
eager‑parsing behavior while tying the value to the repository it
was read from, avoiding cross‑repository state leakage and continuing
the effort to reduce reliance on global configuration state.
Update all references to use `repo_config_values()`.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Usman Akinyemi <usmanakinyemi202@gmail.com>
Signed-off-by: Olamide Caleb Bello <belkid98@gmail.com>
---
builtin/index-pack.c | 3 ++-
diff.c | 3 ++-
environment.c | 6 +++---
environment.h | 2 +-
http-push.c | 3 ++-
object-file.c | 3 ++-
6 files changed, 12 insertions(+), 8 deletions(-)
diff --git a/builtin/index-pack.c b/builtin/index-pack.c
index ca7784dc2c..3942d3e0d0 100644
--- a/builtin/index-pack.c
+++ b/builtin/index-pack.c
@@ -1416,8 +1416,9 @@ static int write_compressed(struct hashfile *f, void *in, unsigned int size)
git_zstream stream;
int status;
unsigned char outbuf[4096];
+ struct repo_config_values *cfg = repo_config_values(the_repository);
- git_deflate_init(&stream, zlib_compression_level);
+ git_deflate_init(&stream, cfg->zlib_compression_level);
stream.next_in = in;
stream.avail_in = size;
diff --git a/diff.c b/diff.c
index 397e38b41c..7d17b0bf3f 100644
--- a/diff.c
+++ b/diff.c
@@ -3589,8 +3589,9 @@ static unsigned char *deflate_it(char *data,
int bound;
unsigned char *deflated;
git_zstream stream;
+ struct repo_config_values *cfg = repo_config_values(the_repository);
- git_deflate_init(&stream, zlib_compression_level);
+ git_deflate_init(&stream, cfg->zlib_compression_level);
bound = git_deflate_bound(&stream, size);
deflated = xmalloc(bound);
stream.next_out = deflated;
diff --git a/environment.c b/environment.c
index 8542ac3141..5b0e88b65c 100644
--- a/environment.c
+++ b/environment.c
@@ -52,7 +52,6 @@ char *git_commit_encoding;
char *git_log_output_encoding;
char *apply_default_whitespace;
char *apply_default_ignorewhitespace;
-int zlib_compression_level = Z_BEST_SPEED;
int pack_compression_level = Z_DEFAULT_COMPRESSION;
int fsync_object_files = -1;
int use_fsync = -1;
@@ -377,7 +376,7 @@ int git_default_core_config(const char *var, const char *value,
level = Z_DEFAULT_COMPRESSION;
else if (level < 0 || level > Z_BEST_COMPRESSION)
die(_("bad zlib compression level %d"), level);
- zlib_compression_level = level;
+ cfg->zlib_compression_level = level;
zlib_compression_seen = 1;
return 0;
}
@@ -389,7 +388,7 @@ int git_default_core_config(const char *var, const char *value,
else if (level < 0 || level > Z_BEST_COMPRESSION)
die(_("bad zlib compression level %d"), level);
if (!zlib_compression_seen)
- zlib_compression_level = level;
+ cfg->zlib_compression_level = level;
if (!pack_compression_seen)
pack_compression_level = level;
return 0;
@@ -721,4 +720,5 @@ void repo_config_values_init(struct repo_config_values *cfg)
cfg->branch_track = BRANCH_TRACK_REMOTE;
cfg->trust_ctime = 1;
cfg->check_stat = 1;
+ cfg->zlib_compression_level = Z_BEST_SPEED;
}
diff --git a/environment.h b/environment.h
index 1d3e2e4f23..93201620af 100644
--- a/environment.h
+++ b/environment.h
@@ -93,6 +93,7 @@ struct repo_config_values {
int apply_sparse_checkout;
int trust_ctime;
int check_stat;
+ int zlib_compression_level;
/* section "branch" config values */
enum branch_track branch_track;
@@ -170,7 +171,6 @@ extern int assume_unchanged;
extern int warn_on_object_refname_ambiguity;
extern char *apply_default_whitespace;
extern char *apply_default_ignorewhitespace;
-extern int zlib_compression_level;
extern int pack_compression_level;
extern unsigned long pack_size_limit_cfg;
diff --git a/http-push.c b/http-push.c
index d143fe2845..8ac107a56e 100644
--- a/http-push.c
+++ b/http-push.c
@@ -369,13 +369,14 @@ static void start_put(struct transfer_request *request)
int hdrlen;
ssize_t size;
git_zstream stream;
+ struct repo_config_values *cfg = repo_config_values(the_repository);
unpacked = odb_read_object(the_repository->objects, &request->obj->oid,
&type, &len);
hdrlen = format_object_header(hdr, sizeof(hdr), type, len);
/* Set it up */
- git_deflate_init(&stream, zlib_compression_level);
+ git_deflate_init(&stream, cfg->zlib_compression_level);
size = git_deflate_bound(&stream, len + hdrlen);
strbuf_grow(&request->buffer.buf, size);
request->buffer.posn = 0;
diff --git a/object-file.c b/object-file.c
index 2acc9522df..7c122ac419 100644
--- a/object-file.c
+++ b/object-file.c
@@ -906,6 +906,7 @@ static int start_loose_object_common(struct odb_source *source,
const struct git_hash_algo *algo = source->odb->repo->hash_algo;
const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;
int fd;
+ struct repo_config_values *cfg = repo_config_values(the_repository);
fd = create_tmpfile(source->odb->repo, tmp_file, filename);
if (fd < 0) {
@@ -921,7 +922,7 @@ static int start_loose_object_common(struct odb_source *source,
}
/* Setup zlib stream for compression */
- git_deflate_init(stream, zlib_compression_level);
+ git_deflate_init(stream, cfg->zlib_compression_level);
stream->next_out = buf;
stream->avail_out = buflen;
algo->init_fn(c);
--
2.53.0.155.g9f36b15afa
^ permalink raw reply related
* [PATCH v4 4/8] environment: move "pack_compression_level" into `struct repo_config_values`
From: Olamide Caleb Bello @ 2026-06-01 15:42 UTC (permalink / raw)
To: git
Cc: phillip.wood123, christian.couder, usmanakinyemi202,
kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260601154211.82370-1-belkid98@gmail.com>
The `pack_compression_level` configuration is currently stored in the
global variable `pack_compression_level`, which makes it shared across
repository instances within a single process.
Store it instead in `repo_config_values`, where eagerly‑parsed
repository configuration lives. `pack_compression_level` is parsed
eagerly because it influences packfile compression, a core operation
where a lazy parse could cause inconsistent behavior and hamper
libification. This preserves the existing eager‑parsing behavior while
tying the value to the repository from which it was read, avoiding
cross‑repository state leakage and continuing the effort to reduce
reliance on global configuration state.
The type remains `int` as it represents a numeric compression level,
not a boolean toggle.
Update all references to use `repo_config_values()`.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Usman Akinyemi <usmanakinyemi202@gmail.com>
Signed-off-by: Olamide Caleb Bello <belkid98@gmail.com>
---
builtin/fast-import.c | 8 +++++---
builtin/pack-objects.c | 17 ++++++++++-------
environment.c | 8 +++++---
environment.h | 2 +-
object-file.c | 3 ++-
5 files changed, 23 insertions(+), 15 deletions(-)
diff --git a/builtin/fast-import.c b/builtin/fast-import.c
index 82bc6dcc00..070a5af3e4 100644
--- a/builtin/fast-import.c
+++ b/builtin/fast-import.c
@@ -965,6 +965,7 @@ static int store_object(
unsigned long hdrlen, deltalen;
struct git_hash_ctx c;
git_zstream s;
+ struct repo_config_values *cfg = repo_config_values(the_repository);
hdrlen = format_object_header((char *)hdr, sizeof(hdr), type,
dat->len);
@@ -1005,7 +1006,7 @@ static int store_object(
} else
delta = NULL;
- git_deflate_init(&s, pack_compression_level);
+ git_deflate_init(&s, cfg->pack_compression_level);
if (delta) {
s.next_in = delta;
s.avail_in = deltalen;
@@ -1032,7 +1033,7 @@ static int store_object(
if (delta) {
FREE_AND_NULL(delta);
- git_deflate_init(&s, pack_compression_level);
+ git_deflate_init(&s, cfg->pack_compression_level);
s.next_in = (void *)dat->buf;
s.avail_in = dat->len;
s.avail_out = git_deflate_bound(&s, s.avail_in);
@@ -1115,6 +1116,7 @@ static void stream_blob(uintmax_t len, struct object_id *oidout, uintmax_t mark)
struct git_hash_ctx c;
git_zstream s;
struct hashfile_checkpoint checkpoint;
+ struct repo_config_values *cfg = repo_config_values(the_repository);
int status = Z_OK;
/* Determine if we should auto-checkpoint. */
@@ -1134,7 +1136,7 @@ static void stream_blob(uintmax_t len, struct object_id *oidout, uintmax_t mark)
crc32_begin(pack_file);
- git_deflate_init(&s, pack_compression_level);
+ git_deflate_init(&s, cfg->pack_compression_level);
hdrlen = encode_in_pack_object_header(out_buf, out_sz, OBJ_BLOB, len);
diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c
index dd2480a73d..8ccbe7e178 100644
--- a/builtin/pack-objects.c
+++ b/builtin/pack-objects.c
@@ -386,8 +386,9 @@ static unsigned long do_compress(void **pptr, unsigned long size)
git_zstream stream;
void *in, *out;
unsigned long maxsize;
+ struct repo_config_values *cfg = repo_config_values(the_repository);
- git_deflate_init(&stream, pack_compression_level);
+ git_deflate_init(&stream, cfg->pack_compression_level);
maxsize = git_deflate_bound(&stream, size);
in = *pptr;
@@ -413,8 +414,9 @@ static unsigned long write_large_blob_data(struct odb_read_stream *st, struct ha
unsigned char ibuf[1024 * 16];
unsigned char obuf[1024 * 16];
unsigned long olen = 0;
+ struct repo_config_values *cfg = repo_config_values(the_repository);
- git_deflate_init(&stream, pack_compression_level);
+ git_deflate_init(&stream, cfg->pack_compression_level);
for (;;) {
ssize_t readlen;
@@ -5003,6 +5005,7 @@ int cmd_pack_objects(int argc,
struct string_list keep_pack_list = STRING_LIST_INIT_NODUP;
struct list_objects_filter_options filter_options =
LIST_OBJECTS_FILTER_INIT;
+ struct repo_config_values *cfg = repo_config_values(the_repository);
struct option pack_objects_options[] = {
OPT_CALLBACK_F('q', "quiet", &progress, NULL,
@@ -5084,7 +5087,7 @@ int cmd_pack_objects(int argc,
N_("ignore packs that have companion .keep file")),
OPT_STRING_LIST(0, "keep-pack", &keep_pack_list, N_("name"),
N_("ignore this pack")),
- OPT_INTEGER(0, "compression", &pack_compression_level,
+ OPT_INTEGER(0, "compression", &cfg->pack_compression_level,
N_("pack compression level")),
OPT_BOOL(0, "keep-true-parents", &grafts_keep_true_parents,
N_("do not hide commits by grafts")),
@@ -5243,10 +5246,10 @@ int cmd_pack_objects(int argc,
if (!reuse_object)
reuse_delta = 0;
- if (pack_compression_level == -1)
- pack_compression_level = Z_DEFAULT_COMPRESSION;
- else if (pack_compression_level < 0 || pack_compression_level > Z_BEST_COMPRESSION)
- die(_("bad pack compression level %d"), pack_compression_level);
+ if (cfg->pack_compression_level == -1)
+ cfg->pack_compression_level = Z_DEFAULT_COMPRESSION;
+ else if (cfg->pack_compression_level < 0 || cfg->pack_compression_level > Z_BEST_COMPRESSION)
+ die(_("bad pack compression level %d"), cfg->pack_compression_level);
if (!delta_search_threads) /* --threads=0 means autodetect */
delta_search_threads = online_cpus();
diff --git a/environment.c b/environment.c
index 5b0e88b65c..d0d3a4b7d2 100644
--- a/environment.c
+++ b/environment.c
@@ -52,7 +52,6 @@ char *git_commit_encoding;
char *git_log_output_encoding;
char *apply_default_whitespace;
char *apply_default_ignorewhitespace;
-int pack_compression_level = Z_DEFAULT_COMPRESSION;
int fsync_object_files = -1;
int use_fsync = -1;
enum fsync_method fsync_method = FSYNC_METHOD_DEFAULT;
@@ -390,7 +389,7 @@ int git_default_core_config(const char *var, const char *value,
if (!zlib_compression_seen)
cfg->zlib_compression_level = level;
if (!pack_compression_seen)
- pack_compression_level = level;
+ cfg->pack_compression_level = level;
return 0;
}
@@ -662,6 +661,8 @@ static int git_default_attr_config(const char *var, const char *value)
int git_default_config(const char *var, const char *value,
const struct config_context *ctx, void *cb)
{
+ struct repo_config_values *cfg = repo_config_values(the_repository);
+
if (starts_with(var, "core."))
return git_default_core_config(var, value, ctx, cb);
@@ -701,7 +702,7 @@ int git_default_config(const char *var, const char *value,
level = Z_DEFAULT_COMPRESSION;
else if (level < 0 || level > Z_BEST_COMPRESSION)
die(_("bad pack compression level %d"), level);
- pack_compression_level = level;
+ cfg->pack_compression_level = level;
pack_compression_seen = 1;
return 0;
}
@@ -721,4 +722,5 @@ void repo_config_values_init(struct repo_config_values *cfg)
cfg->trust_ctime = 1;
cfg->check_stat = 1;
cfg->zlib_compression_level = Z_BEST_SPEED;
+ cfg->pack_compression_level = Z_DEFAULT_COMPRESSION;
}
diff --git a/environment.h b/environment.h
index 93201620af..514576b67a 100644
--- a/environment.h
+++ b/environment.h
@@ -94,6 +94,7 @@ struct repo_config_values {
int trust_ctime;
int check_stat;
int zlib_compression_level;
+ int pack_compression_level;
/* section "branch" config values */
enum branch_track branch_track;
@@ -171,7 +172,6 @@ extern int assume_unchanged;
extern int warn_on_object_refname_ambiguity;
extern char *apply_default_whitespace;
extern char *apply_default_ignorewhitespace;
-extern int pack_compression_level;
extern unsigned long pack_size_limit_cfg;
extern int precomposed_unicode;
diff --git a/object-file.c b/object-file.c
index 7c122ac419..37def5cc59 100644
--- a/object-file.c
+++ b/object-file.c
@@ -1437,8 +1437,9 @@ static int stream_blob_to_pack(struct transaction_packfile *state,
int status = Z_OK;
int write_object = (flags & INDEX_WRITE_OBJECT);
off_t offset = 0;
+ struct repo_config_values *cfg = repo_config_values(the_repository);
- git_deflate_init(&s, pack_compression_level);
+ git_deflate_init(&s, cfg->pack_compression_level);
hdrlen = encode_in_pack_object_header(obuf, sizeof(obuf), OBJ_BLOB, size);
s.next_out = obuf + hdrlen;
--
2.53.0.155.g9f36b15afa
^ permalink raw reply related
* [PATCH v4 5/8] environment: move "precomposed_unicode" into `struct repo_config_values`
From: Olamide Caleb Bello @ 2026-06-01 15:42 UTC (permalink / raw)
To: git
Cc: phillip.wood123, christian.couder, usmanakinyemi202,
kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260601154211.82370-1-belkid98@gmail.com>
The `core.precomposeunicode` configuration is currently stored in the
global variable `precomposed_unicode`, which makes it shared across
repository instances within a single process.
Store it instead in `repo_config_values`, where eagerly‑parsed
repository configuration lives. `core.precomposeunicode` is parsed
eagerly because it controls Unicode path normalization on macOS,
a fundamental filesystem‑level behavior that many operations depend
on; a lazy parse could lead to inconsistent results and hamper
libification. This preserves the existing behavior while tying the
value to the repository from which it was read, avoiding cross‑
repository state leakage and continuing the effort to reduce reliance
on global configuration state.
Change the type of the field from `int` to `bool` since it is parsed
as a boolean value.
Update all references to use `repo_config_values()`.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Usman Akinyemi <usmanakinyemi202@gmail.com>
Signed-off-by: Olamide Caleb Bello <belkid98@gmail.com>
---
compat/precompose_utf8.c | 20 +++++++++++++-------
environment.c | 4 ++--
environment.h | 2 +-
upload-pack.c | 3 ++-
4 files changed, 18 insertions(+), 11 deletions(-)
diff --git a/compat/precompose_utf8.c b/compat/precompose_utf8.c
index 43b3be0114..0e94dbd862 100644
--- a/compat/precompose_utf8.c
+++ b/compat/precompose_utf8.c
@@ -48,16 +48,18 @@ void probe_utf8_pathname_composition(void)
static const char *auml_nfc = "\xc3\xa4";
static const char *auml_nfd = "\x61\xcc\x88";
int output_fd;
- if (precomposed_unicode != -1)
+ struct repo_config_values *cfg = repo_config_values(the_repository);
+
+ if (cfg->precomposed_unicode != -1)
return; /* We found it defined in the global config, respect it */
repo_git_path_replace(the_repository, &path, "%s", auml_nfc);
output_fd = open(path.buf, O_CREAT|O_EXCL|O_RDWR, 0600);
if (output_fd >= 0) {
close(output_fd);
repo_git_path_replace(the_repository, &path, "%s", auml_nfd);
- precomposed_unicode = access(path.buf, R_OK) ? 0 : 1;
+ cfg->precomposed_unicode = access(path.buf, R_OK) ? 0 : 1;
repo_config_set(the_repository, "core.precomposeunicode",
- precomposed_unicode ? "true" : "false");
+ cfg->precomposed_unicode ? "true" : "false");
repo_git_path_replace(the_repository, &path, "%s", auml_nfc);
if (unlink(path.buf))
die_errno(_("failed to unlink '%s'"), path.buf);
@@ -69,14 +71,16 @@ const char *precompose_string_if_needed(const char *in)
{
size_t inlen;
size_t outlen;
+ struct repo_config_values *cfg = repo_config_values(the_repository);
+
if (!in)
return NULL;
if (has_non_ascii(in, (size_t)-1, &inlen)) {
iconv_t ic_prec;
char *out;
- if (precomposed_unicode < 0)
- repo_config_get_bool(the_repository, "core.precomposeunicode", &precomposed_unicode);
- if (precomposed_unicode != 1)
+ if (cfg->precomposed_unicode < 0)
+ repo_config_get_bool(the_repository, "core.precomposeunicode", &cfg->precomposed_unicode);
+ if (cfg->precomposed_unicode != 1)
return in;
ic_prec = iconv_open(repo_encoding, path_encoding);
if (ic_prec == (iconv_t) -1)
@@ -130,7 +134,9 @@ PREC_DIR *precompose_utf8_opendir(const char *dirname)
struct dirent_prec_psx *precompose_utf8_readdir(PREC_DIR *prec_dir)
{
+ struct repo_config_values *cfg = repo_config_values(the_repository);
struct dirent *res;
+
res = readdir(prec_dir->dirp);
if (res) {
size_t namelenz = strlen(res->d_name) + 1; /* \0 */
@@ -149,7 +155,7 @@ struct dirent_prec_psx *precompose_utf8_readdir(PREC_DIR *prec_dir)
prec_dir->dirent_nfc->d_ino = res->d_ino;
prec_dir->dirent_nfc->d_type = res->d_type;
- if ((precomposed_unicode == 1) && has_non_ascii(res->d_name, (size_t)-1, NULL)) {
+ if ((cfg->precomposed_unicode == 1) && has_non_ascii(res->d_name, (size_t)-1, NULL)) {
if (prec_dir->ic_precompose == (iconv_t)-1) {
die("iconv_open(%s,%s) failed, but needed:\n"
" precomposed unicode is not supported.\n"
diff --git a/environment.c b/environment.c
index d0d3a4b7d2..739b647ebe 100644
--- a/environment.c
+++ b/environment.c
@@ -72,7 +72,6 @@ enum object_creation_mode object_creation_mode = OBJECT_CREATION_MODE;
int grafts_keep_true_parents;
int core_sparse_checkout_cone;
int sparse_expect_files_outside_of_patterns;
-int precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */
unsigned long pack_size_limit_cfg;
#ifndef PROTECT_HFS_DEFAULT
@@ -532,7 +531,7 @@ int git_default_core_config(const char *var, const char *value,
}
if (!strcmp(var, "core.precomposeunicode")) {
- precomposed_unicode = git_config_bool(var, value);
+ cfg->precomposed_unicode = git_config_bool(var, value);
return 0;
}
@@ -723,4 +722,5 @@ void repo_config_values_init(struct repo_config_values *cfg)
cfg->check_stat = 1;
cfg->zlib_compression_level = Z_BEST_SPEED;
cfg->pack_compression_level = Z_DEFAULT_COMPRESSION;
+ cfg->precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */
}
diff --git a/environment.h b/environment.h
index 514576b67a..508cb1afbc 100644
--- a/environment.h
+++ b/environment.h
@@ -95,6 +95,7 @@ struct repo_config_values {
int check_stat;
int zlib_compression_level;
int pack_compression_level;
+ int precomposed_unicode;
/* section "branch" config values */
enum branch_track branch_track;
@@ -174,7 +175,6 @@ extern char *apply_default_whitespace;
extern char *apply_default_ignorewhitespace;
extern unsigned long pack_size_limit_cfg;
-extern int precomposed_unicode;
extern int protect_hfs;
extern int protect_ntfs;
diff --git a/upload-pack.c b/upload-pack.c
index 9f6d6fe48c..3a52237134 100644
--- a/upload-pack.c
+++ b/upload-pack.c
@@ -1336,6 +1336,7 @@ static int upload_pack_config(const char *var, const char *value,
void *cb_data)
{
struct upload_pack_data *data = cb_data;
+ struct repo_config_values *cfg = repo_config_values(the_repository);
if (!strcmp("uploadpack.allowtipsha1inwant", var)) {
if (git_config_bool(var, value))
@@ -1366,7 +1367,7 @@ static int upload_pack_config(const char *var, const char *value,
if (value)
data->allow_packfile_uris = 1;
} else if (!strcmp("core.precomposeunicode", var)) {
- precomposed_unicode = git_config_bool(var, value);
+ cfg->precomposed_unicode = git_config_bool(var, value);
} else if (!strcmp("transfer.advertisesid", var)) {
data->advertise_sid = git_config_bool(var, value);
}
--
2.53.0.155.g9f36b15afa
^ permalink raw reply related
* [PATCH v4 6/8] environment: move "core_sparse_checkout_cone" into `struct repo_config_values`
From: Olamide Caleb Bello @ 2026-06-01 15:42 UTC (permalink / raw)
To: git
Cc: phillip.wood123, christian.couder, usmanakinyemi202,
kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260601154211.82370-1-belkid98@gmail.com>
The `core.sparseCheckoutCone` configuration was previously stored in an
uninitialized global `int` variable, risking cross‑repository state
leakage.
Move it into `repo_config_values`, where eagerly‑parsed repository
configuration lives. `core.sparseCheckoutCone` is parsed eagerly
because it determines the fundamental sparse‑checkout mode and is
consulted very early during repository setup; a lazy parse could
leave the sparse‑checkout state undefined and complicate
libification. This preserves the existing behavior while tying the
value to the repository from which it was read, avoiding cross‑
repository state leakage and continuing the effort to reduce reliance
on global configuration state.
Update all references to use `repo_config_values()`.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Usman Akinyemi <usmanakinyemi202@gmail.com>
Signed-off-by: Olamide Caleb Bello <belkid98@gmail.com>
---
builtin/mv.c | 2 +-
builtin/sparse-checkout.c | 37 ++++++++++++++++++++++---------------
dir.c | 3 ++-
environment.c | 4 ++--
environment.h | 2 +-
sparse-index.c | 2 +-
6 files changed, 29 insertions(+), 21 deletions(-)
diff --git a/builtin/mv.c b/builtin/mv.c
index 2215d34e31..ef3a326c90 100644
--- a/builtin/mv.c
+++ b/builtin/mv.c
@@ -574,7 +574,7 @@ int cmd_mv(int argc,
if (ignore_sparse &&
cfg->apply_sparse_checkout &&
- core_sparse_checkout_cone) {
+ cfg->core_sparse_checkout_cone) {
/*
* NEEDSWORK: we are *not* paying attention to
* "out-to-out" move (<source> is out-of-cone and
diff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c
index f4aa405da9..92d017b81f 100644
--- a/builtin/sparse-checkout.c
+++ b/builtin/sparse-checkout.c
@@ -73,7 +73,7 @@ static int sparse_checkout_list(int argc, const char **argv, const char *prefix,
memset(&pl, 0, sizeof(pl));
- pl.use_cone_patterns = core_sparse_checkout_cone;
+ pl.use_cone_patterns = cfg->core_sparse_checkout_cone;
sparse_filename = get_sparse_checkout_filename();
res = add_patterns_from_file_to_list(sparse_filename, "", 0, &pl, NULL, 0);
@@ -334,6 +334,7 @@ static int write_patterns_and_update(struct repository *repo,
FILE *fp;
struct lock_file lk = LOCK_INIT;
int result;
+ struct repo_config_values *cfg = repo_config_values(the_repository);
sparse_filename = get_sparse_checkout_filename();
@@ -353,7 +354,7 @@ static int write_patterns_and_update(struct repository *repo,
if (!fp)
die_errno(_("unable to fdopen %s"), get_lock_file_path(&lk));
- if (core_sparse_checkout_cone)
+ if (cfg->core_sparse_checkout_cone)
write_cone_to_file(fp, pl);
else
write_patterns_to_file(fp, pl);
@@ -402,15 +403,15 @@ static enum sparse_checkout_mode update_cone_mode(int *cone_mode) {
/* If not specified, use previous definition of cone mode */
if (*cone_mode == -1 && cfg->apply_sparse_checkout)
- *cone_mode = core_sparse_checkout_cone;
+ *cone_mode = cfg->core_sparse_checkout_cone;
/* Set cone/non-cone mode appropriately */
cfg->apply_sparse_checkout = 1;
if (*cone_mode == 1 || *cone_mode == -1) {
- core_sparse_checkout_cone = 1;
+ cfg->core_sparse_checkout_cone = 1;
return MODE_CONE_PATTERNS;
}
- core_sparse_checkout_cone = 0;
+ cfg->core_sparse_checkout_cone = 0;
return MODE_ALL_PATTERNS;
}
@@ -577,7 +578,9 @@ static void add_patterns_from_input(struct pattern_list *pl,
FILE *file)
{
int i;
- if (core_sparse_checkout_cone) {
+ struct repo_config_values *cfg = repo_config_values(the_repository);
+
+ if (cfg->core_sparse_checkout_cone) {
struct strbuf line = STRBUF_INIT;
hashmap_init(&pl->recursive_hashmap, pl_hashmap_cmp, NULL, 0);
@@ -636,13 +639,14 @@ static void add_patterns_cone_mode(int argc, const char **argv,
struct pattern_entry *pe;
struct hashmap_iter iter;
struct pattern_list existing;
+ struct repo_config_values *cfg = repo_config_values(the_repository);
char *sparse_filename = get_sparse_checkout_filename();
add_patterns_from_input(pl, argc, argv,
use_stdin ? stdin : NULL);
memset(&existing, 0, sizeof(existing));
- existing.use_cone_patterns = core_sparse_checkout_cone;
+ existing.use_cone_patterns = cfg->core_sparse_checkout_cone;
if (add_patterns_from_file_to_list(sparse_filename, "", 0,
&existing, NULL, 0))
@@ -690,7 +694,7 @@ static int modify_pattern_list(struct repository *repo,
switch (m) {
case ADD:
- if (core_sparse_checkout_cone)
+ if (cfg->core_sparse_checkout_cone)
add_patterns_cone_mode(args->nr, args->v, pl, use_stdin);
else
add_patterns_literal(args->nr, args->v, pl, use_stdin);
@@ -723,11 +727,12 @@ static void sanitize_paths(struct repository *repo,
const char *prefix, int skip_checks)
{
int i;
+ struct repo_config_values *cfg = repo_config_values(the_repository);
if (!args->nr)
return;
- if (prefix && *prefix && core_sparse_checkout_cone) {
+ if (prefix && *prefix && cfg->core_sparse_checkout_cone) {
/*
* The args are not pathspecs, so unfortunately we
* cannot imitate how cmd_add() uses parse_pathspec().
@@ -744,10 +749,10 @@ static void sanitize_paths(struct repository *repo,
if (skip_checks)
return;
- if (prefix && *prefix && !core_sparse_checkout_cone)
+ if (prefix && *prefix && !cfg->core_sparse_checkout_cone)
die(_("please run from the toplevel directory in non-cone mode"));
- if (core_sparse_checkout_cone) {
+ if (cfg->core_sparse_checkout_cone) {
for (i = 0; i < args->nr; i++) {
if (args->v[i][0] == '/')
die(_("specify directories rather than patterns (no leading slash)"));
@@ -769,7 +774,7 @@ static void sanitize_paths(struct repository *repo,
if (S_ISSPARSEDIR(ce->ce_mode))
continue;
- if (core_sparse_checkout_cone)
+ if (cfg->core_sparse_checkout_cone)
die(_("'%s' is not a directory; to treat it as a directory anyway, rerun with --skip-checks"), args->v[i]);
else
warning(_("pass a leading slash before paths such as '%s' if you want a single file (see NON-CONE PROBLEMS in the git-sparse-checkout manual)."), args->v[i]);
@@ -836,6 +841,7 @@ static struct sparse_checkout_set_opts {
static int sparse_checkout_set(int argc, const char **argv, const char *prefix,
struct repository *repo)
{
+ struct repo_config_values *cfg = repo_config_values(the_repository);
int default_patterns_nr = 2;
const char *default_patterns[] = {"/*", "!/*/", NULL};
@@ -873,7 +879,7 @@ static int sparse_checkout_set(int argc, const char **argv, const char *prefix,
* non-cone mode, if nothing is specified, manually select just the
* top-level directory (much as 'init' would do).
*/
- if (!core_sparse_checkout_cone && !set_opts.use_stdin && argc == 0) {
+ if (!cfg->core_sparse_checkout_cone && !set_opts.use_stdin && argc == 0) {
for (int i = 0; i < default_patterns_nr; i++)
strvec_push(&patterns, default_patterns[i]);
} else {
@@ -977,7 +983,7 @@ static int sparse_checkout_clean(int argc, const char **argv,
setup_work_tree();
if (!cfg->apply_sparse_checkout)
die(_("must be in a sparse-checkout to clean directories"));
- if (!core_sparse_checkout_cone)
+ if (!cfg->core_sparse_checkout_cone)
die(_("must be in a cone-mode sparse-checkout to clean directories"));
argc = parse_options(argc, argv, prefix,
@@ -1141,6 +1147,7 @@ static int sparse_checkout_check_rules(int argc, const char **argv, const char *
FILE *fp;
int ret;
struct pattern_list pl = {0};
+ struct repo_config_values *cfg = repo_config_values(the_repository);
char *sparse_filename;
check_rules_opts.cone_mode = -1;
@@ -1152,7 +1159,7 @@ static int sparse_checkout_check_rules(int argc, const char **argv, const char *
check_rules_opts.cone_mode = 1;
update_cone_mode(&check_rules_opts.cone_mode);
- pl.use_cone_patterns = core_sparse_checkout_cone;
+ pl.use_cone_patterns = cfg->core_sparse_checkout_cone;
if (check_rules_opts.rules_file) {
fp = xfopen(check_rules_opts.rules_file, "r");
add_patterns_from_input(&pl, argc, argv, fp);
diff --git a/dir.c b/dir.c
index fcb8f6dd2a..4f493b64c6 100644
--- a/dir.c
+++ b/dir.c
@@ -3508,8 +3508,9 @@ int get_sparse_checkout_patterns(struct pattern_list *pl)
{
int res;
char *sparse_filename = get_sparse_checkout_filename();
+ struct repo_config_values *cfg = repo_config_values(the_repository);
- pl->use_cone_patterns = core_sparse_checkout_cone;
+ pl->use_cone_patterns = cfg->core_sparse_checkout_cone;
res = add_patterns_from_file_to_list(sparse_filename, "", 0, pl, NULL, 0);
free(sparse_filename);
diff --git a/environment.c b/environment.c
index 739b647ebe..b0e873e9f5 100644
--- a/environment.c
+++ b/environment.c
@@ -70,7 +70,6 @@ enum push_default_type push_default = PUSH_DEFAULT_UNSPECIFIED;
#endif
enum object_creation_mode object_creation_mode = OBJECT_CREATION_MODE;
int grafts_keep_true_parents;
-int core_sparse_checkout_cone;
int sparse_expect_files_outside_of_patterns;
unsigned long pack_size_limit_cfg;
@@ -526,7 +525,7 @@ int git_default_core_config(const char *var, const char *value,
}
if (!strcmp(var, "core.sparsecheckoutcone")) {
- core_sparse_checkout_cone = git_config_bool(var, value);
+ cfg->core_sparse_checkout_cone = git_config_bool(var, value);
return 0;
}
@@ -723,4 +722,5 @@ void repo_config_values_init(struct repo_config_values *cfg)
cfg->zlib_compression_level = Z_BEST_SPEED;
cfg->pack_compression_level = Z_DEFAULT_COMPRESSION;
cfg->precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */
+ cfg->core_sparse_checkout_cone = 0;
}
diff --git a/environment.h b/environment.h
index 508cb1afbc..befad9a388 100644
--- a/environment.h
+++ b/environment.h
@@ -96,6 +96,7 @@ struct repo_config_values {
int zlib_compression_level;
int pack_compression_level;
int precomposed_unicode;
+ int core_sparse_checkout_cone;
/* section "branch" config values */
enum branch_track branch_track;
@@ -178,7 +179,6 @@ extern unsigned long pack_size_limit_cfg;
extern int protect_hfs;
extern int protect_ntfs;
-extern int core_sparse_checkout_cone;
extern int sparse_expect_files_outside_of_patterns;
enum rebase_setup_type {
diff --git a/sparse-index.c b/sparse-index.c
index 13629c075d..53cb8d64fc 100644
--- a/sparse-index.c
+++ b/sparse-index.c
@@ -154,7 +154,7 @@ int is_sparse_index_allowed(struct index_state *istate, int flags)
{
struct repo_config_values *cfg = repo_config_values(the_repository);
- if (!cfg->apply_sparse_checkout || !core_sparse_checkout_cone)
+ if (!cfg->apply_sparse_checkout || !cfg->core_sparse_checkout_cone)
return 0;
if (!(flags & SPARSE_INDEX_MEMORY_ONLY)) {
--
2.53.0.155.g9f36b15afa
^ permalink raw reply related
* [PATCH v4 7/8] environment: move "sparse_expect_files_outside_of_patterns" into `repo_config_values`
From: Olamide Caleb Bello @ 2026-06-01 15:42 UTC (permalink / raw)
To: git
Cc: phillip.wood123, christian.couder, usmanakinyemi202,
kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260601154211.82370-1-belkid98@gmail.com>
The `core.sparseCheckoutExpectFilesOutsideOfPatterns` configuration was
previously stored in a global `int` variable, making it shared across
repository instances and risking cross‑repository state leakage.
Store it instead in `repo_config_values`, where eagerly‑parsed
repository configuration lives. This option is parsed eagerly because
it controls how sparse‑checkout paths are interpreted – a fundamental
behavior that many commands rely on; a lazy parse could cause
inconsistent sparse‑checkout handling and complicate libification.
This preserves the existing behavior while tying the value to the
repository from which it was read, avoiding cross‑repository state
leakage and continuing the effort to reduce reliance on global
configuration state.
Update all references to use `repo_config_values()`.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Usman Akinyemi <usmanakinyemi202@gmail.com>
Signed-off-by: Olamide Caleb Bello <belkid98@gmail.com>
---
environment.c | 6 ++++--
environment.h | 5 +++--
sparse-index.c | 2 +-
3 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/environment.c b/environment.c
index b0e873e9f5..57587ede56 100644
--- a/environment.c
+++ b/environment.c
@@ -70,7 +70,6 @@ enum push_default_type push_default = PUSH_DEFAULT_UNSPECIFIED;
#endif
enum object_creation_mode object_creation_mode = OBJECT_CREATION_MODE;
int grafts_keep_true_parents;
-int sparse_expect_files_outside_of_patterns;
unsigned long pack_size_limit_cfg;
#ifndef PROTECT_HFS_DEFAULT
@@ -550,8 +549,10 @@ int git_default_core_config(const char *var, const char *value,
static int git_default_sparse_config(const char *var, const char *value)
{
+ struct repo_config_values *cfg = repo_config_values(the_repository);
+
if (!strcmp(var, "sparse.expectfilesoutsideofpatterns")) {
- sparse_expect_files_outside_of_patterns = git_config_bool(var, value);
+ cfg->sparse_expect_files_outside_of_patterns = git_config_bool(var, value);
return 0;
}
@@ -723,4 +724,5 @@ void repo_config_values_init(struct repo_config_values *cfg)
cfg->pack_compression_level = Z_DEFAULT_COMPRESSION;
cfg->precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */
cfg->core_sparse_checkout_cone = 0;
+ cfg->sparse_expect_files_outside_of_patterns = 0;
}
diff --git a/environment.h b/environment.h
index befad9a388..609cdaa07f 100644
--- a/environment.h
+++ b/environment.h
@@ -98,6 +98,9 @@ struct repo_config_values {
int precomposed_unicode;
int core_sparse_checkout_cone;
+ /* section "sparse" config values */
+ int sparse_expect_files_outside_of_patterns;
+
/* section "branch" config values */
enum branch_track branch_track;
};
@@ -179,8 +182,6 @@ extern unsigned long pack_size_limit_cfg;
extern int protect_hfs;
extern int protect_ntfs;
-extern int sparse_expect_files_outside_of_patterns;
-
enum rebase_setup_type {
AUTOREBASE_NEVER = 0,
AUTOREBASE_LOCAL,
diff --git a/sparse-index.c b/sparse-index.c
index 53cb8d64fc..1ed769b78d 100644
--- a/sparse-index.c
+++ b/sparse-index.c
@@ -675,7 +675,7 @@ void clear_skip_worktree_from_present_files(struct index_state *istate)
struct repo_config_values *cfg = repo_config_values(the_repository);
if (!cfg->apply_sparse_checkout ||
- sparse_expect_files_outside_of_patterns)
+ cfg->sparse_expect_files_outside_of_patterns)
return;
if (clear_skip_worktree_from_present_files_sparse(istate)) {
--
2.53.0.155.g9f36b15afa
^ permalink raw reply related
* [PATCH v4 8/8] environment: move "warn_on_object_refname_ambiguity" into `struct repo_config_values`
From: Olamide Caleb Bello @ 2026-06-01 15:42 UTC (permalink / raw)
To: git
Cc: phillip.wood123, christian.couder, usmanakinyemi202,
kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260601154211.82370-1-belkid98@gmail.com>
The `core.warnAmbiguousRefs` configuration was previously stored in a
global `int` variable, making it shared across repository instances
and risking cross‑repository state leakage.
Store it instead in `repo_config_values`, where eagerly‑parsed
repository configuration lives. This option is parsed eagerly because
ambiguity warnings influence how users interpret object references in
many commands; a lazy parse could cause these warnings to behave
inconsistently or to appear for the wrong repository, confusing users
and hindering libification. This preserves the existing behavior while
tying the value to the repository from which it was read, avoiding
cross‑repository state leakage and continuing the effort to reduce
reliance on global configuration state.
Update all references to use `repo_config_values()`.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Usman Akinyemi <usmanakinyemi202@gmail.com>
Signed-off-by: Olamide Caleb Bello <belkid98@gmail.com>
---
builtin/cat-file.c | 7 ++++---
builtin/pack-objects.c | 7 ++++---
environment.c | 2 +-
environment.h | 2 +-
object-name.c | 3 ++-
revision.c | 7 ++++---
submodule.c | 7 ++++---
7 files changed, 20 insertions(+), 15 deletions(-)
diff --git a/builtin/cat-file.c b/builtin/cat-file.c
index d9fbad5358..cfc5430186 100644
--- a/builtin/cat-file.c
+++ b/builtin/cat-file.c
@@ -901,6 +901,7 @@ static int batch_objects(struct batch_options *opt)
struct strbuf input = STRBUF_INIT;
struct strbuf output = STRBUF_INIT;
struct expand_data data = EXPAND_DATA_INIT;
+ struct repo_config_values *cfg = repo_config_values(the_repository);
int save_warning;
int retval = 0;
@@ -973,8 +974,8 @@ static int batch_objects(struct batch_options *opt)
* warn) ends up dwarfing the actual cost of the object lookups
* themselves. We can work around it by just turning off the warning.
*/
- save_warning = warn_on_object_refname_ambiguity;
- warn_on_object_refname_ambiguity = 0;
+ save_warning = cfg->warn_on_object_refname_ambiguity;
+ cfg->warn_on_object_refname_ambiguity = 0;
if (opt->batch_mode == BATCH_MODE_QUEUE_AND_DISPATCH) {
batch_objects_command(opt, &output, &data);
@@ -1002,7 +1003,7 @@ static int batch_objects(struct batch_options *opt)
cleanup:
strbuf_release(&input);
strbuf_release(&output);
- warn_on_object_refname_ambiguity = save_warning;
+ cfg->warn_on_object_refname_ambiguity = save_warning;
return retval;
}
diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c
index 8ccbe7e178..7df75fe91e 100644
--- a/builtin/pack-objects.c
+++ b/builtin/pack-objects.c
@@ -4788,6 +4788,7 @@ static void get_object_list(struct rev_info *revs, struct strvec *argv)
struct setup_revision_opt s_r_opt = {
.allow_exclude_promisor_objects = 1,
};
+ struct repo_config_values *cfg = repo_config_values(the_repository);
char line[1000];
int flags = 0;
int save_warning;
@@ -4798,8 +4799,8 @@ static void get_object_list(struct rev_info *revs, struct strvec *argv)
/* make sure shallows are read */
is_repository_shallow(the_repository);
- save_warning = warn_on_object_refname_ambiguity;
- warn_on_object_refname_ambiguity = 0;
+ save_warning = cfg->warn_on_object_refname_ambiguity;
+ cfg->warn_on_object_refname_ambiguity = 0;
while (fgets(line, sizeof(line), stdin) != NULL) {
int len = strlen(line);
@@ -4827,7 +4828,7 @@ static void get_object_list(struct rev_info *revs, struct strvec *argv)
die(_("bad revision '%s'"), line);
}
- warn_on_object_refname_ambiguity = save_warning;
+ cfg->warn_on_object_refname_ambiguity = save_warning;
if (use_bitmap_index && !get_object_list_from_bitmap(revs))
return;
diff --git a/environment.c b/environment.c
index 57587ede56..ba2c60103f 100644
--- a/environment.c
+++ b/environment.c
@@ -47,7 +47,6 @@ int minimum_abbrev = 4, default_abbrev = -1;
int ignore_case;
int assume_unchanged;
int is_bare_repository_cfg = -1; /* unspecified */
-int warn_on_object_refname_ambiguity = 1;
char *git_commit_encoding;
char *git_log_output_encoding;
char *apply_default_whitespace;
@@ -725,4 +724,5 @@ void repo_config_values_init(struct repo_config_values *cfg)
cfg->precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */
cfg->core_sparse_checkout_cone = 0;
cfg->sparse_expect_files_outside_of_patterns = 0;
+ cfg->warn_on_object_refname_ambiguity = 1;
}
diff --git a/environment.h b/environment.h
index 609cdaa07f..1ff0a7ba8b 100644
--- a/environment.h
+++ b/environment.h
@@ -97,6 +97,7 @@ struct repo_config_values {
int pack_compression_level;
int precomposed_unicode;
int core_sparse_checkout_cone;
+ int warn_on_object_refname_ambiguity;
/* section "sparse" config values */
int sparse_expect_files_outside_of_patterns;
@@ -174,7 +175,6 @@ extern int has_symlinks;
extern int minimum_abbrev, default_abbrev;
extern int ignore_case;
extern int assume_unchanged;
-extern int warn_on_object_refname_ambiguity;
extern char *apply_default_whitespace;
extern char *apply_default_ignorewhitespace;
extern unsigned long pack_size_limit_cfg;
diff --git a/object-name.c b/object-name.c
index 21dcdc4a0e..319d3db01d 100644
--- a/object-name.c
+++ b/object-name.c
@@ -684,11 +684,12 @@ static int get_oid_basic(struct repository *r, const char *str, int len,
int refs_found = 0;
int at, reflog_len, nth_prior = 0;
int fatal = !(flags & GET_OID_QUIETLY);
+ struct repo_config_values *cfg = repo_config_values(the_repository);
if (len == r->hash_algo->hexsz && !get_oid_hex(str, oid)) {
if (!(flags & GET_OID_SKIP_AMBIGUITY_CHECK) &&
repo_settings_get_warn_ambiguous_refs(r) &&
- warn_on_object_refname_ambiguity) {
+ cfg->warn_on_object_refname_ambiguity) {
refs_found = repo_dwim_ref(r, str, len, &tmp_oid, &real_ref, 0);
if (refs_found > 0) {
warning(warn_msg, len, str);
diff --git a/revision.c b/revision.c
index 599b3a66c3..4e7faa7eb1 100644
--- a/revision.c
+++ b/revision.c
@@ -2922,9 +2922,10 @@ static void read_revisions_from_stdin(struct rev_info *revs,
int seen_end_of_options = 0;
int save_warning;
int flags = 0;
+ struct repo_config_values *cfg = repo_config_values(the_repository);
- save_warning = warn_on_object_refname_ambiguity;
- warn_on_object_refname_ambiguity = 0;
+ save_warning = cfg->warn_on_object_refname_ambiguity;
+ cfg->warn_on_object_refname_ambiguity = 0;
strbuf_init(&sb, 1000);
while (strbuf_getline(&sb, stdin) != EOF) {
@@ -2958,7 +2959,7 @@ static void read_revisions_from_stdin(struct rev_info *revs,
read_pathspec_from_stdin(&sb, prune);
strbuf_release(&sb);
- warn_on_object_refname_ambiguity = save_warning;
+ cfg->warn_on_object_refname_ambiguity = save_warning;
}
static void NORETURN diagnose_missing_default(const char *def)
diff --git a/submodule.c b/submodule.c
index b1a0363f9d..f26235bbb7 100644
--- a/submodule.c
+++ b/submodule.c
@@ -898,12 +898,13 @@ static void collect_changed_submodules(struct repository *r,
struct setup_revision_opt s_r_opt = {
.assume_dashdash = 1,
};
+ struct repo_config_values *cfg = repo_config_values(the_repository);
- save_warning = warn_on_object_refname_ambiguity;
- warn_on_object_refname_ambiguity = 0;
+ save_warning = cfg->warn_on_object_refname_ambiguity;
+ cfg->warn_on_object_refname_ambiguity = 0;
repo_init_revisions(r, &rev, NULL);
setup_revisions_from_strvec(argv, &rev, &s_r_opt);
- warn_on_object_refname_ambiguity = save_warning;
+ cfg->warn_on_object_refname_ambiguity = save_warning;
if (prepare_revision_walk(&rev))
die(_("revision walk setup failed"));
--
2.53.0.155.g9f36b15afa
^ permalink raw reply related
* Re: [PATCH] sub-process: use gentle handshake to avoid die() on startup failure
From: Michael Montalbo @ 2026-06-01 15:51 UTC (permalink / raw)
To: Junio C Hamano; +Cc: Michael Montalbo via GitGitGadget, git
In-Reply-To: <xmqqo6hu92wu.fsf@gitster.g>
On Sun, May 31, 2026 at 11:43 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> "Michael Montalbo via GitGitGadget" <gitgitgadget@gmail.com> writes:
>
> > diff --git a/sub-process.c b/sub-process.c
> > index 83bf0a0e82..22c68bd10d 100644
> > --- a/sub-process.c
> > +++ b/sub-process.c
> > @@ -132,18 +132,19 @@ static int handshake_version(struct child_process *process,
> > if (packet_flush_gently(process->in))
> > return error("Could not write flush packet");
> >
> > - if (!(line = packet_read_line(process->out, NULL)) ||
> > + if (packet_read_line_gently(process->out, NULL, &line) <= 0 ||
> > !skip_prefix(line, welcome_prefix, &p) ||
> > strcmp(p, "-server"))
> > return error("Unexpected line '%s', expected %s-server",
> > line ? line : "<flush packet>", welcome_prefix);
>
> If `packet_read_line_gently()` returns `< 0` (due to an EOF or read
> error), `line` will be `NULL`. The error message printed will be:
>
> `Unexpected line '<flush packet>', expected filter-server`
>
> This is misleading when the remote process didn't send a flush
> packet; it hung up or crashed.
>
Makes sense. Will fix.
>
>
> > - if (!(line = packet_read_line(process->out, NULL)) ||
> > + if (packet_read_line_gently(process->out, NULL, &line) <= 0 ||
> > !skip_prefix(line, "version=", &p) ||
> > strtol_i(p, 10, chosen_version))
> > return error("Unexpected line '%s', expected version",
> > line ? line : "<flush packet>");
>
> Ditto.
>
Will fix.
> > - if ((line = packet_read_line(process->out, NULL)))
> > - return error("Unexpected line '%s', expected flush", line);
> > + if (packet_read_line_gently(process->out, NULL, &line) < 0 || line)
> > + return error("Unexpected line '%s', expected flush",
> > + line ? line : "<read error>");
>
> We catch error return (< 0) or a line with payload (!!line) and
> report an error here, because we want to see <flush> here. OK.
>
>
> > @@ -171,7 +172,7 @@ static int handshake_capabilities(struct child_process *process,
> > if (packet_flush_gently(process->in))
> > return error("Could not write flush packet");
> >
> > - while ((line = packet_read_line(process->out, NULL))) {
> > + while (packet_read_line_gently(process->out, NULL, &line) > 0) {
> > const char *p;
> > if (!skip_prefix(line, "capability=", &p))
> > continue;
>
> While this correctly stops the loop if packet_read_line_gently()
> returns a non-positive value, doesn't it introduce a subtle bug?
>
> `packet_read_line_gently()` returns:
>
> - `> 0` for a normal line (which keeps the loop running).
>
> - `0` for a flush packet (which we expect as the normal terminator
> of the capabilities list, stopping the loop).
>
> - `< 0` for an EOF or read error (which also stops the loop).
>
> In the original code, an EOF or read error would have caused
> `packet_read_line()` to call `die()`, aborting the process.
>
> With the new code, if the child process dies or closes its pipe
> during the capabilities handshake, the loop will terminate, and the
> function will return `0` (success). The parent process will proceed
> as if the capabilities were successfully negotiated. Any further
> communication with the child process would fail so the damage may
> not be huge, but somebody must check if the loop terminated because
> of a flush packet, or an error.
>
> while (1) {
> const char *p;
> int len = packet_read_line_gently(process->out, NULL, &line);
>
> if (len < 0)
> return error(_("subprocess `%s` failed to give capabilities"),
> process->args.v[0]);
> if (!skip_prefix(line, "capability=", &p))
> continue;
> ...
>
> or something, perhaps?
Good catch, thank you. I will update the logic so a failure during
capabilities handshake
is correctly marked an error instead of silently succeeding.
^ permalink raw reply
* [PATCH v2] index-pack: retain child bases in delta cache
From: Arijit Banerjee via GitGitGadget @ 2026-06-01 16:13 UTC (permalink / raw)
To: git
Cc: Ævar Arnfjörð Bjarmason, Junio C Hamano,
Derrick Stolee, Arijit Banerjee, Arijit Banerjee
In-Reply-To: <pull.2131.git.1780070763044.gitgitgadget@gmail.com>
From: Arijit Banerjee <arijit@effectiveailabs.com>
When resolving a delta whose result has children of its own,
index-pack adds the result to work_head, accounts its data in
base_cache_used, and calls prune_base_data(). It then immediately frees
that same data.
This bypasses the existing delta base cache policy and can force later
descendants to reconstruct the queued base again. Let the existing
delta_base_cache_limit pruning policy decide whether to keep or evict
the data instead.
This does not add a new cache or increase the cache limit. The object
data is already accounted in base_cache_used before prune_base_data()
runs, and the existing pruning and base cleanup paths still release it.
On a quiet Ubuntu 24.04 VM with 16 vCPUs, 32 GiB RAM, and local SSD,
direct index-pack timings on single-pack Linux fixtures improved as
follows:
linux blobless: 69.17s -> 57.98s (16.2% faster), RSS flat
linux full: 280.72s -> 236.32s (15.8% faster), RSS +1.9%
Five-repeat medians on public repositories also improved:
git.git: 12.31s -> 10.70s (13.1% faster)
libgit2: 3.35s -> 2.88s (14.0% faster)
redis: 6.52s -> 5.64s (13.5% faster)
cpython: 33.02s -> 31.44s (4.8% faster)
The standard p5302 perf test on a smaller git.git fixture was neutral:
5302.9 index-pack default threads:
11.21(38.07+1.33) -> 11.16(37.90+1.31), -0.4%
t/t5302-pack-index.sh passed, and GitGitGadget's linux-leaks CI also
exercised that test under SANITIZE=leak.
Signed-off-by: Arijit Banerjee <arijit@effectiveailabs.com>
---
index-pack: retain child bases in delta cache
Speed up the local pack indexing phase of clone/fetch for large
delta-compressed packs by keeping reconstructed delta bases available
for reuse when they are queued for later delta resolution.
When index-pack reconstructs a child base and queues it for resolving
descendant deltas, it currently frees that data immediately. This can
force the same base to be reconstructed again. Instead, keep it in the
existing delta base cache and let the existing delta_base_cache_limit
policy decide whether to retain or evict it.
This does not add a new cache or increase the cache limit. The object
data is already accounted in base_cache_used, and prune_base_data() is
already called at this point.
Correctness:
* t/t5302-pack-index.sh passed all 36 tests.
Benchmarks on a quiet Ubuntu 24.04 VM, 16 vCPU, 32 GiB RAM, local SSD:
pack baseline patched wall-time change RSS change linux blobless 69.17s
57.98s 16.2% faster -0.0% linux full 280.72s 236.32s 15.8% faster +1.9%
Five-repeat public-repo medians also improved: git.git 13.1%, libgit2
14.0%, redis 13.5%, cpython 4.8%.
Perf on the linux blobless pack showed the same direction under
profiling: 76.64s baseline vs 61.09s patched, with similar RSS.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2131%2Farijit91%2Findex-pack-retain-child-base-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2131/arijit91/index-pack-retain-child-base-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/2131
Range-diff vs v1:
1: cafb1700be ! 1: 42eca38f51 index-pack: retain child bases in delta cache
@@ Commit message
When resolving a delta whose result has children of its own,
index-pack adds the result to work_head, accounts its data in
- base_cache_used, and calls prune_base_data(). It then immediately
- frees that same data.
+ base_cache_used, and calls prune_base_data(). It then immediately frees
+ that same data.
This bypasses the existing delta base cache policy and can force later
descendants to reconstruct the queued base again. Let the existing
delta_base_cache_limit pruning policy decide whether to keep or evict
the data instead.
+ This does not add a new cache or increase the cache limit. The object
+ data is already accounted in base_cache_used before prune_base_data()
+ runs, and the existing pruning and base cleanup paths still release it.
+
+ On a quiet Ubuntu 24.04 VM with 16 vCPUs, 32 GiB RAM, and local SSD,
+ direct index-pack timings on single-pack Linux fixtures improved as
+ follows:
+
+ linux blobless: 69.17s -> 57.98s (16.2% faster), RSS flat
+ linux full: 280.72s -> 236.32s (15.8% faster), RSS +1.9%
+
+ Five-repeat medians on public repositories also improved:
+
+ git.git: 12.31s -> 10.70s (13.1% faster)
+ libgit2: 3.35s -> 2.88s (14.0% faster)
+ redis: 6.52s -> 5.64s (13.5% faster)
+ cpython: 33.02s -> 31.44s (4.8% faster)
+
+ The standard p5302 perf test on a smaller git.git fixture was neutral:
+
+ 5302.9 index-pack default threads:
+ 11.21(38.07+1.33) -> 11.16(37.90+1.31), -0.4%
+
+ t/t5302-pack-index.sh passed, and GitGitGadget's linux-leaks CI also
+ exercised that test under SANITIZE=leak.
+
Signed-off-by: Arijit Banerjee <arijit@effectiveailabs.com>
## builtin/index-pack.c ##
builtin/index-pack.c | 1 -
1 file changed, 1 deletion(-)
diff --git a/builtin/index-pack.c b/builtin/index-pack.c
index cf0bd8280d..027c64b522 100644
--- a/builtin/index-pack.c
+++ b/builtin/index-pack.c
@@ -1212,7 +1212,6 @@ static void *threaded_second_pass(void *data)
list_add(&child->list, &work_head);
base_cache_used += child->size;
prune_base_data(NULL);
- free_base_data(child);
} else if (child) {
/*
* This child does not have its own children. It may be
base-commit: c69baaf57ba26cf117c2b6793802877f19738b0d
--
gitgitgadget
^ 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