Git development
 help / color / mirror / Atom feed
* [PATCH] checkout: add --fetch to fetch remote before resolving start-point
From: Harald Nordgren via GitGitGadget @ 2026-04-24 10:03 UTC (permalink / raw)
  To: git; +Cc: Harald Nordgren, Harald Nordgren

From: Harald Nordgren <haraldnordgren@gmail.com>

Add a --fetch option to git checkout and git switch, plus a
checkout.autoFetch config to enable it by default. When set and the
start-point argument names a configured remote (either bare, like
"origin", or prefixed, like "origin/foo"), fetch that remote before
resolving the ref. Aborts the checkout if the fetch fails.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
    checkout: add --fetch to fetch remote before resolving start-point
    
    A workflow I run several times a day looks like:
    
    git fetch origin
    git checkout -b new_branch origin/some-branch
    
    
    The first command exists purely to make the second one see an up-to-date
    view of the remote. If I forget it, origin/some-branch points at a stale
    commit, and I end up creating a local branch from the wrong starting
    point.
    
    This series teaches git checkout (and git switch) a new --fetch flag
    that folds the two steps into one:
    
    git checkout --fetch -b new_branch origin/some-branch
    
    
    When the start-point argument names a configured remote — either bare
    (origin, which resolves to the remote's default branch) or in / form —
    git fetch is run before the start-point is resolved. If the fetch fails,
    the checkout aborts and no local branch is created.
    
    A new checkout.autoFetch config option enables the same behavior by
    default, for users who always want it.

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2281%2FHaraldNordgren%2Fcheckout-fetch-start-point-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2281/HaraldNordgren/checkout-fetch-start-point-v1
Pull-Request: https://github.com/git/git/pull/2281

 builtin/checkout.c    | 48 ++++++++++++++++++++++++++++++++++++++--
 t/t7201-co.sh         | 51 +++++++++++++++++++++++++++++++++++++++++++
 t/t9902-completion.sh |  1 +
 3 files changed, 98 insertions(+), 2 deletions(-)

diff --git a/builtin/checkout.c b/builtin/checkout.c
index e031e61886..c8fbc4923b 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -30,7 +30,9 @@
 #include "repo-settings.h"
 #include "resolve-undo.h"
 #include "revision.h"
+#include "run-command.h"
 #include "setup.h"
+#include "strvec.h"
 #include "submodule.h"
 #include "symlinks.h"
 #include "trace2.h"
@@ -61,6 +63,7 @@ struct checkout_opts {
 	int count_checkout_paths;
 	int overlay_mode;
 	int dwim_new_local_branch;
+	int auto_fetch;
 	int discard_changes;
 	int accept_ref;
 	int accept_pathspec;
@@ -112,6 +115,34 @@ struct branch_info {
 	char *checkout;
 };
 
+static void fetch_remote_for_start_point(const char *arg)
+{
+	const char *slash;
+	char *remote_name;
+	struct remote *remote;
+	struct child_process cmd = CHILD_PROCESS_INIT;
+
+	if (!arg || !*arg)
+		return;
+
+	slash = strchr(arg, '/');
+	if (slash == arg)
+		return;
+	remote_name = slash ? xstrndup(arg, slash - arg) : xstrdup(arg);
+
+	remote = remote_get(remote_name);
+	if (!remote || !remote_is_configured(remote, 1)) {
+		free(remote_name);
+		return;
+	}
+
+	strvec_pushl(&cmd.args, "fetch", remote_name, NULL);
+	cmd.git_cmd = 1;
+	free(remote_name);
+	if (run_command(&cmd))
+		die(_("failed to fetch start-point '%s'"), arg);
+}
+
 static void branch_info_release(struct branch_info *info)
 {
 	free(info->name);
@@ -1237,6 +1268,10 @@ static int git_checkout_config(const char *var, const char *value,
 		opts->dwim_new_local_branch = git_config_bool(var, value);
 		return 0;
 	}
+	if (!strcmp(var, "checkout.autofetch")) {
+		opts->auto_fetch = git_config_bool(var, value);
+		return 0;
+	}
 
 	if (starts_with(var, "submodule."))
 		return git_default_submodule_config(var, value, NULL);
@@ -1942,8 +1977,13 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
 			opts->dwim_new_local_branch &&
 			opts->track == BRANCH_TRACK_UNSPECIFIED &&
 			!opts->new_branch;
-		int n = parse_branchname_arg(argc, argv, dwim_ok, which_command,
-					     &new_branch_info, opts, &rev);
+		int n;
+
+		if (opts->auto_fetch)
+			fetch_remote_for_start_point(argv[0]);
+
+		n = parse_branchname_arg(argc, argv, dwim_ok, which_command,
+					 &new_branch_info, opts, &rev);
 		argv += n;
 		argc -= n;
 	} else if (!opts->accept_ref && opts->from_treeish) {
@@ -2052,6 +2092,8 @@ int cmd_checkout(int argc,
 		OPT_BOOL(0, "overlay", &opts.overlay_mode, N_("use overlay mode (default)")),
 		OPT_BOOL(0, "auto-advance", &opts.auto_advance,
 			 N_("auto advance to the next file when selecting hunks interactively")),
+		OPT_BOOL(0, "fetch", &opts.auto_fetch,
+			 N_("fetch from the remote first if <start-point> is a remote-tracking ref")),
 		OPT_END()
 	};
 
@@ -2102,6 +2144,8 @@ int cmd_switch(int argc,
 			 N_("second guess 'git switch <no-such-branch>'")),
 		OPT_BOOL(0, "discard-changes", &opts.discard_changes,
 			 N_("throw away local modifications")),
+		OPT_BOOL(0, "fetch", &opts.auto_fetch,
+			 N_("fetch from the remote first if <start-point> is a remote-tracking ref")),
 		OPT_END()
 	};
 
diff --git a/t/t7201-co.sh b/t/t7201-co.sh
index 9bcf7c0b40..60ddebd9c3 100755
--- a/t/t7201-co.sh
+++ b/t/t7201-co.sh
@@ -801,4 +801,55 @@ test_expect_success 'tracking info copied with autoSetupMerge=inherit' '
 	test_cmp_config "" --default "" branch.main2.merge
 '
 
+test_expect_success 'setup upstream for --fetch tests' '
+	git checkout main &&
+	git init fetch_upstream &&
+	test_commit -C fetch_upstream u_main &&
+	git remote add fetch_upstream fetch_upstream &&
+	git fetch fetch_upstream &&
+	git -C fetch_upstream checkout -b fetch_new &&
+	test_commit -C fetch_upstream u_new
+'
+
+test_expect_success 'checkout --fetch -b picks up branch created upstream after clone' '
+	git checkout main &&
+	test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_new &&
+	git checkout --fetch -b local_new fetch_upstream/fetch_new &&
+	test_cmp_rev refs/remotes/fetch_upstream/fetch_new HEAD
+'
+
+test_expect_success 'checkout --fetch with bare remote name fetches the remote' '
+	git checkout main &&
+	git -C fetch_upstream checkout -b fetch_new2 &&
+	test_commit -C fetch_upstream u_new2 &&
+	test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_new2 &&
+	git checkout --fetch -b local_from_remote fetch_upstream &&
+	git rev-parse --verify refs/remotes/fetch_upstream/fetch_new2
+'
+
+test_expect_success 'checkout --fetch aborts and does not create branch on fetch failure' '
+	git checkout main &&
+	test_might_fail git branch -D bogus &&
+	test_must_fail git checkout --fetch -b bogus fetch_upstream/does_not_exist &&
+	test_must_fail git rev-parse --verify refs/heads/bogus
+'
+
+test_expect_success 'checkout.autoFetch=true enables fetching without --fetch' '
+	git checkout main &&
+	git -C fetch_upstream checkout -b fetch_cfg &&
+	test_commit -C fetch_upstream u_cfg &&
+	test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_cfg &&
+	git -c checkout.autoFetch=true checkout -b local_cfg fetch_upstream/fetch_cfg &&
+	test_cmp_rev refs/remotes/fetch_upstream/fetch_cfg HEAD
+'
+
+test_expect_success 'switch --fetch -c picks up branch created upstream after clone' '
+	git checkout main &&
+	git -C fetch_upstream checkout -b fetch_switch &&
+	test_commit -C fetch_upstream u_switch &&
+	test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_switch &&
+	git switch --fetch -c local_switch fetch_upstream/fetch_switch &&
+	test_cmp_rev refs/remotes/fetch_upstream/fetch_switch HEAD
+'
+
 test_done
diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh
index 2f9a597ec7..dc1d63669f 100755
--- a/t/t9902-completion.sh
+++ b/t/t9902-completion.sh
@@ -2602,6 +2602,7 @@ test_expect_success 'double dash "git checkout"' '
 	--ignore-other-worktrees Z
 	--recurse-submodules Z
 	--auto-advance Z
+	--fetch Z
 	--progress Z
 	--guess Z
 	--no-guess Z

base-commit: 94f057755b7941b321fd11fec1b2e3ca5313a4e0
-- 
gitgitgadget

^ permalink raw reply related

* Re: [PATCH v2 6/9] update-ref: handle rejections while adding updates
From: Karthik Nayak @ 2026-04-24  9:35 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git, gitster
In-Reply-To: <aendymWafXrTblbQ@pks.im>

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

Patrick Steinhardt <ps@pks.im> writes:

> On Thu, Apr 23, 2026 at 10:40:35AM +0200, Karthik Nayak wrote:
>> When using git-update-ref(1) with the '--batch-updates' flag, updates
>> rejected by the reference backend are displayed to the user while other
>> updates are applied. This only applies during the commit phase of the
>> transaction.
>>
>> In the following commits, we'll also extend `ref_transaction_update()`
>> to reject updates before a transaction is prepared/committed. In
>> preparation, modify the code in update-ref to also handle non-generic
>> rejections from `ref_transaction_update()`. This involves propagating
>> information to each of the commands on whether updates are allowed to be
>> rejected, and also checking for rejections and only dying for generic
>> failures.
>
> I noticed that you didn't address my feedback on the changed ordering of
> errors I posted on the preceding version. Was this intentional?
>
> Patrick

Huh. I do remember adding it in. I'll check my reflog and fix this.
Thanks for checking again.

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

^ permalink raw reply

* Re: [PATCH v2 2/9] refs: introduce `ref_store_init_options`
From: Karthik Nayak @ 2026-04-24  9:34 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git, gitster
In-Reply-To: <aendvlxDYMHNn4Sq@pks.im>

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

Patrick Steinhardt <ps@pks.im> writes:

> On Thu, Apr 23, 2026 at 10:40:31AM +0200, Karthik Nayak wrote:
>> diff --git a/refs/files-backend.c b/refs/files-backend.c
>> index b3b0c25f84..78150ad209 100644
>> --- a/refs/files-backend.c
>> +++ b/refs/files-backend.c
>> @@ -120,11 +120,13 @@ static struct ref_store *files_ref_store_init(struct repository *repo,
>>  					 &ref_common_dir);
>>
>>  	base_ref_store_init(ref_store, repo, refdir.buf, &refs_be_files);
>> -	refs->store_flags = flags;
>> +
>>  	refs->gitcommondir = strbuf_detach(&ref_common_dir, NULL);
>>  	refs->packed_ref_store =
>> -		packed_ref_store_init(repo, NULL, refs->gitcommondir, flags);
>> +		packed_ref_store_init(repo, payload, refs->gitcommondir, opts);
>
> Is this change here intentional? Doesn't seem to be related to any of
> the other changes here.
>

Definitely not, I think this was a messy rebase.

>> diff --git a/refs/refs-internal.h b/refs/refs-internal.h
>> index 2d963cc4f4..f49b3807bf 100644
>> --- a/refs/refs-internal.h
>> +++ b/refs/refs-internal.h
>> @@ -385,6 +385,15 @@ struct ref_store;
>>  				 REF_STORE_ODB | \
>>  				 REF_STORE_MAIN)
>>
>> +/*
>> + * Options for initializing the ref backend. All backend-agnostic information
>> + * which backends required will be held here.
>> + */
>> +struct ref_store_init_options {
>> +	/* The kind of operations that the ref_store is allowed to perform. */
>> +	unsigned int access_flags;
>
> Smells like something that should be converted into an enum eventually,
> but that definitely is out of scope for this patch series.
>
> Patrick

Yeah, this is good #leftoverbits task

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

^ permalink raw reply

* Re: Bug: Hierarchical Aliases no longer work in 2.54.0
From: Jonatan Holmgren @ 2026-04-24  7:29 UTC (permalink / raw)
  To: michael.grossfeld; +Cc: git
In-Reply-To: <PH7PR12MB73313034573C59C73F821BBFE52A2@PH7PR12MB7331.namprd12.prod.outlook.com>

That's a curious bug, sorry to hear I broke you/your team's workflow.  
Yes, three-level aliases were previously accidentally supported, now 
explicitly but with a new syntax (this was to support non-ascii 
characters). I'll send a patch fixing this but a release cycle will 
indeed take time.

Thanks!

^ permalink raw reply

* Re: [PATCH 2/2] builtin/history: introduce "fixup" subcommand
From: Patrick Steinhardt @ 2026-04-24  6:53 UTC (permalink / raw)
  To: D. Ben Knoble; +Cc: git, Elijah Newren
In-Reply-To: <CALnO6CAZQxvqEqDhahFs7NcjENrU=Dg=cbFDkEeAE3+h_3R+8g@mail.gmail.com>

On Thu, Apr 23, 2026 at 05:18:50PM -0400, D. Ben Knoble wrote:
> On Thu, Apr 23, 2026 at 2:55 AM Patrick Steinhardt <ps@pks.im> wrote:
> > On Wed, Apr 22, 2026 at 03:06:12PM -0400, D. Ben Knoble wrote:
> > > On Wed, Apr 22, 2026 at 6:30 AM Patrick Steinhardt <ps@pks.im> wrote:
> > > > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> > > > index 24dc907033..3cdfc8ba02 100644
> > > > --- a/Documentation/git-history.adoc
> > > > +++ b/Documentation/git-history.adoc
> > > > @@ -53,6 +55,19 @@ COMMANDS
> > > >
> > > >  The following commands are available to rewrite history in different ways:
> > > >
> > > > +`fixup <commit>`::
> > > > +       Apply the currently staged changes to the specified commit. The staged
> > > > +       changes are incorporated into the target commit's tree via a three-way
> > > > +       merge, using HEAD's tree as the merge base, which is equivalent to
> > > > +       linkgit:git-cherry-pick[1].
> > >
> > > I'm not quite sure what, as a user of "git history fixup," I'm
> > > supposed to take from this. Does it make conflicts less likely when
> > > creating the new fixup? I imagine it doesn't help with conflicts
> > > between <commit> and HEAD that newly arise.
> > >
> > > Anyway, I'd think the mechanics are less relevant than the end-user
> > > behavior at this point in the doc, unless the equivalence with
> > > cherry-pick is supposed to tell me something about that behavior.
> >
> > There's at least two more or less obvious variants to do this:
> >
> >   - You generate the diff between HEAD and index and then try to reapply
> >     the patch on top of the target commit.
> >
> >   - You perform the three-way merge.
> >
> > The second item is definitely more robust compared to generating the
> > diff and reapplying it, and we use the exact same strategy to perform
> > cherry-picks nowadays.
> >
> > > > diff --git a/builtin/history.c b/builtin/history.c
> > > > index 549e352c74..6299f0dfa9 100644
> > > > --- a/builtin/history.c
> > > > +++ b/builtin/history.c
> > [snip]
> > > > +       /*
> > > > +        * Perform the three-way merge to reapply changes in the index onto the
> > > > +        * target commit. This is using basically the same logic as a
> > > > +        * cherry-pick, where the base commit is our HEAD, ours is the original
> > > > +        * tree and theirs is the index tree.
> > > > +        */
> > >
> > > OTOH, this explanation helps quite a bit here :)
> >
> > Hm, okay. I felt that this explanation here is even more technical. How
> > about:
> >
> >     `fixup <commit>`::
> >         Apply the currently staged changes to the specified commit. This
> >         is done by performing a three-way merge between the HEAD commit,
> >         the target commit and the tree generated from staged changes.
> >         This is using the same logic as linkgit:git-cherry-pick[1].
> >
> > Not sure that this is an improvement? Happy to hear other suggestions.
> >
> > Thanks!
> >
> > Patrick
> 
> Hm. I think what I meant is that the in-code comment makes sense to
> describe internals; for users, I'm not sure what I should get out of
> that description of fixup.
> 
> What I (think I) really care about is that it behaves a bit like `git
> rebase -i` with a "fixup" command (modulo conflicts). Especially since
> this is quite a bit more porcelain than plumbing, no?
> 
> Idk. If the 3-way merge is valuable to keep, maybe it belongs in a
> second paragraph just to push it out of the way of the primary
> description ("Apply the currently staged changes to the specified
> commit")?

Ah, that's what you're getting at! I totally misunderstood what you
wanted to say, this makes a lot more sense. How about this:

    `fixup <commit>`::
        Apply the currently staged changes to the specified commit. This
        is similar in nature to `git commit --fixup=<commit>` followed
        by `git rebase --autosquash <commit>~`. Changes are applied to
        the target commit by performing a three-way merge between the
        HEAD commit, the target commit and the tree generated from
        staged changes.

Maybe there should be a new paragraph before we start talking about the
technical details?

Thanks!

Patrick

^ permalink raw reply

* Re: [PATCH] parse-options: fix sparse 'plain integer as NULL pointer'
From: Junio C Hamano @ 2026-04-24  2:47 UTC (permalink / raw)
  To: Ramsay Jones; +Cc: Jiamu Sun, GIT Mailing-list
In-Reply-To: <7aac2206-8b60-484f-a5f6-4943348ad3f6@ramsayjones.plus.com>

Another thing from GitHub CI

https://github.com/git/git/actions/runs/24825391649/job/72659919418#step:9:144

  Error: parse-options.c:680:30: comparison of integer expressions of
different signedness: 'unsigned int' and 'int' [-Werror=sign-compare]
    680 |        (n < cmds->nr && best == (intptr_t)cmds->items[n].util);
        |                              ^~

2026年4月24日(金) 1:05 Ramsay Jones <ramsay@ramsayjones.plus.com>:
>
>
> Signed-off-by: Ramsay Jones <ramsay@ramsayjones.plus.com>
> ---
>
> Hi Jiamu Sun,
>
> If you need to re-roll your 'js/parseopt-subcommand-autocorrection'
> branch, could you please squash this into the patch corresponding
> to commit b9e6a2d30a ("parseopt: autocorrect mistyped subcommands",
> 2026-04-23).
>
> Thanks.
>
> ATB,
> Ramsay Jones
>
>  parse-options.c | 2 +-
>  1 file changed, 1 insertion(+), 1 deletion(-)
>
> diff --git a/parse-options.c b/parse-options.c
> index d60e7bd3c9..14f3f385eb 100644
> --- a/parse-options.c
> +++ b/parse-options.c
> @@ -658,7 +658,7 @@ static const char *autocorrect_subcommand(const char *cmd,
>
>         for_each_string_list_item(cand, cmds) {
>                 if (starts_with(cand->string, cmd)) {
> -                       cand->util = 0;
> +                       cand->util = NULL;
>                 } else {
>                         int edit = levenshtein(cmd, cand->string,
>                                                0, 2, 1, 3) + 1;
> --
> 2.54.0

^ permalink raw reply

* Re: Bug: Hierarchical Aliases no longer work in 2.54.0
From: Michael Grossfeld @ 2026-04-23 22:55 UTC (permalink / raw)
  To: peff; +Cc: Michael.Grossfeld, git, jonatan
In-Reply-To: <20260423211237.GA1906241@coredump.intra.peff.net>

> In the short-term, you can work around it by using the new syntax:
>
>  [alias "pull.sub"]
>  command = ...whatever...

Sounds good. I'll likely write a script for my team to convert their
existing aliases depending on their git version.

> That does still break a historical alias if you happened to call it
> "foo.command". I'm not sure if we want to try to be even more thorough
> and fall back on that case, or if we're getting now into unlikely
> hypotheticals.

For my purposes, this would be fine and work for me. As the hierarchical
aliases are already unlikely, I imagine "foo.command" existing is even more
unlikely.

^ permalink raw reply

* [PATCH v3 0/2] revision.c: implement --reverse=before for walks
From: Mirko Faina @ 2026-04-23 22:51 UTC (permalink / raw)
  To: git
  Cc: Mirko Faina, Junio C Hamano, Jeff King, Jean-Noël Avila,
	Patrick Steinhardt, Tian Yuchen, Ben Knoble
In-Reply-To: <20260422002840.303477-4-mroik@delayed.space>

I've fixed the docs with the suggested changes by Jeff and applied some
styling fixes.

[1/2] revision.c: implement --reverse=before for walks (Mirko Faina)
[2/2] revision.c: reduce memory usage on reverse before (Mirko Faina)

 Documentation/rev-list-options.adoc | 16 +++++--
 revision.c                          | 73 +++++++++++++++++++++++++++--
 revision.h                          |  8 +++-
 t/t4202-log.sh                      | 66 ++++++++++++++++++++++++++
 4 files changed, 153 insertions(+), 10 deletions(-)

Range-diff against v2:
1:  599a247d82 ! 1:  4864ac46dd revision.c: implement --reverse=before for walks
    @@ Documentation/rev-list-options.adoc: With `--topo-order`, they would show 8 6 5
     +	`--walk-reflogs`. If `after`, output the commits chosen to be
     +	shown (see 'Commit Limiting' section above) in reverse order. If
     +	`before`, reverse the commits before filtering with `Commit
    -+	Limiting` options. This option can be used multiple times, last
    -+	one is applied. When the argument for `--reverse` is omitted, if
    -+	the current state is in no reverse, it defaults to `after`. If
    -+	it is in any reversed state, it restores the original ordering
    -+	by removing the reverse state.
    ++	Limiting` options. When multiple `--reverse=` options are given,
    ++	the final option overrides any previous options. The `--reverse`
    ++	option (with no specifier) behaves as `--reverse=after`, except
    ++	that, for historical reasons, it negates any previous reversed
    ++	state (so `--reverse --reverse` does nothing, nor does
    ++	`--reverse=before --reverse`. Note that `--reverse=before
    ++	--reverse --reverse` is the same as `--reverse=after`).
      endif::git-shortlog[]
      
      ifndef::git-shortlog[]
2:  480b322cf8 ! 2:  00489b0e52 revision.c: reduce memory usage on reverse before
    @@ revision.c: static struct commit *get_revision_internal(struct rev_info *revs)
      }
      
     +static void retrieve_with_window(struct rev_info *revs, int max_count,
    -+			  struct commit_list **reversed)
    ++			  	 struct commit_list **reversed)
     +{
     +	struct commit *c;
     +	struct commit_list *into_queue = NULL;
    @@ revision.c: static struct commit *get_revision_internal(struct rev_info *revs)
     +		}
     +	}
     +
    -+	while (outo_count) {
    -+		c = pop_commit(&outo_queue);
    -+		outo_count--;
    ++	while ((c = pop_commit(&outo_queue)))
     +		commit_list_insert(c, reversed);
    -+	}
    -+
    -+	while (into_count) {
    -+		c = pop_commit(&into_queue);
    -+		into_count--;
    ++	while ((c = pop_commit(&into_queue)))
     +		commit_list_insert(c, &outo_queue);
    -+		outo_count++;
    -+	}
    -+
    -+	while (outo_count) {
    -+		c = pop_commit(&outo_queue);
    -+		outo_count--;
    ++	while ((c = pop_commit(&outo_queue)))
     +		commit_list_insert(c, reversed);
    -+	}
     +}
     +
      struct commit *get_revision(struct rev_info *revs)
-- 
2.54.0


^ permalink raw reply

* [PATCH v3 1/2] revision.c: implement --reverse=before for walks
From: Mirko Faina @ 2026-04-23 22:51 UTC (permalink / raw)
  To: git
  Cc: Mirko Faina, Junio C Hamano, Jeff King, Jean-Noël Avila,
	Patrick Steinhardt, Tian Yuchen, Ben Knoble
In-Reply-To: <cover.1776984666.git.mroik@delayed.space>

In a revision walk `--reverse` can only be applied after any commit
limiting option. This makes getting a limited amount of commits from the
tail impossible. E.g.

    git log --reverse --max-count=3

Some would expect this to give back the first 3 commits of the project.
Instead it returns the last 3 but in reversed order.

Teach `get_revision()` to accpet an argument `(after|before)` from the
CLI, and apply the reversal before or after the commit limiting options
based on this argument. If no argument is provided default to the
current behaviour, applying `--reverse` after the commit limiting
options.

Signed-off-by: Mirko Faina <mroik@delayed.space>
---
 Documentation/rev-list-options.adoc | 16 +++++--
 revision.c                          | 31 ++++++++++++--
 revision.h                          |  8 +++-
 t/t4202-log.sh                      | 66 +++++++++++++++++++++++++++++
 4 files changed, 113 insertions(+), 8 deletions(-)

diff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc
index 2d195a1474..e97f6f2aff 100644
--- a/Documentation/rev-list-options.adoc
+++ b/Documentation/rev-list-options.adoc
@@ -914,10 +914,18 @@ With `--topo-order`, they would show 8 6 5 3 7 4 2 1 (or 8 7 4 2 6 5
 avoid showing the commits from two parallel development track mixed
 together.
 
-`--reverse`::
-	Output the commits chosen to be shown (see 'Commit Limiting'
-	section above) in reverse order. Cannot be combined with
-	`--walk-reflogs`.
+`--[no-]reverse[=(after|before)]`::
+	Accepts `after` or `before`. Cannot be combined with
+	`--walk-reflogs`. If `after`, output the commits chosen to be
+	shown (see 'Commit Limiting' section above) in reverse order. If
+	`before`, reverse the commits before filtering with `Commit
+	Limiting` options. When multiple `--reverse=` options are given,
+	the final option overrides any previous options. The `--reverse`
+	option (with no specifier) behaves as `--reverse=after`, except
+	that, for historical reasons, it negates any previous reversed
+	state (so `--reverse --reverse` does nothing, nor does
+	`--reverse=before --reverse`. Note that `--reverse=before
+	--reverse --reverse` is the same as `--reverse=after`).
 endif::git-shortlog[]
 
 ifndef::git-shortlog[]
diff --git a/revision.c b/revision.c
index 599b3a66c3..d581f5e38e 100644
--- a/revision.c
+++ b/revision.c
@@ -2686,7 +2686,16 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
 			git_log_output_encoding = xstrdup("");
 		return argcount;
 	} else if (!strcmp(arg, "--reverse")) {
-		revs->reverse ^= 1;
+		revs->reverse = !revs->reverse;
+	} else if (skip_prefix(arg, "--reverse=", &optarg)) {
+		if (!strcmp(optarg, "after"))
+			revs->reverse = REVERSE_AFTER;
+		else if(!strcmp(optarg, "before"))
+			revs->reverse = REVERSE_BEFORE;
+		else
+			die(_("unknown value for --reverse: %s"), optarg);
+	} else if (!strcmp(arg, "--no-reverse")) {
+		revs->reverse = NO_REVERSE;
 	} else if (!strcmp(arg, "--children")) {
 		revs->children.name = "children";
 		revs->limited = 1;
@@ -4525,19 +4534,35 @@ struct commit *get_revision(struct rev_info *revs)
 {
 	struct commit *c;
 	struct commit_list *reversed;
+	int max_count = revs->max_count;
+
+	if (revs->reverse && !revs->reverse_output_stage) {
+		if (revs->reverse == 3) {
+			BUG("allowed values for reverse are 0, 1 and 2");
+			revs->reverse = 1;
+		}
+
+		if (revs->reverse == REVERSE_BEFORE)
+			revs->max_count = -1;
 
-	if (revs->reverse) {
 		reversed = NULL;
 		while ((c = get_revision_internal(revs)))
 			commit_list_insert(c, &reversed);
 		commit_list_free(revs->commits);
 		revs->commits = reversed;
-		revs->reverse = 0;
 		revs->reverse_output_stage = 1;
+
+		if (revs->reverse == REVERSE_BEFORE)
+			revs->max_count = max_count;
 	}
 
 	if (revs->reverse_output_stage) {
+		if (revs->reverse == REVERSE_BEFORE && revs->max_count == 0)
+			return NULL;
+
 		c = pop_commit(&revs->commits);
+		if (revs->reverse == REVERSE_BEFORE)
+			revs->max_count--;
 		if (revs->track_linear)
 			revs->linear = !!(c && c->object.flags & TRACK_LINEAR);
 		return c;
diff --git a/revision.h b/revision.h
index 584f1338b5..02881577dc 100644
--- a/revision.h
+++ b/revision.h
@@ -121,6 +121,12 @@ struct ref_exclusions {
 struct oidset;
 struct topo_walk_info;
 
+enum rev_reverse {
+	NO_REVERSE = 0,
+	REVERSE_AFTER = 1,
+	REVERSE_BEFORE = 2,
+};
+
 struct rev_info {
 	/* Starting list */
 	struct commit_list *commits;
@@ -167,6 +173,7 @@ struct rev_info {
 			ignore_missing_links:1;
 
 	/* Traversal flags */
+	enum rev_reverse reverse:2;
 	unsigned int	dense:1,
 			prune:1,
 			no_walk:1,
@@ -196,7 +203,6 @@ struct rev_info {
 			rewrite_parents:1,
 			print_parents:1,
 			show_decorations:1,
-			reverse:1,
 			reverse_output_stage:1,
 			cherry_pick:1,
 			cherry_mark:1,
diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index 05cee9e41b..3bfe2c99b8 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -1882,6 +1882,72 @@ test_expect_success 'log --graph with --name-status' '
 	test_cmp_graph --name-status tangle..reach
 '
 
+cat >expect <<-\EOF
+c3f451c Merge tag 'reach'
+046b221 to remove
+EOF
+
+test_expect_success 'log --reverse --oneline --max-count=2' '
+	test_when_finished git reset --hard HEAD~1 &&
+	touch to_remove &&
+	git add to_remove &&
+	git commit -m "to remove" &&
+	git log --reverse --oneline --max-count=2 >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'log --reverse --reverse --reverse --oneline --max-count=2' '
+	test_when_finished git reset --hard HEAD~1 &&
+	touch to_remove &&
+	git add to_remove &&
+	git commit -m "to remove" &&
+	git log --reverse --reverse --reverse --oneline --max-count=2 >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'log --reverse=after --oneline --max-count=2' '
+	test_when_finished git reset --hard HEAD~1 &&
+	touch to_remove &&
+	git add to_remove &&
+	git commit -m "to remove" &&
+	git log --reverse=after --oneline --max-count=2 >actual &&
+	test_cmp expect actual
+'
+
+cat >expect <<-\EOF
+3a2fdcb initial
+f7dab8e second
+EOF
+
+test_expect_success 'log --reverse=before --oneline --max-count=2' '
+	test_when_finished rm actual &&
+	git log --reverse=before --oneline --max-count=2 >actual &&
+	test_cmp expect actual
+'
+
+cat >expect <<-\EOF
+046b221 to remove
+c3f451c Merge tag 'reach'
+EOF
+
+test_expect_success 'log --reverse --reverse --oneline --max-count=2' '
+	test_when_finished git reset --hard HEAD~1 &&
+	touch to_remove &&
+	git add to_remove &&
+	git commit -m "to remove" &&
+	git log --reverse --reverse --oneline --max-count=2 >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'log --reverse --no-reverse --oneline --max-count=2' '
+	test_when_finished git reset --hard HEAD~1 &&
+	touch to_remove &&
+	git add to_remove &&
+	git commit -m "to remove" &&
+	git log --reverse --no-reverse --oneline --max-count=2 >actual &&
+	test_cmp expect actual
+'
+
 cat >expect <<-\EOF
 * reach
 |
-- 
2.54.0


^ permalink raw reply related

* [PATCH v3 2/2] revision.c: reduce memory usage on reverse before
From: Mirko Faina @ 2026-04-23 22:52 UTC (permalink / raw)
  To: git
  Cc: Mirko Faina, Junio C Hamano, Jeff King, Jean-Noël Avila,
	Patrick Steinhardt, Tian Yuchen, Ben Knoble
In-Reply-To: <cover.1776984666.git.mroik@delayed.space>

Due to the nature of --reverse=before we have to walk all of the history
and store each non-filtered processed commit, this can be expensive on
memory for projects with a long history. When --max-count is being used
we don't really have to keep every processed commit, we can discard
older commits (as in have been processed before than the ones we're now
considering, from a chronological commit order they are the newer
commits) as we surpass the --max-count limit.

Teach get_revision() to keep only the newer commits as we walk a
revision with --reverse=before and --max-count=<k>. We do this through a
simple queue. With N nodes and K as the --max-count argument, assuming K
< N, we go from a space complexity of O(N) to O(K). When it comes down
to time complexity, the queue has an ammortized time of O(1) for pops,
so the complexity remains O(N).

Signed-off-by: Mirko Faina <mroik@delayed.space>
---
 revision.c | 42 ++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 40 insertions(+), 2 deletions(-)

diff --git a/revision.c b/revision.c
index d581f5e38e..03acff9bac 100644
--- a/revision.c
+++ b/revision.c
@@ -4530,6 +4530,40 @@ static struct commit *get_revision_internal(struct rev_info *revs)
 	return c;
 }
 
+static void retrieve_with_window(struct rev_info *revs, int max_count,
+			  	 struct commit_list **reversed)
+{
+	struct commit *c;
+	struct commit_list *into_queue = NULL;
+	struct commit_list *outo_queue = NULL;
+	int into_count = 0;
+	int outo_count = 0;
+
+	while ((c = get_revision_internal(revs))) {
+		commit_list_insert(c, &into_queue);
+		into_count++;
+		if (into_count + outo_count > max_count) {
+			if (!outo_count) {
+				while (into_count) {
+					c = pop_commit(&into_queue);
+					into_count--;
+					commit_list_insert(c, &outo_queue);
+					outo_count++;
+				}
+			}
+			pop_commit(&outo_queue);
+			outo_count--;
+		}
+	}
+
+	while ((c = pop_commit(&outo_queue)))
+		commit_list_insert(c, reversed);
+	while ((c = pop_commit(&into_queue)))
+		commit_list_insert(c, &outo_queue);
+	while ((c = pop_commit(&outo_queue)))
+		commit_list_insert(c, reversed);
+}
+
 struct commit *get_revision(struct rev_info *revs)
 {
 	struct commit *c;
@@ -4546,8 +4580,12 @@ struct commit *get_revision(struct rev_info *revs)
 			revs->max_count = -1;
 
 		reversed = NULL;
-		while ((c = get_revision_internal(revs)))
-			commit_list_insert(c, &reversed);
+		if (revs->reverse == REVERSE_BEFORE && max_count != -1) {
+			retrieve_with_window(revs, max_count, &reversed);
+		} else {
+			while ((c = get_revision_internal(revs)))
+				commit_list_insert(c, &reversed);
+		}
 		commit_list_free(revs->commits);
 		revs->commits = reversed;
 		revs->reverse_output_stage = 1;
-- 
2.54.0


^ permalink raw reply related

* Re: Bug: Hierarchical Aliases no longer work in 2.54.0
From: Michael Grossfeld @ 2026-04-23 22:46 UTC (permalink / raw)
  To: l.s.r; +Cc: Michael.Grossfeld, git, jonatan
In-Reply-To: <ea07acab-313d-435d-8328-e601fee980c3@web.de>

> Broken by ac1f12a9de4 (alias: support non-alphanumeric names via
> subsection syntax, 2026-02-18).

> Alias sections were not documented before.  How did you discover them?

Sheer dumb luck. I gravitated to it rather than a dash/hyphen based approach when I was creating aliases for my team.

> I think the previous behavior can be brought back while keeping the
> new feature, except for aliases that end in ".command".

That would work for me.

> Which works, right?

Yes, doing 'alias.pull.sub.command' works, but for the users on my team that have the old aliases, they are crashing.

^ permalink raw reply

* Re: Bug: Hierarchical Aliases no longer work in 2.54.0
From: René Scharfe @ 2026-04-23 21:36 UTC (permalink / raw)
  To: Grossfeld, Michael, git@vger.kernel.org; +Cc: Jonatan Holmgren
In-Reply-To: <PH7PR12MB73313034573C59C73F821BBFE52A2@PH7PR12MB7331.namprd12.prod.outlook.com>

On 4/23/26 8:19 PM, Grossfeld, Michael wrote:
> Hello all! Seeing this issue on 2.54.0 and it didn't look like anyone had reported it yet.
> 
>> What did you do before the bug happened? (Steps to reproduce your issue)
> 
> Attempting to use the hierarchical alias "pull.sub", which was working in 2.53.0, is no longer working in 2.54.0.
> It returns the following error: "git: 'pull.sub' is not a git command. See 'git --help'."
> >> What did you expect to happen? (Expected behavior)
> 
> The git alias should have firsted pulled, then updated submodules recursively.
> 
>> What happened instead? (Actual behavior)
> 
> It reports the following error: "git: 'pull.sub' is not a git command. See 'git --help'."
> 
>> What's different between what you expected and what actually happened?
> 
> git 2.53.0 to git 2.54.0.
> 
>> Anything else you want to add:
> 
> The alias was defined in my gitconfig as in 2.53.0, and remains this way:
> 
> [alias "pull"]
>         sub = "!f() { git pull origin --recurse-submodules=no --ff-only; echo Updating Submodules...; git submodule update --recursive --jobs=16 --progress; }; f"

Broken by ac1f12a9de4 (alias: support non-alphanumeric names via
subsection syntax, 2026-02-18).

Alias sections were not documented before.  How did you discover them?

I think the previous behavior can be brought back while keeping the
new feature, except for aliases that end in ".command".

> It was written via this command:
>         git config --global alias.pull.sub '!f() { git pull origin --recurse-submodules=no --ff-only -p; echo Updating Submodules...; git submodule update --recursive --jobs=16; }; f'
> 
> Trying to do the following (with .command):
>         git config --global alias.pull.sub.command '!f() { git pull origin --recurse-submodules=no --ff-only -p; echo Updating Submodules...; git submodule update --recursive --jobs=16; }; f'
> 
> Results in a section of the gitconfig that looks like this:
> 
> [alias "pull.sub"]
>         command = "!f() { git pull origin --recurse-submodules=no --ff-only -p; echo Updating Submodules...; git submodule update --recursive --jobs=16; }; f"

Which works, right?

> [System Info]
> git version:
> git version 2.54.0.windows.1
> cpu: x86_64
> built from commit: 2b8a3ab140826ac423c2845ef81d4c6ac4f7bf3c
> sizeof-long: 4
> sizeof-size_t: 8
> shell-path: D:/git-sdk-64-build-installers/usr/bin/sh
> rust: disabled
> feature: fsmonitor--daemon
> gettext: enabled
> libcurl: 8.19.0
> OpenSSL: OpenSSL 3.5.6 7 Apr 2026
> zlib: 1.3.2
> SHA-1: SHA1_DC
> SHA-256: SHA256_BLK
> default-ref-format: files
> default-hash: sha1
> uname: Windows 10.0 26200
> compiler info: gnuc: 15.2
> libc info: no libc information available
> $SHELL (typically, interactive shell): D:\develop\tools\Git\usr\bin\bash.exe
> 
> Thanks for the help!
> 
> Michael Grossfeld
> AMD


^ permalink raw reply

* Re: Advice on per-worktree private gitignore?
From: brian m. carlson @ 2026-04-23 21:56 UTC (permalink / raw)
  To: D. Ben Knoble; +Cc: Git
In-Reply-To: <CALnO6CCXmA+ATT7CuyWkU6P8qmLCCpMi5Ppr1c78s0heznpVyw@mail.gmail.com>

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

On 2026-04-23 at 21:26:05, D. Ben Knoble wrote:
> Today we have $GIT_DIR/info/exclude for the main worktree, but $(git
> rev-parse --git-dir)/info/exclude for secondary worktrees does not
> actually contribute to ignore specs.
> 
> Is this a "we never got around to implementing that", an intentional
> omission, or something else? Since --git-dir is described as parsing
> $GIT_DIR, I would naturally combine that with the gitignore(1) manual
> to think that the worktree.git/info/exclude should work.
> 
> (Currently it seems that main-worktree/.git/info/exclude applies to
> all worktrees, which may not be desirable in some circumstances.)

I'm not aware of it being an intentional omission.  I think what you
want might be useful in some circumstances, but there also might be
circumstances where it's not wanted and the user might want the settings
in the main worktree to be used everywhere.

So I'd say that we could add it as an optional extension, like
`extensions.worktreeConfig`.
-- 
brian m. carlson (they/them)
Toronto, Ontario, CA

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

^ permalink raw reply

* Advice on per-worktree private gitignore?
From: D. Ben Knoble @ 2026-04-23 21:26 UTC (permalink / raw)
  To: Git

Today we have $GIT_DIR/info/exclude for the main worktree, but $(git
rev-parse --git-dir)/info/exclude for secondary worktrees does not
actually contribute to ignore specs.

Is this a "we never got around to implementing that", an intentional
omission, or something else? Since --git-dir is described as parsing
$GIT_DIR, I would naturally combine that with the gitignore(1) manual
to think that the worktree.git/info/exclude should work.

(Currently it seems that main-worktree/.git/info/exclude applies to
all worktrees, which may not be desirable in some circumstances.)

Thanks,
-- 
D. Ben Knoble

^ permalink raw reply

* Re: [PATCH 2/2] builtin/history: introduce "fixup" subcommand
From: D. Ben Knoble @ 2026-04-23 21:18 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git, Elijah Newren
In-Reply-To: <aenCRKxak1l6GE3H@pks.im>

On Thu, Apr 23, 2026 at 2:55 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> On Wed, Apr 22, 2026 at 03:06:12PM -0400, D. Ben Knoble wrote:
> > On Wed, Apr 22, 2026 at 6:30 AM Patrick Steinhardt <ps@pks.im> wrote:
> > > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> > > index 24dc907033..3cdfc8ba02 100644
> > > --- a/Documentation/git-history.adoc
> > > +++ b/Documentation/git-history.adoc
> > > @@ -53,6 +55,19 @@ COMMANDS
> > >
> > >  The following commands are available to rewrite history in different ways:
> > >
> > > +`fixup <commit>`::
> > > +       Apply the currently staged changes to the specified commit. The staged
> > > +       changes are incorporated into the target commit's tree via a three-way
> > > +       merge, using HEAD's tree as the merge base, which is equivalent to
> > > +       linkgit:git-cherry-pick[1].
> >
> > I'm not quite sure what, as a user of "git history fixup," I'm
> > supposed to take from this. Does it make conflicts less likely when
> > creating the new fixup? I imagine it doesn't help with conflicts
> > between <commit> and HEAD that newly arise.
> >
> > Anyway, I'd think the mechanics are less relevant than the end-user
> > behavior at this point in the doc, unless the equivalence with
> > cherry-pick is supposed to tell me something about that behavior.
>
> There's at least two more or less obvious variants to do this:
>
>   - You generate the diff between HEAD and index and then try to reapply
>     the patch on top of the target commit.
>
>   - You perform the three-way merge.
>
> The second item is definitely more robust compared to generating the
> diff and reapplying it, and we use the exact same strategy to perform
> cherry-picks nowadays.
>
> > > diff --git a/builtin/history.c b/builtin/history.c
> > > index 549e352c74..6299f0dfa9 100644
> > > --- a/builtin/history.c
> > > +++ b/builtin/history.c
> [snip]
> > > +       /*
> > > +        * Perform the three-way merge to reapply changes in the index onto the
> > > +        * target commit. This is using basically the same logic as a
> > > +        * cherry-pick, where the base commit is our HEAD, ours is the original
> > > +        * tree and theirs is the index tree.
> > > +        */
> >
> > OTOH, this explanation helps quite a bit here :)
>
> Hm, okay. I felt that this explanation here is even more technical. How
> about:
>
>     `fixup <commit>`::
>         Apply the currently staged changes to the specified commit. This
>         is done by performing a three-way merge between the HEAD commit,
>         the target commit and the tree generated from staged changes.
>         This is using the same logic as linkgit:git-cherry-pick[1].
>
> Not sure that this is an improvement? Happy to hear other suggestions.
>
> Thanks!
>
> Patrick

Hm. I think what I meant is that the in-code comment makes sense to
describe internals; for users, I'm not sure what I should get out of
that description of fixup.

What I (think I) really care about is that it behaves a bit like `git
rebase -i` with a "fixup" command (modulo conflicts). Especially since
this is quite a bit more porcelain than plumbing, no?

Idk. If the 3-way merge is valuable to keep, maybe it belongs in a
second paragraph just to push it out of the way of the primary
description ("Apply the currently staged changes to the specified
commit")?

Thanks.

-- 
D. Ben Knoble

^ permalink raw reply

* Re: Bug: Hierarchical Aliases no longer work in 2.54.0
From: Jeff King @ 2026-04-23 21:12 UTC (permalink / raw)
  To: Grossfeld, Michael; +Cc: Jonatan Holmgren, git@vger.kernel.org
In-Reply-To: <PH7PR12MB73313034573C59C73F821BBFE52A2@PH7PR12MB7331.namprd12.prod.outlook.com>

On Thu, Apr 23, 2026 at 06:19:42PM +0000, Grossfeld, Michael wrote:

> Hello all! Seeing this issue on 2.54.0 and it didn't look like anyone had reported it yet.

Thanks for the report. I think you're the first.

> > What did you do before the bug happened? (Steps to reproduce your issue)
> 
> Attempting to use the hierarchical alias "pull.sub", which was working
> in 2.53.0, is no longer working in 2.54.0.
> It returns the following error: "git: 'pull.sub' is not a git command.
> See 'git --help'."

Here's a shorter reproduction recipe:

  git -c alias.foo.bar='!echo ok' foo.bar

With v2.53 it produces "ok", and in v2.54 you get:

  git: 'foo.bar' is not a git command. See 'git --help'.

This is due to the introduction of the three-level alias syntax in
ac1f12a9de (alias: support non-alphanumeric names via subsection syntax,
2026-02-18). We now allow "git foo" to expand based on both alias.foo
and alias.foo.command, the latter of which allows more flexible syntax.

But now we think you are trying to set the "sub" key of the "pull"
alias, which is obviously nonsense. I don't think three-level config
like this was ever a planned feature in the original alias expansion,
but it did indeed work. So I think this is a regression worth fixing.

In the short-term, you can work around it by using the new syntax:

  [alias "pull.sub"]
  command = ...whatever...

And I think we'd want a fix something like this:

diff --git a/alias.c b/alias.c
index ec9833dd30..58f21ac6ba 100644
--- a/alias.c
+++ b/alias.c
@@ -34,8 +34,20 @@ static int config_alias_cb(const char *var, const char *value,
 	if (subsection && !subsection_len)
 		subsection = NULL;
 
-	if (subsection && strcmp(key, "command"))
-		return 0;
+	if (subsection && strcmp(key, "command")) {
+		/*
+		 * We have historically support the "alias.name" form when
+		 * "name" happens to contain dots (e.g., alias.foo.bar to allow
+		 * "git foo.bar". But our parsing above would split that into
+		 * subsection "foo".
+		 *
+		 * If we do not understand the final key in a subsection-style
+		 * variable, fall back to treating it as a two-level alias.
+		 */
+		key = subsection;
+		subsection = NULL;
+		subsection_len = 0;
+	}
 
 	if (data->alias) {
 		int match;

That does still break a historical alias if you happened to call it
"foo.command". I'm not sure if we want to try to be even more thorough
and fall back on that case, or if we're getting now into unlikely
hypotheticals.

-Peff

^ permalink raw reply related

* Bug: Hierarchical Aliases no longer work in 2.54.0
From: Grossfeld, Michael @ 2026-04-23 18:19 UTC (permalink / raw)
  To: git@vger.kernel.org

Hello all! Seeing this issue on 2.54.0 and it didn't look like anyone had reported it yet.

> What did you do before the bug happened? (Steps to reproduce your issue)

Attempting to use the hierarchical alias "pull.sub", which was working in 2.53.0, is no longer working in 2.54.0.
It returns the following error: "git: 'pull.sub' is not a git command. See 'git --help'."

> What did you expect to happen? (Expected behavior)

The git alias should have firsted pulled, then updated submodules recursively.

> What happened instead? (Actual behavior)

It reports the following error: "git: 'pull.sub' is not a git command. See 'git --help'."

> What's different between what you expected and what actually happened?

git 2.53.0 to git 2.54.0.

> Anything else you want to add:

The alias was defined in my gitconfig as in 2.53.0, and remains this way:

[alias "pull"]
        sub = "!f() { git pull origin --recurse-submodules=no --ff-only; echo Updating Submodules...; git submodule update --recursive --jobs=16 --progress; }; f"

It was written via this command:
        git config --global alias.pull.sub '!f() { git pull origin --recurse-submodules=no --ff-only -p; echo Updating Submodules...; git submodule update --recursive --jobs=16; }; f'

Trying to do the following (with .command):
        git config --global alias.pull.sub.command '!f() { git pull origin --recurse-submodules=no --ff-only -p; echo Updating Submodules...; git submodule update --recursive --jobs=16; }; f'

Results in a section of the gitconfig that looks like this:

[alias "pull.sub"]
        command = "!f() { git pull origin --recurse-submodules=no --ff-only -p; echo Updating Submodules...; git submodule update --recursive --jobs=16; }; f"

[System Info]
git version:
git version 2.54.0.windows.1
cpu: x86_64
built from commit: 2b8a3ab140826ac423c2845ef81d4c6ac4f7bf3c
sizeof-long: 4
sizeof-size_t: 8
shell-path: D:/git-sdk-64-build-installers/usr/bin/sh
rust: disabled
feature: fsmonitor--daemon
gettext: enabled
libcurl: 8.19.0
OpenSSL: OpenSSL 3.5.6 7 Apr 2026
zlib: 1.3.2
SHA-1: SHA1_DC
SHA-256: SHA256_BLK
default-ref-format: files
default-hash: sha1
uname: Windows 10.0 26200
compiler info: gnuc: 15.2
libc info: no libc information available
$SHELL (typically, interactive shell): D:\develop\tools\Git\usr\bin\bash.exe

Thanks for the help!

Michael Grossfeld
AMD

^ permalink raw reply

* Re: [PATCH v2 2/3] builtin/log: prefetch necessary blobs for `git cherry`
From: Elijah Newren @ 2026-04-23 17:38 UTC (permalink / raw)
  To: phillip.wood; +Cc: Elijah Newren via GitGitGadget, git
In-Reply-To: <2abdc8ba-e361-492c-88b7-0c807ee9fb4d@gmail.com>

Hi Phillip,

On Thu, Apr 23, 2026 at 8:15 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
> On 21/04/2026 22:28, Elijah Newren wrote:
> > On Sun, Apr 19, 2026 at 7:04 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
> >> On 18/04/2026 01:32, Elijah Newren via GitGitGadget wrote:

> "--reapply-cherry-picks --empty=drop" is certainly more efficient. When
> we're computing patch ids do we do it for every upstream commit or just
> the ones that modify the set of paths that are modified in the branch
> we're rebasing?

You are correct that the patch id computations won't look at file
contents of commits unless they modify the same set of files as one of
the commits in our topic branch, but in order to determine the set of
commits which modify the same paths as commits in the branch we're
rebasing, we have to walk the upstream commits and do a tree-diff for
every one of them.  Yes, commits and trees tend to be much smaller
than blobs, but the number of trees/commits we have to look at may be
far larger than the number of blobs.  The biggest repositories are
constantly pushing so many commits that they are at a size where even
a merge-base operation can start to feel expensive.

> It is a shame that we don't have a config setting for
> "-reapply-cherry-picks" as it is easy to forget to pass that option.
> Unfortunately it is not supported by the apply backend which makes such
> a setting potentially confusing.

Indeed.

> >  The omission of
> > a --no-reapply-cherry-picks option in git-replay wasn't a lack of
> > effort or oversight, but a deliberate choice where I'd rather hold off
> > (possibly indefinitely) on implementing it.  So I'm a bit reluctant to
> > make the performance hazard less visible without also asking whether
> > we should even be doing that piece of the operation.
> >
> > I only implemented the git cherry fix because of a specific customer
> > situation where the operation was already baked into tooling, and
> > prefetching at least makes the worst case tolerable.
>
> I'm a bit surprised customers aren't complaining about tools that use
> "git rebase" being slow.

Are you sure they aren't complaining?

The merging parts of a rebase operation do have batch prefetching
already (up to 3 batches per commit; done that way to minimize the
number of objects downloaded because sometimes 2 or more of those
batches can be skipped entirely and trying to combine them into a
single batch would only be doable by downloading far more than
needed).  But, as you're alluding to, the --no-reapply-cherry-picks
part does not.

I'll note that GitHub tends to focus far more on the server side; it's
just that in this particular case with a special customer, they had me
dig a little closer to their client side operations.  In their case,
they were using git-replay rather than git-rebase, so they'd have no
reason to complain about rebase.  git-replay shares the same batch
prefetching for merge operations that rebase has, and doesn't have a
--no-reapply-cherry-picks behavior that can even be selected.
Honestly, I think the main reason this customer was also using
git-cherry was because I didn't get the drop-commits-that-become-empty
logic in the early versions of git-replay.  You added that to
git-replay (thanks again!), but after they had already built their
tooling.  This is only a guess on my part; they may have other reasons
for actively wanting git-cherry, but I think it might be worthwhile
for me to ask them if they can upgrade git versions (to get your fixes
for empty commits in replay) and then drop the calls to git-cherry.
However, I didn't want it to sound like I was pushing them to change
their workflows at my convenience, and hence this patch so that things
can be fast even if they keep the git-cherry in there.

> > I don't want to
> > hold myself to doing the same for the cherry_pick_list() path, but I'm
> > fairly confident the code here can be re-used for those other cases
> > and I'd help review a patch from anyone who wants to carry it forward.
> >
> > Anyway, you are making the right connection, it's just that my
> > personal answer is to let some other interested individual do it.
>
> Fair enough

Thanks for taking a look and asking interesting questions.

Elijah

^ permalink raw reply

* [PATCH v3 8/8] env: move "warn_on_object_refname_ambiguity" into `struct repo_config_values`
From: Olamide Caleb Bello @ 2026-04-23 16:54 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, gitster, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260423165432.143598-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

* [PATCH v3 7/8] env: move "sparse_expect_files_outside_of_patterns" into `repo_config_values`
From: Olamide Caleb Bello @ 2026-04-23 16:54 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, gitster, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260423165432.143598-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 v3 6/8] env: move "core_sparse_checkout_cone" into `struct repo_config_values`
From: Olamide Caleb Bello @ 2026-04-23 16:54 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, gitster, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260423165432.143598-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 v3 5/8] environment: move "precomposed_unicode" into `struct repo_config_values`
From: Olamide Caleb Bello @ 2026-04-23 16:54 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, gitster, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260423165432.143598-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 v3 4/8] environment: move "pack_compression_level" into `struct repo_config_values`
From: Olamide Caleb Bello @ 2026-04-23 16:54 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, gitster, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260423165432.143598-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 v3 3/8] environment: move `zlib_compression_level` into `struct repo_config_values`
From: Olamide Caleb Bello @ 2026-04-23 16:54 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, gitster, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260423165432.143598-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 v3 2/8] environment: move "check_stat" into `struct repo_config_values`
From: Olamide Caleb Bello @ 2026-04-23 16:54 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, gitster, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260423165432.143598-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


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