Git development
 help / color / mirror / Atom feed
* [PATCH v3 8/8] refs: drop local buffer in `refs_compute_filesystem_location()`
From: Patrick Steinhardt @ 2026-06-18  6:54 UTC (permalink / raw)
  To: git; +Cc: Karthik Nayak, Jeff King, Justin Tobler
In-Reply-To: <20260618-b4-pks-refs-avoid-chdir-notify-reparent-v3-0-2a5669e8f486@pks.im>

We're using a local buffer in `refs_compute_filesystem_location()` that
is only used so that we can fill it and then call `strbuf_realpath()` on
its result. This roundtrip isn't necessary though: `strbuf_realpath()`
already knows to use a single buffer as both input and output at the
same time. So all this does is to add a bit of confusion and an extra
memory allocation.

Drop the local buffer.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 refs.c | 8 ++------
 1 file changed, 2 insertions(+), 6 deletions(-)

diff --git a/refs.c b/refs.c
index e69b9b8ac8..4912510590 100644
--- a/refs.c
+++ b/refs.c
@@ -3571,8 +3571,6 @@ void refs_compute_filesystem_location(const char *gitdir, const char *payload,
 				      bool *is_worktree, struct strbuf *refdir,
 				      struct strbuf *ref_common_dir)
 {
-	struct strbuf sb = STRBUF_INIT;
-
 	*is_worktree = get_common_dir_noenv(ref_common_dir, gitdir);
 
 	if (!payload) {
@@ -3586,8 +3584,8 @@ void refs_compute_filesystem_location(const char *gitdir, const char *payload,
 	}
 
 	if (!is_absolute_path(payload)) {
-		strbuf_addf(&sb, "%s/%s", ref_common_dir->buf, payload);
-		strbuf_realpath(ref_common_dir, sb.buf, 1);
+		strbuf_addf(ref_common_dir, "/%s", payload);
+		strbuf_realpath(ref_common_dir, ref_common_dir->buf, 1);
 	} else {
 		strbuf_realpath(ref_common_dir, payload, 1);
 	}
@@ -3600,6 +3598,4 @@ void refs_compute_filesystem_location(const char *gitdir, const char *payload,
 			BUG("worktree path does not contain slash");
 		strbuf_addf(refdir, "/worktrees/%s", wt_id + 1);
 	}
-
-	strbuf_release(&sb);
 }

-- 
2.55.0.rc0.786.g65d90a0328.dirty


^ permalink raw reply related

* [ANNOUNCE] Git for Windows 2.55.0-rc1
From: Johannes Schindelin @ 2026-06-18  7:07 UTC (permalink / raw)
  To: git, git-packagers

Dear Git users,

I hereby announce that Git for Windows 2.55.0-rc1 is available from:

    https://github.com/git-for-windows/git/releases/tag/v2.55.0-rc1.windows.1

Changes since Git for Windows v2.54.0 (April 20th 2026)

Following the MSYS2 project, on which Git for Windows is based, Windows
8.1 support will be dropped after Git for Windows v2.55.

New Features

  * Comes with Git v2.55.0-rc1.
  * Comes with the MSYS2 runtime (Git for Windows flavor) based on
    Cygwin v3.6.9.
  * Comes with Git Credential Manager v2.8.0.
  * Comes with cURL v8.20.0.
  * Comes with less 702.
  * The FSCache now accelerates more git add scenarios.
  * Comes with OpenSSL v3.5.7.
  * The diff helper handling Word documents was ported from Perl to
    Rust.
  * Comes with Bash v5.3.15.

Bug Fixes

  * A regression in v2.54.0 that could cause endless "Unlink of file
    '.git/objects/pack/pack-.idx' failed. Should I try again?" loops on
    older Windows 10 versions during git fetch operations was fixed.
  * A bug that prevented proper shutdown of processes launched via Git
    Bash under certain circumstances was fixed.
  * A bug was fixed which could cause parallel checkouts to fail under
    certain circumstances when the FSCache is enabled.
  * Git Bash (MinTTY) now respects screen scaling settings under more
    circumstances.

Git-2.55.0-rc1-64-bit.exe | 40ccf96f6cb90e1c1e987108d339bf59cf883f4f62fe1d811bf7f453f216a3c2
Git-2.55.0-rc1-arm64.exe | 32ea9f89ebdcb4b5386bb8d91ac09bddca05f97aab2bb97f3a30e4a2ee13dfd7
PortableGit-2.55.0-rc1-64-bit.7z.exe | cea2707ee1a25ef33d9db4235c1b4ae33d22c2fe618dd3d4ec8574b34ec6f3d9
PortableGit-2.55.0-rc1-arm64.7z.exe | 70da0c29b0c60d7417d698c0d1850a2c409ab89cf9d5431ab75ff203356f5132
MinGit-2.55.0-rc1-64-bit.zip | bb7d17aa2e1cf1b015b7fe2f0312465fa0e035699b0970a5898c07595a61985f
MinGit-2.55.0-rc1-arm64.zip | 49df32406f57d2886c93acf8ce2fcb9640a1b73c1829fdbb36ea069c41d216c7
MinGit-2.55.0-rc1-32-bit.zip | 7d8c7828ee2490d406a4a9132a6a2e61f955c26c0a9cdebaf05ccde9bf156ebb
MinGit-2.55.0-rc1-busybox-64-bit.zip | 22abdf14fceaa1172a3416df33fb535a790faab7584a8ba903f3f88d4d0c45af
MinGit-2.55.0-rc1-busybox-32-bit.zip | 0139489b7baae8c9738cd3ea8dcbaf946c4f758716e9d64a6b2491db0c0cc229
Git-2.55.0-rc1-64-bit.tar.bz2 | 87be9bf3d21a53b6e415e0650b4c4632e4226ae9c148a6d5b090e3521d5cfa6e
Git-2.55.0-rc1-arm64.tar.bz2 | 9d7786f3dfb29d1715ff2a73a0f08abcd298fe610c70d1117dcffdb0d15e43ce

Ciao,
Johannes

^ permalink raw reply

* [PATCH v2] SubmittingPatches: address design critiques
From: Junio C Hamano @ 2026-06-18  8:50 UTC (permalink / raw)
  To: git
In-Reply-To: <xmqqv7bhxiby.fsf@gitster.g>

Contributors sometimes fail to answer fundamental design or
viability comments from reviewers and submit subsequent rounds
without addressing them.  When design decisions are resolved on the
mailing list, the final justification should be recorded in the
commit messages.

Instruct authors to be particularly mindful of critiques regarding
high-level design or viability, to defend their choices on the list,
and to accompany new iterations with clearer explanations in the cover
letter, responses, and revised commit messages. Also instruct them to
explicitly document the resolution of these concerns in the commit
message body to keep the historical record complete.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---

 * Rephrased the instruction in the first hunk somewhat to be a bit
   more explicit, and added a missing verb to the second hunk.

 Documentation/SubmittingPatches | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches
index f042bb5aaf..bbe759f3d9 100644
--- a/Documentation/SubmittingPatches
+++ b/Documentation/SubmittingPatches
@@ -51,6 +51,21 @@ area.
   respond to them with "Reply-All" on the mailing list, while taking
   them into account while preparing an updated set of patches.
 +
+You should be particularly mindful of critiques regarding the
+high-level design or viability of your proposal (e.g., questioning
+whether the feature is worth implementing, or if the chosen approach
+is appropriate).  Defend your design decisions on the list first, to
+avoid wasting effort on an implementation whose design is not yet
+solid.
++
+Make sure that any new version is accompanied by a much clearer
+explanation and justification (in the cover letter, your responses,
+and in the revised commit messages).  Aim to make the reviewers say
+"it is now clear why we may want to do this with the updated version".
++
+Topics that fail to address fundamental design critiques without
+resolution will not be considered ready for merging.
++
 It is often beneficial to allow some time for reviewers to provide
 feedback before sending a new version, rather than sending an updated
 series immediately after receiving a review. This helps collect broader
@@ -323,6 +338,10 @@ The body should provide a meaningful commit message, which:
 
 . alternate solutions considered but discarded, if any.
 
+. records the resolution of design or viability concerns raised by the
+  community during the review, if any, ensuring the historical record
+  explains why the chosen approach was accepted over alternatives.
+
 [[present-tense]]
 The problem statement that describes the status quo is written in the
 present tense.  Write "The code does X when it is given input Y",

Range-diff against v1:
1:  eb5f96ab04 ! 1:  aecdcf0bda SubmittingPatches: address design critiques
    @@ Documentation/SubmittingPatches: area.
        respond to them with "Reply-All" on the mailing list, while taking
        them into account while preparing an updated set of patches.
      +
    -+You would want to be particularly mindful of critiques regarding the
    ++You should be particularly mindful of critiques regarding the
     +high-level design or viability of your proposal (e.g., questioning
     +whether the feature is worth implementing, or if the chosen approach
    -+is appropriate).  You want to defend your design decisions on the list
    -+first, because you do not want to spend too much effort in the
    -+implementation if the design is not yet solid.
    ++is appropriate).  Defend your design decisions on the list first, to
    ++avoid wasting effort on an implementation whose design is not yet
    ++solid.
     ++
    -+Also, make sure that any new version is accompanied by a much clearer
    ++Make sure that any new version is accompanied by a much clearer
     +explanation and justification (in the cover letter, your responses,
     +and in the revised commit messages).  Aim to make the reviewers say
     +"it is now clear why we may want to do this with the updated version".
    @@ Documentation/SubmittingPatches: The body should provide a meaningful commit mes
      
      . alternate solutions considered but discarded, if any.
      
    -+. the resolution of design or viability concerns raised by the
    ++. records the resolution of design or viability concerns raised by the
     +  community during the review, if any, ensuring the historical record
     +  explains why the chosen approach was accepted over alternatives.
     +
-- 
2.55.0-rc1-93-ge727df1850


^ permalink raw reply related

* Re: [PATCH] SubmittingPatches: address design critiques
From: Kristoffer Haugsbakk @ 2026-06-18  8:55 UTC (permalink / raw)
  To: Michael Montalbo, Junio C Hamano; +Cc: git
In-Reply-To: <CAC2QwmJdF+YzAQE3WDEaUrurLVkYcAA0Cgs1YAqyxYcQ0jKfqA@mail.gmail.com>

On Thu, Jun 18, 2026, at 05:53, Michael Montalbo wrote:
> Junio C Hamano wrote:
>>[snip]
>
> Two small suggestions: open with a direct imperative and replace
> "effort in the implementation" with "effort on the implementation".
>[snip]

The threading doesn’t work here. This is in reply to:

https://lore.kernel.org/git/xmqqv7bhxiby.fsf@gitster.g/

But your email has no `In-Reply-To` header.

^ permalink raw reply

* Re: [PATCH 1/2] environment: move ignore_case into repo_config_values
From: Tian Yuchen @ 2026-06-18 10:56 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, ps, phillip.wood123, johannes.schindelin, stolee,
	Christian Couder, Ayush Chandekar, Olamide Caleb Bello
In-Reply-To: <xmqqh5n1w0i7.fsf@gitster.g>

On 6/18/26 01:16, Junio C Hamano wrote:
> Tian Yuchen <cat@malon.dev> writes:
> 
>> Note that the newly introduced getter, 'repo_get_ignore_case()',
>> intentionally avoids checking 'repo->gitdir'. This could safely
>> accommodates early dynamic probing of the filesystem during
>> 'git init' or clone operations, where the 'gitdir' might not be fully
>> initialized but the filesystem capability must be recorded.
> 
> Why "could"?  It either "safely accommodates" or it doesn't.
> 

Okay, will change in the next reroll.

> I do not quite understand the logic behind this part.  Why is it OK
> to punt until .gitdir is ready for trust-executable-bit, like it is
> done in f951ed98 (environment: move trust_executable_bit into
> repo_config_values, 2026-06-13)
> 
> diff --git a/environment.c b/environment.c
> index fc3ed8bb1c..75069a884d 100644
> --- a/environment.c
> +++ b/environment.c
> @@ -142,6 +141,13 @@ int is_bare_repository(void)
>   	return is_bare_repository_cfg && !repo_get_work_tree(the_repository);
>   }
>   
> +int repo_trust_executable_bit(struct repository *repo)
> +{
> +	return repo->gitdir?
> +		repo_config_values(repo)->trust_executable_bit :
> +		1;
> +}
> +
> 
> or hfs/ntfs in 71386c21 (environment: move 'protect_hfs' and
> 'protect_ntfs' into 'repo_config_values', 2026-06-10)
> 
> diff --git a/environment.c b/environment.c
> index fc3ed8bb1c..683fe1b4d3 100644
> --- a/environment.c
> +++ b/environment.c
> @@ -142,6 +140,20 @@ int is_bare_repository(void)
>   	return is_bare_repository_cfg && !repo_get_work_tree(the_repository);
>   }
>   
> +int repo_protect_ntfs(struct repository *repo)
> +{
> +	return repo->gitdir ?
> +		repo_config_values(repo)->protect_ntfs :
> +		PROTECT_NTFS_DEFAULT;
> +}
> +
> +int repo_protect_hfs(struct repository *repo)
> +{
> +	return repo->gitdir ?
> +		repo_config_values(repo)->protect_hfs :
> +		PROTECT_HFS_DEFAULT;
> +}
> +
>   int have_git_dir(void)
>   {
>   	return startup_info->have_repository
> 
> but not for this bit?

You're right, I made a mistake.

Earlier, when I was testing using 'repo->gitdir', the CI tests failed, 
and I thought it was because the test script was forcing initial values 
too early. I just realized the error was somewhere else. I'll fix it.

> 
>> +int repo_get_ignore_case(struct repository *repo)
>> +{
>> +	if (repo)
>> +		return repo_config_values(repo)->ignore_case;
>> +	return 0;
>> +}
> 
> What makes ignore-case so special?  Doesn't the same logic apply to
> the other three bits?
> 
> Or use a more direct
> 
> 	if (repo && repo->initialized)
> 		...;
> 
> for all three, as repo_config_values(repo) barfs when repo is not
> initialized?
> 
> I dunno.

That makes some sense. The reasoning behind using 'repo->gitdir' is that 
"as long as I know where .git is, I assume the repository has been 
initialized," which may indeed be less precise than 'repo->initialized'.

Btw, I think it's better to change the getter's name from 
'repo_get_ignore_case()' to 'repo_ignore_case()'. This way, it will be 
consistent with the previous flags.

Thanks, yuchen

^ permalink raw reply

* [PATCH v2 0/2] environment: move ignore_case into repo_config_values
From: Tian Yuchen @ 2026-06-18 11:42 UTC (permalink / raw)
  To: git; +Cc: ps, phillip.wood123, johannes.schindelin, stolee, Tian Yuchen
In-Reply-To: <20260617154929.564498-1-cat@malon.dev>

The 'core.ignorecase' configuration, stored as the global variable
'ignore_case', acts as a core filesystem capability flag.

This series continues the ongoing libification effort by moving
this global variable into 'struct repo_config_values', tying it
to the specific repository instance it was read from. This allows
us to encapsulate the configuration without altering its
eager-parsing behavior.

The getter function 'repo_ignore_case()' is introduced so
that we can safely retrieve the configuration value whilst
maintaining the correct fallback logic.

RFC Questions:

dir.c --- Performance overhead?

compat/win32/path-utils.c --- Is it appropriate to include the
repository.h header file?

Related materials:

 [1] In this patch to migrate protect_hfs and protect_ntfs, the approach
of introducing getters has been endorsed.

 [2] Derrick Stolee's previous attempt. The reasons for the failure are
also mentioned in [1].

Changes since V1:

 - s/repo_get_ignore_case()/repo_ignore_case()

 - Use repo->initialized instead of repo->gitdir

Thanks!

Mentored-by: Christian Couder christian.couder@gmail.com
Mentored-by: Ayush Chandekar ayu.chandekar@gmail.com
Mentored-by: Olamide Caleb Bello belkid98@gmail.com
Signed-off-by: Tian Yuchen cat@malon.dev

[1] https://lore.kernel.org/git/20260606143412.15443-1-cat@malon.dev/
[2] https://lore.kernel.org/git/2b4198c09cb6c04c60608d19072d419503dfe5df.1685716421.git.gitgitgadget@gmail.com/

Tian Yuchen (2):
  environment: move ignore_case into repo_config_values
  config: use repo_ignore_case() to access core.ignorecase

 apply.c                             |  2 +-
 builtin/fetch.c                     |  2 +-
 builtin/mv.c                        |  2 +-
 compat/win32/path-utils.c           |  3 ++-
 dir.c                               | 18 +++++++++---------
 environment.c                       | 11 +++++++++--
 environment.h                       |  9 ++++++++-
 fsmonitor.c                         |  2 +-
 name-hash.c                         |  6 +++---
 read-cache.c                        |  6 +++---
 refs/files-backend.c                |  4 ++--
 submodule.c                         |  2 +-
 t/helper/test-lazy-init-name-hash.c |  2 +-
 unpack-trees.c                      |  2 +-
 14 files changed, 43 insertions(+), 28 deletions(-)

-- 
2.43.0


^ permalink raw reply

* [PATCH v2 1/2] environment: move ignore_case into repo_config_values
From: Tian Yuchen @ 2026-06-18 11:42 UTC (permalink / raw)
  To: git
  Cc: ps, phillip.wood123, johannes.schindelin, stolee, Tian Yuchen,
	Christian Couder, Ayush Chandekar, Olamide Caleb Bello
In-Reply-To: <20260618114207.605211-1-cat@malon.dev>

The 'core.ignorecase' configuration which is stored as the
global variable 'ignore_case' acts as a core filesystem
capability flag.

Move this global variable into 'struct repo_config_values' to tie it
to the specific repository instance it was read from. This reduces
global state and aligns with the ongoing libification effort.

To ensure code readability, the getter function
'repo_ignore_case()' is introduced.

Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Ayush Chandekar <ayu.chandekar@gmail.com>
Mentored-by: Olamide Caleb Bello <belkid98@gmail.com>
Signed-off-by: Tian Yuchen <cat@malon.dev>
---
 environment.c | 8 ++++++++
 environment.h | 8 ++++++++
 2 files changed, 16 insertions(+)

diff --git a/environment.c b/environment.c
index fc3ed8bb1c..bfa3cb3045 100644
--- a/environment.c
+++ b/environment.c
@@ -142,6 +142,13 @@ int is_bare_repository(void)
 	return is_bare_repository_cfg && !repo_get_work_tree(the_repository);
 }
 
+int repo_ignore_case(struct repository *repo)
+{
+	return (repo && repo->initialized) ?
+		repo_config_values(repo)->ignore_case :
+		0;
+}
+
 int have_git_dir(void)
 {
 	return startup_info->have_repository
@@ -720,5 +727,6 @@ void repo_config_values_init(struct repo_config_values *cfg)
 {
 	cfg->attributes_file = NULL;
 	cfg->apply_sparse_checkout = 0;
+	cfg->ignore_case = 0;
 	cfg->branch_track = BRANCH_TRACK_REMOTE;
 }
diff --git a/environment.h b/environment.h
index 9eb97b3869..39a8bf0b49 100644
--- a/environment.h
+++ b/environment.h
@@ -91,6 +91,7 @@ struct repo_config_values {
 	/* section "core" config values */
 	char *attributes_file;
 	int apply_sparse_checkout;
+	int ignore_case;
 
 	/* section "branch" config values */
 	enum branch_track branch_track;
@@ -123,6 +124,13 @@ int git_default_config(const char *, const char *,
 int git_default_core_config(const char *var, const char *value,
 			    const struct config_context *ctx, void *cb);
 
+/*
+ * Getter for the `ignore_case` field of `struct repo_config_values`.
+ * It checks `repo->initialized` to prevent calling repo_config_values()`
+ * before the repository setup is fully complete or in non-git environments.
+ */
+int repo_ignore_case(struct repository *repo);
+
 void repo_config_values_init(struct repo_config_values *cfg);
 
 /*
-- 
2.43.0


^ permalink raw reply related

* [PATCH v2 2/2] config: use repo_ignore_case() to access core.ignorecase
From: Tian Yuchen @ 2026-06-18 11:42 UTC (permalink / raw)
  To: git
  Cc: ps, phillip.wood123, johannes.schindelin, stolee, Tian Yuchen,
	Christian Couder, Ayush Chandekar, Olamide Caleb Bello
In-Reply-To: <20260618114207.605211-1-cat@malon.dev>

Replace the accesses to the global 'ignore_case' variable with
calls to 'repo_ignore_case(the_repository)'. This step eliminates
the 'ignore_case' global state.

Note on compat/win32/path-utils.c:
To eliminate the global state, several helper functions
(e.g. 'win32_fspathncmp()') now read from
'repo_ignore_case(the_repository)'. While this introduces
dependency on 'repository.h' into the 'compat/', it avoids massive
refactoring of the signatures across the codebase.

Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Ayush Chandekar <ayu.chandekar@gmail.com>
Mentored-by: Olamide Caleb Bello <belkid98@gmail.com>
Signed-off-by: Tian Yuchen <cat@malon.dev>
---
 apply.c                             |  2 +-
 builtin/fetch.c                     |  2 +-
 builtin/mv.c                        |  2 +-
 compat/win32/path-utils.c           |  3 ++-
 dir.c                               | 18 +++++++++---------
 environment.c                       |  3 +--
 environment.h                       |  1 -
 fsmonitor.c                         |  2 +-
 name-hash.c                         |  6 +++---
 read-cache.c                        |  6 +++---
 refs/files-backend.c                |  4 ++--
 submodule.c                         |  2 +-
 t/helper/test-lazy-init-name-hash.c |  2 +-
 unpack-trees.c                      |  2 +-
 14 files changed, 27 insertions(+), 28 deletions(-)

diff --git a/apply.c b/apply.c
index 249248d4f2..620c88d2a0 100644
--- a/apply.c
+++ b/apply.c
@@ -4008,7 +4008,7 @@ static int path_is_beyond_symlink_1(struct apply_state *state, struct strbuf *na
 			struct cache_entry *ce;
 
 			ce = index_file_exists(state->repo->index, name->buf,
-					       name->len, ignore_case);
+					       name->len, repo_ignore_case(the_repository));
 			if (ce && S_ISLNK(ce->ce_mode))
 				return 1;
 		} else {
diff --git a/builtin/fetch.c b/builtin/fetch.c
index e4e8a72ed9..073e716bc4 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1819,7 +1819,7 @@ static void ref_transaction_rejection_handler(const char *refname,
 {
 	struct ref_rejection_data *data = cb_data;
 
-	if (err == REF_TRANSACTION_ERROR_CASE_CONFLICT && ignore_case &&
+	if (err == REF_TRANSACTION_ERROR_CASE_CONFLICT && repo_ignore_case(the_repository) &&
 	    !data->case_sensitive_msg_shown) {
 		error(_("You're on a case-insensitive filesystem, and the remote you are\n"
 			"trying to fetch from has references that only differ in casing. It\n"
diff --git a/builtin/mv.c b/builtin/mv.c
index 948b330639..d60582262c 100644
--- a/builtin/mv.c
+++ b/builtin/mv.c
@@ -419,7 +419,7 @@ int cmd_mv(int argc,
 			goto act_on_entry;
 		}
 		if (lstat(dst, &st) == 0 &&
-		    (!ignore_case || strcasecmp(src, dst))) {
+		    (!repo_ignore_case(the_repository) || strcasecmp(src, dst))) {
 			bad = _("destination exists");
 			if (force) {
 				/*
diff --git a/compat/win32/path-utils.c b/compat/win32/path-utils.c
index 966ef779b9..f779f367cf 100644
--- a/compat/win32/path-utils.c
+++ b/compat/win32/path-utils.c
@@ -2,6 +2,7 @@
 
 #include "../../git-compat-util.h"
 #include "../../environment.h"
+#include "../../repository.h"
 
 int win32_has_dos_drive_prefix(const char *path)
 {
@@ -75,7 +76,7 @@ int win32_fspathncmp(const char *a, const char *b, size_t count)
 		} else if (is_dir_sep(*b))
 			return +1;
 
-		diff = ignore_case ?
+		diff = repo_ignore_case(the_repository) ?
 			(unsigned char)tolower(*a) - (int)(unsigned char)tolower(*b) :
 			(unsigned char)*a - (int)(unsigned char)*b;
 		if (diff)
diff --git a/dir.c b/dir.c
index 33c81c256e..540dd372c1 100644
--- a/dir.c
+++ b/dir.c
@@ -126,7 +126,7 @@ int count_slashes(const char *s)
 
 int git_fspathcmp(const char *a, const char *b)
 {
-	return ignore_case ? strcasecmp(a, b) : strcmp(a, b);
+	return repo_ignore_case(the_repository) ? strcasecmp(a, b) : strcmp(a, b);
 }
 
 int fspatheq(const char *a, const char *b)
@@ -136,7 +136,7 @@ int fspatheq(const char *a, const char *b)
 
 int git_fspathncmp(const char *a, const char *b, size_t count)
 {
-	return ignore_case ? strncasecmp(a, b, count) : strncmp(a, b, count);
+	return repo_ignore_case(the_repository) ? strncasecmp(a, b, count) : strncmp(a, b, count);
 }
 
 int paths_collide(const char *a, const char *b)
@@ -153,7 +153,7 @@ int paths_collide(const char *a, const char *b)
 
 unsigned int fspathhash(const char *str)
 {
-	return ignore_case ? strihash(str) : strhash(str);
+	return repo_ignore_case(the_repository) ? strihash(str) : strhash(str);
 }
 
 int git_fnmatch(const struct pathspec_item *item,
@@ -202,7 +202,7 @@ static int fnmatch_icase_mem(const char *pattern, int patternlen,
 		use_str = str_buf.buf;
 	}
 
-	if (ignore_case)
+	if (repo_ignore_case(the_repository))
 		flags |= WM_CASEFOLD;
 	match_status = wildmatch(use_pat, use_str, flags);
 
@@ -1851,7 +1851,7 @@ static struct dir_entry *dir_add_name(struct dir_struct *dir,
 				      struct index_state *istate,
 				      const char *pathname, int len)
 {
-	if (index_file_exists(istate, pathname, len, ignore_case))
+	if (index_file_exists(istate, pathname, len, repo_ignore_case(the_repository)))
 		return NULL;
 
 	ALLOC_GROW(dir->entries, dir->nr+1, dir->internal.alloc);
@@ -1888,7 +1888,7 @@ static enum exist_status directory_exists_in_index_icase(struct index_state *ist
 	if (index_dir_exists(istate, dirname, len))
 		return index_directory;
 
-	ce = index_file_exists(istate, dirname, len, ignore_case);
+	ce = index_file_exists(istate, dirname, len, repo_ignore_case(the_repository));
 	if (ce && S_ISGITLINK(ce->ce_mode))
 		return index_gitdir;
 
@@ -1907,7 +1907,7 @@ static enum exist_status directory_exists_in_index(struct index_state *istate,
 {
 	int pos;
 
-	if (ignore_case)
+	if (repo_ignore_case(the_repository))
 		return directory_exists_in_index_icase(istate, dirname, len);
 
 	pos = index_name_pos(istate, dirname, len);
@@ -2447,7 +2447,7 @@ static enum path_treatment treat_path(struct dir_struct *dir,
 
 	/* Always exclude indexed files */
 	has_path_in_index = !!index_file_exists(istate, path->buf, path->len,
-						ignore_case);
+						repo_ignore_case(the_repository));
 	if (dtype != DT_DIR && has_path_in_index)
 		return path_none;
 
@@ -3201,7 +3201,7 @@ static int cmp_icase(char a, char b)
 {
 	if (a == b)
 		return 0;
-	if (ignore_case)
+	if (repo_ignore_case(the_repository))
 		return toupper(a) - toupper(b);
 	return a - b;
 }
diff --git a/environment.c b/environment.c
index bfa3cb3045..c288c3613d 100644
--- a/environment.c
+++ b/environment.c
@@ -46,7 +46,6 @@ int trust_ctime = 1;
 int check_stat = 1;
 int has_symlinks = 1;
 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;
@@ -342,7 +341,7 @@ int git_default_core_config(const char *var, const char *value,
 	}
 
 	if (!strcmp(var, "core.ignorecase")) {
-		ignore_case = git_config_bool(var, value);
+		cfg->ignore_case = git_config_bool(var, value);
 		return 0;
 	}
 
diff --git a/environment.h b/environment.h
index 39a8bf0b49..c15121db65 100644
--- a/environment.h
+++ b/environment.h
@@ -171,7 +171,6 @@ extern int trust_ctime;
 extern int check_stat;
 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;
diff --git a/fsmonitor.c b/fsmonitor.c
index d07dc18967..107767527e 100644
--- a/fsmonitor.c
+++ b/fsmonitor.c
@@ -453,7 +453,7 @@ static void fsmonitor_refresh_callback(struct index_state *istate, char *name)
 	 * case-insensitive file system, try again using the name-hash
 	 * and dir-name-hash.
 	 */
-	if (!nr_in_cone && ignore_case) {
+	if (!nr_in_cone && repo_ignore_case(the_repository)) {
 		nr_in_cone = handle_using_name_hash_icase(istate, name);
 		if (!nr_in_cone)
 			nr_in_cone = handle_using_dir_name_hash_icase(
diff --git a/name-hash.c b/name-hash.c
index b91e276267..83757db874 100644
--- a/name-hash.c
+++ b/name-hash.c
@@ -126,7 +126,7 @@ static void hash_index_entry(struct index_state *istate, struct cache_entry *ce)
 		hashmap_add(&istate->name_hash, &ce->ent);
 	}
 
-	if (ignore_case)
+	if (repo_ignore_case(the_repository))
 		add_dir_entry(istate, ce);
 }
 
@@ -207,7 +207,7 @@ static int lookup_lazy_params(struct index_state *istate)
 	 * code to build the "istate->name_hash".  We don't
 	 * need the complexity here.
 	 */
-	if (!ignore_case)
+	if (!repo_ignore_case(the_repository))
 		return 0;
 
 	nr_cpus = online_cpus();
@@ -651,7 +651,7 @@ void remove_name_hash(struct index_state *istate, struct cache_entry *ce)
 	ce->ce_flags &= ~CE_HASHED;
 	hashmap_remove(&istate->name_hash, &ce->ent, ce);
 
-	if (ignore_case)
+	if (repo_ignore_case(the_repository))
 		remove_dir_entry(istate, ce);
 }
 
diff --git a/read-cache.c b/read-cache.c
index 21829102ae..fcdf0e5ef1 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -760,12 +760,12 @@ int add_to_index(struct index_state *istate, const char *path, struct stat *st,
 	 * case of the file being added to the repository matches (is folded into) the existing
 	 * entry's directory case.
 	 */
-	if (ignore_case) {
+	if (repo_ignore_case(the_repository)) {
 		adjust_dirname_case(istate, ce->name);
 	}
 	if (!(flags & ADD_CACHE_RENORMALIZE)) {
 		alias = index_file_exists(istate, ce->name,
-					  ce_namelen(ce), ignore_case);
+					  ce_namelen(ce), repo_ignore_case(the_repository));
 		if (alias &&
 		    !ce_stage(alias) &&
 		    !ie_match_stat(istate, alias, st, ce_option)) {
@@ -786,7 +786,7 @@ int add_to_index(struct index_state *istate, const char *path, struct stat *st,
 	} else
 		set_object_name_for_intent_to_add_entry(ce);
 
-	if (ignore_case && alias && different_name(ce, alias))
+	if (repo_ignore_case(the_repository) && alias && different_name(ce, alias))
 		ce = create_alias_ce(istate, ce, alias);
 	ce->ce_flags |= CE_ADDED;
 
diff --git a/refs/files-backend.c b/refs/files-backend.c
index a4c7858787..c1da06b1d5 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -806,7 +806,7 @@ static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs,
 		} else {
 			unable_to_lock_message(ref_file.buf, myerr, err);
 			if (myerr == EEXIST) {
-				if (ignore_case &&
+				if (repo_ignore_case(the_repository) &&
 				    transaction_has_case_conflicting_update(transaction, update)) {
 					/*
 					 * In case-insensitive filesystems, ensure that conflicts within a
@@ -920,7 +920,7 @@ static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs,
 		 * conflicts between 'foo' and 'Foo/bar'. So let's lowercase
 		 * the refname.
 		 */
-		if (ignore_case) {
+		if (repo_ignore_case(the_repository)) {
 			struct strbuf lower = STRBUF_INIT;
 
 			strbuf_addstr(&lower, refname);
diff --git a/submodule.c b/submodule.c
index a939ff5072..6e7f8b9f7c 100644
--- a/submodule.c
+++ b/submodule.c
@@ -2389,7 +2389,7 @@ static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodu
 
 	/* Prevent conflicts on case-folding filesystems */
 	repo_config_get_bool(the_repository, "core.ignorecase", &config_ignorecase);
-	if (ignore_case || config_ignorecase) {
+	if (repo_ignore_case(the_repository) || config_ignorecase) {
 		bool suffixes_match = !strcmp(last_submodule_name, submodule_name);
 		return check_casefolding_conflict(git_dir, submodule_name,
 						  suffixes_match);
diff --git a/t/helper/test-lazy-init-name-hash.c b/t/helper/test-lazy-init-name-hash.c
index e542985c94..43cead6d7d 100644
--- a/t/helper/test-lazy-init-name-hash.c
+++ b/t/helper/test-lazy-init-name-hash.c
@@ -218,7 +218,7 @@ int cmd__lazy_init_name_hash(int argc, const char **argv)
 	/*
 	 * istate->dir_hash is only created when ignore_case is set.
 	 */
-	ignore_case = 1;
+	repo_config_values(the_repository)->ignore_case = 1;
 
 	if (dump) {
 		if (perf || analyze > 0)
diff --git a/unpack-trees.c b/unpack-trees.c
index 998a1e6dc7..d13b004f71 100644
--- a/unpack-trees.c
+++ b/unpack-trees.c
@@ -2428,7 +2428,7 @@ static int check_ok_to_remove(const char *name, int len, int dtype,
 	 *
 	 * Ignore that lstat() if it matches.
 	 */
-	if (ignore_case && icase_exists(o, name, len, st))
+	if (repo_ignore_case(the_repository) && icase_exists(o, name, len, st))
 		return 0;
 
 	if (o->internal.dir &&
-- 
2.43.0


^ permalink raw reply related

* Re: [PATCH] Fix typo in MaintNotes regarding versioning scheme
From: Tuomas Ahola @ 2026-06-18 11:48 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Silas Poulson, gitgitgadget, git
In-Reply-To: <xmqqfr6czmye.fsf@gitster.g>

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

> Silas Poulson <silas@dyalog.com> writes:
> 
> > I'm aware this is a very minor change, but it would be good to not let 
> > this fall through the cracks.
> 
> Thanks for noticing a typo.
> 
> Will update before the next issue is sent to the mailing list.  No
> point in changing it before that.

On that occasion, please consider also these fixes:
-----8<-----
Subject: [PATCH] MaintNotes: fix typos and grammar

Signed-off-by: Tuomas Ahola <taahol@utu.fi>
---
 MaintNotes | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/MaintNotes b/MaintNotes
index 12ba677c36..44b29c9e10 100644
--- a/MaintNotes
+++ b/MaintNotes
@@ -82,7 +82,7 @@ available at:
 There is a volunteer-run newsletter to serve our community ("Git Rev
 News" https://git.github.io/rev_news/).
 
-Git is a member project of software freedom conservancy, a non-profit
+Git is a member project of Software Freedom Conservancy, a non-profit
 organization (https://sfconservancy.org/).  To reach a committee of
 liaisons to the conservancy, contact them at <git@sfconservancy.org>.
 
@@ -245,7 +245,7 @@ by others may cause conflicts with their own work, and find people who
 are working on these topics to talk to before the potential conflicts
 get out of control.  It would be a good idea to fork your work from
 maint or master and to (1) test it by itself, (2) test a temporary
-merge of it to "next" and (3) test a temporary merge to it to "seen",
+merge of it to "next" and (3) test a temporary merge of it to "seen",
 before sending it to the list (or asking GitGitGadget to send it to
 the list).
 
@@ -262,10 +262,10 @@ using the topics that didn't make the cut in the feature release.
 Some topics that used to be in "next" during the previous cycle may
 get ejected from "next" when this happens.
 
-A natural consequence of how "next" and "seen" bundles topics together
-is that until a topic is merged to "next", updates to it is expected
+A natural consequence of how "next" and "seen" bundle topics together
+is that until a topic is merged to "next", updates to it are expected
 by replacing the patch(es) in the topic with an improved version, and
-once a topic is merged to "next", updates to it needs to come as
+once a topic is merged to "next", updates to it need to come as
 incremental patches, pointing out what was wrong in the previous
 patches and how the problem was corrected.  The idea is that if many
 reviewers thought it has seen enough eyeballs and is good enough for

base-commit: f9b08c9b285c9154e41b9f5fce7506018b83dfcb
-- 
2.30.2


^ permalink raw reply related

* Re: [PATCH v2 0/7] More work supporting objects larger than 4GB on Windows
From: Patrick Steinhardt @ 2026-06-18 12:13 UTC (permalink / raw)
  To: Johannes Schindelin via GitGitGadget
  Cc: git, Kristofer Karlsson, Johannes Schindelin
In-Reply-To: <pull.2137.v2.git.1781524349.gitgitgadget@gmail.com>

On Mon, Jun 15, 2026 at 11:52:22AM +0000, Johannes Schindelin via GitGitGadget wrote:
> This patch series tries to address the problems pointed out by the expensive
> tests that now run in CI: t5608 and t7508 verify various aspects about
> objects larger than 4GB, which Git does not currently handle correctly when
> run on a platform where size_t is 64-bit and unsigned long is 32-bit.
> 
> Changes vs v1:
> 
>  * Rebased onto master, which merged ps/odb-source-loose (with which these
>    patches previously conflicted rather badly).
>  * Removed superfluous size_t s variables (thanks, Patrick!).

I skimmed those parts that I was previously commenting on and am
happy with those changes. Thanks!

Patrick

^ permalink raw reply

* Re: [PATCH v2 1/5] SubmittingPatches: encourage trailer use for substantial help
From: Kristoffer Haugsbakk @ 2026-06-18 12:21 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git, Patrick Steinhardt
In-Reply-To: <xmqq4ij0vo8f.fsf@gitster.g>

On Wed, Jun 17, 2026, at 23:41, Junio C Hamano wrote:
> kristofferhaugsbakk@fastmail.com writes:
>
>> diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches
>> index 176567738d4..0b12badf86d 100644
>> --- a/Documentation/SubmittingPatches
>> +++ b/Documentation/SubmittingPatches
>> @@ -443,8 +443,16 @@ identifying, and not misleading.
>>  The goal of this policy is to allow us to have sufficient information to contact
>>  you if questions arise about your contribution.
>>
>> +=== Commit trailers
>>  [[commit-trailers]]
>> -If you like, you can put extra trailers at the end:
>
> I think majority of AsciiDoc files in this project places [[anchor]]
> before the "=== title" of a section.  For example, here is how the
> patch flow section begins in SubmittingPatches:
>
>     [[patch-flow]]
>     === A typical life cycle of a patch series
>
>     To help us understand the reason behind various guidelines given later
>     in the document, first let's understand how the life cycle of a
>     typical patch series for this project goes.
>
> I do not offhand know which way is kosher, but we should be
> consistent either way.

Your suspicion is correct. Skimming this I cannot seem to find any
examples where the anchor goes after the title.

    git grep --extended-regexp -C1 '^\[\[' -- 'Documentation/*adoc'

I’ll fix it.

^ permalink raw reply

* Re: [PATCH] checkout: add --fetch to fetch remote before resolving start-point
From: Harald Nordgren @ 2026-06-18 12:36 UTC (permalink / raw)
  To: D. Ben Knoble; +Cc: Harald Nordgren via GitGitGadget, git
In-Reply-To: <CALnO6CCNoo8y2V5KmE0KQ6qDurZELipFowcr=ZpZ3ocVB-uLjA@mail.gmail.com>

Hi Ben!

Trying to shore up some support for this topic. How do you feel about this now?


Harald

^ permalink raw reply

* Re: [PATCH v6] checkout: extend --track with a "fetch" mode to refresh start-point
From: Harald Nordgren @ 2026-06-18 12:38 UTC (permalink / raw)
  To: Phillip Wood; +Cc: gitgitgadget, git
In-Reply-To: <f23eb128-958f-475f-911b-eac4f6daddff@gmail.com>

Hi Phillip!

How do you feel now, is it worth it for us to move forward with this
topic or not?


Harald

On Fri, May 8, 2026 at 3:15 PM Phillip Wood <phillip.wood123@gmail.com> wrote:
>
> Hi Harald
>
> On 07/05/2026 21:12, Harald Nordgren wrote:
> > Is this ready to move to next?
>
> I'm not particularly enthusiastic one way or the other about adding
> this, but so long as we only try to fetch when the user explicitly asks
> for it I don't particularly object. However having had a quick scan of
> the implementation I have a few comments
>
> * "--track=inherit,direct" is nonsense and should be rejected
>
> * currently "--track" has "last one wins" behavior so
>    "--track=inherit --track=direct" behaves like "--track=direct". We
>    should probably keep that so that "--track=fetch --track=direct"
>    behaves like "--track=direct", not "--track=fetch,direct"
>
> * if "git fetch" fails and the remote tracking ref already exists then
>    we should print a warning and carry on rather than dying which is more
>    convenient if the user or remote server are offline.
>
> * "git checkout --track=fetch origin/branch" should respect
>    remote.origin.fetch so that we fetch the ref that we're going to
>    checkout. I wonder if we can share this logic with the code that
>    sets the upstream branch.
>
> * "git checkout --track=fetch origin" should only fetch the remote
>    ref that we're going to checkout, not all the refs from origin. i.e.
>    it should read origin/HEAD to work out what to fetch.
>
> Thanks
>
> Phillip
>

^ permalink raw reply

* Re: [PATCH v5 2/2] graph: indent visual root in graph
From: Pablo Sabater @ 2026-06-18 12:42 UTC (permalink / raw)
  To: Jeff King
  Cc: git, ayu.chandekar, chandrapratap3519, christian.couder, gitster,
	jltobler, karthik.188, phillip.wood, siddharthasthana31
In-Reply-To: <20260617202744.GA3465855@coredump.intra.peff.net>

El mié, 17 jun 2026 a las 22:27, Jeff King (<peff@peff.net>) escribió:
>
> On Sat, Jun 13, 2026 at 09:09:16PM +0200, Pablo Sabater wrote:
>
> > +/*
> > + * Iterates the commits queue searching for the next visible commit, once found
> > + * sets visibleness and visual-root flags.
> > + * Knowing if the next commit is also a visual root avoids redundant indentations
> > + *
> > + * NEEDSWORK: The queue is actively being modified by the walker, for each commit
> > + * its parents and itself get simplified and their flags set, but for the next
> > + * unrelated commit or the grandparents they are not simplified yet, which means
> > + * that a commit whose parents are all filtered will not be marked as a visual
> > + * root candidate at the lookahead.
> > + * This causes the lookahead to fail, failing to set the cascade flag to avoid
> > + * redundant indentations.
> > + * See 'test_expect_failure' at t4218-log-graph-indentation.sh.
> > + */
> > +static void graph_peek_next_visible(struct git_graph *graph,
> > +                                 struct graph_lookahead_flags *flags)
> > +{
> > +     struct commit_list *cl;
> > +
> > +     flags->is_next_visible = 0;
> > +     flags->is_next_visual_root = 0;
> > +     flags->next_has_column = 0;
> > +
> > +     for (cl = graph->revs->commits; cl; cl = cl->next) {
> > +             if (get_commit_action(graph->revs, cl->item) != commit_show)
> > +                     continue;
> > [...]
>
> I have a feeling this may interact badly with the prio-queue introduced
> by dd4bc01c0a (revision: use priority queue for non-limited streaming
> walks, 2026-05-27). In that commit, get_revision_1() sucks all of the
> commits from revs->commits into revs->commit_queue, and then traversal
> puts the parents into that queue, not the commits list.
>
> So during the traversal, revs->commits does not hold the complete queue
> anymore. I think it does see _some_ commits, since some get placed
> directly into revs->commits and then later moved next time
> get_revision() is called. But if we instrument the code like this:
>
> diff --git a/graph.c b/graph.c
> index e0d1e2a510..8a5f17a089 100644
> --- a/graph.c
> +++ b/graph.c
> @@ -926,6 +926,10 @@ static void graph_peek_next_visible(struct git_graph *graph,
>         flags->is_next_visual_root = 0;
>         flags->next_has_column = 0;
>
> +       warning("peeking at visible commits: %d in list, %d in queue",
> +               commit_list_count(graph->revs->commits),
> +               (int)graph->revs->commit_queue.nr);
> +
>         for (cl = graph->revs->commits; cl; cl = cl->next) {
>                 if (get_commit_action(graph->revs, cl->item) != commit_show)
>                         continue;
>
> and run something like:
>
>   ./git log --graph --oneline -- Makefile
>
> we can see that we're always considering just one commit, while there
> may be dozens or hundreds in the queue.
>
> I'm not sure what the solution is. This function wants to peek ahead in
> queue order, possibly through multiple entries. But a heap-based queue
> inherently only supports peeking at the first entry.

Hi Jeff!

Yeah, I haven't read dd4bc01c0a yet but from what you say it prob
won't work anymore, I didn't know about that series, about the
lookahead I think it could still work with some tweaks, the important
part is to set the three lookahead flags.

From what I understood, we can only get the direct next commit, but no
more reliably ordered.

The flags should be fine:

- 'is_next_visible' could need to traverse multiple entries, but it
doesn't need them to be in order. We just need to know if something
will be rendered after.
- 'next_has_column' only needs the first entry.
- 'is_next_visual_root' only needs the first entry to know if it could
be a visual root, and also if it is not the last one (but we don't
need them to be ordered for this last part).

Should I work with 'next' as a base to have dd4bc01c0a? (Sorry I've
just worked with master).

I'll try to make it work but if not, the lookahead works to avoid
_redundant_ indentations, but it would still work correctly without
it.

>
> None of the tests seem to fail, but I'm not sure if that's because I'm
> way off base in my analysis, or there's a gap in the test coverage, or
> if this case is part of the expect_failure ones mentioned in the
> comment.
>
> I noticed because I have another topic which drops the revs->commits
> list entirely (and just always uses the queue), which of course doesn't
> compile when merged with this (I merge with 'jch' for my daily driver,
> which now includes this patch).
>
> -Peff

Thanks,
Pablo

^ permalink raw reply

* [PATCH v14 0/2] checkout: --track=fetch
From: Harald Nordgren via GitGitGadget @ 2026-06-18 12:44 UTC (permalink / raw)
  To: git
  Cc: Ramsay Jones, D. Ben Knoble, Kristoffer Haugsbakk, Marc Branchaud,
	Phillip Wood, Harald Nordgren
In-Reply-To: <pull.2281.v13.git.git.1779565714.gitgitgadget@gmail.com>

Extend checkout --track with a fetch mode to refresh start-point.

Changes in v14:

 * Handle .h files in a better way.

Changes in v13:

 * Create a preparatory commit that exposes find_tracking_remote_for_ref()
   and advise_ambiguous_fetch_refspec() from branch.c, so checkout can reuse
   the same lookup git branch --track uses.
 * Use advise_ambiguous_fetch_refspec() for the "multiple remotes match"
   case, so the wording matches git branch --track.

Harald Nordgren (2):
  branch: expose helpers for finding the remote owning a tracking ref
  checkout: extend --track with a "fetch" mode to refresh start-point

 Documentation/git-checkout.adoc |  17 +-
 Documentation/git-switch.adoc   |   5 +-
 branch.c                        |  96 ++++++-----
 branch.h                        |  16 ++
 builtin/checkout.c              | 139 +++++++++++++++-
 t/t7201-co.sh                   | 276 ++++++++++++++++++++++++++++++++
 6 files changed, 498 insertions(+), 51 deletions(-)


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

Range-diff vs v13:

 1:  2369afad24 ! 1:  f79689c23d branch: expose helpers for finding the remote owning a tracking ref
     @@ branch.h
       #define BRANCH_H
       
      +#include "refspec.h"
     -+#include "string-list.h"
      +
     ++struct string_list;
       struct repository;
       struct strbuf;
       
 2:  60adf0e67d ! 2:  8518f090b1 checkout: extend --track with a "fetch" mode to refresh start-point
     @@ builtin/checkout.c
      +#include "run-command.h"
       #include "sequencer.h"
       #include "setup.h"
     - #include "strvec.h"
     + #include "sparse-index.h"
      @@ builtin/checkout.c: struct checkout_opts {
       	int count_checkout_paths;
       	int overlay_mode;

-- 
gitgitgadget

^ permalink raw reply

* [PATCH v14 1/2] branch: expose helpers for finding the remote owning a tracking ref
From: Harald Nordgren via GitGitGadget @ 2026-06-18 12:44 UTC (permalink / raw)
  To: git
  Cc: Ramsay Jones, D. Ben Knoble, Kristoffer Haugsbakk, Marc Branchaud,
	Phillip Wood, Harald Nordgren, Harald Nordgren
In-Reply-To: <pull.2281.v14.git.git.1781786652.gitgitgadget@gmail.com>

From: Harald Nordgren <haraldnordgren@gmail.com>

The remote-lookup that setup_tracking() does is useful outside
branch.c too; for example, deciding which remote to "git fetch"
from given a remote-tracking ref.

Move 'struct tracking' to branch.h and add two helpers backed by the
existing for_each_remote walk: find_tracking_remote_for_ref() and
advise_ambiguous_fetch_refspec(). setup_tracking() uses both. No
behavior change.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 branch.c | 96 ++++++++++++++++++++++++++++++--------------------------
 branch.h | 16 ++++++++++
 2 files changed, 68 insertions(+), 44 deletions(-)

diff --git a/branch.c b/branch.c
index 243db7d0fc..46ae7f0035 100644
--- a/branch.c
+++ b/branch.c
@@ -20,16 +20,9 @@
 #include "run-command.h"
 #include "strmap.h"
 
-struct tracking {
-	struct refspec_item spec;
-	struct string_list *srcs;
-	const char *remote;
-	int matches;
-};
-
 struct find_tracked_branch_cb {
 	struct tracking *tracking;
-	struct string_list ambiguous_remotes;
+	struct string_list *ambiguous_remotes;
 };
 
 static int find_tracked_branch(struct remote *remote, void *priv)
@@ -45,10 +38,10 @@ static int find_tracked_branch(struct remote *remote, void *priv)
 			break;
 		case 2:
 			/* there are at least two remotes; backfill the first one */
-			string_list_append(&ftb->ambiguous_remotes, tracking->remote);
+			string_list_append(ftb->ambiguous_remotes, tracking->remote);
 			/* fall through */
 		default:
-			string_list_append(&ftb->ambiguous_remotes, remote->name);
+			string_list_append(ftb->ambiguous_remotes, remote->name);
 			free(tracking->spec.src);
 			string_list_clear(tracking->srcs, 0);
 		break;
@@ -59,6 +52,51 @@ static int find_tracked_branch(struct remote *remote, void *priv)
 	return 0;
 }
 
+void find_tracking_remote_for_ref(struct tracking *tracking,
+				  struct string_list *ambiguous_remotes)
+{
+	struct find_tracked_branch_cb ftb_cb = {
+		.tracking = tracking,
+		.ambiguous_remotes = ambiguous_remotes,
+	};
+
+	for_each_remote(find_tracked_branch, &ftb_cb);
+}
+
+void advise_ambiguous_fetch_refspec(const char *dst,
+				    const struct string_list *ambiguous_remotes)
+{
+	struct strbuf remotes_advice = STRBUF_INIT;
+	struct string_list_item *item;
+
+	if (!advice_enabled(ADVICE_AMBIGUOUS_FETCH_REFSPEC))
+		return;
+
+	for_each_string_list_item(item, ambiguous_remotes)
+		/*
+		 * TRANSLATORS: This is a line listing a remote with duplicate
+		 * refspecs in the advice message below. For RTL languages you'll
+		 * probably want to swap the "%s" and leading "  " space around.
+		 */
+		strbuf_addf(&remotes_advice, _("  %s\n"), item->string);
+
+	/*
+	 * TRANSLATORS: The second argument is a \n-delimited list of
+	 * duplicate refspecs, composed above.
+	 */
+	advise(_("There are multiple remotes whose fetch refspecs map to the remote\n"
+		 "tracking ref '%s':\n"
+		 "%s"
+		 "\n"
+		 "This is typically a configuration error.\n"
+		 "\n"
+		 "To support setting up tracking branches, ensure that\n"
+		 "different remotes' fetch refspecs map into different\n"
+		 "tracking namespaces."), dst,
+	       remotes_advice.buf);
+	strbuf_release(&remotes_advice);
+}
+
 static int should_setup_rebase(const char *origin)
 {
 	switch (autorebase) {
@@ -254,11 +292,8 @@ static void setup_tracking(const char *new_ref, const char *orig_ref,
 {
 	struct tracking tracking;
 	struct string_list tracking_srcs = STRING_LIST_INIT_DUP;
+	struct string_list ambiguous_remotes = STRING_LIST_INIT_DUP;
 	int config_flags = quiet ? 0 : BRANCH_CONFIG_VERBOSE;
-	struct find_tracked_branch_cb ftb_cb = {
-		.tracking = &tracking,
-		.ambiguous_remotes = STRING_LIST_INIT_DUP,
-	};
 
 	if (!track)
 		BUG("asked to set up tracking, but tracking is disallowed");
@@ -267,7 +302,7 @@ static void setup_tracking(const char *new_ref, const char *orig_ref,
 	tracking.spec.dst = (char *)orig_ref;
 	tracking.srcs = &tracking_srcs;
 	if (track != BRANCH_TRACK_INHERIT)
-		for_each_remote(find_tracked_branch, &ftb_cb);
+		find_tracking_remote_for_ref(&tracking, &ambiguous_remotes);
 	else if (inherit_tracking(&tracking, orig_ref))
 		goto cleanup;
 
@@ -293,34 +328,7 @@ static void setup_tracking(const char *new_ref, const char *orig_ref,
 	if (tracking.matches > 1) {
 		int status = die_message(_("not tracking: ambiguous information for ref '%s'"),
 					    orig_ref);
-		if (advice_enabled(ADVICE_AMBIGUOUS_FETCH_REFSPEC)) {
-			struct strbuf remotes_advice = STRBUF_INIT;
-			struct string_list_item *item;
-
-			for_each_string_list_item(item, &ftb_cb.ambiguous_remotes)
-				/*
-				 * TRANSLATORS: This is a line listing a remote with duplicate
-				 * refspecs in the advice message below. For RTL languages you'll
-				 * probably want to swap the "%s" and leading "  " space around.
-				 */
-				strbuf_addf(&remotes_advice, _("  %s\n"), item->string);
-
-			/*
-			 * TRANSLATORS: The second argument is a \n-delimited list of
-			 * duplicate refspecs, composed above.
-			 */
-			advise(_("There are multiple remotes whose fetch refspecs map to the remote\n"
-				 "tracking ref '%s':\n"
-				 "%s"
-				 "\n"
-				 "This is typically a configuration error.\n"
-				 "\n"
-				 "To support setting up tracking branches, ensure that\n"
-				 "different remotes' fetch refspecs map into different\n"
-				 "tracking namespaces."), orig_ref,
-			       remotes_advice.buf);
-			strbuf_release(&remotes_advice);
-		}
+		advise_ambiguous_fetch_refspec(orig_ref, &ambiguous_remotes);
 		exit(status);
 	}
 
@@ -347,7 +355,7 @@ static void setup_tracking(const char *new_ref, const char *orig_ref,
 
 cleanup:
 	string_list_clear(&tracking_srcs, 0);
-	string_list_clear(&ftb_cb.ambiguous_remotes, 0);
+	string_list_clear(&ambiguous_remotes, 0);
 }
 
 int read_branch_desc(struct strbuf *buf, const char *branch_name)
diff --git a/branch.h b/branch.h
index 3dc6e2a0ff..c2e6725491 100644
--- a/branch.h
+++ b/branch.h
@@ -1,9 +1,25 @@
 #ifndef BRANCH_H
 #define BRANCH_H
 
+#include "refspec.h"
+
+struct string_list;
 struct repository;
 struct strbuf;
 
+struct tracking {
+	struct refspec_item spec;
+	struct string_list *srcs;
+	const char *remote;
+	int matches;
+};
+
+void find_tracking_remote_for_ref(struct tracking *tracking,
+				  struct string_list *ambiguous_remotes);
+
+void advise_ambiguous_fetch_refspec(const char *dst,
+				    const struct string_list *ambiguous_remotes);
+
 enum branch_track {
 	BRANCH_TRACK_UNSPECIFIED = -1,
 	BRANCH_TRACK_NEVER = 0,
-- 
gitgitgadget


^ permalink raw reply related

* [PATCH v14 2/2] checkout: extend --track with a "fetch" mode to refresh start-point
From: Harald Nordgren via GitGitGadget @ 2026-06-18 12:44 UTC (permalink / raw)
  To: git
  Cc: Ramsay Jones, D. Ben Knoble, Kristoffer Haugsbakk, Marc Branchaud,
	Phillip Wood, Harald Nordgren, Harald Nordgren
In-Reply-To: <pull.2281.v14.git.git.1781786652.gitgitgadget@gmail.com>

From: Harald Nordgren <haraldnordgren@gmail.com>

Add a "fetch" mode to the "--track" option of "git checkout" / "git
switch" that refreshes <start-point> before checking it out:

    git checkout -b new_branch --track=fetch origin/some-branch

is shorthand for

    git fetch origin some-branch
    git checkout -b new_branch --track origin/some-branch

Identify the remote whose configured fetch refspec maps to
<start-point> using find_tracking_remote_for_ref() (the same lookup
"--track" uses to pick which remote to record in
branch.<name>.remote), then run "git fetch <remote> <src-ref>" for
just that ref so other remote-tracking branches are left untouched.
When <start-point> is a bare <remote> (e.g. "origin"), follow
refs/remotes/<remote>/HEAD to learn which branch to refresh. If
"git fetch" fails but the remote-tracking ref already exists locally,
warn and proceed from the existing tip; otherwise abort.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 Documentation/git-checkout.adoc |  17 +-
 Documentation/git-switch.adoc   |   5 +-
 builtin/checkout.c              | 139 +++++++++++++++-
 t/t7201-co.sh                   | 276 ++++++++++++++++++++++++++++++++
 4 files changed, 430 insertions(+), 7 deletions(-)

diff --git a/Documentation/git-checkout.adoc b/Documentation/git-checkout.adoc
index a8b3b8c2e2..20b6cae60e 100644
--- a/Documentation/git-checkout.adoc
+++ b/Documentation/git-checkout.adoc
@@ -158,11 +158,26 @@ of it").
 	resets _<branch>_ to the start point instead of failing.
 
 `-t`::
-`--track[=(direct|inherit)]`::
+`--track[=(direct|inherit|fetch)[,...]]`::
 	When creating a new branch, set up "upstream" configuration. See
 	`--track` in linkgit:git-branch[1] for details. As a convenience,
 	--track without -b implies branch creation.
 +
+The argument is a comma-separated list. `direct` (the default) and
+`inherit` select the tracking mode and are mutually exclusive. Adding
+`fetch` requests that the remote be fetched before _<start-point>_ is
+resolved, so the new branch starts from a fresh tip: when
+_<start-point>_ is in _<remote>/<branch>_ form, only that branch is
+updated; when _<start-point>_ is a bare _<remote>_ (e.g. `origin`), the
+branch named by _<remote>/HEAD_ is updated, and the checkout fails
+with a hint to configure that symref if it is not set. The checkout
+also fails if no configured remote's fetch refspec maps to
+_<start-point>_, or if more than one does (in which case the `fetch`
+cannot be unambiguously routed). If the fetch itself fails and the
+corresponding remote-tracking ref already exists, a warning is printed
+and the checkout proceeds from the existing tip; otherwise the checkout
+is aborted.
++
 If no `-b` option is given, the name of the new branch will be
 derived from the remote-tracking branch, by looking at the local part of
 the refspec configured for the corresponding remote, and then stripping
diff --git a/Documentation/git-switch.adoc b/Documentation/git-switch.adoc
index d6c4f229a5..a8730b1da8 100644
--- a/Documentation/git-switch.adoc
+++ b/Documentation/git-switch.adoc
@@ -155,10 +155,11 @@ variable.
 	attached to a terminal, regardless of `--quiet`.
 
 `-t`::
-`--track[ (direct|inherit)]`::
+`--track[=(direct|inherit|fetch)[,...]]`::
 	When creating a new branch, set up "upstream" configuration.
 	`-c` is implied. See `--track` in linkgit:git-branch[1] for
-	details.
+	details, and `--track` in linkgit:git-checkout[1] for the
+	`fetch` mode.
 +
 If no `-c` option is given, the name of the new branch will be derived
 from the remote-tracking branch, by looking at the local part of the
diff --git a/builtin/checkout.c b/builtin/checkout.c
index b78b3a1d16..37caceaefd 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -25,10 +25,12 @@
 #include "preload-index.h"
 #include "read-cache.h"
 #include "refs.h"
+#include "refspec.h"
 #include "remote.h"
 #include "repo-settings.h"
 #include "resolve-undo.h"
 #include "revision.h"
+#include "run-command.h"
 #include "sequencer.h"
 #include "setup.h"
 #include "sparse-index.h"
@@ -63,6 +65,7 @@ struct checkout_opts {
 	int count_checkout_paths;
 	int overlay_mode;
 	int dwim_new_local_branch;
+	int fetch;
 	int discard_changes;
 	int accept_ref;
 	int accept_pathspec;
@@ -116,6 +119,129 @@ struct branch_info {
 	char *checkout;
 };
 
+static void fetch_remote_for_start_point(const char *arg, int quiet)
+{
+	struct strbuf dst = STRBUF_INIT;
+	struct tracking tracking;
+	struct string_list tracking_srcs = STRING_LIST_INIT_DUP;
+	struct string_list ambiguous_remotes = STRING_LIST_INIT_DUP;
+	struct child_process cmd = CHILD_PROCESS_INIT;
+	struct object_id oid;
+	struct remote *named_remote;
+	int bare_ns;
+
+	strbuf_addf(&dst, "refs/remotes/%s", arg);
+	if (check_refname_format(dst.buf, 0))
+		die(_("cannot fetch start-point '%s': not a valid "
+		      "remote-tracking name"), arg);
+
+	named_remote = remote_get(arg);
+	bare_ns = !strchr(arg, '/') ||
+		(named_remote && remote_is_configured(named_remote, 1));
+	if (bare_ns) {
+		char *head_path = xstrfmt("refs/remotes/%s/HEAD", arg);
+		const char *head_target =
+			refs_resolve_ref_unsafe(get_main_ref_store(the_repository),
+						head_path,
+						RESOLVE_REF_READING |
+						RESOLVE_REF_NO_RECURSE,
+						&oid, NULL);
+		if (head_target &&
+		    starts_with(head_target, dst.buf) &&
+		    head_target[dst.len] == '/' &&
+		    !check_refname_format(head_target, 0)) {
+			strbuf_reset(&dst);
+			strbuf_addstr(&dst, head_target);
+			bare_ns = 0;
+		}
+		free(head_path);
+	}
+
+	memset(&tracking, 0, sizeof(tracking));
+	tracking.spec.dst = dst.buf;
+	tracking.srcs = &tracking_srcs;
+	find_tracking_remote_for_ref(&tracking, &ambiguous_remotes);
+
+	if (tracking.matches > 1) {
+		int status = die_message(_("cannot fetch start-point '%s': "
+					   "fetch refspecs of multiple remotes "
+					   "map to '%s'"), arg, dst.buf);
+		advise_ambiguous_fetch_refspec(dst.buf, &ambiguous_remotes);
+		exit(status);
+	}
+
+	if (!tracking.matches) {
+		if (bare_ns && named_remote &&
+		    remote_is_configured(named_remote, 1))
+			die(_("cannot fetch start-point '%s': "
+			      "'refs/remotes/%s/HEAD' is not set; run "
+			      "'git remote set-head %s --auto' to set it"),
+			    arg, arg, arg);
+		die(_("cannot fetch start-point '%s': no configured remote's "
+		      "fetch refspec matches it"), arg);
+	}
+
+	strvec_push(&cmd.args, "fetch");
+	if (quiet)
+		strvec_push(&cmd.args, "--quiet");
+	strvec_pushl(&cmd.args, tracking.remote,
+		     tracking_srcs.items[0].string, NULL);
+	cmd.git_cmd = 1;
+	if (run_command(&cmd)) {
+		if (!refs_read_ref(get_main_ref_store(the_repository),
+				   dst.buf, &oid))
+			warning(_("failed to fetch start-point '%s'; "
+				  "using existing '%s'"), arg, dst.buf);
+		else
+			die(_("failed to fetch start-point '%s'"), arg);
+	}
+
+	string_list_clear(&tracking_srcs, 0);
+	string_list_clear(&ambiguous_remotes, 0);
+	strbuf_release(&dst);
+}
+
+static int parse_opt_checkout_track(const struct option *opt,
+				    const char *arg, int unset)
+{
+	struct checkout_opts *opts = opt->value;
+	struct string_list tokens = STRING_LIST_INIT_DUP;
+	struct string_list_item *item;
+	int saw_direct = 0;
+	int ret = 0;
+
+	opts->fetch = 0;
+	if (unset) {
+		opts->track = BRANCH_TRACK_NEVER;
+		return 0;
+	}
+	opts->track = BRANCH_TRACK_EXPLICIT;
+	if (!arg)
+		return 0;
+
+	string_list_split(&tokens, arg, ",", -1);
+	for_each_string_list_item(item, &tokens) {
+		if (!strcmp(item->string, "fetch"))
+			opts->fetch = 1;
+		else if (!strcmp(item->string, "direct"))
+			saw_direct = 1;
+		else if (!strcmp(item->string, "inherit"))
+			opts->track = BRANCH_TRACK_INHERIT;
+		else {
+			ret = error(_("option `%s' expects \"%s\", \"%s\", "
+				      "or \"%s\""),
+				    "--track", "direct", "inherit", "fetch");
+			goto out;
+		}
+	}
+	if (saw_direct && opts->track == BRANCH_TRACK_INHERIT)
+		ret = error(_("option `%s' cannot combine \"%s\" and \"%s\""),
+			    "--track", "direct", "inherit");
+out:
+	string_list_clear(&tokens, 0);
+	return ret;
+}
+
 static void branch_info_release(struct branch_info *info)
 {
 	free(info->name);
@@ -1786,10 +1912,10 @@ static struct option *add_common_switch_branch_options(
 {
 	struct option options[] = {
 		OPT_BOOL('d', "detach", &opts->force_detach, N_("detach HEAD at named commit")),
-		OPT_CALLBACK_F('t', "track",  &opts->track, "(direct|inherit)",
+		OPT_CALLBACK_F('t', "track",  opts, "(direct|inherit|fetch)[,...]",
 			N_("set branch tracking configuration"),
 			PARSE_OPT_OPTARG,
-			parse_opt_tracking_mode),
+			parse_opt_checkout_track),
 		OPT__FORCE(&opts->force, N_("force checkout (throw away local modifications)"),
 			   PARSE_OPT_NOCOMPLETE),
 		OPT_STRING(0, "orphan", &opts->new_orphan_branch, N_("new-branch"), N_("new unborn branch")),
@@ -1994,8 +2120,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->fetch)
+			fetch_remote_for_start_point(argv[0], opts->quiet);
+
+		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) {
diff --git a/t/t7201-co.sh b/t/t7201-co.sh
index 7613b1d2a4..1e321b1512 100755
--- a/t/t7201-co.sh
+++ b/t/t7201-co.sh
@@ -870,4 +870,280 @@ test_expect_success 'tracking info copied with autoSetupMerge=inherit' '
 	test_cmp_config "" --default "" branch.main2.merge
 '
 
+test_expect_success 'setup upstream for --track=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 --track=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 --track=fetch -b local_new fetch_upstream/fetch_new &&
+	test_cmp_rev refs/remotes/fetch_upstream/fetch_new HEAD &&
+	test_cmp_config fetch_upstream branch.local_new.remote &&
+	test_cmp_config refs/heads/fetch_new branch.local_new.merge
+'
+
+test_expect_success 'checkout --track=fetch <remote>/<branch> leaves other tracking branches untouched' '
+	git checkout main &&
+	git -C fetch_upstream checkout -b fetch_target &&
+	test_commit -C fetch_upstream u_target_pre &&
+	git -C fetch_upstream checkout -b fetch_other &&
+	test_commit -C fetch_upstream u_other_pre &&
+	git fetch fetch_upstream &&
+	other_before=$(git rev-parse refs/remotes/fetch_upstream/fetch_other) &&
+	git -C fetch_upstream checkout fetch_target &&
+	test_commit -C fetch_upstream u_target_post &&
+	git -C fetch_upstream checkout fetch_other &&
+	test_commit -C fetch_upstream u_other_post &&
+	git checkout --track=fetch -b local_target fetch_upstream/fetch_target &&
+	test_cmp_rev refs/remotes/fetch_upstream/fetch_target HEAD &&
+	test "$(git rev-parse refs/remotes/fetch_upstream/fetch_other)" = "$other_before"
+'
+
+test_expect_success 'checkout --track=fetch with bare remote name fetches only <remote>/HEAD target' '
+	git checkout main &&
+	git -C fetch_upstream checkout main &&
+	git remote set-head fetch_upstream main &&
+	git -C fetch_upstream checkout -b fetch_unrelated &&
+	test_commit -C fetch_upstream u_unrelated_pre &&
+	git fetch fetch_upstream fetch_unrelated &&
+	unrelated_before=$(git rev-parse refs/remotes/fetch_upstream/fetch_unrelated) &&
+	git -C fetch_upstream checkout main &&
+	test_commit -C fetch_upstream u_main_post &&
+	git -C fetch_upstream checkout fetch_unrelated &&
+	test_commit -C fetch_upstream u_unrelated_post &&
+	git checkout --track=fetch -b local_from_remote fetch_upstream &&
+	test_cmp_rev refs/remotes/fetch_upstream/main HEAD &&
+	test "$(git rev-parse refs/remotes/fetch_upstream/fetch_unrelated)" = "$unrelated_before"
+'
+
+test_expect_success 'checkout --track=fetch aborts and does not create branch when no existing ref' '
+	git checkout main &&
+	test_might_fail git branch -D bogus &&
+	test_must_fail git checkout --track=fetch -b bogus fetch_upstream/does_not_exist &&
+	test_must_fail git rev-parse --verify refs/heads/bogus
+'
+
+test_expect_success 'checkout --track=fetch warns and proceeds when fetch fails but ref exists' '
+	git checkout main &&
+	git -C fetch_upstream checkout -b fetch_offline &&
+	test_commit -C fetch_upstream u_offline &&
+	git fetch fetch_upstream fetch_offline &&
+	saved_url=$(git config remote.fetch_upstream.url) &&
+	test_when_finished "git config remote.fetch_upstream.url \"$saved_url\"" &&
+	git config remote.fetch_upstream.url ./does-not-exist &&
+	git checkout --track=fetch -b local_offline fetch_upstream/fetch_offline 2>err &&
+	test_grep "failed to fetch" err &&
+	test_cmp_rev refs/remotes/fetch_upstream/fetch_offline HEAD
+'
+
+test_expect_success 'checkout --track=fetch resolves through configured fetch refspec' '
+	git checkout main &&
+	git remote add fetch_custom ./fetch_upstream &&
+	test_when_finished "git remote remove fetch_custom" &&
+	git config --replace-all remote.fetch_custom.fetch \
+		"+refs/heads/*:refs/remotes/custom-ns/*" &&
+	git -C fetch_upstream checkout -b fetch_refspec &&
+	test_commit -C fetch_upstream u_refspec &&
+	test_must_fail git rev-parse --verify refs/remotes/custom-ns/fetch_refspec &&
+	git checkout --track=fetch -b local_refspec custom-ns/fetch_refspec &&
+	test_cmp_rev refs/remotes/custom-ns/fetch_refspec HEAD
+'
+
+test_expect_success 'checkout --track=fetch on namespace bare name follows <ns>/HEAD' '
+	git checkout main &&
+	git remote add fetch_ns ./fetch_upstream &&
+	test_when_finished "git remote remove fetch_ns" &&
+	test_when_finished "git update-ref -d refs/remotes/ns_alias/HEAD" &&
+	git config --replace-all remote.fetch_ns.fetch \
+		"+refs/heads/*:refs/remotes/ns_alias/*" &&
+	git fetch fetch_ns &&
+	git symbolic-ref refs/remotes/ns_alias/HEAD refs/remotes/ns_alias/main &&
+	git -C fetch_upstream checkout main &&
+	test_commit -C fetch_upstream u_ns_post &&
+	git checkout --track=fetch -b local_ns ns_alias &&
+	test_cmp_rev refs/remotes/ns_alias/main HEAD &&
+	test_cmp_config fetch_ns branch.local_ns.remote &&
+	test_cmp_config refs/heads/main branch.local_ns.merge
+'
+
+test_expect_success '--track=fetch on bare hierarchical remote name follows <ns>/HEAD' '
+	git checkout main &&
+	git remote add nested/bare ./fetch_upstream &&
+	test_when_finished "git remote remove nested/bare" &&
+	test_when_finished "git update-ref -d refs/remotes/nested/bare/HEAD" &&
+	git fetch nested/bare &&
+	git symbolic-ref refs/remotes/nested/bare/HEAD \
+		refs/remotes/nested/bare/main &&
+	git -C fetch_upstream checkout main &&
+	test_commit -C fetch_upstream u_nested_bare_post &&
+	git checkout --track=fetch -b local_nested_bare nested/bare &&
+	test_cmp_rev refs/remotes/nested/bare/main HEAD
+'
+
+test_expect_success 'checkout --track=fetch handles hierarchical remote name' '
+	git checkout main &&
+	git remote add nested/remote ./fetch_upstream &&
+	test_when_finished "git remote remove nested/remote" &&
+	git -C fetch_upstream checkout -b fetch_hier &&
+	test_commit -C fetch_upstream u_hier &&
+	test_must_fail git rev-parse --verify refs/remotes/nested/remote/fetch_hier &&
+	git checkout --track=fetch -b local_hier nested/remote/fetch_hier &&
+	test_cmp_rev refs/remotes/nested/remote/fetch_hier HEAD
+'
+
+test_expect_success 'checkout --track=fetch dies on bare remote name with no <ns>/HEAD' '
+	git checkout main &&
+	git remote add fetch_nohead ./fetch_upstream &&
+	test_when_finished "git remote remove fetch_nohead" &&
+	test_might_fail git symbolic-ref -d refs/remotes/fetch_nohead/HEAD &&
+	test_must_fail git checkout --track=fetch -b local_nohead fetch_nohead 2>err &&
+	test_grep "refs/remotes/fetch_nohead/HEAD" err &&
+	test_grep "git remote set-head fetch_nohead --auto" err &&
+	test_must_fail git rev-parse --verify refs/heads/local_nohead
+'
+
+test_expect_success 'checkout --track=fetch on bare unknown name does not suggest set-head' '
+	git checkout main &&
+	test_must_fail git rev-parse --verify refs/remotes/no_such_ns/HEAD &&
+	test_must_fail git config --get remote.no_such_ns.url &&
+	test_must_fail git checkout --track=fetch -b local_unknown no_such_ns 2>err &&
+	test_grep "no configured remote" err &&
+	test_grep ! "set-head" err &&
+	test_must_fail git rev-parse --verify refs/heads/local_unknown
+'
+
+test_expect_success 'checkout --track=fetch rejects <ns>/HEAD pointing outside namespace' '
+	git checkout main &&
+	git remote add fetch_crossns ./fetch_upstream &&
+	test_when_finished "git remote remove fetch_crossns" &&
+	test_when_finished "git update-ref -d refs/remotes/fetch_crossns/HEAD" &&
+	git fetch fetch_crossns &&
+	git symbolic-ref refs/remotes/fetch_crossns/HEAD \
+		refs/remotes/fetch_upstream/u_main &&
+	test_must_fail git checkout --track=fetch -b local_crossns fetch_crossns 2>err &&
+	test_grep "refs/remotes/fetch_crossns/HEAD" err &&
+	test_must_fail git rev-parse --verify refs/heads/local_crossns
+'
+
+test_expect_success 'checkout --track=fetch dies on ambiguous fetch refspec match' '
+	git checkout main &&
+	git remote add fetch_ambig_a ./fetch_upstream &&
+	git remote add fetch_ambig_b ./fetch_upstream &&
+	test_when_finished "git remote remove fetch_ambig_a" &&
+	test_when_finished "git remote remove fetch_ambig_b" &&
+	git config --replace-all remote.fetch_ambig_a.fetch \
+		"+refs/heads/*:refs/remotes/ambig_ns/*" &&
+	git config --replace-all remote.fetch_ambig_b.fetch \
+		"+refs/heads/*:refs/remotes/ambig_ns/*" &&
+	git -C fetch_upstream checkout -b fetch_ambig &&
+	test_commit -C fetch_upstream u_ambig &&
+	test_must_fail git checkout --track=fetch -b local_ambig ambig_ns/fetch_ambig 2>err &&
+	test_grep "fetch_ambig_a" err &&
+	test_grep "fetch_ambig_b" err &&
+	test_grep "tracking namespaces" err &&
+	test_must_fail git rev-parse --verify refs/heads/local_ambig
+'
+
+test_expect_success 'checkout --track=fetch rejects invalid refname components' '
+	git checkout main &&
+	test_must_fail git checkout --track=fetch -b local_invalid "foo..bar" 2>err &&
+	test_grep "valid" err &&
+	test_must_fail git rev-parse --verify refs/heads/local_invalid
+'
+
+test_expect_success 'checkout --track=fetch,inherit rejects invalid refname components' '
+	git checkout main &&
+	test_must_fail git checkout --track=fetch,inherit -b local_invalid \
+		"foo..bar" 2>err &&
+	test_grep "valid" err &&
+	test_must_fail git rev-parse --verify refs/heads/local_invalid
+'
+
+test_expect_success 'checkout --track=inherit,direct is rejected' '
+	test_must_fail git checkout --track=inherit,direct -b bad fetch_upstream/fetch_new 2>err &&
+	test_grep "cannot combine" err
+'
+
+test_expect_success 'checkout --track=direct,inherit is rejected' '
+	test_must_fail git checkout --track=direct,inherit -b bad fetch_upstream/fetch_new 2>err &&
+	test_grep "cannot combine" err
+'
+
+test_expect_success 'checkout --track=fetch then --track=direct drops fetch (last-one-wins)' '
+	git checkout main &&
+	git -C fetch_upstream checkout -b fetch_lastwin &&
+	test_commit -C fetch_upstream u_lastwin &&
+	test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_lastwin &&
+	test_must_fail git checkout --track=fetch --track=direct \
+		-b local_lastwin fetch_upstream/fetch_lastwin &&
+	test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_lastwin
+'
+
+test_expect_success 'checkout --track=fetch then --no-track drops fetch' '
+	git checkout main &&
+	git -C fetch_upstream checkout -b fetch_notrack &&
+	test_commit -C fetch_upstream u_notrack &&
+	test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_notrack &&
+	test_must_fail git checkout --track=fetch --no-track \
+		-b local_notrack fetch_upstream/fetch_notrack &&
+	test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_notrack
+'
+
+test_expect_success 'checkout --track=fetch,inherit fetches remote-tracking start-point' '
+	git checkout main &&
+	git -C fetch_upstream checkout -b fetch_inherit &&
+	test_commit -C fetch_upstream u_inherit &&
+	test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_inherit &&
+	git checkout --track=fetch,inherit -b local_inherit \
+		fetch_upstream/fetch_inherit &&
+	test_cmp_rev refs/remotes/fetch_upstream/fetch_inherit HEAD
+'
+
+test_expect_success 'checkout --track=fetch,inherit errors when start-point does not map to a remote' '
+	git checkout main &&
+	test_must_fail git checkout --track=fetch,inherit -b bad main 2>err &&
+	test_grep "no configured remote" err &&
+	test_must_fail git rev-parse --verify refs/heads/bad
+'
+
+test_expect_success 'checkout --track=fetch on local start-point errors' '
+	git checkout main &&
+	test_must_fail git checkout --track=fetch -b bad main 2>err &&
+	test_grep "no configured remote" err &&
+	test_must_fail git rev-parse --verify refs/heads/bad
+'
+
+test_expect_success 'checkout --track=bogus reports an error' '
+	git checkout main &&
+	test_must_fail git checkout --track=bogus -b bogus_branch fetch_upstream/fetch_new 2>err &&
+	test_grep "expects" err
+'
+
+test_expect_success 'checkout -q --track=fetch silences the fetch output' '
+	git checkout main &&
+	git -C fetch_upstream checkout -b fetch_quiet &&
+	test_commit -C fetch_upstream u_quiet &&
+	test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_quiet &&
+	git checkout -q --track=fetch -b local_quiet \
+		fetch_upstream/fetch_quiet 2>err &&
+	test_grep ! "-> fetch_upstream/fetch_quiet" err &&
+	test_cmp_rev refs/remotes/fetch_upstream/fetch_quiet HEAD
+'
+
+test_expect_success 'switch --track=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 --track=fetch -c local_switch fetch_upstream/fetch_switch &&
+	test_cmp_rev refs/remotes/fetch_upstream/fetch_switch HEAD
+'
+
 test_done
-- 
gitgitgadget

^ permalink raw reply related

* Re: [PATCH v2 0/2] environment: move ignore_case into repo_config_values
From: Junio C Hamano @ 2026-06-18 13:14 UTC (permalink / raw)
  To: Tian Yuchen; +Cc: git, ps, phillip.wood123, johannes.schindelin, stolee
In-Reply-To: <20260618114207.605211-1-cat@malon.dev>

Tian Yuchen <cat@malon.dev> writes:

> Related materials:
>
>  [1] In this patch to migrate protect_hfs and protect_ntfs, the approach
> of introducing getters has been endorsed.
>
>  [2] Derrick Stolee's previous attempt. The reasons for the failure are
> also mentioned in [1].

[1] here refers to the starting message of the whole hfs/ntfs thing.
Do you mean that people must read the entire thread to find out what
the reasons for the failure was?  For that matter, it is not clear,
unless readers read the whole thread, where the approach of using
getters was "endorsed", either.

> [1] https://lore.kernel.org/git/20260606143412.15443-1-cat@malon.dev/
> [2] https://lore.kernel.org/git/2b4198c09cb6c04c60608d19072d419503dfe5df.1685716421.git.gitgitgadget@gmail.com/

> Changes since V1:
>
>  - s/repo_get_ignore_case()/repo_ignore_case()
>
>  - Use repo->initialized instead of repo->gitdir

I do not think I have any objections to these changes from the
previous iteration.  There may be some other things in the new
iteration but I'll have to go in and read the patches to find them
out (if they exist).

Thanks.

^ permalink raw reply

* Re: [PATCH v5 2/2] graph: indent visual root in graph
From: Junio C Hamano @ 2026-06-18 13:31 UTC (permalink / raw)
  To: Pablo Sabater
  Cc: Jeff King, git, ayu.chandekar, chandrapratap3519,
	christian.couder, jltobler, karthik.188, phillip.wood,
	siddharthasthana31
In-Reply-To: <CAN5EUNSQY2oK7BE4J9Y8APfkP6eJxta050OUu=RoJYhXOjX_OA@mail.gmail.com>

Pablo Sabater <pabloosabaterr@gmail.com> writes:

> Should I work with 'next' as a base to have dd4bc01c0a? (Sorry I've
> just worked with master).

As dd4bc01c (revision: use priority queue for non-limited streaming
walks, 2026-05-27) is already in 'master', you should be able to
work with 'master' that is no stale than 6e148f82 (Merge branch
'kk/streaming-walk-pqueue', 2026-06-16).

^ permalink raw reply

* Re: [PATCH v14 4/6] branch: add --prune-merged <branch>
From: Phillip Wood @ 2026-06-18 13:42 UTC (permalink / raw)
  To: Harald Nordgren
  Cc: Harald Nordgren via GitGitGadget, git, Kristoffer Haugsbakk,
	Johannes Sixt
In-Reply-To: <CAHwyqnUsjpCHfS=eBphmkdDGYpQZ_LQUJi1mjrxV8ZXi+w4yhg@mail.gmail.com>

Hi Harald

On 16/06/2026 20:15, Harald Nordgren wrote:
>>> diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
>>> index 4e7deddc04..27ea1319bb 100755
>>> --- a/t/t3200-branch.sh
>>> +++ b/t/t3200-branch.sh
>>> @@ -1809,4 +1809,205 @@ test_expect_success '--forked requires a value' '
>>>        test_grep "requires a value" err
>>>    '
>>>
>>> +test_expect_success '--prune-merged: setup' '
>>> +     test_create_repo pm-upstream &&
>>
>> The rest of this test would be easier to read if we did
>>
>>          (
>>                  cd pm-upstream &&
>>                  ...
>>          )
>>
>> rather than prefixing every command with "-C pm-upstream"
> 
> I feel like the discussion to nest or not to nest has come up many
> times in other topics as well. I don't feel strongly about either way,
> but I just want to flag that if I change it now, another reviewer
> might ask me to change it back later.
> 
> Should the rules be to nest inside of setup functions (and helpers?)
> but not inside the actual tests?

I think it depends on how many commands you're running in a row in the 
same directory. In this case we're running quite a few commands so it 
seems clearer to use a subshell. In the later tests we're switching 
between repositories and running fewer commands in each one so it is 
less clear that using subshells is clearer. Also later on we're using 
test_config() which I don't think works in a subshell because it relies 
on test_when_finished().

One thing I've just thought of related to this patch is whether we want 
to protect branches that are the upstreams of branches that are not 
slated for deletion. With stacked branches it is possible that a branch 
has been merged but has other branches stacked on top of it that have 
not been merged. If we build an strset of branches that we want to 
delete, then loop over all branches and if there are any that are not in 
the to be deleted set which have their upstream in that set we'd remove 
the upstream branch from the set. Once we've done that we can convert 
the set to an strvec to pass to delete_branches()

Thanks

Phillip

> 
>>> +     test_commit -C pm-upstream base &&
>>> +     git -C pm-upstream checkout -b next &&
>>> +     test_commit -C pm-upstream one-commit &&
>>> +     test_commit -C pm-upstream two-commit &&
>>> +     git -C pm-upstream branch one HEAD~ &&
>>> +     git -C pm-upstream branch two HEAD &&
>>> +     git -C pm-upstream branch wip main &&
>>> +     git -C pm-upstream checkout main &&
>>> +     test_create_repo pm-fork
>>> +'
>>> +
>>> +test_expect_success '--prune-merged deletes branches integrated into upstream' '
>>> +     test_when_finished "rm -rf pm-merged" &&
>>> +     git clone pm-upstream pm-merged &&
>>> +     git -C pm-merged remote add fork ../pm-fork &&
>>> +     test_config -C pm-merged remote.pushDefault fork &&
>>> +     test_config -C pm-merged push.default current &&
>>
>> So we clone upstream and add fork as the default push remote. I find the
>> pm- prefixes rather distracting. It would be clearer to me if we just
>> called the repositories "upstream", "fork" and "repo"
> 
> Good point.
> 
>>> +     test_must_fail git -C pm-local rev-parse --verify refs/heads/one
>>> +'
>>> +
>>> +test_expect_success '--prune-merged warns instead of erroring on un-integrated commits' '
>>> +     test_when_finished "rm -rf pm-unmerged" &&
>>> +     git clone pm-upstream pm-unmerged &&
>>> +     git -C pm-unmerged remote add fork ../pm-fork &&
>>> +     test_config -C pm-unmerged remote.pushDefault fork &&
>>> +     test_config -C pm-unmerged push.default current &&
>>> +     git -C pm-unmerged checkout -b wip origin/wip &&
>>> +     git -C pm-unmerged branch --set-upstream-to=origin/next wip &&
>>> +     test_commit -C pm-unmerged local-only &&
>>> +     git -C pm-unmerged checkout - &&
>>> +
>>> +     git -C pm-unmerged branch --prune-merged "origin/*" 2>err &&
>>> +     test_grep "not fully merged" err &&
>>> +     test_grep ! "If you are sure you want to delete it" err &&
>>
>> I'm always suspicious of test_grep when we know what the output should
>> look like - it might be better to use test_cmp. This test does not check
>> that we also delete branches that are merged when we see one that isn't.
>>
>> I'm going to stop here - the tests I've read seem to me to be too much
>> like unit tests checking one aspect of the implementation in isolation
>> rather than checking that the whole feature works as expected.
> 
> I'll respond to the rest here: Excellent points regarding the testing
> aboce, I will take a look at doing this.
> 
> 
> Harald
> 


^ permalink raw reply

* Re: [PATCH v15 0/7] branch: delete-merged
From: Phillip Wood @ 2026-06-18 13:48 UTC (permalink / raw)
  To: Harald Nordgren, phillip.wood
  Cc: Harald Nordgren via GitGitGadget, git, Kristoffer Haugsbakk,
	Johannes Sixt
In-Reply-To: <CAHwyqnWFM2jskm6soEu58tp_TgO3fmuODD-yTiK6-4Hpv8SMLQ@mail.gmail.com>

Hi Harald

On 17/06/2026 20:11, Harald Nordgren wrote:
>> Right but you sent that version a few hours after I'd posted a partial
>> review which concluded by saying I'd finish it the next day. If you send
>> a new version when you are waiting for further comments it clutters the
>> list because you know you're going to have to post another revision when
>> you get the rest of the comments. Anyone reviewing the interim version
>> is wasting their time. When you receive review comments, by all means
>> start thinking about them and updating your local copy but please don't
>> post a new version until the discussion on the previous version has
>> settled down.
> 
> That's fair. Sorry about that.
> 
> Will you let me know when your review here is finished?

I've just sent a mail with another comment but that concudes this round 
unless you have any questions about it.

> 
> I received the same feedback from Junio before, so I'm not unaware of
> this problem. I am trying to slow down. I often prepare the work as
> soon as I get some comments -- I'm on paternity leave so I have a lot
> of time when the baby is sleeping -- 

Congratulations - I hope the baby is sleeping at night as well in the day!

> then I actively hold off on
> sending to not overload the rest of you. But at the same time I think
> it's valuable to keep up a certain pace. It's a balancing act.
It is worth waiting for the discussion to settle on each round, I'll try 
and be clear when I've finished looking at each revision. I'm sure other 
folks would appreciate you looking at their patches and commenting on 
them while you're waiting for feedback on yours, especially the GSoC 
project students.

Thanks

Phillip

^ permalink raw reply

* [PATCH] zlib: properly clamp to uLong
From: Johannes Schindelin via GitGitGadget @ 2026-06-18 13:50 UTC (permalink / raw)
  To: git; +Cc: Johannes Schindelin, Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

On platforms where `unsigned long` and `size_t` differ in bit size, we
want to clamp the buffers we pass to zlib to the former's size, as per
d05d666977 (git-zlib: handle data streams larger than 4GB, 2026-05-08).

The logic introduced in that commit performs a clamping to the bits,
though, which fails to do what is needed here: If too many bytes are
available in the buffers, we need to clamp to the maximum value of an
`unsigned long`. Otherwise, we ask zlib to use too small buffers, in the
worst case using 0 as the size (think: a value whose 32 lowest bits are
all zero).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
    zlib: properly clamp to uLong
    
    I re-read this logic earlier this week... and I am quite convinced that
    it needs to be fixed.

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2153%2Fdscho%2Ffix-ulong-clamping-for-zlib-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2153/dscho/fix-ulong-clamping-for-zlib-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2153

 git-zlib.c | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/git-zlib.c b/git-zlib.c
index b91cb323ae..d21adb3bf5 100644
--- a/git-zlib.c
+++ b/git-zlib.c
@@ -38,12 +38,17 @@ static inline uInt zlib_buf_cap(unsigned long len)
 	return (ZLIB_BUF_MAX < len) ? ZLIB_BUF_MAX : len;
 }
 
+static inline uLong zlib_uLong_cap(size_t s)
+{
+	return s < ULONG_MAX_VALUE ? (uLong)s : ULONG_MAX_VALUE;
+}
+
 static void zlib_pre_call(git_zstream *s)
 {
 	s->z.next_in = s->next_in;
 	s->z.next_out = s->next_out;
-	s->z.total_in = (uLong)(s->total_in & ULONG_MAX_VALUE);
-	s->z.total_out = (uLong)(s->total_out & ULONG_MAX_VALUE);
+	s->z.total_in = zlib_uLong_cap(s->total_in);
+	s->z.total_out = zlib_uLong_cap(s->total_out);
 	s->z.avail_in = zlib_buf_cap(s->avail_in);
 	s->z.avail_out = zlib_buf_cap(s->avail_out);
 }
@@ -60,7 +65,7 @@ static void zlib_post_call(git_zstream *s, int status)
 	 * We track our own totals and verify only the low bits match.
 	 */
 	if ((s->z.total_out & ULONG_MAX_VALUE) !=
-	    ((s->total_out + bytes_produced) & ULONG_MAX_VALUE))
+	    ((zlib_uLong_cap(s->total_out) + bytes_produced) & ULONG_MAX_VALUE))
 		BUG("total_out mismatch");
 	/*
 	 * zlib does not update total_in when it returns Z_NEED_DICT,
@@ -68,7 +73,7 @@ static void zlib_post_call(git_zstream *s, int status)
 	 */
 	if (status != Z_NEED_DICT &&
 	    (s->z.total_in & ULONG_MAX_VALUE) !=
-	    ((s->total_in + bytes_consumed) & ULONG_MAX_VALUE))
+	    ((zlib_uLong_cap(s->total_in) + bytes_consumed) & ULONG_MAX_VALUE))
 		BUG("total_in mismatch");
 
 	s->total_out += bytes_produced;

base-commit: 7a094d68a27e321a99c8ab6b700909e503904bd9
-- 
gitgitgadget

^ permalink raw reply related

* Re: [PATCH] gitlab-ci: migrate Windows builds away from Chocolatey
From: Justin Tobler @ 2026-06-18 14:03 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git
In-Reply-To: <ajOE2XMBzgrXxbH8@pks.im>

On 26/06/18 07:40AM, Patrick Steinhardt wrote:
> On Wed, Jun 17, 2026 at 03:03:39PM -0500, Justin Tobler wrote:
> > On 26/06/15 02:21PM, Patrick Steinhardt wrote:
> > >    before_script:
> > >      - *windows_before_script
> > > -    - choco install -y git meson ninja rust-ms
> > > -    - Import-Module $env:ChocolateyInstall\helpers\chocolateyProfile.psm1
> > > -    - refreshenv
> > > +    - ./ci/install-dependencies.ps1
> > > +    - $env:Path = "C:\Meson;C:\Rust\bin;$env:Path"
> > 
> > I assume Git is already discoverable on the path?
> 
> Good question -- in fact it's not, but in Meson we know to use the
> well-known path of "C:\Program Files\Git" automatically and that's why
> we don't have to add it here. That certainly is a bit hacky, but I'm not
> sure whether we need to change it.
> 
> Just let me know if you think so.

If it's only Meson that needs to locate Git and it is already capable of
doing that without updating the path here, this is probably fine as-is.
We could maybe explain this to future reader in a comment? But I'm not
sure it matters too much and is likely not worth a reroll IMO.

Overall this patch looks good to me.

-Justin

^ permalink raw reply

* Re: [PATCH v2 7/8] refs: fix recursing `get_main_ref_store()` with "onbranch" config
From: Justin Tobler @ 2026-06-18 14:15 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git, Karthik Nayak, Jeff King
In-Reply-To: <ajOJM8EvGWWkYNuL@pks.im>

On 26/06/18 07:59AM, Patrick Steinhardt wrote:
> On Wed, Jun 17, 2026 at 01:41:40PM -0500, Justin Tobler wrote:
> > Is this really the best signal to indicate that a repository ref store
> > has not been initialized? Temporarily setting the storage format to
> > REF_STORAGE_FORMAT_UNKNOWN feels rather awkward and suggests to me that
> > `include_by_branch()` probably shouldn't be using it to begin with if
> > its not reliable.
> 
> True, but we don't really have a better signal to the best of my
> knowledge. Ideally, we'd be able to use the existence `r->refs_private`
> as signal. But that doesn't really work as the reference database is
> lazily constructed, and the recursion happens in the exact function that
> would construct it in the first place. And there indeed are cases where
> reading the configuration is the first caller of `get_main_ref_store()`.

Ok, my first thought was also whether we could use the existence of the
ref store as a signal, but I guess that won't work here.

> My first internal iteration tried to make this non-lazily constructed so
> that we can use it as a proper signal. But that led to a bunch of
> problems where we now parsed configuration way earlier than we currently
> do, and that in turn led to all kinds of errors. I was able to fix all
> of those errors except one: we expect `git config set` to work in a
> misconfigured repository so that the user can fix the misconfig without
> having to manually edit the Git configuration files. But when
> constructing the refdb eagerly we will die early in such cases.
> 
> We could again work around that issue, but that unfortunately evolved
> into a proper mess that I eventually discarded as unworkable. I think
> this is an inherent design flaw: constructing the refdb requires us to
> be able to parse the configuration, but constructing the configuration
> may require us to construct the refdb. So this awkwardness is built into
> Git's design, unfortunately.
> 
> So I'd really love to have a better signal, as I fully agree that the
> above workaround is nothing more but a hack. But I'm just not sure what
> that signal would be. And this version here does exactly what we want:
> we honor "onbranch" conditionals in all cases, except when constructing
> the main reference store. Even if it's ugly.

Could we embed an `initialized` boolean in `struct ref_store` that gets
set when the ref store is properly initialized and use that as a signal
instead? I'm not sure how complex introducing this would be though.

-Justin

^ permalink raw reply

* [PATCH] help: prompt user to run corrected command on typo
From: calicomills @ 2026-06-18 14:26 UTC (permalink / raw)
  To: git; +Cc: gitster

From 0dc9e5c4593611b75e7003e8fdbea9370524c05b Mon Sep 17 00:00:00 2001
From: calicomills <jishnuck26@gmail.com>
Date: Thu, 18 Jun 2026 19:47:12 +0530
Subject: [PATCH] help: prompt user to run corrected command on typo

When a user mistypes a git command and there is exactly one similar
command, git currently prints a suggestion but exits, requiring the
user to retype the corrected command manually.

Instead, when stdin and stderr are both connected to a terminal and
there is a single best match, prompt the user with:

  Did you mean 'git checkout neo'? [y/N]

The full corrected invocation (command + original arguments) is shown
in the prompt so the user knows exactly what will run. Answering 'y'
re-executes git with the corrected command and all original arguments.
Answering anything else exits as before.

When there are multiple similarly-named commands, or when running
non-interactively (scripts, pipes), the original behaviour of printing
the suggestion list and exiting is preserved.

The help_unknown_cmd() signature is updated to accept the full args
vector so the prompt can include the original arguments alongside the
corrected command name.

Add tests to t9003 covering:
- non-interactive single match: falls back to suggestion list
- non-interactive multiple matches: falls back to suggestion list
- interactive single match, 'y': corrected command runs (TTY prereq)
- interactive single match, 'n': exits cleanly (TTY prereq)

Signed-off-by: calicomills <jishnuck26@gmail.com>
---
 builtin/help.c              |  2 +-
 git.c                       |  2 +-
 help.c                      | 40 ++++++++++++++++++++++------
 help.h                      |  3 ++-
 t/t9003-help-autocorrect.sh | 53 +++++++++++++++++++++++++++++++++++++
 5 files changed, 89 insertions(+), 11 deletions(-)

diff --git a/builtin/help.c b/builtin/help.c
index a140339999..b17e61ccc8 100644
--- a/builtin/help.c
+++ b/builtin/help.c
@@ -618,7 +618,7 @@ static char *check_git_cmd(const char *cmd)
 	}
 
 	if (exclude_guides)
-		return help_unknown_cmd(cmd);
+		return help_unknown_cmd(cmd, NULL);
 
 	return xstrdup(cmd);
 }
diff --git a/git.c b/git.c
index 36f08891ef..d379cc85bb 100644
--- a/git.c
+++ b/git.c
@@ -994,7 +994,7 @@ int cmd_main(int argc, const char **argv)
 			exit(1);
 		}
 		if (!done_help) {
-			char *assumed = help_unknown_cmd(cmd);
+			char *assumed = help_unknown_cmd(cmd, &args);
 			strvec_replace(&args, 0, assumed);
 			free(assumed);
 			cmd = args.v[0];
diff --git a/help.c b/help.c
index 46241492ce..30f32a7206 100644
--- a/help.c
+++ b/help.c
@@ -641,7 +641,7 @@ static const char bad_interpreter_advice[] =
 	N_("'%s' appears to be a git command, but we were not\n"
 	"able to execute it. Maybe git-%s is broken?");
 
-char *help_unknown_cmd(const char *cmd)
+char *help_unknown_cmd(const char *cmd, const struct strvec *args)
 {
 	struct help_unknown_cmd_config cfg = { 0 };
 	int i, n, best_similarity = 0;
@@ -762,13 +762,37 @@ char *help_unknown_cmd(const char *cmd)
 	fprintf_ln(stderr, _("git: '%s' is not a git command. See 'git --help'."), cmd);
 
 	if (SIMILAR_ENOUGH(best_similarity)) {
-		fprintf_ln(stderr,
-			   Q_("\nThe most similar command is",
-			      "\nThe most similar commands are",
-			   n));
-
-		for (i = 0; i < n; i++)
-			fprintf(stderr, "\t%s\n", main_cmds.names[i]->name);
+		if (n == 1 && isatty(0) && isatty(2)) {
+			char *answer;
+			struct strbuf msg = STRBUF_INIT;
+			struct strbuf full_cmd = STRBUF_INIT;
+			strbuf_addstr(&full_cmd, main_cmds.names[0]->name);
+			if (args) {
+				for (size_t j = 1; j < args->nr; j++) {
+					strbuf_addch(&full_cmd, ' ');
+					strbuf_addstr(&full_cmd, args->v[j]);
+				}
+			}
+			strbuf_addf(&msg, _("\nDid you mean 'git %s'? [y/N] "),
+				    full_cmd.buf);
+			strbuf_release(&full_cmd);
+			answer = git_prompt(msg.buf, PROMPT_ECHO);
+			strbuf_release(&msg);
+			if (starts_with(answer, "y") || starts_with(answer, "Y")) {
+				char *assumed = xstrdup(main_cmds.names[0]->name);
+				cmdnames_release(&cfg.aliases);
+				cmdnames_release(&main_cmds);
+				cmdnames_release(&other_cmds);
+				return assumed;
+			}
+		} else {
+			fprintf_ln(stderr,
+				   Q_("\nThe most similar command is",
+				      "\nThe most similar commands are",
+				   n));
+			for (i = 0; i < n; i++)
+				fprintf(stderr, "\t%s\n", main_cmds.names[i]->name);
+		}
 	}
 
 	exit(1);
diff --git a/help.h b/help.h
index c54bf0977d..a8c465b3df 100644
--- a/help.h
+++ b/help.h
@@ -32,7 +32,8 @@ void list_all_other_cmds(struct string_list *list);
 void list_cmds_by_category(struct string_list *list,
 			   const char *category);
 void list_cmds_by_config(struct string_list *list);
-char *help_unknown_cmd(const char *cmd);
+#include "strvec.h"
+char *help_unknown_cmd(const char *cmd, const struct strvec *args);
 void load_command_list(const char *prefix,
 		       struct cmdnames *main_cmds,
 		       struct cmdnames *other_cmds);
diff --git a/t/t9003-help-autocorrect.sh b/t/t9003-help-autocorrect.sh
index 8da318d2b5..6fe2da1595 100755
--- a/t/t9003-help-autocorrect.sh
+++ b/t/t9003-help-autocorrect.sh
@@ -70,4 +70,57 @@ test_expect_success 'autocorrect works in work tree created from bare repo' '
 	git -C worktree -c help.autocorrect=immediate status
 '
 
+# Default behaviour (no help.autocorrect set): when there is exactly one
+# similar command but the session is non-interactive, fall back to printing
+# the suggestion list and exiting rather than showing a prompt.
+test_expect_success 'default: single match non-interactive shows suggestion and fails' '
+	test_might_fail git config --unset help.autocorrect &&
+
+	test_must_fail git lfg 2>actual &&
+	grep "most similar command" actual &&
+	grep "lgf" actual
+'
+
+test_expect_success 'default: multiple matches non-interactive shows list and fails' '
+	test_might_fail git config --unset help.autocorrect &&
+
+	test_must_fail git com 2>actual &&
+	grep "most similar commands" actual &&
+	grep "commit" actual
+'
+
+# Interactive prompt tests require a real TTY.  On macOS the TTY prereq is
+# skipped due to IO::Pty reliability issues; these tests run on Linux CI.
+test_expect_success TTY 'default: single match interactive, answer y runs command' '
+	git config --unset help.autocorrect &&
+
+	write_script git-typotest <<-\EOF &&
+		echo typotest-ran
+	EOF
+	PATH="$PATH:." export PATH &&
+
+	# Feed "y" to /dev/tty via a wrapper that answers the prompt
+	write_script answer-prompt <<-\EOF &&
+		# Write the answer to the controlling terminal
+		printf "y\n" >/dev/tty
+		exec "$@"
+	EOF
+
+	test_terminal ./answer-prompt git typotest 2>err >out &&
+	grep "typotest-ran" out &&
+	grep "Did you mean" err
+'
+
+test_expect_success TTY 'default: single match interactive, answer n exits cleanly' '
+	git config --unset help.autocorrect &&
+
+	write_script answer-prompt-no <<-\EOF &&
+		printf "n\n" >/dev/tty
+		exec "$@"
+	EOF
+
+	test_must_fail test_terminal ./answer-prompt-no git typotest 2>err &&
+	grep "Did you mean" err
+'
+
 test_done
-- 
2.50.1



^ 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