Git development
 help / color / mirror / Atom feed
* [PATCH 0/3] commit-reach: replace queue_has_nonstale with a counter
From: Kristofer Karlsson via GitGitGadget @ 2026-05-24 17:42 UTC (permalink / raw)
  To: git; +Cc: Kristofer Karlsson

paint_down_to_common() and ahead_behind() terminate when every commit in
their priority queue is STALE. The current check, queue_has_nonstale(), does
an O(n) linear scan of the queue on every iteration, costing O(n*m) total
where n is the queue size and m is the number of commits processed. This
series replaces that scan with an O(1) counter.

Performance measurements with git merge-base --all and git for-each-ref
--format='%(ahead-behind:...)':

git.git (merge-base)
                                          Baseline  Dedup  Dedup+Ctr
seen..next, 33 merge bases:               157ms    165ms    143ms
seen..master, 1 base:                      47ms     40ms     44ms
master..next, 1 base:                      62ms     60ms     63ms

(seen=fe056fe1, next=c82f1880, master=6a4418c3)

Large monorepo, 2.4M commits (merge-base)
                                          Baseline        Dedup+Ctr
component import, wide frontier (1):      8083ms           3778ms
component import, wide frontier (2):      5664ms           4207ms
component import, wide frontier (3):      4558ms           1796ms

Large monorepo, 2.4M commits (ahead-behind)
                                          Baseline        Dedup+Ctr
component import, wide frontier (1):      8216ms           4145ms
component import, wide frontier (2):      6107ms           4528ms
component import, wide frontier (3):      4725ms           1999ms

Linear history (merge-base), no regression:
master vs HEAD~10000:                     4410ms           4180ms
master vs HEAD~50000:                     4412ms           4494ms


The improvement depends on how wide the frontier gets during the walk.
Component imports in the monorepo create wide frontiers where the queue
grows large, making the O(n) scan expensive -- up to 2.5x speedup for
merge-base and 2.4x for ahead-behind. Linear history and simple merges show
no regression.

With a very narrow frontier the counter approach adds a small constant
overhead per iteration (maintaining the counter and the ENQUEUED flag)
compared to the old scan which would return almost immediately. Both are
O(1) and cheap in that scenario, so it should not matter in practice -- the
benchmark numbers above confirm this.

Kristofer Karlsson (3):
  commit-reach: deduplicate queue entries in paint_down_to_common
  commit-reach: optimize queue scan in paint_down_to_common
  commit-reach: optimize queue scan in ahead_behind

 commit-reach.c | 58 ++++++++++++++++++++++++++++++++++++--------------
 object.h       |  2 +-
 2 files changed, 43 insertions(+), 17 deletions(-)


base-commit: 6a4418c36d6bad69a599044b3cf49dcbd049cb45
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2124%2Fspkrka%2Fqueue-has-nonstale-v3-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2124/spkrka/queue-has-nonstale-v3-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2124
-- 
gitgitgadget

^ permalink raw reply

* [PATCH 1/3] commit-reach: deduplicate queue entries in paint_down_to_common
From: Kristofer Karlsson via GitGitGadget @ 2026-05-24 17:42 UTC (permalink / raw)
  To: git; +Cc: Kristofer Karlsson, Kristofer Karlsson
In-Reply-To: <pull.2124.git.1779644541.gitgitgadget@gmail.com>

From: Kristofer Karlsson <krka@spotify.com>

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

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

Signed-off-by: Kristofer Karlsson <krka@spotify.com>
---
 commit-reach.c | 19 +++++++++++++++----
 object.h       |  2 +-
 2 files changed, 16 insertions(+), 5 deletions(-)

diff --git a/commit-reach.c b/commit-reach.c
index d3a9b3ed6f..c16d4b061c 100644
--- a/commit-reach.c
+++ b/commit-reach.c
@@ -17,8 +17,9 @@
 #define PARENT2		(1u<<17)
 #define STALE		(1u<<18)
 #define RESULT		(1u<<19)
+#define ENQUEUED	(1u<<20)
 
-static const unsigned all_flags = (PARENT1 | PARENT2 | STALE | RESULT);
+static const unsigned all_flags = (PARENT1 | PARENT2 | STALE | RESULT | ENQUEUED);
 
 static int compare_commits_by_gen(const void *_a, const void *_b)
 {
@@ -39,6 +40,14 @@ static int compare_commits_by_gen(const void *_a, const void *_b)
 	return 0;
 }
 
+static void maybe_enqueue(struct prio_queue *queue, struct commit *c)
+{
+	if (c->object.flags & ENQUEUED)
+		return;
+	c->object.flags |= ENQUEUED;
+	prio_queue_put(queue, c);
+}
+
 static int queue_has_nonstale(struct prio_queue *queue)
 {
 	for (size_t i = 0; i < queue->nr; i++) {
@@ -70,11 +79,11 @@ static int paint_down_to_common(struct repository *r,
 		commit_list_append(one, result);
 		return 0;
 	}
-	prio_queue_put(&queue, one);
+	maybe_enqueue(&queue, one);
 
 	for (i = 0; i < n; i++) {
 		twos[i]->object.flags |= PARENT2;
-		prio_queue_put(&queue, twos[i]);
+		maybe_enqueue(&queue, twos[i]);
 	}
 
 	while (queue_has_nonstale(&queue)) {
@@ -83,6 +92,8 @@ static int paint_down_to_common(struct repository *r,
 		int flags;
 		timestamp_t generation = commit_graph_generation(commit);
 
+		commit->object.flags &= ~ENQUEUED;
+
 		if (min_generation && generation > last_gen)
 			BUG("bad generation skip %"PRItime" > %"PRItime" at %s",
 			    generation, last_gen,
@@ -124,7 +135,7 @@ static int paint_down_to_common(struct repository *r,
 					     oid_to_hex(&p->object.oid));
 			}
 			p->object.flags |= flags;
-			prio_queue_put(&queue, p);
+			maybe_enqueue(&queue, p);
 		}
 	}
 
diff --git a/object.h b/object.h
index d814647ebe..05cbf728e9 100644
--- a/object.h
+++ b/object.h
@@ -74,7 +74,7 @@ void object_array_init(struct object_array *array);
  * bundle.c:                                        16
  * http-push.c:                          11-----14
  * commit-graph.c:                                15
- * commit-reach.c:                                  16-----19
+ * commit-reach.c:                                  16-------20
  * builtin/last-modified.c:                         1617
  * sha1-name.c:                                              20
  * list-objects-filter.c:                                      21
-- 
gitgitgadget


^ permalink raw reply related

* [PATCH 2/3] commit-reach: optimize queue scan in paint_down_to_common
From: Kristofer Karlsson via GitGitGadget @ 2026-05-24 17:42 UTC (permalink / raw)
  To: git; +Cc: Kristofer Karlsson, Kristofer Karlsson
In-Reply-To: <pull.2124.git.1779644541.gitgitgadget@gmail.com>

From: Kristofer Karlsson <krka@spotify.com>

paint_down_to_common() terminates when every commit remaining in its
priority queue is STALE. This was checked by queue_has_nonstale(),
which performed an O(n) linear scan of the entire queue on every
iteration, resulting in O(n*m) total overhead where n is the queue
size and m is the number of commits processed.

Replace this with an O(1) nonstale_count that tracks the number of
non-stale commits currently in the queue. The counter is incremented
by maybe_enqueue() and decremented on dequeue and by mark_stale()
when a commit transitions to STALE while still in the queue. Since
each commit appears at most once (guaranteed by the ENQUEUED flag
from the previous commit), the counter is exact.

ahead_behind() also uses queue_has_nonstale() and will be converted
in the next commit.

Signed-off-by: Kristofer Karlsson <krka@spotify.com>
---
 commit-reach.c | 28 +++++++++++++++++++++++-----
 1 file changed, 23 insertions(+), 5 deletions(-)

diff --git a/commit-reach.c b/commit-reach.c
index c16d4b061c..356ff52d08 100644
--- a/commit-reach.c
+++ b/commit-reach.c
@@ -40,12 +40,25 @@ static int compare_commits_by_gen(const void *_a, const void *_b)
 	return 0;
 }
 
-static void maybe_enqueue(struct prio_queue *queue, struct commit *c)
+static void maybe_enqueue(struct prio_queue *queue, struct commit *c,
+			  int *nonstale_count)
 {
 	if (c->object.flags & ENQUEUED)
 		return;
 	c->object.flags |= ENQUEUED;
 	prio_queue_put(queue, c);
+	if (!(c->object.flags & STALE))
+		(*nonstale_count)++;
+}
+
+static void mark_stale(struct commit *c, unsigned queued_flag,
+		       int *nonstale_count)
+{
+	if (!(c->object.flags & STALE)) {
+		if (c->object.flags & queued_flag)
+			(*nonstale_count)--;
+		c->object.flags |= STALE;
+	}
 }
 
 static int queue_has_nonstale(struct prio_queue *queue)
@@ -68,6 +81,7 @@ static int paint_down_to_common(struct repository *r,
 {
 	struct prio_queue queue = { compare_commits_by_gen_then_commit_date };
 	int i;
+	int nonstale_count = 0;
 	timestamp_t last_gen = GENERATION_NUMBER_INFINITY;
 	struct commit_list **tail = result;
 
@@ -79,20 +93,22 @@ static int paint_down_to_common(struct repository *r,
 		commit_list_append(one, result);
 		return 0;
 	}
-	maybe_enqueue(&queue, one);
+	maybe_enqueue(&queue, one, &nonstale_count);
 
 	for (i = 0; i < n; i++) {
 		twos[i]->object.flags |= PARENT2;
-		maybe_enqueue(&queue, twos[i]);
+		maybe_enqueue(&queue, twos[i], &nonstale_count);
 	}
 
-	while (queue_has_nonstale(&queue)) {
+	while (nonstale_count > 0) {
 		struct commit *commit = prio_queue_get(&queue);
 		struct commit_list *parents;
 		int flags;
 		timestamp_t generation = commit_graph_generation(commit);
 
 		commit->object.flags &= ~ENQUEUED;
+		if (!(commit->object.flags & STALE))
+			nonstale_count--;
 
 		if (min_generation && generation > last_gen)
 			BUG("bad generation skip %"PRItime" > %"PRItime" at %s",
@@ -134,8 +150,10 @@ static int paint_down_to_common(struct repository *r,
 				return error(_("could not parse commit %s"),
 					     oid_to_hex(&p->object.oid));
 			}
+			if (flags & STALE)
+				mark_stale(p, ENQUEUED, &nonstale_count);
 			p->object.flags |= flags;
-			maybe_enqueue(&queue, p);
+			maybe_enqueue(&queue, p, &nonstale_count);
 		}
 	}
 
-- 
gitgitgadget


^ permalink raw reply related

* [PATCH 3/3] commit-reach: optimize queue scan in ahead_behind
From: Kristofer Karlsson via GitGitGadget @ 2026-05-24 17:42 UTC (permalink / raw)
  To: git; +Cc: Kristofer Karlsson, Kristofer Karlsson
In-Reply-To: <pull.2124.git.1779644541.gitgitgadget@gmail.com>

From: Kristofer Karlsson <krka@spotify.com>

Apply the same nonstale_count optimization from the previous commit
to ahead_behind(). This replaces the remaining caller of the O(n)
queue_has_nonstale() scan with an O(1) counter check, allowing
queue_has_nonstale() to be removed.

ahead_behind() already deduplicates queue entries using the PARENT2
flag (via insert_no_dup), so the counter is maintained through
insert_no_dup() and mark_stale() using PARENT2 as the queued_flag.

Signed-off-by: Kristofer Karlsson <krka@spotify.com>
---
 commit-reach.c | 27 ++++++++++++---------------
 1 file changed, 12 insertions(+), 15 deletions(-)

diff --git a/commit-reach.c b/commit-reach.c
index 356ff52d08..41deb8fc78 100644
--- a/commit-reach.c
+++ b/commit-reach.c
@@ -61,16 +61,6 @@ static void mark_stale(struct commit *c, unsigned queued_flag,
 	}
 }
 
-static int queue_has_nonstale(struct prio_queue *queue)
-{
-	for (size_t i = 0; i < queue->nr; i++) {
-		struct commit *commit = queue->array[i].data;
-		if (!(commit->object.flags & STALE))
-			return 1;
-	}
-	return 0;
-}
-
 /* all input commits in one and twos[] must have been parsed! */
 static int paint_down_to_common(struct repository *r,
 				struct commit *one, int n,
@@ -1051,12 +1041,15 @@ struct commit_list *get_reachable_subset(struct commit **from, size_t nr_from,
 define_commit_slab(bit_arrays, struct bitmap *);
 static struct bit_arrays bit_arrays;
 
-static void insert_no_dup(struct prio_queue *queue, struct commit *c)
+static void insert_no_dup(struct prio_queue *queue, struct commit *c,
+			  int *nonstale_count)
 {
 	if (c->object.flags & PARENT2)
 		return;
 	prio_queue_put(queue, c);
 	c->object.flags |= PARENT2;
+	if (!(c->object.flags & STALE))
+		(*nonstale_count)++;
 }
 
 static struct bitmap *get_bit_array(struct commit *c, int width)
@@ -1082,6 +1075,7 @@ void ahead_behind(struct repository *r,
 {
 	struct prio_queue queue = { .compare = compare_commits_by_gen_then_commit_date };
 	size_t width = DIV_ROUND_UP(commits_nr, BITS_IN_EWORD);
+	int nonstale_count = 0;
 
 	if (!commits_nr || !counts_nr)
 		return;
@@ -1100,14 +1094,17 @@ void ahead_behind(struct repository *r,
 		struct bitmap *bitmap = get_bit_array(c, width);
 
 		bitmap_set(bitmap, i);
-		insert_no_dup(&queue, c);
+		insert_no_dup(&queue, c, &nonstale_count);
 	}
 
-	while (queue_has_nonstale(&queue)) {
+	while (nonstale_count > 0) {
 		struct commit *c = prio_queue_get(&queue);
 		struct commit_list *p;
 		struct bitmap *bitmap_c = get_bit_array(c, width);
 
+		if (!(c->object.flags & STALE))
+			nonstale_count--;
+
 		for (size_t i = 0; i < counts_nr; i++) {
 			int reach_from_tip = !!bitmap_get(bitmap_c, counts[i].tip_index);
 			int reach_from_base = !!bitmap_get(bitmap_c, counts[i].base_index);
@@ -1136,9 +1133,9 @@ void ahead_behind(struct repository *r,
 			 * queue is STALE.
 			 */
 			if (bitmap_popcount(bitmap_p) == commits_nr)
-				p->item->object.flags |= STALE;
+				mark_stale(p->item, PARENT2, &nonstale_count);
 
-			insert_no_dup(&queue, p->item);
+			insert_no_dup(&queue, p->item, &nonstale_count);
 		}
 
 		free_bit_array(c);
-- 
gitgitgadget

^ permalink raw reply related

* Re: [PATCH 1/5] xdiff: support external hunks via xpparam_t
From: Michael Montalbo @ 2026-05-24 18:01 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Michael Montalbo via GitGitGadget, git
In-Reply-To: <xmqqtsrxi43j.fsf@gitster.g>

On Sun, May 24, 2026 at 1:50 AM Junio C Hamano <gitster@pobox.com> wrote:
>
> Michael Montalbo <mmontalbo@gmail.com> writes:
>
> >> > +      * Clear changed[] arrays.  xdl_prepare_env() may have dirtied
> >> > +      * them via xdl_cleanup_records().  The allocation is nrec + 2
> >> > +      * elements; changed points one past the start (see xprepare.c).
> >> > +      */
> >> > +     memset(xe->xdf1.changed - 1, 0,
> >> > +            (xe->xdf1.nrec + 2) * sizeof(bool));
> >> > +     memset(xe->xdf2.changed - 1, 0,
> >> > +            (xe->xdf2.nrec + 2) * sizeof(bool));
> >>
> >> This, especially the starting offset of -1, looks horrible.  The
> >> internal layout of xdfenv_t might happen to match the way the above
> >> code expects, which is how xdl_prepare_ctx() may have give you, but
> >> it somehow feels brittle.  I guess the assumption that changed[]
> >> does not point at the beginning of the allocated area (e.g., it is a
> >> no-no to free(xe->xdf1.changed) or realloc() it) is so pervasive that
> >> it cannot be helped.  Sigh.
> >>
> >
> > Agreed it is ugly. I wanted to make sure the entire changed[] including
> > sentinels were clear as a defensive measure for downstream callers
> > (xdl_change_compact). I agree this results in something that is ugly
> > and brittle, but in the end I thought it was superior to relying on the
> > fact that upstream zeroes the entire changed[] array. Maybe if the
> > comment was more explicit about why this is happening it would be
> > helpful?
>
> Perhaps make these memset() into calls to a helper function that is
> defined in xdiff/xprepare.c with a descriptive name and placed near
> where xdl_prepare_ctx() is.  That way, the patch in question does
> not even have to expose the strangeness of changed[] (i.e., it has 2
> more elements than it would normally contain to make the memory
> region for changed[-1] and changed[N] valid, and freeing it requires
> free(changed-1)) to the code path.  It only needs to say "Hey, I am
> clearing changed[] arrays because of XXX" without having to say "by
> the way, the memory layout of changed[] is strange this way", the
> latter of which is not exactly of interest for readers of this code.
>
> >     /*
> >      * Clear changed[] arrays including sentinels.
> >      * xdl_prepare_env() may have dirtied them via
> >      * xdl_cleanup_records(), and xdl_change_compact() reads
> >      * the sentinel at changed[-1] during backward scans.
> >      */
>
> And this belongs in xdiff/xprepare.c near that new helper function.

That sounds a lot nicer. Will update.

^ permalink raw reply

* ds/path-walk-filters (was: What's cooking in git.git (May 2026, #06))
From: Derrick Stolee @ 2026-05-24 18:05 UTC (permalink / raw)
  To: Junio C Hamano, git, me
In-Reply-To: <xmqqzf1qsdfa.fsf@gitster.g>

On 5/23/26 5:06 AM, Junio C Hamano wrote:

> * ds/path-walk-filters (2026-05-13) 14 commits
>   - path-walk: support `combine` filter
>   - path-walk: support `object:type` filter
>   - path-walk: support `tree:0` filter
>   - t6601: tag otherwise-unreachable trees
>   - pack-objects: support sparse:oid filter with path-walk
>   - path-walk: add pl_sparse_trees to control tree pruning
>   - path-walk: support blob size limit filter
>   - backfill: die on incompatible filter options
>   - path-walk: support blobless filter
>   - path-walk: always emit directly-requested objects
>   - t/perf: add pack-objects filter and path-walk benchmark
>   - pack-objects: pass --objects with --path-walk
>   - t5620: make test work with path-walk var
>   - Merge branch 'en/backfill-fixes-and-edges' into ds/path-walk-filters
> 
>   The "git pack-objects --path-walk" traversal has been integrated
>   with several object filters, including blobless and sparse filters.
> 
>   Comments?
>   source: <pull.2101.v4.git.1778707135.gitgitgadget@gmail.com>

Taylor has completed review on a small re-roll v5.

Thanks,
-Stolee


^ permalink raw reply

* [PATCH 0/1] bugfix git subtree split
From: Roland Conybeare @ 2026-05-24 21:23 UTC (permalink / raw)
  To: git; +Cc: Roland Conybeare

I have a project that combines multiple independent repos
into an unmbrella repo, relying on git subtree.
Encountered a unrecoverable fatal error
from 'git subtree split' with error

    fatal: cache for <hash> already exists!

Problem arises because history to be split contains merge commits
that cause DAG traversal to consider the same umbrella commit on
multiple paths. The fatal triggers when 'git subtree split' tries
to cache the same commit twice; enclosed patch prunes these duplicate
paths.

Roland Conybeare (1):
  subtree: fix cache_set failure on commit reachable by multiple paths

 contrib/subtree/git-subtree.sh | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)


base-commit: 6a4418c36d6bad69a599044b3cf49dcbd049cb45
--
2.50.1

^ permalink raw reply

* [PATCH 1/1] subtree: fix cache_set failure on commit reachable by multiple paths
From: Roland Conybeare @ 2026-05-24 21:23 UTC (permalink / raw)
  To: git; +Cc: Roland Conybeare
In-Reply-To: <20260524212339.1493145-1-rconybeare@gmail.com>

When splitting a subtree, committs that do not intersect prefix
receive identity mapping (oldrev -> oldrev). If such commit
is reachable by multiple paths in the revision DAG, the cache_set()
function may be called twice for the same (oldrev -> newrev) pair.

This triggers fatal error "cache for <hash> already exists"

Bugfix is to make cache_set() idempotent when the same
(oldrev -> newrev) pair appears multiple times.

Signed-off-by: Roland Conybeare <rconybeare@gmail.com>
---
 contrib/subtree/git-subtree.sh | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/contrib/subtree/git-subtree.sh b/contrib/subtree/git-subtree.sh
index 791fd8260c..64590e05e0 100755
--- a/contrib/subtree/git-subtree.sh
+++ b/contrib/subtree/git-subtree.sh
@@ -343,7 +343,13 @@ cache_set () {
 		test "$oldrev" != "latest_new" &&
 		test -e "$cachedir/$oldrev"
 	then
-		die "fatal: cache for $oldrev already exists!"
+		existing=$(cat "$cachedir/$oldrev")
+		if test "$existing" = "$newrev"
+		then
+			return
+		else
+			die "fatal: cache for $oldrev already exists!"
+		fi
 	fi
 	echo "$newrev" >"$cachedir/$oldrev"
 }
-- 
2.50.1


^ permalink raw reply related

* Re: [PATCH 1/2] t1092: test 'git restore' with sparse index
From: Junio C Hamano @ 2026-05-24 22:51 UTC (permalink / raw)
  To: Derrick Stolee via GitGitGadget; +Cc: git, Derrick Stolee
In-Reply-To: <7c56d038307d54929d9eaa9b8cb3cf26af181702.1779644412.git.gitgitgadget@gmail.com>

"Derrick Stolee via GitGitGadget" <gitgitgadget@gmail.com> writes:

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

Interesting.  So, ideally we should be able to say "we are doing
everything because the user gave us '.' from the top level of the
working tree, so let's see each entry and decide what to do.  Ah we
have this tree entry in this sparse index, and that is outside the
directories we are dealing with in this working tree that is
sparsely checked out, so we would skip", and for that we have no
need to expand the index.  But in reality, what happens is "OK, '.'
so we need to deal with everything. Let's expand.", which would
break the contents of such a "skipped" tree out to constituent
paths, all of which inherits the SKIP_WORKTREE bit to tell us that
these paths are outside the directories we are dealing with".

The end result in the working tree should be the same, but we
unnecessarily expand the index.  Correctness wins with a room for
improvement in the performance, which is what we want to see and
then improve ;-)  Nice.

> For now, establish the current behavior, including the sparse index
> expansion, in the t1092 test case as a baseline.
>
> Signed-off-by: Derrick Stolee <stolee@gmail.com>
> ---
>  t/t1092-sparse-checkout-compatibility.sh | 50 ++++++++++++++++++++++++
>  1 file changed, 50 insertions(+)
>
> diff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh
> index d98cb4ac11..d69434e7ab 100755
> --- a/t/t1092-sparse-checkout-compatibility.sh
> +++ b/t/t1092-sparse-checkout-compatibility.sh
> @@ -2573,4 +2573,54 @@ test_expect_success 'sparse-index is not expanded: merge-ours' '
>  	ensure_not_expanded merge -s ours merge-right
>  '
>  
> +test_expect_success 'restore --staged with sparse definition' '
> +	init_repos &&
> +
> +	# Stage changes within the sparse definition
> +	test_all_match git checkout -b restore-staged-1 base &&
> +	test_all_match git reset --soft update-deep &&
> +	test_all_match git restore --staged . &&
> +	test_all_match git status --porcelain=v2 &&
> +	test_all_match git diff --cached
> +'
> +
> +test_expect_success 'restore --staged with outside sparse definition' '
> +	init_repos &&
> +
> +	# Stage changes that include paths outside the sparse definition.
> +	# Although the working tree differs between full and sparse checkouts
> +	# after restore, the state of the index should be the same.
> +	test_all_match git checkout -b restore-staged-2 base &&
> +	test_all_match git reset --soft update-folder1 &&
> +	test_sparse_match git restore --staged . &&
> +	git -C full-checkout restore --staged . &&
> +	test_all_match git ls-files -s -- folder1 &&
> +	test_all_match git diff --cached -- folder1
> +'
> +
> +test_expect_success 'restore --staged with wildcards' '
> +	init_repos &&
> +
> +	test_all_match git checkout -b restore-staged-3 base &&
> +	test_all_match git reset --soft update-deep &&
> +	test_all_match git restore --staged "deep/*" &&
> +	test_all_match git status --porcelain=v2 &&
> +	test_all_match git diff --cached
> +'
> +
> +test_expect_success 'sparse-index is expanded: restore --staged' '
> +	init_repos &&
> +
> +	git -C sparse-index checkout -b restore-staged-exp base &&
> +	git -C sparse-index reset --soft update-folder1 &&
> +	ensure_expanded restore --staged .
> +'
> +
> +test_expect_success 'sparse-index is expanded: restore --source --staged' '
> +	init_repos &&
> +
> +	git -C sparse-index checkout -b restore-source-staged base &&
> +	ensure_expanded restore --source update-folder1 --staged .
> +'
> +
>  test_done

^ permalink raw reply

* Re: [PATCH 2/2] restore: avoid sparse index expansion
From: Junio C Hamano @ 2026-05-24 23:05 UTC (permalink / raw)
  To: Derrick Stolee via GitGitGadget; +Cc: git, Derrick Stolee
In-Reply-To: <47542cbd42eb13b63d0d852fb2f5bf967952b318.1779644412.git.gitgitgadget@gmail.com>

"Derrick Stolee via GitGitGadget" <gitgitgadget@gmail.com> writes:

> From: Derrick Stolee <stolee@gmail.com>
>
> Teach update_some() to handle sparse directory entries at the tree
> level rather than expanding the entire sparse index. When iterating a
> source tree during checkout/restore operations:
>
>  - If a directory matches a sparse directory entry with the same OID,
>    skip it entirely (no change needed).
>
>  - If the OID differs and we are in non-overlay mode (e.g., restore
>    --staged), update the sparse directory entry's OID in place. This
>    is semantically correct because non-overlay mode removes paths not
>    in the source tree anyway.
>
>  - In overlay mode (e.g., checkout <tree> -- .), fall through to
>    recursive descent so individual file entries are preserved
>    correctly.
>
> Also switch from index_name_pos() to index_name_pos_sparse() for
> individual file lookups to avoid triggering ensure_full_index() when
> the file is already individually tracked in the index.
>
> Update the test expectation in t1092 to assert that 'restore --staged'
> no longer expands the sparse index.
>
> Signed-off-by: Derrick Stolee <stolee@gmail.com>
> ---
>  builtin/checkout.c                       | 57 +++++++++++++++++++++---
>  t/t1092-sparse-checkout-compatibility.sh |  8 ++--
>  2 files changed, 55 insertions(+), 10 deletions(-)
>
> diff --git a/builtin/checkout.c b/builtin/checkout.c
> index 1345e8574a..67f03dea10 100644
> --- a/builtin/checkout.c
> +++ b/builtin/checkout.c
> @@ -31,6 +31,7 @@
>  #include "revision.h"
>  #include "sequencer.h"
>  #include "setup.h"
> +#include "sparse-index.h"
>  #include "strvec.h"
>  #include "submodule.h"
>  #include "symlinks.h"
> @@ -142,14 +143,56 @@ static int post_checkout_hook(struct commit *old_commit, struct commit *new_comm
>  }
>  
>  static int update_some(const struct object_id *oid, struct strbuf *base,
> -		       const char *pathname, unsigned mode, void *context UNUSED)
> +		       const char *pathname, unsigned mode, void *context)
>  {
>  	int len;
>  	struct cache_entry *ce;
>  	int pos;
> +	int overlay_mode = context ? *((int *)context) : 1;
>  
> -	if (S_ISDIR(mode))
> +	if (S_ISDIR(mode)) {
> +		/*
> +		 * If this directory exists as a sparse directory entry in
> +		 * the index, we can handle it at the tree level without
> +		 * descending into individual files.
> +		 */
> +		if (the_repository->index->sparse_index) {

I wonder if this deep nesting is a sign that the newly added code
from here to ...

> +			struct strbuf dirpath = STRBUF_INIT;
> +
> +			strbuf_addbuf(&dirpath, base);
> +			strbuf_addstr(&dirpath, pathname);
> +			strbuf_addch(&dirpath, '/');
> +
> +			pos = index_name_pos_sparse(the_repository->index,
> +						    dirpath.buf, dirpath.len);
> +			if (pos >= 0) {
> +				struct cache_entry *old =
> +					the_repository->index->cache[pos];
> +				if (S_ISSPARSEDIR(old->ce_mode)) {
> +					if (oideq(oid, &old->oid)) {
> +						strbuf_release(&dirpath);
> +						return 0;
> +					}
> +					if (!overlay_mode) {
> +						/*
> +						 * In non-overlay mode (e.g.,
> +						 * restore --staged), we can
> +						 * replace the sparse dir OID
> +						 * directly since files not in
> +						 * the source tree should be
> +						 * removed anyway.
> +						 */
> +						oidcpy(&old->oid, oid);
> +						old->ce_flags |= CE_UPDATE;
> +						strbuf_release(&dirpath);
> +						return 0;
> +					}
> +				}
> +			}
> +			strbuf_release(&dirpath);
> +		}

... here may become easier to understand if it is made into a small
helper function with a descriptive name.

>  		return READ_TREE_RECURSIVE;
> +	}
>  
>  	len = base->len + strlen(pathname);
>  	ce = make_empty_cache_entry(the_repository->index, len);
> @@ -165,7 +208,7 @@ static int update_some(const struct object_id *oid, struct strbuf *base,
>  	 * entry in place. Whether it is UPTODATE or not, checkout_entry will
>  	 * do the right thing.
>  	 */
> -	pos = index_name_pos(the_repository->index, ce->name, ce->ce_namelen);
> +	pos = index_name_pos_sparse(the_repository->index, ce->name, ce->ce_namelen);
>  	if (pos >= 0) {
>  		struct cache_entry *old = the_repository->index->cache[pos];
>  		if (ce->ce_mode == old->ce_mode &&
> @@ -182,10 +225,11 @@ static int update_some(const struct object_id *oid, struct strbuf *base,
>  	return 0;
>  }
>  
> -static int read_tree_some(struct tree *tree, const struct pathspec *pathspec)
> +static int read_tree_some(struct tree *tree, const struct pathspec *pathspec,
> +			  int overlay_mode)
>  {
>  	read_tree(the_repository, tree,
> -		  pathspec, update_some, NULL);
> +		  pathspec, update_some, &overlay_mode);
>  
>  	/* update the index with the given tree's info
>  	 * for all args, expanding wildcards, and exit
> @@ -580,7 +624,8 @@ static int checkout_paths(const struct checkout_opts *opts,
>  		return error(_("index file corrupt"));
>  
>  	if (opts->source_tree)
> -		read_tree_some(opts->source_tree, &opts->pathspec);
> +		read_tree_some(opts->source_tree, &opts->pathspec,
> +			       opts->overlay_mode);
>  	if (opts->merge)
>  		unmerge_index(the_repository->index, &opts->pathspec, CE_MATCHED);
>  
> diff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh
> index d69434e7ab..8186da5c88 100755
> --- a/t/t1092-sparse-checkout-compatibility.sh
> +++ b/t/t1092-sparse-checkout-compatibility.sh
> @@ -2608,19 +2608,19 @@ test_expect_success 'restore --staged with wildcards' '
>  	test_all_match git diff --cached
>  '
>  
> -test_expect_success 'sparse-index is expanded: restore --staged' '
> +test_expect_success 'sparse-index is not expanded: restore --staged' '
>  	init_repos &&
>  
>  	git -C sparse-index checkout -b restore-staged-exp base &&
>  	git -C sparse-index reset --soft update-folder1 &&
> -	ensure_expanded restore --staged .
> +	ensure_not_expanded restore --staged .
>  '
>  
> -test_expect_success 'sparse-index is expanded: restore --source --staged' '
> +test_expect_success 'sparse-index is not expanded: restore --source --staged' '
>  	init_repos &&
>  
>  	git -C sparse-index checkout -b restore-source-staged base &&
> -	ensure_expanded restore --source update-folder1 --staged .
> +	ensure_not_expanded restore --source update-folder1 --staged .
>  '

Very nice.

^ permalink raw reply

* Re: [PATCH 1/3] commit-reach: deduplicate queue entries in paint_down_to_common
From: Junio C Hamano @ 2026-05-24 23:40 UTC (permalink / raw)
  To: Kristofer Karlsson via GitGitGadget; +Cc: git, Kristofer Karlsson
In-Reply-To: <1d3751569ba3a5f0c353fb468578d6c5bcd0b738.1779644541.git.gitgitgadget@gmail.com>

"Kristofer Karlsson via GitGitGadget" <gitgitgadget@gmail.com>
writes:

> diff --git a/commit-reach.c b/commit-reach.c
> index d3a9b3ed6f..c16d4b061c 100644
> --- a/commit-reach.c
> +++ b/commit-reach.c
> @@ -17,8 +17,9 @@
>  #define PARENT2		(1u<<17)
>  #define STALE		(1u<<18)
>  #define RESULT		(1u<<19)
> +#define ENQUEUED	(1u<<20)
>  
> -static const unsigned all_flags = (PARENT1 | PARENT2 | STALE | RESULT);
> +static const unsigned all_flags = (PARENT1 | PARENT2 | STALE | RESULT | ENQUEUED);
> ...
> diff --git a/object.h b/object.h
> index d814647ebe..05cbf728e9 100644
> --- a/object.h
> +++ b/object.h
> @@ -74,7 +74,7 @@ void object_array_init(struct object_array *array);
>   * bundle.c:                                        16
>   * http-push.c:                          11-----14
>   * commit-graph.c:                                15
> - * commit-reach.c:                                  16-----19
> + * commit-reach.c:                                  16-------20
>   * builtin/last-modified.c:                         1617
>   * sha1-name.c:                                              20
>   * list-objects-filter.c:                                      21

Not directly the fault of this series, but we'd need to audit and
update this table of bit assignment to match more recent reality.

For example, there no longer exists sha1-name.c but the table claims
that bit 20 is in use for its own purpose, and it being stale makes
it harder to audit and ensure that this new use would not crash with
these existing uses (note. there are other uses of bit 20 in other
subsystems).

FWIW, object-name.c, which was formerly known as sha1-name.c, uses
the bit 20 as ONELINE_SEEN bit, which is used to turn textual object
names like :/string (i.e., commit with that string in its message)
into raw object name, and bit 20 is cleared from all the objects
involved in the search before the helper function returns.
Presumably, once commit-reach.c starts queueing commits and reuses
this bit for its own purpose, we will never try to parse a textual
commit object name to clobber what we thought is ENQUEUED bit,
breaking the code introduced here, so we are probably safe against
its use.

I didn't check all other uses of bit 20, though.


^ permalink raw reply

* How does git track history overwrites?
From: Jens Tröger @ 2026-05-24 23:41 UTC (permalink / raw)
  To: git

Hello,

I’m looking for details and some clarification on a `git fetch` behavior I observed, but can’t quite explain. More context is in this Github comment:

  https://github.com/jenstroeger/python-package-template/pull/1190#discussion_r3288253713

but it boils down to this:

  /tmp/bla > git -c protocol.version=2 fetch origin dda8db18cfc68df532abf33b185ecd12d5b7b326 --depth=1

It seems that sha dda8db1 (tag 1.20.0 previously pointed at it) was replaced due to a suspected history overwrite with fda7769 (tag 1.20.0 now points at it) and git figures that out:

  ...

  From https://github.com/adamchainz/blacken-docs
  * branch dda8db18cfc68df532abf33b185ecd12d5b7b326 -> FETCH_HEAD

And then:

  /tmp/bla > git checkout FETCH_HEAD
  Note: switching to 'FETCH_HEAD’

  ...

  HEAD is now at fda7769 Version 1.20.0

And:

  /tmp/bla > cat .git/HEAD 
  fda77690955e9b63c6687d8806bafd56a526e45f
  /tmp/bla > cat .git/FETCH_HEAD 
  dda8db18cfc68df532abf33b185ecd12d5b7b326 'dda8db18cfc68df532abf33b185ecd12d5b7b326' of https://github.com/adamchainz/blacken-docs

I’d like to understand the details some more, and how I could manually make that connection?

Thank you!
Jens


^ permalink raw reply

* Re: [PATCH v5 00/13] pack-objects: integrate --path-walk and some --filter options
From: Junio C Hamano @ 2026-05-24 23:44 UTC (permalink / raw)
  To: Taylor Blau
  Cc: Derrick Stolee via GitGitGadget, git, christian.couder,
	johannes.schindelin, johncai86, karthik.188, kristofferhaugsbakk,
	newren, peff, ps, Derrick Stolee
In-Reply-To: <ahDbS+CtwsGx62Q3@nand.local>

Taylor Blau <me@ttaylorr.com> writes:

> On Fri, May 22, 2026 at 06:24:24PM +0000, Derrick Stolee via GitGitGadget wrote:
>> Range-diff vs v4:
>>
>>   1:  0840110116 =  1:  0840110116 t5620: make test work with path-walk var
>>   2:  d7c87545f3 =  2:  d7c87545f3 pack-objects: pass --objects with --path-walk
>>   3:  fb8a0f9c43 !  3:  697ef716d2 t/perf: add pack-objects filter and path-walk benchmark
>>      @@ t/perf/p5315-pack-objects-filter.sh (new)
>>       +		awk "{print \$4;}" >top-dirs &&
>>       +	top_nr=$(wc -l <top-dirs) &&
>>       +
>>      -+	>depth2-dirs &&
>>       +	while read tdir
>>       +	do
>>      -+		git ls-tree -d --name-only "HEAD:$tdir" 2>/dev/null || return 1
>>      -+	done <top-dirs >depth2-dirs.raw &&
>>      -+	sed "s|^|$tdir/|" <depth2-dirs.raw >depth2-dirs &&
>>      ++		git ls-tree -d --format="$tdir/%(path)" "HEAD:$tdir" || return 1
>>      ++	done <top-dirs >depth2-dirs &&
>>       +
>>       +	d2_nr=$(wc -l <depth2-dirs) &&
>>       +
>>   4:  e77c8a6bbc =  4:  91845bcef0 path-walk: always emit directly-requested objects
>>   5:  f4904f81e0 =  5:  fdb9361198 path-walk: support blobless filter
>>   6:  f37467e46f =  6:  89726faf7e backfill: die on incompatible filter options
>>   7:  133c1b156c =  7:  3884d4737f path-walk: support blob size limit filter
>>   8:  0f517be8e3 =  8:  31b4ef0fa1 path-walk: add pl_sparse_trees to control tree pruning
>>   9:  b4dc09ab69 =  9:  7d8f0aa036 pack-objects: support sparse:oid filter with path-walk
>>  10:  0b1eed0790 = 10:  a68676d0de t6601: tag otherwise-unreachable trees
>>  11:  b23244c4c2 = 11:  b0db73c6cc path-walk: support `tree:0` filter
>>  12:  7e1e503361 = 12:  6845988f50 path-walk: support `object:type` filter
>>  13:  a615b1a707 = 13:  d33d899251 path-walk: support `combine` filter
>
> The range-diff looks good to me. Thanks!

Good.  And as you in <ahDbS+CtwsGx62Q3@nand.local> were already
happy with everything else in the previous iteration of this series,
not just the changes the range-diff output shows, but also the
non-changes from the previous iteration, look good to you ;-).

Let's mark the topic for 'next'.  Thanks, both.

^ permalink raw reply

* Re: [PATCH] receive-pack: fix updateInstead with core.worktree
From: Junio C Hamano @ 2026-05-25  0:20 UTC (permalink / raw)
  To: Alyssa Ross; +Cc: git, Ævar Arnfjörð Bjarmason
In-Reply-To: <20260522154418.5883-1-hi@alyssa.is>

Alyssa Ross <hi@alyssa.is> writes:

> This used to work, but when push_to_checkout() started being called
> before push_to_deploy(), ...

We tend to try describing where things started breaking a bit more
precisely.  The above seems to say that you know that in the past
push_to__checkout() was not called before push_to_deploy(), and it
no longer is the case these days?  Can you spell out in what commit
that change happened (refer to the commit using the "git show -s
--pretty=reference" format)?  I.e.

	... but when X started doing Y at a8cc5943 (hooks: fix an
	obscure TOCTOU "did we just run a hook?" race, 2022-03-07),
	<<this bad thing>> started to happen.

It isn't really we are exercising "checkout" and "deploy" both at
the same time, but an old commit started to always call _checkout
only to see if that actually invokes the hook, and if it didn't,
then call _deploy.  The intent still is to use either one of these,
but as you exactly identified what is wrong in the current code, the
call to _checkout that is only done to probe if it is used at all
started to contaminate the environment with that commit.

So this change ...

> -	strvec_pushf(env, "GIT_WORK_TREE=%s", absolute_path(work_tree));
>  	strvec_pushv(&opt.env, env->v);
> +	strvec_pushf(&opt.env, "GIT_WORK_TREE=%s", absolute_path(work_tree));
>  	strvec_push(&opt.args, hash_to_hex(hash));

... looks like absolutely the right thing to do.  And ...

>  	if (run_hooks_opt(the_repository, push_to_checkout_hook, &opt))
>  		return "push-to-checkout hook declined";
> diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
> index 117cfa051f..f51fb11a6d 100755
> --- a/t/t5516-fetch-push.sh
> +++ b/t/t5516-fetch-push.sh
> @@ -1791,6 +1791,17 @@ test_expect_success 'updateInstead with push-to-checkout hook' '
>  	)
>  '
>  
> +test_expect_success 'denyCurrentBranch and core.worktree' '
> +	test_when_finished "rm -fr cloned cloned.git" &&
> +	git clone --separate-git-dir cloned.git . cloned &&
> +	git --git-dir cloned.git config receive.denyCurrentBranch updateInstead &&
> +	git --git-dir cloned.git config core.worktree "$PWD/cloned" &&
> +        test_commit raspberry &&
> +	git push cloned.git HEAD:main &&
> +	test_path_exists cloned/raspberry.t &&
> +	test_must_fail git push --delete cloned.git main
> +'

... a test that protects similar breakage in the future is also
excellent.

>  test_expect_success 'denyCurrentBranch and worktrees' '
>  	test_when_finished "rm -fr cloned && git worktree remove --force new-wt" &&
>  	git worktree add new-wt &&
>
> base-commit: aec3f587505a472db67e9462d0702e7d463a449d

^ permalink raw reply

* Re: I discovered a minor issue with `git fetch`.
From: brian m. carlson @ 2026-05-25  0:45 UTC (permalink / raw)
  To: SURA; +Cc: git
In-Reply-To: <CAD6AYr9YmcnkdW=Nx=HUKcuaNbv1ukrAbXRnKyGibCQDy8N3hQ@mail.gmail.com>

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

On 2026-05-22 at 07:45:25, SURA wrote:
> Hello everyone
> 
> The child processes spawned by `git fetch` can become zombie processes.
> In most scenarios, these zombie processes are reaped by Process 1, so
> this typically doesn't cause any problems.
> 
> However, within a Docker container, the application service itself is
> sometimes designated as Process 1 (for instance, a service written in
> Go). Since these application services lack the capability to reap
> zombie processes, the zombies will gradually exhaust the available PID
> resources.
> This issue was discovered within a legacy service. A few days after
>
> upgrading to Git 2.53.0, the system's PID resources were exhausted by
> zombie processes. This is likely the result of recent changes, as this
> problem did not exist in earlier versions (2.4x).
> 
> To be honest, this is not an urgent matter; I have already deployed
> `tini` as the init process (PID 1) to prevent the service from
> becoming unavailable.

While there has been some discussion about this on the list in the
recent past, using something like tini is the right move in the general
case.  There are a variety of programs which might daemonize a
background process for whatever reason and those will necessarily
require an init process to reap children.  It's considered a standard
requirement that PID 1 has that ability and Git doesn't provide it.
-- 
brian m. carlson (they/them)
Toronto, Ontario, CA

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

^ permalink raw reply

* Re: [PATCH 1/3] commit-reach: deduplicate queue entries in paint_down_to_common
From: Derrick Stolee @ 2026-05-25  1:43 UTC (permalink / raw)
  To: Junio C Hamano, Kristofer Karlsson via GitGitGadget
  Cc: git, Kristofer Karlsson
In-Reply-To: <xmqqpl2kgyvy.fsf@gitster.g>

On 5/24/26 7:40 PM, Junio C Hamano wrote:
> "Kristofer Karlsson via GitGitGadget" <gitgitgadget@gmail.com>
> writes:
> 
>> diff --git a/commit-reach.c b/commit-reach.c
>> index d3a9b3ed6f..c16d4b061c 100644
>> --- a/commit-reach.c
>> +++ b/commit-reach.c
>> @@ -17,8 +17,9 @@
>>   #define PARENT2		(1u<<17)
>>   #define STALE		(1u<<18)
>>   #define RESULT		(1u<<19)
>> +#define ENQUEUED	(1u<<20)
>>   
>> -static const unsigned all_flags = (PARENT1 | PARENT2 | STALE | RESULT);
>> +static const unsigned all_flags = (PARENT1 | PARENT2 | STALE | RESULT | ENQUEUED);
>> ...
>> diff --git a/object.h b/object.h
>> index d814647ebe..05cbf728e9 100644
>> --- a/object.h
>> +++ b/object.h
>> @@ -74,7 +74,7 @@ void object_array_init(struct object_array *array);
>>    * bundle.c:                                        16
>>    * http-push.c:                          11-----14
>>    * commit-graph.c:                                15
>> - * commit-reach.c:                                  16-----19
>> + * commit-reach.c:                                  16-------20
>>    * builtin/last-modified.c:                         1617
>>    * sha1-name.c:                                              20
>>    * list-objects-filter.c:                                      21
> 
> Not directly the fault of this series, but we'd need to audit and
> update this table of bit assignment to match more recent reality.
> 
> For example, there no longer exists sha1-name.c but the table claims
> that bit 20 is in use for its own purpose, and it being stale makes
> it harder to audit and ensure that this new use would not crash with
> these existing uses (note. there are other uses of bit 20 in other
> subsystems).

It would be worth adding an update patch before this patch, that
only makes these adjustments

> FWIW, object-name.c, which was formerly known as sha1-name.c, uses
> the bit 20 as ONELINE_SEEN bit, which is used to turn textual object
> names like :/string (i.e., commit with that string in its message)
> into raw object name, and bit 20 is cleared from all the objects
> involved in the search before the helper function returns.

This appears to me like the only interaction that _could_ have
overlap with paint_down_to_common().

> Presumably, once commit-reach.c starts queueing commits and reuses
> this bit for its own purpose, we will never try to parse a textual
> commit object name to clobber what we thought is ENQUEUED bit,
> breaking the code introduced here, so we are probably safe against
> its use.
> 
> I didn't check all other uses of bit 20, though.

FLAG_LINK in builtin/index-pack.c and FLAG_OPEN in
builtin/unpack-objects.c both seem to be completely independent from
this use in commit-reach.c.

Thanks,
-Stolee




^ permalink raw reply

* Re: [PATCH 2/3] commit-reach: optimize queue scan in paint_down_to_common
From: Derrick Stolee @ 2026-05-25  1:59 UTC (permalink / raw)
  To: Kristofer Karlsson via GitGitGadget, git; +Cc: Kristofer Karlsson
In-Reply-To: <4742f5e634b55820f3b5a626ec97e24617fdae3d.1779644541.git.gitgitgadget@gmail.com>

On 5/24/26 1:42 PM, Kristofer Karlsson via GitGitGadget wrote:
> From: Kristofer Karlsson <krka@spotify.com>
> 
> paint_down_to_common() terminates when every commit remaining in its
> priority queue is STALE. This was checked by queue_has_nonstale(),
> which performed an O(n) linear scan of the entire queue on every
> iteration, resulting in O(n*m) total overhead where n is the queue
> size and m is the number of commits processed.
> 
> Replace this with an O(1) nonstale_count that tracks the number of
> non-stale commits currently in the queue. The counter is incremented
> by maybe_enqueue() and decremented on dequeue and by mark_stale()
> when a commit transitions to STALE while still in the queue. Since
> each commit appears at most once (guaranteed by the ENQUEUED flag
> from the previous commit), the counter is exact.

This idea has a lot of merit, but I'm a bit concerned about the
organization of data. My ideas of how to improve things may also
impact patch 1's use of ENQUEUED.

> -static void maybe_enqueue(struct prio_queue *queue, struct commit *c)
> +static void maybe_enqueue(struct prio_queue *queue, struct commit *c,
> +			  int *nonstale_count)
>   {
>   	if (c->object.flags & ENQUEUED)
>   		return;
>   	c->object.flags |= ENQUEUED;
>   	prio_queue_put(queue, c);
> +	if (!(c->object.flags & STALE))
> +		(*nonstale_count)++;
> +}
> +
> +static void mark_stale(struct commit *c, unsigned queued_flag,
> +		       int *nonstale_count)
> +{
> +	if (!(c->object.flags & STALE)) {
> +		if (c->object.flags & queued_flag)
> +			(*nonstale_count)--;
> +		c->object.flags |= STALE;
> +	}
>   }

These two methods have some concerns on my end:

1. We need to store the nonstale count somewhere other than the
    priority queue, even though it's necessarily representing a
    subset of the commits within the queue.

2. mark_stale() needs a queued_flag. (I need to check to see if
    this is indeed changing in multiple callers or should always
    be ENQUEUED).

>   static int queue_has_nonstale(struct prio_queue *queue)
> @@ -68,6 +81,7 @@ static int paint_down_to_common(struct repository *r,
>   {
>   	struct prio_queue queue = { compare_commits_by_gen_then_commit_date };
>   	int i;
> +	int nonstale_count = 0;

My preference would be to create a new struct that contains a
prio_queue as a member _and_ a nonstale_count. It could initialize
with compare_commits_by_gen_then_commit_date by default.

The important thing is that consumers of such a "stale-tracking"
queue would not be setting the STALE or ENQUEUED bits themselves,
but instead the queue would be responsible for that.

This could allow us to simplify callers by always assuming we can
"add" an element to the queue and the queue will use its ENQUEUED
bit to prevent duplicates from reaching its internal prio_queue.

Such a data structure could be private to commit-reach.c for now,
since all the methods that would use it seem to be colocated there.

This is a big ask, but I'm interested to see if such an approach
would simplify things here.

Here's a potential breakdown of how to build such a thing in
"small" patches:

1. Create the data structure and update paint_down_to_common and
    ahead_behind to use that structure, but still use the existing
    prio_queue methods on its internal member.

2. Add the ENQUEUED bit and methods on the new struct that add
    that bit as it adds commits to the inner prio_queue. It would
    also ignore commits that already have that bit. (Should it
    also remove the bit as commits are removed from the queue?)

3. Now add the nonstale_count (or stale count?) to the struct and
    have it control the STALE bit modifications, with increasing
    the stale count when ENQUEUED is live, and decreasing the stale
    count as such a STALE object is dequeued.

I like the idea of this being encapsulated within the struct and
its helper methods. But the proof will be in the implementation.

Thanks,
-Stolee


^ permalink raw reply

* Re: [PATCH v4 0/2] includeIf: add "worktree" condition for matching working tree path
From: Junio C Hamano @ 2026-05-25  2:14 UTC (permalink / raw)
  To: Chen Linxuan via B4 Relay
  Cc: git, Kristoffer Haugsbakk, Patrick Steinhardt, Chen Linxuan,
	Phillip Wood
In-Reply-To: <20260513-includeif-worktree-v4-0-f8e6212d1fba@black-desk.cn>

Chen Linxuan via B4 Relay <devnull+me.black-desk.cn@kernel.org>
writes:

> The `includeIf` mechanism already supports matching on the `.git`
> directory path (`gitdir`) and the currently checked out branch
> (`onbranch`).  But in multi-worktree setups the `.git` directory of a
> linked worktree points into the main repository's `.git/worktrees/`
> area, which makes `gitdir` patterns cumbersome when one wants to
> include config based on the working tree's checkout path instead.
>
> Introduce two new condition keywords:
>
>   - `worktree:<pattern>` matches the realpath of the current worktree's
>     working directory against a glob pattern.
>   - `worktree/i:<pattern>` is the case-insensitive variant.
>
> Supported pattern features: glob wildcards, `**/` and `/**`, `~`
> expansion, `./` relative paths, and trailing-`/` prefix matching.
> The condition never matches in a bare repository.
>
> Signed-off-by: Chen Linxuan <me@black-desk.cn>
> ---

The test in this series fails in GitHub CI for Windows, it seems.

https://github.com/git/git/actions/runs/26377220573/job/77639885088


^ permalink raw reply

* Re: [PATCH v4 0/2] includeIf: add "worktree" condition for matching working tree path
From: Chen Linxuan @ 2026-05-25  2:36 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Chen Linxuan via B4 Relay, git, Kristoffer Haugsbakk,
	Patrick Steinhardt, Chen Linxuan, Phillip Wood
In-Reply-To: <xmqqbje4grra.fsf@gitster.g>

On Mon, May 25, 2026 at 10:14 AM Junio C Hamano <gitster@pobox.com> wrote:
>
> Chen Linxuan via B4 Relay <devnull+me.black-desk.cn@kernel.org>
> writes:
>
> > The `includeIf` mechanism already supports matching on the `.git`
> > directory path (`gitdir`) and the currently checked out branch
> > (`onbranch`).  But in multi-worktree setups the `.git` directory of a
> > linked worktree points into the main repository's `.git/worktrees/`
> > area, which makes `gitdir` patterns cumbersome when one wants to
> > include config based on the working tree's checkout path instead.
> >
> > Introduce two new condition keywords:
> >
> >   - `worktree:<pattern>` matches the realpath of the current worktree's
> >     working directory against a glob pattern.
> >   - `worktree/i:<pattern>` is the case-insensitive variant.
> >
> > Supported pattern features: glob wildcards, `**/` and `/**`, `~`
> > expansion, `./` relative paths, and trailing-`/` prefix matching.
> > The condition never matches in a bare repository.
> >
> > Signed-off-by: Chen Linxuan <me@black-desk.cn>
> > ---
>
> The test in this series fails in GitHub CI for Windows, it seems.
>
> https://github.com/git/git/actions/runs/26377220573/job/77639885088

It seems that "includeIf.worktree:/.path" not working on windows.

Will be updated in V5

>
>

^ permalink raw reply

* [PATCH] SubmittingPatches: proactively monitor GHCI pages
From: Junio C Hamano @ 2026-05-25  2:58 UTC (permalink / raw)
  To: git

Even those contributors who do not come from GGG and do not first
push their changes to their repositories on GitHub with CI enabled,
can still monitor the CI runs triggered by integration of their
topic to 'seen' and other branches to notice a breakage their topic
caused to the system.

Encourage them to help the project by keeping an eye on these CI
runs.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 Documentation/SubmittingPatches | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git c/Documentation/SubmittingPatches w/Documentation/SubmittingPatches
index e270ccbe85..ad2dce1998 100644
--- c/Documentation/SubmittingPatches
+++ w/Documentation/SubmittingPatches
@@ -792,6 +792,17 @@ relevant for debugging.
 Then fix the problem and push your fix to your GitHub fork. This will
 trigger a new CI build to ensure all tests pass.
 
+Even if you do not use GitHub CI to test your changes, pay close
+attention to new failures on the branches when the maintainer pushes
+out after your topic gets merged to the 'seen' branch to make sure
+that your topic is not breaking the CI, and retract your breaking
+topic quickly while you fix the breakage you caused.
+
+To see maintainer's push, keep an eye on this page:
+
+  `https://github.com/git/git/actions/workflows/main.yml?query=event%3Apush+actor%3Agitster`
+
+
 [[mua]]
 == MUA specific hints
 

^ permalink raw reply related

* Re: [PATCH v4 0/2] includeIf: add "worktree" condition for matching working tree path
From: Junio C Hamano @ 2026-05-25  3:01 UTC (permalink / raw)
  To: Chen Linxuan
  Cc: Chen Linxuan via B4 Relay, git, Kristoffer Haugsbakk,
	Patrick Steinhardt, Phillip Wood
In-Reply-To: <CAC1kPDNKfm9Q=FWJkvpUSBmpmxL+RaOCifST8p=ViDwqVceNsg@mail.gmail.com>

Chen Linxuan <me@black-desk.cn> writes:

> On Mon, May 25, 2026 at 10:14 AM Junio C Hamano <gitster@pobox.com> wrote:
>>
>> Chen Linxuan via B4 Relay <devnull+me.black-desk.cn@kernel.org>
>> writes:
>>
>> > The `includeIf` mechanism already supports matching on the `.git`
>> > directory path (`gitdir`) and the currently checked out branch
>> > (`onbranch`).  But in multi-worktree setups the `.git` directory of a
>> > linked worktree points into the main repository's `.git/worktrees/`
>> > area, which makes `gitdir` patterns cumbersome when one wants to
>> > include config based on the working tree's checkout path instead.
>> >
>> > Introduce two new condition keywords:
>> >
>> >   - `worktree:<pattern>` matches the realpath of the current worktree's
>> >     working directory against a glob pattern.
>> >   - `worktree/i:<pattern>` is the case-insensitive variant.
>> >
>> > Supported pattern features: glob wildcards, `**/` and `/**`, `~`
>> > expansion, `./` relative paths, and trailing-`/` prefix matching.
>> > The condition never matches in a bare repository.
>> >
>> > Signed-off-by: Chen Linxuan <me@black-desk.cn>
>> > ---
>>
>> The test in this series fails in GitHub CI for Windows, it seems.
>>
>> https://github.com/git/git/actions/runs/26377220573/job/77639885088
>
> It seems that "includeIf.worktree:/.path" not working on windows.
>
> Will be updated in V5

The topic has long been merged to 'next', but I've reverted the
topic out of 'next', so I can take a new iteration.

Thanks.

^ permalink raw reply

* [PATCH v5 1/2] config: refactor include_by_gitdir() into include_by_path()
From: Chen Linxuan via B4 Relay @ 2026-05-25  3:20 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Junio C Hamano, Patrick Steinhardt,
	Chen Linxuan, Phillip Wood
In-Reply-To: <20260525-includeif-worktree-v5-0-1efe525d025a@black-desk.cn>

From: Chen Linxuan <me@black-desk.cn>

The include_by_gitdir() function matches the realpath of a given
path against a glob pattern, but its interface is tightly coupled to
the gitdir condition: it takes a struct config_options *opts and
extracts opts->git_dir internally.

Refactor it into a more generic include_by_path() helper that takes
a const char *path parameter directly, and update the gitdir and
gitdir/i callers to pass opts->git_dir explicitly.  No behavior
change, just preparing for the addition of a new worktree condition
that will reuse the same path-matching logic with a different path.

Signed-off-by: Chen Linxuan <me@black-desk.cn>
---
 config.c | 19 ++++++++-----------
 1 file changed, 8 insertions(+), 11 deletions(-)

diff --git a/config.c b/config.c
index a1b92fe083cf..d95e2804c29b 100644
--- a/config.c
+++ b/config.c
@@ -235,23 +235,20 @@ static int prepare_include_condition_pattern(const struct key_value_info *kvi,
 	return 0;
 }
 
-static int include_by_gitdir(const struct key_value_info *kvi,
-			     const struct config_options *opts,
-			     const char *cond, size_t cond_len, int icase)
+static int include_by_path(const struct key_value_info *kvi,
+			   const char *path,
+			   const char *cond, size_t cond_len, int icase)
 {
 	struct strbuf text = STRBUF_INIT;
 	struct strbuf pattern = STRBUF_INIT;
 	size_t prefix;
 	int ret = 0;
-	const char *git_dir;
 	int already_tried_absolute = 0;
 
-	if (opts->git_dir)
-		git_dir = opts->git_dir;
-	else
+	if (!path)
 		goto done;
 
-	strbuf_realpath(&text, git_dir, 1);
+	strbuf_realpath(&text, path, 1);
 	strbuf_add(&pattern, cond, cond_len);
 	ret = prepare_include_condition_pattern(kvi, &pattern, &prefix);
 	if (ret < 0)
@@ -284,7 +281,7 @@ static int include_by_gitdir(const struct key_value_info *kvi,
 		 * which'll do the right thing
 		 */
 		strbuf_reset(&text);
-		strbuf_add_absolute_path(&text, git_dir);
+		strbuf_add_absolute_path(&text, path);
 		already_tried_absolute = 1;
 		goto again;
 	}
@@ -400,9 +397,9 @@ static int include_condition_is_true(const struct key_value_info *kvi,
 	const struct config_options *opts = inc->opts;
 
 	if (skip_prefix_mem(cond, cond_len, "gitdir:", &cond, &cond_len))
-		return include_by_gitdir(kvi, opts, cond, cond_len, 0);
+		return include_by_path(kvi, opts->git_dir, cond, cond_len, 0);
 	else if (skip_prefix_mem(cond, cond_len, "gitdir/i:", &cond, &cond_len))
-		return include_by_gitdir(kvi, opts, cond, cond_len, 1);
+		return include_by_path(kvi, opts->git_dir, cond, cond_len, 1);
 	else if (skip_prefix_mem(cond, cond_len, "onbranch:", &cond, &cond_len))
 		return include_by_branch(inc, cond, cond_len);
 	else if (skip_prefix_mem(cond, cond_len, "hasconfig:remote.*.url:", &cond,

-- 
2.53.0



^ permalink raw reply related

* [PATCH v5 0/2] includeIf: add "worktree" condition for matching working tree path
From: Chen Linxuan via B4 Relay @ 2026-05-25  3:20 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Junio C Hamano, Patrick Steinhardt,
	Chen Linxuan, Phillip Wood

The `includeIf` mechanism already supports matching on the `.git`
directory path (`gitdir`) and the currently checked out branch
(`onbranch`).  But in multi-worktree setups the `.git` directory of a
linked worktree points into the main repository's `.git/worktrees/`
area, which makes `gitdir` patterns cumbersome when one wants to
include config based on the working tree's checkout path instead.

Introduce two new condition keywords:

  - `worktree:<pattern>` matches the realpath of the current worktree's
    working directory against a glob pattern.
  - `worktree/i:<pattern>` is the case-insensitive variant.

Supported pattern features: glob wildcards, `**/` and `/**`, `~`
expansion, `./` relative paths, and trailing-`/` prefix matching.
The condition never matches in a bare repository.

Signed-off-by: Chen Linxuan <me@black-desk.cn>
---
Changes in v5:
- Fix Windows CI failure: use `**` glob pattern instead of `/` in the
  "worktree without repository" tests, since `/` as a path pattern is
  Unix-specific and does not match Windows paths.
  Github CI pass: https://github.com/black-desk/git/actions/runs/26380466288
- Add a test verifying case-sensitive matching by default, with the
  `!CASE_INSENSITIVE_FS` prerequisite (suggested by Patrick Steinhardt).
- Link to v4: https://lore.kernel.org/r/20260513-includeif-worktree-v4-0-f8e6212d1fba@black-desk.cn

Changes in v4:
- Deduplicate the worktree pattern documentation by referencing the
  gitdir syntax instead of repeating the full pattern description
  (suggested by Patrick Steinhardt).
- Add documentation comparing includeIf "worktree:" with
  extensions.worktreeConfig, including a concrete use case example
  (suggested by Phillip Wood, Junio C Hamano).
- Add a test verifying that the worktree condition does not match
  during early config reading (suggested by Patrick Steinhardt).
- Add tests for the non-repository (nongit) scenario (suggested by
  Patrick Steinhardt).
- Add a test for the case-insensitive "worktree/i" variant
- Link to v3: https://lore.kernel.org/r/20260403-includeif-worktree-v3-0-109ce5782b03@black-desk.cn

Changes in v3:
- Apply Junio's suggestion.
- Link to v2: https://lore.kernel.org/r/20260402-includeif-worktree-v2-0-36e339b898d7@black-desk.cn

Changes in v2:

- Add missing signed-off-by lines.
- Link to v1: https://lore.kernel.org/r/20260401-includeif-worktree-v1-0-906db69f2c79@black-desk.cn

---
Chen Linxuan (2):
      config: refactor include_by_gitdir() into include_by_path()
      config: add "worktree" and "worktree/i" includeIf conditions

 Documentation/config.adoc |  48 ++++++++++++++++++
 config.c                  |  25 +++++----
 t/t1305-config-include.sh | 126 ++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 188 insertions(+), 11 deletions(-)
---
base-commit: 56a4f3c3a221adf1df9b39da69b8a6890f803157
change-id: 20260401-includeif-worktree-fcb64950dfba

Best regards,
-- 
Chen Linxuan <me@black-desk.cn>



^ permalink raw reply

* [PATCH v5 2/2] config: add "worktree" and "worktree/i" includeIf conditions
From: Chen Linxuan via B4 Relay @ 2026-05-25  3:20 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Junio C Hamano, Patrick Steinhardt,
	Chen Linxuan, Phillip Wood
In-Reply-To: <20260525-includeif-worktree-v5-0-1efe525d025a@black-desk.cn>

From: Chen Linxuan <me@black-desk.cn>

The includeIf mechanism already supports matching on the .git
directory path (gitdir) and the currently checked out branch
(onbranch).  But in multi-worktree setups the .git directory of a
linked worktree points into the main repository's .git/worktrees/
area, which makes gitdir patterns cumbersome when one wants to
include config based on the working tree's checkout path instead.

Introduce two new condition keywords:

  - worktree:<pattern> matches the realpath of the current worktree's
    working directory (i.e. repo_get_work_tree()) against a glob
    pattern.  This is the path returned by git rev-parse
    --show-toplevel.

  - worktree/i:<pattern> is the case-insensitive variant.

The implementation reuses the include_by_path() helper introduced in
the previous commit, passing the worktree path in place of the
gitdir.  The condition never matches in bare repositories (where
there is no worktree) or during early config reading (where no
repository is available).

Add documentation describing the new conditions, including a comparison
with extensions.worktreeConfig.  Add tests covering bare repositories,
multiple worktrees, symlinked worktree paths, case-sensitive and
case-insensitive matching, early config reading, and non-repository
scenarios.

Signed-off-by: Chen Linxuan <me@black-desk.cn>
---
 Documentation/config.adoc |  48 ++++++++++++++++++
 config.c                  |   6 +++
 t/t1305-config-include.sh | 126 ++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 180 insertions(+)

diff --git a/Documentation/config.adoc b/Documentation/config.adoc
index dcea3c0c15e2..ba800f67fbeb 100644
--- a/Documentation/config.adoc
+++ b/Documentation/config.adoc
@@ -146,6 +146,46 @@ refer to linkgit:gitignore[5] for details. For convenience:
 	This is the same as `gitdir` except that matching is done
 	case-insensitively (e.g. on case-insensitive file systems)
 
+`worktree`::
+	The data that follows the keyword `worktree` and a colon is used as a
+	glob pattern. If the working directory of the current worktree matches
+	the pattern, the include condition is met.
++
+The worktree location is the path where files are checked out (as returned
+by `git rev-parse --show-toplevel`). This is different from `gitdir`, which
+matches the `.git` directory path. In a linked worktree, the worktree path
+is the directory where that worktree's files are located, not the main
+repository's `.git` directory.
++
+The pattern uses the same glob syntax as `gitdir` (including `~/`, `./`,
+`**/`, and trailing-`/` prefix matching). This condition will never match
+in a bare repository (which has no worktree).
++
+This is useful when you want to apply configuration based on where the
+working tree is located on the filesystem. For example, a contributor who
+works on the same project both personally and as an employee can use
+different `user.name` and `user.email` values depending on which directory
+the worktree is checked out under:
++
+----
+[includeIf "worktree:/home/user/work/"]
+    path = ~/.config/git/work.inc
+[includeIf "worktree:/home/user/personal/"]
+    path = ~/.config/git/personal.inc
+----
++
+While `extensions.worktreeConfig` (see linkgit:git-worktree[1]) also supports
+per-worktree configuration, it stores the config inside each repository's
+`.git/config.worktree` file and requires running `git config --worktree`
+inside each worktree individually. In contrast, `includeIf "worktree:..."`
+can be set once in a global or system-level configuration file (e.g.
+`~/.config/git/config`) and applies to all repositories at once based on
+their worktree location.
+
+`worktree/i`::
+	This is the same as `worktree` except that matching is done
+	case-insensitively (e.g. on case-insensitive file systems)
+
 `onbranch`::
 	The data that follows the keyword `onbranch` and a colon is taken to be a
 	pattern with standard globbing wildcards and two additional
@@ -244,6 +284,14 @@ Example
 [includeIf "gitdir:~/to/group/"]
 	path = /path/to/foo.inc
 
+; include if the worktree is at /path/to/project-build
+[includeIf "worktree:/path/to/project-build"]
+	path = build-config.inc
+
+; include for all worktrees inside /path/to/group
+[includeIf "worktree:/path/to/group/"]
+	path = group-config.inc
+
 ; relative paths are always relative to the including
 ; file (if the condition is true); their location is not
 ; affected by the condition
diff --git a/config.c b/config.c
index d95e2804c29b..c250e56214d8 100644
--- a/config.c
+++ b/config.c
@@ -400,6 +400,12 @@ static int include_condition_is_true(const struct key_value_info *kvi,
 		return include_by_path(kvi, opts->git_dir, cond, cond_len, 0);
 	else if (skip_prefix_mem(cond, cond_len, "gitdir/i:", &cond, &cond_len))
 		return include_by_path(kvi, opts->git_dir, cond, cond_len, 1);
+	else if (skip_prefix_mem(cond, cond_len, "worktree:", &cond, &cond_len))
+		return include_by_path(kvi, inc->repo ? repo_get_work_tree(inc->repo) : NULL,
+				       cond, cond_len, 0);
+	else if (skip_prefix_mem(cond, cond_len, "worktree/i:", &cond, &cond_len))
+		return include_by_path(kvi, inc->repo ? repo_get_work_tree(inc->repo) : NULL,
+				       cond, cond_len, 1);
 	else if (skip_prefix_mem(cond, cond_len, "onbranch:", &cond, &cond_len))
 		return include_by_branch(inc, cond, cond_len);
 	else if (skip_prefix_mem(cond, cond_len, "hasconfig:remote.*.url:", &cond,
diff --git a/t/t1305-config-include.sh b/t/t1305-config-include.sh
index f3892578e4ff..9ca76e3408da 100755
--- a/t/t1305-config-include.sh
+++ b/t/t1305-config-include.sh
@@ -396,4 +396,130 @@ test_expect_success 'onbranch without repository but explicit nonexistent Git di
 	test_must_fail nongit git --git-dir=nonexistent config get foo.bar
 '
 
+# worktree: conditional include tests
+
+test_expect_success 'conditional include, worktree bare repo' '
+	git init --bare wt-bare &&
+	(
+		cd wt-bare &&
+		echo "[includeIf \"worktree:/\"]path=bar-bare" >>config &&
+		echo "[test]wtbare=1" >bar-bare &&
+		test_must_fail git config test.wtbare
+	)
+'
+
+test_expect_success 'conditional include, worktree multiple worktrees' '
+	git init wt-multi &&
+	(
+		cd wt-multi &&
+		test_commit initial &&
+		git worktree add -b linked-branch ../wt-linked HEAD &&
+		git worktree add -b prefix-branch ../wt-prefix/linked HEAD
+	) &&
+	wt_main="$(cd wt-multi && pwd)" &&
+	wt_linked="$(cd wt-linked && pwd)" &&
+	wt_prefix_parent="$(cd wt-prefix && pwd)" &&
+	cat >>wt-multi/.git/config <<-EOF &&
+	[includeIf "worktree:$wt_main"]
+		path = main-config
+	[includeIf "worktree:$wt_linked"]
+		path = linked-config
+	[includeIf "worktree:$wt_prefix_parent/"]
+		path = prefix-config
+	EOF
+	echo "[test]mainvar=main" >wt-multi/.git/main-config &&
+	echo "[test]linkedvar=linked" >wt-multi/.git/linked-config &&
+	echo "[test]prefixvar=prefix" >wt-multi/.git/prefix-config &&
+	echo main >expect &&
+	git -C wt-multi config test.mainvar >actual &&
+	test_cmp expect actual &&
+	test_must_fail git -C wt-multi config test.linkedvar &&
+	test_must_fail git -C wt-multi config test.prefixvar &&
+	echo linked >expect &&
+	git -C wt-linked config test.linkedvar >actual &&
+	test_cmp expect actual &&
+	test_must_fail git -C wt-linked config test.mainvar &&
+	test_must_fail git -C wt-linked config test.prefixvar &&
+	echo prefix >expect &&
+	git -C wt-prefix/linked config test.prefixvar >actual &&
+	test_cmp expect actual &&
+	test_must_fail git -C wt-prefix/linked config test.mainvar &&
+	test_must_fail git -C wt-prefix/linked config test.linkedvar
+'
+
+test_expect_success SYMLINKS 'conditional include, worktree resolves symlinks' '
+	mkdir real-wt &&
+	ln -s real-wt link-wt &&
+	git init link-wt/repo &&
+	(
+		cd link-wt/repo &&
+		# repo->worktree resolves symlinks, so use real path in pattern
+		echo "[includeIf \"worktree:**/real-wt/repo\"]path=bar-link" >>.git/config &&
+		echo "[test]wtlink=2" >.git/bar-link &&
+		echo 2 >expect &&
+		git config test.wtlink >actual &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success !CASE_INSENSITIVE_FS 'conditional include, worktree, case sensitive' '
+	git init wt-case &&
+	(
+		cd wt-case &&
+		test_commit initial &&
+		wt_path="$(pwd)" &&
+		wt_upper=$(echo "$wt_path" | tr a-z A-Z) &&
+		echo "[includeIf \"worktree:$wt_upper\"]path=case-inc" >>.git/config &&
+		echo "[test]wtcase=1" >.git/case-inc &&
+		test_must_fail git config test.wtcase
+	)
+'
+
+test_expect_success 'conditional include, worktree, icase' '
+	git init wt-icase &&
+	(
+		cd wt-icase &&
+		test_commit initial &&
+		wt_path="$(pwd)" &&
+		wt_upper=$(echo "$wt_path" | tr a-z A-Z) &&
+		echo "[includeIf \"worktree/i:$wt_upper\"]path=icase-inc" >>.git/config &&
+		echo "[test]wticase=1" >.git/icase-inc &&
+		echo 1 >expect &&
+		git config test.wticase >actual &&
+		test_cmp expect actual
+	)
+'
+
+# The "worktree" condition cannot match during early config reading
+# because the repository object is not yet fully initialized and
+# repo_get_work_tree() returns NULL.
+test_expect_success 'conditional include, worktree does not match in early config' '
+	git init wt-early &&
+	(
+		cd wt-early &&
+		test_commit initial &&
+		wt_path="$(pwd)" &&
+		echo "[includeIf \"worktree:$wt_path\"]path=early-inc" >>.git/config &&
+		echo "[test]wtearly=1" >.git/early-inc &&
+		test-tool config read_early_config test.wtearly >actual &&
+		test_must_be_empty actual
+	)
+'
+
+test_expect_success 'conditional include, worktree without repository' '
+	test_when_finished "rm -f .gitconfig config.inc" &&
+	git config set -f .gitconfig "includeIf.worktree:**.path" config.inc &&
+	git config set -f config.inc foo.bar baz &&
+	git config get foo.bar &&
+	test_must_fail nongit git config get foo.bar
+'
+
+test_expect_success 'conditional include, worktree without repository but explicit nonexistent Git directory' '
+	test_when_finished "rm -f .gitconfig config.inc" &&
+	git config set -f .gitconfig "includeIf.worktree:**.path" config.inc &&
+	git config set -f config.inc foo.bar baz &&
+	git config get foo.bar &&
+	test_must_fail nongit git --git-dir=nonexistent config get foo.bar
+'
+
 test_done

-- 
2.53.0



^ permalink raw reply related

* Re: How does git track history overwrites?
From: Chris Torek @ 2026-05-25  3:46 UTC (permalink / raw)
  To: Jens Tröger; +Cc: git
In-Reply-To: <089615C1-6526-4ADC-926A-6A232F330DA2@light-speed.de>

On Sun, May 24, 2026 at 4:44 PM Jens Tröger <jens.troeger@light-speed.de> wrote:
> I’m looking for details and some clarification on a `git fetch` behavior I observed, but can’t quite explain. ...

This isn't really specific to "git fetch" at all, except for the
usage of FETCH_HEAD.

To really understand this properly, we need to understand
the root of a seeming contradiction:

1. Once saved in Git, no commit (in fact, no internal object of any sort)
   can ever be changed.
2. And yet, "git rebase" and force-push operations seem to rewrite
   history.

How can commits be immutable and yet rewrite-able? The trick here
lies in how we (humans) *find* commits.

Inside a Git repository, the "true name" of any commit (or indeed
any internal object) is its raw hash ID, such as your example of
dda8db18cfc68df532abf33b185ecd12d5b7b326. The hash ID (or
"object ID", though right now there are only two forms, a SHA1
hash or a SHA256 hash) is specific to that one object once it is
created, and forever more can never be used for any other object.
It will always mean that original object, as long as that object
exists.

Thus, as long as that commit exists, it's *that* commit, with *that*
ID, and no other.

But we (humans) don't *use* hash IDs. They're too cumbersome.
So Git provides us with the ability to translate a name to an ID:

> It seems that sha dda8db1 (tag 1.20.0 previously pointed at it)

The *name* refs/tags/1.20.0 used to produce the above ID.

> was replaced ... with fda7769 (tag 1.20.0 now points at it)

Some human directed Git to forcibly replace the hash ID associated
with the tag, in some repository or repositories.

(As the manuals note, this kind of forcible replacement of tags is
often a bad idea. It's usually better, once the tag has escaped the
confinement of a single repository anyway, to just admit that you
goofed up and make a new tag.)

If you use raw hash IDs, you can never be bitten by this kind of
tag replacement, but of course that's a bad idea for different
(and presumably obvious) reasons. I couldn't possibly name the
hash ID without using cut-and-paste here. I can *type* "1.20.0"
repeatedly without error though.

(There are additional considerations, having to do with how Git
cleans up unwanted leftover junk, via git gc / git maintenance. In
particular Git uses the human-readable names to figure out which
objects are useful, and which are unwanted junk. So you have to
identify *some* commits with names, or they'll eventually get
garbage-collected.)

[At this point, you ran git fetch with a raw hash ID, and:]

>   From https://github.com/adamchainz/blacken-docs
>   * branch dda8db18cfc68df532abf33b185ecd12d5b7b326 -> FETCH_HEAD

When git fetch obtains something from another different Git repository,
the new things have the same IDs in both repositories. Normally we do
this by *name* (branch or tag name), but for historical reasons, the fetch
operation deposits a hash ID (often along with additional information)
 in the file `.git/FETCH_HEAD`. This file then works as a pseudo-name
for the branch, tag, or commit(s) thus obtained:

> And then:
>
>   /tmp/bla > git checkout FETCH_HEAD
>   Note: switching to 'FETCH_HEAD’

This gives you a "detached HEAD" state, using the hash ID stored in
.git/FETCH_HEAD. That hash ID will be overwritten (thus lost) by the
*next* git fetch, so you're expected to save it in some more-permanent
name if you want it to stick around.

The key difference between a branch name and a tag name is that
branch names are *expected* to map to different hash IDs over time,
with updates adding new commits to the branch causing the branch
name to remember the latest commit's ID. Each commit in turn
remembers the IDs of its parent commit or commits, so knowing
the *last* one suffices to allow Git to find *every* one.

Rewriting history with rebase consists of copying old (presumably
bad) commits to new (presumably good/better) ones, whose backwards
links to each previous commit chain through the new-and-improved
commits until you reach the point where the rewrite joins existing
history. Then we update the branch name to remember the latest
of the new-and-improved commits, and it *seems* that we've changed
history. The old history is still in there, and will stick around for quite
a while (at least a month by default, in standard clones) "just in case".

Tag names are not supposed to move, and whether someone else's tag
update to their clone changes your own clone's tags is something
you can control to some extent. It's not a good idea to depend on
other people's clones to follow tag changes, but it's also not a
good idea to depend on your own or other people's clones *not* to
follow such changes, since both behaviors are possible.

Chris

^ permalink raw reply


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