Git development
 help / color / mirror / Atom feed
* [PATCH] sequencer: honor --empty when a fixup!/squash! empties its target
From: Farid Zakaria @ 2026-07-10  4:13 UTC (permalink / raw)
  To: git
  Cc: Phillip Wood, Elijah Newren, Patrick Steinhardt, Junio C Hamano,
	Farid Zakaria

When "git rebase --autosquash" melds a "fixup!" or "squash!" commit into
its target, the result can be a commit that no longer changes anything
relative to its parent, for example when the melded change reverts the
target.  Rather than dropping or keeping this empty commit, the rebase
stops with

	You asked to amend the most recent commit, but doing so would
	make it empty. ...

and the "--empty" option has no effect on it.  This makes backing a
change out of a series awkward: reverting a commit as a "fixup!" and
running "git rebase --autosquash --empty=drop" ought to remove both the
commit and its revert, but it halts instead.

The reason is that allow_empty() decides emptiness with
is_index_unchanged(), which compares the index to HEAD.  A "fixup!" is
applied by amending HEAD, so the commit it produces has HEAD's parent as
its parent; it is empty when the index matches the tree of that parent,
not of HEAD.  A meld that cancels out its target is therefore never
recognized as having become empty, and falls through to "git commit
--amend", which refuses to create an empty commit.

Teach is_index_unchanged() to compare against the tree of HEAD's parent
when amending, and teach allow_empty() to classify the result as "became
empty" (and thus subject to --empty) unless the commit being melded into
was itself already empty, in which case it "started empty" and is
governed by allow_empty as before.

When --empty=drop applies, the emptied commit has already been created
by the preceding "pick", so drop it by moving HEAD back to its parent.
Do so before the rewritten-commit list is flushed, so that --update-refs
and the other rewrite consumers map the dropped commit to its parent.

Signed-off-by: Farid Zakaria <farid.m.zakaria@gmail.com>
---
At Meta we maintain a fork of LLVM that we regularly rebase onto
upstream.  A set of internal patches rides on top, and we keep each one
as a single commit by folding follow-up changes into it with autosquash
"fixup!" commits.  That works well for evolving a patch, but not for
retiring one: to back an internal patch out today we delete it from the
history by hand with an interactive rebase and then force-push, which is
easy to get wrong on a shared branch.

It would be nicer to retire a patch the same way we amend one: commit a
revert of it as a "fixup!" and let autosquash fold the two together.
The net change is empty, so the commit should just drop out of the
series.  Today it does not -- the rebase stops instead.

For example, starting from a commit we want to retire:

    $ git log --oneline
    4d5e6f7 add feature patch
    9a1b2c3 base

    # revert the feature and mark the revert as a fixup of it
    $ git revert --no-edit HEAD
    $ git commit --amend -m "fixup! add feature patch"

    $ git rebase -i --autosquash --empty=drop 9a1b2c3
    Rebasing (2/2)
    You asked to amend the most recent commit, but doing so would
    make it empty. You can repeat your command with --allow-empty [...]
    Could not apply 8e9f0a1... # fixup! add feature patch

The "--empty=drop" is ignored.  "--empty" only governs commits that are
picked empty, whereas a "fixup!" is applied by amending, and the
emptiness of an amended commit is measured against the wrong parent.  So
the rebase falls through to "git commit --amend", which refuses to
create an empty commit, and halts.

With this patch the emptied commit is recognized and handled according
to "--empty", the same as any other commit that becomes empty during a
rebase:

    $ git rebase -i --autosquash --empty=drop 9a1b2c3
    Rebasing (2/2)
    dropping 8e9f0a1... fixup! add feature patch -- resulting commit is empty
    Successfully rebased and updated refs/heads/main.

    $ git log --oneline
    9a1b2c3 base

"--empty=keep" retains it as an empty commit, and "--empty=stop" (the
default under "-i") halts so the user can decide -- matching how these
options already behave for commits that become empty when picked.

One open question, for a possible follow-up.  A natural next step would
be a "revert!" autosquash directive (and a "git commit --revert" to
create it), mirroring "fixup!"/"squash!", so
that retiring a patch would not require generating the reverse diff by
hand.  I have deliberately left it out of this series, because its
semantics are not obvious: in particular, whether a "revert!" commit
should carry the reverse patch as its own content (and thus be an
ordinary fixup that this patch already drops), or be an empty marker
that instructs the rebase to revert the target commit during the meld.
Opinions on whether such a directive is wanted, and which of those two
shapes is preferred, would be welcome before I attempt it.
---
base-commit: f60db8d575adb79761d363e026fb49bddf330c73
---
 Documentation/git-rebase.adoc | 12 ++++++
 sequencer.c                   | 96 +++++++++++++++++++++++++++++++++++++++----
 t/t3415-rebase-autosquash.sh  | 64 +++++++++++++++++++++++++++++
 3 files changed, 163 insertions(+), 9 deletions(-)

diff --git a/Documentation/git-rebase.adoc b/Documentation/git-rebase.adoc
index f6c22d1598..7eb8bbe95f 100644
--- a/Documentation/git-rebase.adoc
+++ b/Documentation/git-rebase.adoc
@@ -282,6 +282,11 @@ by `git log --cherry-mark ...`) are detected and dropped as a
 preliminary step (unless `--reapply-cherry-picks` or `--keep-base` is
 passed).
 +
+A commit can also become empty as a result of `--autosquash`, when a
+`fixup!` or `squash!` commit cancels out all of the changes of the
+commit it is melded into.  Such a commit is treated the same way and is
+dropped, kept, or stopped at according to this option.
++
 See also INCOMPATIBLE OPTIONS below.
 
 --no-keep-empty::
@@ -591,6 +596,13 @@ changed from `pick` to `squash`, `fixup` or `fixup -C`, respectively, and they
 are moved right after the commit they modify.  The `--interactive` option can
 be used to review and edit the todo list before proceeding.
 +
+If melding a `fixup!` or `squash!` commit cancels out all of the changes of
+the commit it is applied to, the result is an empty commit.  The handling of
+these empty commits can be configured with the `--empty` option: the emptied
+commit is dropped, kept, or stopped at.  This makes it possible to back a
+change out of a series by committing a revert of it as a `fixup!` and letting
+`--autosquash --empty=drop` remove both.
++
 The recommended way to create commits with squash markers is by using the
 `--squash`, `--fixup`, `--fixup=amend:` or `--fixup=reword:` options of
 linkgit:git-commit[1], which take the target commit as an argument and
diff --git a/sequencer.c b/sequencer.c
index 0fe8fed6c3..435b100e3d 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -823,7 +823,7 @@ static struct object_id *get_cache_tree_oid(struct index_state *istate)
 	return &istate->cache_tree->oid;
 }
 
-static int is_index_unchanged(struct repository *r)
+static int is_index_unchanged(struct repository *r, int amend)
 {
 	struct object_id head_oid, *cache_tree_oid;
 	const struct object_id *head_tree_oid;
@@ -856,7 +856,26 @@ static int is_index_unchanged(struct repository *r)
 		if (repo_parse_commit(r, head_commit))
 			return -1;
 
-		head_tree_oid = get_commit_tree_oid(head_commit);
+		if (amend) {
+			/*
+			 * When amending (e.g. melding a "fixup!" or "squash!"),
+			 * the commit we are about to create replaces HEAD, so
+			 * its parent is HEAD's parent.  It is therefore empty
+			 * when the index matches the tree of HEAD's parent
+			 * rather than the tree of HEAD itself.
+			 */
+			if (head_commit->parents) {
+				struct commit *parent =
+					head_commit->parents->item;
+				if (repo_parse_commit(r, parent))
+					return -1;
+				head_tree_oid = get_commit_tree_oid(parent);
+			} else {
+				head_tree_oid = the_hash_algo->empty_tree;
+			}
+		} else {
+			head_tree_oid = get_commit_tree_oid(head_commit);
+		}
 	}
 
 	if (!(cache_tree_oid = get_cache_tree_oid(istate)))
@@ -1786,7 +1805,7 @@ static int is_original_commit_empty(struct commit *commit)
  */
 static int allow_empty(struct repository *r,
 		       struct replay_opts *opts,
-		       struct commit *commit)
+		       struct commit *commit, int amend)
 {
 	int index_unchanged, originally_empty;
 
@@ -1798,13 +1817,33 @@ static int allow_empty(struct repository *r,
 	 * drop_redundant_commits determine whether the commit should be kept or
 	 * dropped. If neither is specified, halt.
 	 */
-	index_unchanged = is_index_unchanged(r);
+	index_unchanged = is_index_unchanged(r, amend);
 	if (index_unchanged < 0)
 		return index_unchanged;
 	if (!index_unchanged)
 		return 0; /* we do not have to say --allow-empty */
 
-	originally_empty = is_original_commit_empty(commit);
+	/*
+	 * When amending (melding a "fixup!"/"squash!"), the resulting commit
+	 * replaces HEAD, so whether it "started" empty or "became" empty is
+	 * decided by whether the commit being melded into was itself empty: if
+	 * HEAD had content that the fixup cancelled out, the commit became empty
+	 * and is subject to keep/drop_redundant; if HEAD was already empty, the
+	 * commit started empty and is subject to allow_empty as usual.
+	 */
+	if (amend) {
+		struct object_id head_oid;
+		struct commit *head_commit;
+
+		if (repo_get_oid(r, "HEAD", &head_oid))
+			return error(_("could not resolve HEAD commit"));
+		head_commit = lookup_commit_reference(r, &head_oid);
+		if (!head_commit)
+			return -1;
+		originally_empty = is_original_commit_empty(head_commit);
+	} else {
+		originally_empty = is_original_commit_empty(commit);
+	}
 	if (originally_empty < 0)
 		return originally_empty;
 	if (originally_empty)
@@ -2260,6 +2299,30 @@ static const char *reflog_message(struct replay_opts *opts,
 	return buf.buf;
 }
 
+/*
+ * A "fixup!"/"squash!" that melds into HEAD may empty it out.  In that case,
+ * with --empty=drop, we want to drop the commit entirely.  Since the commit
+ * being amended has already been created (by the preceding "pick"), and the
+ * index and worktree already match the tree of its parent, dropping it is a
+ * matter of moving HEAD back to that parent.
+ */
+static int reset_head_to_parent(struct repository *r, struct replay_opts *opts,
+				struct object_id *head)
+{
+	struct commit *head_commit = lookup_commit_reference(r, head);
+
+	if (!head_commit || repo_parse_commit(r, head_commit))
+		return error(_("could not parse HEAD commit"));
+	if (!head_commit->parents)
+		return error(_("cannot drop the root commit"));
+
+	return refs_update_ref(get_main_ref_store(r),
+			       reflog_message(opts, "fixup",
+					      "dropping emptied commit"),
+			       "HEAD", &head_commit->parents->item->object.oid,
+			       head, 0, UPDATE_REFS_MSG_ON_ERR);
+}
+
 static int do_pick_commit(struct repository *r,
 			  struct todo_item *item,
 			  struct replay_opts *opts,
@@ -2493,7 +2556,7 @@ static int do_pick_commit(struct repository *r,
 	}
 
 	drop_commit = 0;
-	allow = allow_empty(r, opts, commit);
+	allow = allow_empty(r, opts, commit, flags & AMEND_MSG);
 	if (allow < 0) {
 		res = allow;
 		goto leave;
@@ -2506,9 +2569,24 @@ static int do_pick_commit(struct repository *r,
 		unlink(git_path_merge_msg(r));
 		refs_delete_ref(get_main_ref_store(r), "", "AUTO_MERGE",
 				NULL, REF_NO_DEREF);
-		fprintf(stderr,
-			_("dropping %s %s -- patch contents already upstream\n"),
-			oid_to_hex(&commit->object.oid), msg.subject);
+		if (flags & AMEND_MSG) {
+			/*
+			 * The "fixup!"/"squash!" emptied out the commit it was
+			 * melded into; that commit was already created by the
+			 * preceding "pick", so drop it by moving HEAD back to
+			 * its parent.
+			 */
+			res = reset_head_to_parent(r, opts, &head);
+			if (res)
+				goto leave;
+			fprintf(stderr,
+				_("dropping %s %s -- resulting commit is empty\n"),
+				oid_to_hex(&commit->object.oid), msg.subject);
+		} else {
+			fprintf(stderr,
+				_("dropping %s %s -- patch contents already upstream\n"),
+				oid_to_hex(&commit->object.oid), msg.subject);
+		}
 	} /* else allow == 0 and there's nothing special to do */
 	if (!opts->no_commit && !drop_commit) {
 		if (author || command == TODO_REVERT || (flags & AMEND_MSG))
diff --git a/t/t3415-rebase-autosquash.sh b/t/t3415-rebase-autosquash.sh
index 5033411a43..508dcc7527 100755
--- a/t/t3415-rebase-autosquash.sh
+++ b/t/t3415-rebase-autosquash.sh
@@ -510,4 +510,68 @@ test_expect_success 'pick and fixup respect commit.cleanup' '
 	test_commit_message HEAD -m "something"
 '
 
+test_expect_success 'fixup! that empties its target is dropped with --empty=drop' '
+	git reset --hard base &&
+	test_commit --no-tag addX fileX 1 &&
+	test_commit --no-tag changeX fileX 2 &&
+	test_commit --no-tag later fileW hello &&
+	echo 1 >fileX &&
+	git commit -m "fixup! changeX" fileX &&
+
+	git rebase -i --autosquash --empty=drop HEAD~4 &&
+
+	git log --format=%s >actual &&
+	! grep changeX actual &&
+	grep addX actual &&
+	grep later actual &&
+	echo 1 >expect &&
+	test_cmp expect fileX &&
+	echo hello >expect &&
+	test_cmp expect fileW
+'
+
+test_expect_success 'fixup! that empties its target is kept with --empty=keep' '
+	git reset --hard base &&
+	test_commit --no-tag addY fileY 1 &&
+	test_commit --no-tag changeY fileY 2 &&
+	echo 1 >fileY &&
+	git commit -m "fixup! changeY" fileY &&
+
+	git rebase -i --autosquash --empty=keep HEAD~3 &&
+
+	git log --format=%s >actual &&
+	grep changeY actual &&
+	: "the retained commit is empty" &&
+	git diff --exit-code HEAD~1 HEAD &&
+	echo 1 >expect &&
+	test_cmp expect fileY
+'
+
+test_expect_success 'fixup! that empties its target stops with --empty=stop' '
+	git reset --hard base &&
+	test_commit --no-tag addZ fileZ 1 &&
+	test_commit --no-tag changeZ fileZ 2 &&
+	echo 1 >fileZ &&
+	git commit -m "fixup! changeZ" fileZ &&
+
+	test_when_finished "git rebase --abort" &&
+	test_must_fail git rebase -i --autosquash --empty=stop HEAD~3
+'
+
+test_expect_success 'squash! that empties its target is dropped with --empty=drop' '
+	git reset --hard base &&
+	test_commit --no-tag addS fileS 1 &&
+	test_commit --no-tag changeS fileS 2 &&
+	echo 1 >fileS &&
+	git commit -m "squash! changeS" fileS &&
+
+	git rebase -i --autosquash --empty=drop HEAD~3 &&
+
+	git log --format=%s >actual &&
+	! grep changeS actual &&
+	grep addS actual &&
+	echo 1 >expect &&
+	test_cmp expect fileS
+'
+
 test_done




^ permalink raw reply related

* Re: [PATCH 10/11] bisect: ensure non-NULL `head` before using it
From: Junio C Hamano @ 2026-07-10  4:01 UTC (permalink / raw)
  To: Johannes Schindelin via GitGitGadget; +Cc: git, Johannes Schindelin
In-Reply-To: <428a3a006bbcb165a96495bbc2c5fc04e5b15db4.1783590159.git.gitgitgadget@gmail.com>

"Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
writes:

> When `refs_resolve_ref_unsafe()` is called to resolve HEAD, and returns
> NULL (e.g., HEAD does not exist as a proper ref), the code falls back to
> `repo_get_oid("HEAD")` to try to resolve the OID directly. If that
> succeeds, execution continues with `head` still set to NULL.
>
> Later, that variable is passed to `repo_get_oid()` and `starts_with()`,
> both of which would dereference the NULL pointer.
>
> The scenario "`refs_resolve_ref_unsafe()` returns NULL but
> `repo_get_oid()` succeeds" can happen when HEAD is a detached bare OID
> that the ref backend cannot resolve symbolically (a potential edge case
> with the reftable backend) but the OID itself is valid. In this case,
> the bisect-start file does not yet exist (this is a fresh "git bisect
> start"), so the else branch is taken with the NULL `head`.

I agree that setting head to the string "HEAD" is a good solution to
ensure that !starts_with(), !repo_get_oid(), and skip_prefix() are
not called with NULL.

However, I am not sure I understand your "can happen" scenario.

I naively thought that the only case where HEAD does not resolve to
an object correctly is when HEAD is a symbolic ref pointing to an
unborn branch.

Is the bug in your "can happen" scenario something we can
demonstrate?  If so, could you add a test to prevent regressions in
the future?

Thanks.


> Simply assign "HEAD" to `head` as a fallback to address this.
>
> Pointed out by Coverity.
>
> Assisted-by: Claude Opus 4.6
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
>  builtin/bisect.c | 4 +++-
>  1 file changed, 3 insertions(+), 1 deletion(-)
>
> diff --git a/builtin/bisect.c b/builtin/bisect.c
> index 6ff600c856..a69771c6d3 100644
> --- a/builtin/bisect.c
> +++ b/builtin/bisect.c
> @@ -811,9 +811,11 @@ static enum bisect_error bisect_start(struct bisect_terms *terms, int argc,
>  	 */
>  	head = refs_resolve_ref_unsafe(get_main_ref_store(the_repository),
>  				       "HEAD", 0, &head_oid, &flags);
> -	if (!head)
> +	if (!head) {
>  		if (repo_get_oid(the_repository, "HEAD", &head_oid))
>  			return error(_("bad HEAD - I need a HEAD"));
> +		head = "HEAD";
> +	}
>  
>  	/*
>  	 * Check if we are bisecting

^ permalink raw reply

* Re: [PATCH v7 3/3] replay: offer an option to linearize the commit topology
From: Elijah Newren @ 2026-07-10  3:47 UTC (permalink / raw)
  To: Toon Claes; +Cc: git, Johannes Schindelin
In-Reply-To: <20260707-toon-git-replay-drop-merges-v7-3-808ab9b4afa6@iotcl.com>

Hi Toon!

Thanks for continuing to work on the series.  Sorry that I've been out
on vacation for 3+ weeks and then playing catch up.  You addressed all
my v2 feedback, and most things in this latest v7 look good.  I do
have one substantive concern with this patch, which I'll cover in
detail below.

On Tue, Jul 7, 2026 at 12:07 PM Toon Claes <toon@iotcl.com> wrote:
>
> One of the stated goals of git-replay(1) is to allow implementing the
> git-rebase(1) functionality on the server side.
>
> The default mode of git-rebase(1) is to act as if `--no-rebase-merges`
> was given. This mode drops merge commits instead of replaying them, and
> linearizes the history into a sequence of regular (single-parent)
> commits.
>
> Add option `--linearize` to git-replay(1) to do the same.

Right, `--linearize` exists to change how merges are handled.  I'd
argue that if there are no merges, then you should get the same
behavior whether or not --linearize appears on your command line.

> Each replayed
> commit is stacked on top of the previously replayed one. When a merge is
> encountered, the commits reachable from all of its sides are replayed
> into the single line and the merge itself is dropped.
>
> If a ref was pointing to a merge commit, that ref is updated to the
> merge's last replayed ancestor.

This is a good description of the net effect of linearizing a single
branch.  I think it describes rebasing multiple branches at once much
less well -- see below.

> git-replay(1) accepts multiple revision ranges, for example:

I think I know what you mean, but this isn't quite right:
git-replay(1) only ever accepts a single revision range.  From
gitrevisions(7) (also in git-rev-parse(1)):

       Commands that are specifically designed to take two distinct ranges
       (e.g. "git range-diff R1 R2" to compare two ranges) do exist, but they
       are exceptions. Unless otherwise noted, all "git" commands that operate
       on a set of commits work on a single revision range. In other words,
       writing two "two-dot range notation" next to each other, e.g.

           $ git log A..B C..D

       does not specify two revision ranges for most commands. Instead it will
       name a single connected set of commits, i.e. those that are reachable
       from either B or D but are reachable from neither A or C.

You could say that replay accepts multiple branches (references)
within its revision range -- but even then that comes with an "in some
cases" qualifier: `--advance` (and more recently, `--revert`)
specifically reject multiple positive refs, precisely because (a)
simply concatenating branches is surprising, and (b) the resulting
order is ill-defined (or at least looks arbitrary to the user).

>     $ git replay --onto main topic1 topic2
>
> Without `--linearize` this replays 'topic1' and 'topic2' onto 'main'
> independently and updates both refs.

And, if there are no merges anywhere in the range, I'd argue that
adding --linearize either ought to do the same thing -- or else error
out that multiple positive refs are not allowed with `--linearize`,
the way `--advance` and `--revert` already do.

> With `--linearize` the whole set is flattened into one line: the ranges
> are stacked on top of each other rather than replayed side by side, so
> both refs end up pointing at different points along that single history.

To me, this is a significant principle of least astonishment violation.

> Replaying all revision ranges into one single linear history is
> intentional and it's the only way to ensure predictable results.

I have to push back on both "only" and "predictable".

Regarding "only", there are at least two other choices:
  * make --linearize incompatible with multiple positive refs
  * More involved implementation (quick sketch): (a) Track a
last_commit per branch specified on the command line, (b) Make the
revision walk keep track of which branches each walked commit is
reachable from, (c) for each commit to be replayed, for each branch
it's reachable from, update the appropriate last_commit[branch].
(Except that when last_commit[branchA] == last_commit[branchB] and a
commit is reachable from both branchA & branchB, you only replay the
commit once.)

Regarding "predictable", I'd like to split predictability into two
pieces: guessable by the user, and consistent with other replay
commands.  This behavior gives us neither:
  * guessable by the user:
    * which of the multiple branches specified on the command line is
first in your concatenated linearization?  It's decided by rev-walk,
not what the user wrote.
  * consistent:
    * why does a merge-free topology behave differently with
--linearize than without it?
    * why do `--advance` and `--revert` both refuse multiple positive
refs to avoid exactly this "which branch first" concatenation, while
`--onto --linearize` embraces it?

For what it's worth, looking back at the v5 thread, it seems the `base
= last_commit` rule came in to fix the real bug Junio and Phillip
pointed out there -- that without it, only one side of a linearized
merge survived.  That fix is clearly correct for the single-branch
case.  My worry is only that applying it unconditionally reintroduces
the multiple-positive-refs ordering problem we deliberately avoid
elsewhere.  Making `--linearize` reject multiple positive refs would
keep the merge-flattening fix while sidestepping this entirely.

> A user
> who wants to linearize ranges independently is advised to use separate
> git-replay(1) invocations.

Which, to me, is another argument for just disallowing multiple
positive refs under `--linearize`: if the recommended way to do it is
separate invocations anyway, we may as well require them.

> Linearizing is a distinct operation, and flattening merge commits is
> just one aspect of that. Recreating merges would be a separate mode, so
> rather than mirror git-rebase(1)'s `--rebase-merges[=<mode>]` interface,
> git-replay(1) uses its own `--linearize` option.

No disagreement here on this point.


Thanks,
Elijah

^ permalink raw reply

* Re: [PATCH] gpg-interface: still print ssh signatures when allowed signers file is not set
From: Grayson Tinker @ 2026-07-10  3:42 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, Patrick Steinhardt, Elijah Newren, Fabian Stelzer, Jeff King,
	René Scharfe
In-Reply-To: <xmqqtsq7haev.fsf@gitster.g>

On Thu, Jul 9, 2026 at 6:59 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> If a user runs 'git log --show-signature -100', they will be spammed
> with this message 100 times.  Because it bypasses the
> advise_if_enabled() mechanism, there is no way for them to disable
> it.

The downside to using advise_if_enabled is that this single line would get
turned into three, with a newline in the middle, which would decently
disrupt the view of the log until the hint is disabled. I'm not sure what the
best solution is here; I lean towards keeping it as is to reduce the overall
noise level, but perhaps those who use this feature more would disagree.

(The previous message was also printed every time, FWIW. So at the
very least this isn't worse behavior.)

I'll make the hint disableable if you'd prefer.

> However, doesn't cryptographic verification still provide value on
> its own?  Even without allowedSignersFile, the signature at least
> guarantees the commit content hasn't been modified since it was
> signed, even if the signer's identity remains unverified.  If some
> users rely on this purely cryptographic validation, they probably
> won't want to maintain an allowed signers file, and they would
> definitely want a way to squelch this repetitive advice.

Allowing for this usecase was exactly the intent of this patch; I am
this type of user and had some annoyance with this.

Thanks!

^ permalink raw reply

* Re: [PATCH 09/11] pack-bitmap: handle missing bitmap for base MIDX
From: Junio C Hamano @ 2026-07-10  3:41 UTC (permalink / raw)
  To: Johannes Schindelin via GitGitGadget
  Cc: git, Johannes Schindelin, Taylor Blau
In-Reply-To: <0b27860478a284719755b8ac2386862c1fc3d0e7.1783590159.git.gitgitgadget@gmail.com>

"Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
writes:

> This can happen in practice with incremental MIDX chains: the base MIDX
> may have been written without `--write-bitmap-index`, or the bitmap may
> have been pruned while the incremental layer's bitmap still references
> it.
>
> Check the return value and go to the cleanup label (which unmaps the
> current bitmap and returns -1) so the caller falls back to non-bitmap
> object enumeration, matching the handling of other bitmap loading
> failures in the same function.

Nicely reasoned.  It would have been nicer to CC those who are more
familiar with the area, though.

Cc'ed Taylor for incremental MIDX expertise just in case.

Thanks.

>
> Pointed out by Coverity.
>
> Assisted-by: Claude Opus 4.6
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
>  pack-bitmap.c | 4 ++++
>  1 file changed, 4 insertions(+)
>
> diff --git a/pack-bitmap.c b/pack-bitmap.c
> index e8a82945cc..ca7998c10b 100644
> --- a/pack-bitmap.c
> +++ b/pack-bitmap.c
> @@ -523,6 +523,10 @@ static int open_midx_bitmap_1(struct bitmap_index *bitmap_git,
>  
>  	if (midx->base_midx) {
>  		bitmap_git->base = prepare_midx_bitmap_git(midx->base_midx);
> +		if (!bitmap_git->base) {
> +			warning(_("could not open bitmap for base MIDX"));
> +			goto cleanup;
> +		}
>  		bitmap_git->base_nr = bitmap_git->base->base_nr + 1;
>  	} else {
>  		bitmap_git->base_nr = 0;

^ permalink raw reply

* Re: [PATCH 08/11] revision: avoid dereferencing NULL in `add_parents_only()`
From: Junio C Hamano @ 2026-07-10  3:41 UTC (permalink / raw)
  To: Johannes Schindelin via GitGitGadget; +Cc: git, Johannes Schindelin
In-Reply-To: <0675767797f103b79ab936e01bfd06747725bcad.1783590159.git.gitgitgadget@gmail.com>

"Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
writes:

> From: Johannes Schindelin <johannes.schindelin@gmx.de>
>
> This function resolves revision suffixes like commit^@ (all parents),
> commit^! (commit minus parents), and commit^-N (exclude Nth parent). It
> calls `get_reference()` in a loop to peel through tag objects until it
> reaches a commit.
>
> The existing NULL check after `get_reference()` only handles the
> ignore_missing case, but get_reference() can return NULL through three
> distinct paths:

Nicely spotted.  It sounds like something a test can ensure does not
to regress in the future, unless I am misreading this explanation.
Could you include such a test?

Thanks.

> diff --git a/revision.c b/revision.c
> index e91d7e1f11..7f3999b551 100644
> --- a/revision.c
> +++ b/revision.c
> @@ -1903,8 +1903,13 @@ static int add_parents_only(struct rev_info *revs, const char *arg_, int flags,
>  		return 0;
>  	while (1) {
>  		it = get_reference(revs, arg, &oid, 0);
> -		if (!it && revs->ignore_missing)
> -			return 0;
> +		if (!it) {
> +			if (revs->ignore_missing)
> +				return 0;
> +			if (revs->do_not_die_on_missing_objects)
> +				return 0;
> +			return -1;
> +		}
>  		if (it->type != OBJ_TAG)
>  			break;
>  		if (!((struct tag*)it)->tagged)

^ permalink raw reply

* Re: [PATCH 06/11] bisect: handle NULL commit in `bisect_successful()`
From: Junio C Hamano @ 2026-07-10  3:31 UTC (permalink / raw)
  To: Johannes Schindelin via GitGitGadget; +Cc: git, Johannes Schindelin
In-Reply-To: <704137510808ade246c6f1463e88a8e3041e0f7d.1783590159.git.gitgitgadget@gmail.com>

"Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
writes:

> diff --git a/builtin/bisect.c b/builtin/bisect.c
> index e7c2d2f3bb..6ff600c856 100644
> --- a/builtin/bisect.c
> +++ b/builtin/bisect.c
> @@ -663,6 +663,11 @@ static int bisect_successful(struct bisect_terms *terms)
>  
>  	refs_read_ref(get_main_ref_store(the_repository), bad_ref, &oid);
>  	commit = lookup_commit_reference_by_name(bad_ref);
> +	if (!commit) {
> +		res = error(_("could not find commit for '%s'"), bad_ref);
> +		free(bad_ref);
> +		return res;
> +	}

Catching this case as an error is the right thing to do, but there is
a bit of an impedance mismatch between the return value from error()
and the status passed around in the bisect codebase.

The bisect.h header defines an enum bisect_error type, and I think
the sole caller of this function, bisect_next(), expects to see
BISECT_FAILED.  It may happen to be the same -1 that error()
returns, but for longer term maintainability, I would prefer to see
it done more like:

	error(_("..."));
	free(bad_ref);
	return BISECT_FAILED;

or something along those lines.

Thanks.

>  	repo_format_commit_message(the_repository, commit, "%s", &commit_name,
>  				   &pp);

^ permalink raw reply

* Re: [PATCH 05/11] mailsplit: move NULL check before first use of file handle
From: Junio C Hamano @ 2026-07-10  3:31 UTC (permalink / raw)
  To: Johannes Schindelin via GitGitGadget; +Cc: git, Johannes Schindelin
In-Reply-To: <41eef047ae6e3c332e1c8f96a9f9abf55d5004fc.1783590159.git.gitgitgadget@gmail.com>

"Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
writes:

> diff --git a/builtin/mailsplit.c b/builtin/mailsplit.c
> index 264df6259a..0993418e63 100644
> --- a/builtin/mailsplit.c
> +++ b/builtin/mailsplit.c
> @@ -225,14 +225,14 @@ static int split_mbox(const char *file, const char *dir, int allow_bare,
>  	FILE *f = !strcmp(file, "-") ? stdin : fopen(file, "r");
>  	int file_done = 0;
>  
> -	if (isatty(fileno(f)))
> -		warning(_("reading patches from stdin/tty..."));
> -
>  	if (!f) {
>  		error_errno("cannot open mbox %s", file);
>  		goto out;
>  	}
>  
> +	if (isatty(fileno(f)))
> +		warning(_("reading patches from stdin/tty..."));
> +
>  	do {
>  		peek = fgetc(f);
>  		if (peek == EOF) {

Ah, obviously correct.  Cannot believe nobody noticed this since it
was first written in 7b20af6a06 (am/apply: warn if we end up reading
patches from terminal, 2022-03-03).

Thanks.

^ permalink raw reply

* Re: [PATCH 04/11] reftable/stack: guard against NULL list_file in stack_destroy
From: Junio C Hamano @ 2026-07-10  3:21 UTC (permalink / raw)
  To: Johannes Schindelin via GitGitGadget; +Cc: git, Johannes Schindelin
In-Reply-To: <d7bc7fce35bb169a20a4ae9a1630e7080e133b23.1783590159.git.gitgitgadget@gmail.com>

"Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
writes:

> From: Johannes Schindelin <johannes.schindelin@gmx.de>
>
> When reftable_new_stack() fails partway through initialization
> (e.g., reftable_buf_addstr returns an OOM error before
> reftable_buf_detach assigns p->list_file), it jumps to the error
> path which calls reftable_stack_destroy(p). At that point,
> p->list_file is still NULL because the detach never happened.
>
> reftable_stack_destroy() passes st->list_file unconditionally to
> read_lines(), which calls open(filename, O_RDONLY). Passing NULL
> to open() is undefined behavior and will typically crash.
>
> Guard the read_lines() call with a NULL check on st->list_file.
> When list_file is NULL, there are no table files to clean up
> anyway, so skipping read_lines is the correct behavior.

Nice spotting and recovery.  Well done.

>
> Pointed out by Coverity.
>
> Assisted-by: Claude Opus 4.6
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
>  reftable/stack.c | 3 ++-
>  1 file changed, 2 insertions(+), 1 deletion(-)
>
> diff --git a/reftable/stack.c b/reftable/stack.c
> index 1fba96ddb3..3fc3c0b2d1 100644
> --- a/reftable/stack.c
> +++ b/reftable/stack.c
> @@ -171,7 +171,8 @@ void reftable_stack_destroy(struct reftable_stack *st)
>  		st->merged = NULL;
>  	}
>  
> -	err = read_lines(st->list_file, &names);
> +	if (st->list_file)
> +		err = read_lines(st->list_file, &names);
>  	if (err < 0) {
>  		REFTABLE_FREE_AND_NULL(names);
>  	}

^ permalink raw reply

* Re: [PATCH 03/11] remote: guard `remote_tracking()` against NULL remote
From: Junio C Hamano @ 2026-07-10  3:21 UTC (permalink / raw)
  To: Johannes Schindelin via GitGitGadget; +Cc: git, Johannes Schindelin
In-Reply-To: <dcaefc598779123cea19807877e074acb3e1575a.1783590159.git.gitgitgadget@gmail.com>

"Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
writes:

> However, it requires quite involved reasoning to reach that conclusion,
> and is therefore fragile. Just return -1 ("no tracking ref") when there
> is no remote to work with.

In a case like this, where the function is designed not to be called
with NULL remote, I would prefer to have an explicit BUG() rather
than sweeping the problem under the rug.  That would make sure your
investigation and involved reasoning done here remain relevant if
the BUG() triggers due to careless changes to the caller in the
future.

Thanks.

> Pointed out by Coverity.
>
> Assisted-by: Claude Opus 4.6
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
>  remote.c | 2 ++
>  1 file changed, 2 insertions(+)
>
> diff --git a/remote.c b/remote.c
> index 00723b385e..34d0367f11 100644
> --- a/remote.c
> +++ b/remote.c
> @@ -2681,6 +2681,8 @@ static int remote_tracking(struct remote *remote, const char *refname,
>  {
>  	char *dst;
>  
> +	if (!remote)
> +		return -1; /* no remote to look up tracking ref */
>  	dst = apply_refspecs(&remote->fetch, refname);
>  	if (!dst)
>  		return -1; /* no tracking ref for refname at remote */

^ permalink raw reply

* Re: [PATCH 02/11] diff: handle NULL return from repo_get_commit_tree()
From: Junio C Hamano @ 2026-07-10  3:11 UTC (permalink / raw)
  To: Johannes Schindelin via GitGitGadget; +Cc: git, Johannes Schindelin
In-Reply-To: <4fdba0542b3d643affe32ec35f27fdbabccf54d0.1783590159.git.gitgitgadget@gmail.com>

"Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
writes:

> diff --git a/builtin/diff.c b/builtin/diff.c
> index 4b46e394ce..18b1083e98 100644
> --- a/builtin/diff.c
> +++ b/builtin/diff.c
> @@ -579,9 +579,13 @@ int cmd_diff(int argc,
>  		obj = deref_tag(the_repository, obj, NULL, 0);
>  		if (!obj)
>  			die(_("invalid object '%s' given."), name);
> -		if (obj->type == OBJ_COMMIT)
> -			obj = &repo_get_commit_tree(the_repository,
> -						    ((struct commit *)obj))->object;
> +		if (obj->type == OBJ_COMMIT) {
> +			struct tree *tree = repo_get_commit_tree(
> +				the_repository, (struct commit *)obj);
> +			if (!tree)
> +				die(_("unable to read tree object for commit '%s'"), name);
> +			obj = &tree->object;
> +		}

Obviously correct.

>  		if (obj->type == OBJ_TREE) {
>  			if (sdiff.skip && bitmap_get(sdiff.skip, i))

^ permalink raw reply

* Re: [PATCH 01/11] diffcore-break: guard against NULLed queue entries in merge loop
From: Junio C Hamano @ 2026-07-10  3:11 UTC (permalink / raw)
  To: Johannes Schindelin via GitGitGadget; +Cc: git, Johannes Schindelin
In-Reply-To: <df00334f8b8cb85a928e1ca22aa12dd6b87fb154.1783590159.git.gitgitgadget@gmail.com>

"Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
writes:

> From: Johannes Schindelin <johannes.schindelin@gmx.de>
>
> The outer loop in `diffcore_merge_broken()` sets `q->queue[j]` to NULL
> when it merges a broken pair back together, and has a NULL check to skip
> such entries on subsequent iterations. The inner loop, however, lacks
> this guard: when it scans forward looking for a matching peer, it can
> encounter a slot that was NULLed by a previous outer-loop iteration and
> dereference it unconditionally.
>
> In practice this requires at least two broken pairs whose peers
> both survive rename/copy detection and appear later in the queue,
> which is rare but not impossible.

Interesting find.  This is an ancient part of the codebase that
nobody has touched in the past 21 years since eeaa460314 ([PATCH]
diff: Update -B heuristics., 2005-06-03) introduced it ;-).

Well spotted.

> Add the same `if (!pp) continue` guard to the inner loop.
>
> Pointed out by Coverity.
>
> Assisted-by: Claude Opus 4.6
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
>  diffcore-break.c | 2 ++
>  1 file changed, 2 insertions(+)
>
> diff --git a/diffcore-break.c b/diffcore-break.c
> index 17b5ad1fed..b5bcc956cc 100644
> --- a/diffcore-break.c
> +++ b/diffcore-break.c
> @@ -289,6 +289,8 @@ void diffcore_merge_broken(void)
>  			 */
>  			for (j = i + 1; j < q->nr; j++) {
>  				struct diff_filepair *pp = q->queue[j];
> +				if (!pp)
> +					continue;
>  				if (pp->broken_pair &&
>  				    !strcmp(pp->one->path, pp->two->path) &&
>  				    !strcmp(p->one->path, pp->two->path)) {

^ permalink raw reply

* Re: [PATCH] gpg-interface: still print ssh signatures when allowed signers file is not set
From: Junio C Hamano @ 2026-07-10  1:59 UTC (permalink / raw)
  To: Grayson Tinker
  Cc: git, Patrick Steinhardt, Elijah Newren, Fabian Stelzer, Jeff King,
	René Scharfe
In-Reply-To: <20260625194330.3711-1-graysontinker@gmail.com>

Grayson Tinker <graysontinker@gmail.com> writes:

> "show-signature" errors when the allowed signers file is not configured,
> which means that the user can't see the key that the ref was signed with
> without creating and configuring the file. Change the logic so that the file
> is only used when configured, and so the signature status is always displayed.
>
> Example of previous output:
> ```
> error: gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification
> commit b437db5ddc38ebda223bbae2087eee90a7b1c6e2 (HEAD -> master)
> No signature
> Author: Grayson Tinker <graysontinker@gmail.com>
> ```
>
> Example of new output:
> ```
> commit b437db5ddc38ebda223bbae2087eee90a7b1c6e2 (HEAD -> master)
> hint: Configure gpg.ssh.allowedSignersFile for automatic principal matching
> Good "git" signature with ED25519-SK key SHA256:yTU4KFs/g6MY7biDSlVStB63Gi1rCKg7dOFDXbe0yuw
> Author: Grayson Tinker <graysontinker@gmail.com>
> ```

While I haven't closely looked at the parts of this patch that I did
not quote here, this specific section caught my eye:

> @@ -528,6 +528,10 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc,
>  		pipe_command(&ssh_keygen, sigc->payload, sigc->payload_len,
>  				   &ssh_keygen_out, 0, &ssh_keygen_err, 0);
>  
> +		if (!ssh_allowed_signers) {
> +			advise(_("Configure gpg.ssh.allowedSignersFile for automatic principal matching\n"));
> +		}

If a user runs 'git log --show-signature -100', they will be spammed
with this message 100 times.  Because it bypasses the
advise_if_enabled() mechanism, there is no way for them to disable
it.

Since I don't use SSH signing, I'm curious: how common or useful is
it to run log --show-signature without allowedSignersFile
configured?  If it serves no purpose at all, then perhaps this
warning is acceptable, as users would have to configure the variable
to get any utility out of the command. 

However, doesn't cryptographic verification still provide value on
its own?  Even without allowedSignersFile, the signature at least
guarantees the commit content hasn't been modified since it was
signed, even if the signer's identity remains unverified.  If some
users rely on this purely cryptographic validation, they probably
won't want to maintain an allowed signers file, and they would
definitely want a way to squelch this repetitive advice. 

Thanks.

^ permalink raw reply

* What's cooking in git.git (Jul 2026, #04)
From: Junio C Hamano @ 2026-07-10  0:51 UTC (permalink / raw)
  To: git

Here are the topics that have been cooking in my tree.  Commits
prefixed with '+' are in 'next' (being in 'next' is a sign that a
topic is stable enough to be used and is a candidate to be in a
future release).  Commits prefixed with '-' are only in 'seen', and
aren't considered "accepted" at all. They may be annotated with a URL
to a message that raises issues but they are by no means exhaustive.
A topic without enough support may be discarded after a long period
of no activity (of course, it can be resubmitted when new interest
arises).

The first batch of topics have now graduated to the 'master' branch.

Copies of the source code to Git live in many repositories, and the
following is a list of the ones I push into or their mirrors.  Some
repositories have only a subset of branches.

With maint, master, next, seen, todo:

	git://git.kernel.org/pub/scm/git/git.git/
	git://repo.or.cz/alt-git.git/
	https://kernel.googlesource.com/pub/scm/git/git/
	https://github.com/git/git/
	https://gitlab.com/git-scm/git/

With all the integration branches and topics broken out:

	https://github.com/gitster/git/

Even though the preformatted documentation in HTML and man format
are not sources, they are published in these repositories for
convenience (replace "htmldocs" with "manpages" for the manual
pages):

	git://git.kernel.org/pub/scm/git/git-htmldocs.git/
	https://github.com/gitster/git-htmldocs.git/

Release tarballs are available at:

	https://www.kernel.org/pub/software/scm/git/

--------------------------------------------------
[New Topics]

* dm/submodule-update-i-shorthand (2026-07-07) 1 commit
 - submodule--helper: accept '-i' shorthand for update --init

 The '-i' shorthand for the '--init' option, which was accepted by the
 'git submodule update' command until it was broken in a modernization
 of the option-parsing code, has been restored.

 Will merge to 'next'?
 source: <20260708-submodule-init-v1-1-719456077262@atmark-techno.com>


* hf/unpack-trees-quadratic-scan (2026-07-08) 1 commit
 - unpack-trees: avoid quadratic index scan in next_cache_entry()

 The cache-scanning loop in 'next_cache_entry()' has been optimized
 to avoid rescanning already-unpacked index entries, preventing a
 quadratic performance slow-down when diffing the working tree
 against a commit with a pathspec matching early index entries.

 Will merge to 'next'?
 cf. <xmqqpl0xqh3n.fsf@gitster.g>
 source: <pull.2353.v2.git.git.1783546933992.gitgitgadget@gmail.com>


* jc/relnotes-2.55-rust-fix (2026-07-07) 1 commit
 - Rust: fix description in Release Notes to 2.55

 A description in the release notes for Git 2.55.0 has been
 retroactively updated to clarify that Rust support is enabled by
 default, but still optional, and will become mandatory in Git 3.0.

 Will merge to 'next'?
 source: <xmqqpl0y4rpg.fsf@gitster.g>


* jc/submitting-patches-abandoning (2026-07-08) 1 commit
 - SubmittingPatches: document how to retract a topic

 The 'SubmittingPatches' document has been updated to explicitly
 describe the expectation for contributors to retract or abandon their
 patch series when they are no longer pursuing it.

 Will merge to 'next'?
 cf. <ak6U07K1dQPlXxIp@nixos>
 source: <xmqqpl0xv25e.fsf@gitster.g>


* jk/git-hash-cleanups (2026-07-07) 8 commits
  (merged to 'next' on 2026-07-09 at 12a4856545)
 + hash: check ctx->active flag in all wrapper functions
 + http: use idempotent git_hash_discard()
 + csum-file: use idempotent git_hash_discard()
 + hash: make git_hash_discard() idempotent
 + hash: document function pointers and wrappers
 + hash: convert remaining direct function calls
 + hash: use git_hash_init() consistently
 + Merge branch 'jk/hash-algo-leak-fixes' into jk/git-hash-cleanups
 (this branch uses jk/hash-algo-leak-fixes.)

 The 'git_hash_*()' wrappers have been updated to be used consistently
 across the codebase instead of direct calls to members of 'struct
 git_hash_algo', and 'git_hash_discard()' has been made idempotent to
 simplify cleanups.

 Will merge to 'master'.
 cf. <ak4E4-jmgYFSI75O@pks.im>
 source: <20260708035235.GA41491@coredump.intra.peff.net>


* mm/lib-httpd-cgi-safe (2026-07-07) 3 commits
 - t/README: document writing concurrency-safe helpers
 - t/lib-httpd: make http-429 first-request check atomic
 - t/lib-httpd: fix apply-one-time-script race under concurrent requests

 CGI helper scripts used by HTTP-related test scripts have been updated
 to use atomic filesystem operations, preventing race conditions when
 Apache handles concurrent requests.

 Expecting a reroll.
 cf. <CAC2QwmKuHUP6_287T9SOLdjLdb=b4EqV4qJ_NnYCkGP0-d6qHA@mail.gmail.com>
 source: <pull.2171.git.1783479584.gitgitgadget@gmail.com>


* mm/sideband-ansi-sgr-colon-fix (2026-05-13) 1 commit
  (merged to 'next' on 2026-07-09 at fd2b979b73)
 + sideband: allow ANSI SGR with colon-separated subfields

 The sideband demultiplexer has been updated to recognize ANSI SGR
 escape sequences that use colon-separated subfields (e.g., for
 256-color or true-color codes).

 Will merge to 'master'.
 cf. <8addf7c0-ae39-f1c0-20ab-52114702aaf6@gmx.de>
 source: <20260513070803.163546-1-grawity@nullroute.lt>


* ps/odb-pluggable-housekeeping (2026-07-07) 11 commits
 - odb: make optimizations pluggable
 - builtin/gc: fix signedness issues in ODB-related functionality
 - builtin/gc: refactor ODB optimizations to operate on "files" source
 - builtin/gc: introduce `odb_optimize_required()`
 - builtin/gc: move geometric repacking into `odb_optimize()`
 - builtin/gc: introduce object database optimization options
 - builtin/gc: inline config values specific to the "files" backend
 - builtin/gc: make repack arguments self-contained
 - builtin/gc: extract object database optimizations into separate function
 - builtin/gc: move worktree and rerere tasks before object optimizations
 - odb: run "pre-auto-gc" hook for all maintenance tasks

 Object database housekeeping in 'git gc' and 'git maintenance' has
 been refactored to be pluggable. The files-backend specific logic,
 including incremental and geometric repacking as well as object
 pruning, has been moved out of the command implementation and into the
 files object database source, enabling future alternative object
 database backends to implement their own housekeeping services.

 Expecting a reroll.
 cf. <ak4CHGpIhVIT9sd2@pks.im>
 source: <20260707-b4-pks-odb-optimize-v1-0-aae607667be4@pks.im>


* tc/bundle-uri-empty-fix (2026-07-08) 2 commits
 - bundle-uri: stop sending invalid bundle configuration
 - bundle-uri: drain remaining response on invalid bundle-uri lines

 The client-side parser of server-advertised bundle-URI list has been
 updated to drain the remaining response in order to avoid protocol
 desynchronization when the server sends a misconfigured list. Also,
 the server-side has been taught to omit empty configuration values
 instead of sending invalid key-value lines.

 Will merge to 'next'?
 cf. <xmqqtsq9qj5k.fsf@gitster.g>
 source: <20260708-toon-bundle-uri-no-uri-v2-0-09a03d8db556@iotcl.com>


* gr/t1410-reflog-exit-code (2026-07-08) 1 commit
 - t1410-reflog.sh: avoid suppressing git's exit code in pipelines

 The pipelines in 't1410-reflog.sh' have been replaced with the
 'test_stdout_line_count' helper to avoid suppressing the exit code of
 'git' commands, ensuring failures are not hidden from the test suite.

 Will merge to 'next'?
 cf. <xmqqtsq8p18x.fsf@gitster.g>
 source: <20260709051229.40363-1-gatlavishweshwarreddy26@gmail.com>


* js/coverity-fixes-null-safety (2026-07-09) 11 commits
 - shallow: fix NULL dereference
 - bisect: ensure non-NULL `head` before using it
 - pack-bitmap: handle missing bitmap for base MIDX
 - revision: avoid dereferencing NULL in `add_parents_only()`
 - replay: die when --onto does not peel to a commit
 - bisect: handle NULL commit in `bisect_successful()`
 - mailsplit: move NULL check before first use of file handle
 - reftable/stack: guard against NULL list_file in stack_destroy
 - remote: guard `remote_tracking()` against NULL remote
 - diff: handle NULL return from repo_get_commit_tree()
 - diffcore-break: guard against NULLed queue entries in merge loop

 Various code paths have been hardened against potential NULL-pointer
 dereferences and invalid file descriptor accesses flagged by
 Coverity.

 Needs review.
 source: <pull.2174.git.1783590159.gitgitgadget@gmail.com>


* ps/odb-for-each-object-filter (2026-07-09) 8 commits
 - builtin/cat-file: filter objects via object database
 - odb: introduce object filters to `odb_for_each_object()`
 - pack-bitmap: introduce function to open bitmap for a single source
 - pack-bitmap: iterate object sources when opening bitmaps
 - pack-bitmap: allow aborting iteration of bitmapped objects
 - pack-bitmap: mark object filter as `const`
 - odb/source-packed: improve lookup when enumerating objects
 - Merge branch 'ps/odb-drop-whence' into ps/odb-for-each-object-filter
 (this branch uses ps/odb-drop-whence.)

 The object database enumeration interface 'odb_for_each_object()'
 has learned to accept object filters, allowing the underlying
 backends to optimize the traversal by using reachability bitmaps
 when available. 'git cat-file --batch-all-objects' has been updated
 to use this generic interface, simplifying its code and avoiding
 direct access to ODB backend internals.

 Needs review.
 source: <20260709-pks-odb-for-each-object-filter-v1-0-82fe014b12b3@pks.im>


* ps/refs-wo-the-repository (2026-07-09) 8 commits
 . refs: remove remaining uses of `the_repository`
 . worktree: pass repository to public functions
 . worktree: pass repository to file-local functions
 . worktree: refactor code to use available repositories
 . refs/files: drop `USE_THE_REPOSITORY_VARIABLE`
 . refs/packed: drop `USE_THE_REPOSITORY_VARIABLE`
 . refs/packed: de-globalize handling of "core.packedRefsTimeout"
 . Merge branch 'ps/refs-writing-subcommands' into ps/refs-wo-the-repository
 (this branch uses ps/refs-writing-subcommands.)

 The ref subsystem and the worktree API have been refactored to pass
 a repository pointer down the call chain, allowing them to drop
 references to the global 'the_repository' variable. As part of this,
 the handling of the 'core.packedRefsTimeout' configuration has been
 moved into the per-repository ref store structure.

 Needs review.
 source: <20260709-pks-refs-wo-the-repository-v1-0-1ad6f27529c9@pks.im>


* kk/commit-graph-topo-levels-fix (2026-07-09) 2 commits
 - commit-graph: propagate topo_levels slab to all chain layers
 - commit-graph: add trace2 instrumentation for generation DFS

 The 'topo_levels' slab was only propagated to the topmost layer of a
 split commit-graph chain, causing incremental writes to recompute
 topological levels for commits in base layers. This has been
 corrected.

 Will merge to 'next'?
 cf. <ak-ljlV33GLigFf6@pks.im>
 source: <pull.2170.v2.git.1783609382.gitgitgadget@gmail.com>

--------------------------------------------------
[Stalled]

* kh/doc-trailers (2026-06-10) 10 commits
 - doc: interpret-trailers: document comment line treatment
 - doc: interpret-trailers: commit to “trailer block” term
 - doc: interpret-trailers: join new-trailers again
 - doc: interpret-trailers: add key format example
 - doc: interpret-trailers: explain key format
 - doc: interpret-trailers: explain the format after the intro
 - doc: interpret-trailers: not just for commit messages
 - doc: interpret-trailers: use “metadata” in Name as well
 - doc: interpret-trailers: replace “lines” with “metadata”
 - doc: interpret-trailers: stop fixating on RFC 822

 Documentation for 'git interpret-trailers' has been updated to explain
 the format of trailer keys (alphanumeric characters and hyphens),
 replace outdated terminology, define key terms upfront, and document
 how comment lines in the input are treated.

 Expecting a reroll for too long. stalled.
 cf. <729baf6b-53ea-4e8d-95ab-5935667e66c2@app.fastmail.com>
 source: <V3_CV_doc_int-tr_key_format.8a3@msgid.xyz>


* sn/rebase-update-refs-symrefs (2026-06-03) 1 commit
 - rebase: skip branch symref aliases

 'git rebase --update-refs' has been taught to resolve local branch
 symrefs to their referents before queuing updates, ensuring aliases of
 the current branch are skipped and duplicate updates are avoided to
 prevent failures when branch aliases are present.

 Waiting for response(s) to review comment(s) for too long, stalled.
 cf. <f982c386-e329-4ab0-b695-e540bcb9de3d@gmail.com>
 source: <pull.2126.v2.git.1780482436865.gitgitgadget@gmail.com>


* ap/http-redirect-wwwauth-fix (2026-06-02) 1 commit
 - http: preserve wwwauth_headers across redirects

 When 'cURL' follows a redirect, the 'WWW-Authenticate' headers from
 the redirect target were lost because 'credential_from_url()' cleared
 the credential state. This has been fixed by preserving the collected
 headers across the redirect update.

 Will discard.
 cf. <xmqqmrw2zavx.fsf@gitster.g>
 source: <20260602161150.1527493-1-aplattner@nvidia.com>


* jt/config-lock-timeout (2026-05-17) 1 commit
 - config: retry acquiring config.lock, configurable via core.configLockTimeout

 Configuration file locking has been updated to retry for a short
 period, avoiding failures when multiple processes attempt to update
 the configuration simultaneously.

 Waiting for response(s) to review comment(s) for too long, stalled.
 cf. <agrIrGwSMFlKTx9x@pks.im>
 source: <20260517132111.1014901-1-joerg@thalheim.io>

--------------------------------------------------
[Cooking]

* ds/sparse-index-ita-crash (2026-07-06) 1 commit
 - sparse-index: avoid crash on intent-to-add entry outside the cone

 A crash in the sparse-index collapse code when encountering an
 invalidated cache-tree node (due to an intent-to-add path) has been
 fixed by avoiding collapsing such subtrees.

 Needs review.
 source: <pull.2167.git.1783345853272.gitgitgadget@gmail.com>


* ij/subtree-reject-v2-config (2026-07-06) 2 commits
 - git-subtree: Bail out if we find output from Rust rewrite (test)
 - git-subtree: Bail out if we find output from Rust rewrite

 The shell script implementation of 'git subtree' has been updated to
 check for the presence of the configuration file of the new Rust
 implementation, preventing users from accidentally running the old
 script on repositories already managed by the new tool.

 Expecting a reroll.
 cf. <27215.27575.968985.583226@chiark.greenend.org.uk>
 source: <20260706115816.20267-1-ijackson@chiark.greenend.org.uk>


* kk/reftable-tombstone-quadratic-fix (2026-07-09) 2 commits
 - reftable: fix quadratic behavior in the presence of tombstones
 - t/perf: add perf test for ref tombstone scenarios

 The performance of ref updates and reads using the reftable backend in
 the presence of many deletion tombstone records has been optimized by
 removing the tombstone suppression flag from the merged iterator and
 instead skipping tombstones at higher-level call sites where iteration
 bounds are known.

 Expecting a reroll.
 cf. <CAL71e4PrtZwB8TMg3eBj=LzC7ik+C8yxLYEEEP7SDgMPiWSs0Q@mail.gmail.com>
 source: <pull.2166.v2.git.1783598912.gitgitgadget@gmail.com>


* rs/blame-abbrev-marks (2026-07-06) 1 commit
  (merged to 'next' on 2026-07-08 at e4962bd3d5)
 + blame: reserve mark column only if necessary

 The alignment of commit object name abbreviations in 'git blame'
 output has been optimized to reserve a column for marks (caret,
 question mark, or asterisk) only when such marks are actually shown.

 Will merge to 'master'.
 cf. <xmqqzf0397u1.fsf@gitster.g>
 source: <92991b5e-0667-4315-89d5-1514a5499297@web.de>


* jm/t0213-skip-emulated-ancestry-tests (2026-07-06) 1 commit
 - t0213: skip ancestry tests under user-mode emulation

 The 'TRACE2_ANCESTRY' prerequisite in the 't0213' test script has been
 refined to avoid failures under user-mode emulation, by verifying that
 the ancestry collector reports the expected process names rather than
 the emulator binary name.

 Needs review.
 source: <pull.2168.git.1783359242130.gitgitgadget@gmail.com>


* bc/parse-options-exit-0-on-help (2026-07-07) 4 commits
 - parse-options: exit 0 on -h
 - rev-parse: have --parseopt callers exit 0 on --help
 - parse-options: add a separate case for help output on error
 - t1517: skip svn tests if svn is not installed

 Option parsing with 'git rev-parse --parseopt' and most 'git'
 subcommands has been updated to exit with 0 (instead of 129) when the
 help option ('-h' or '--help') is requested directly by the user,
 aligning with standard Unix convention.

 Will merge to 'next'.
 cf. <20260708035930.GB41684@coredump.intra.peff.net>
 source: <20260708001557.3581080-1-sandals@crustytoothpaste.net>


* mg/meson-hook-list-buildfix (2026-07-01) 1 commit
  (merged to 'next' on 2026-07-08 at 10763a0ebc)
 + meson: restore hook-list.h to builtin_sources

 A racy build failure under Meson has been corrected by ensuring that
 the generated header file 'hook-list.h' is built before compiling
 files in 'builtin_sources' that depend on it.

 Will merge to 'master'.
 cf. <akZGJP1kVtjBFN_e@pks.im>
 source: <20260701193928.358825-1-floppym@gentoo.org>


* zy/apply-abandoned-header-fix (2026-07-01) 1 commit
 - apply: avoid leaking abandoned git-header state

 A candidate 'git diff' header parsed by 'git apply' has been isolated
 in a temporary structure, preventing any partially parsed state from
 polluting the main patch structure and causing assertions to trip if
 the header is ultimately rejected.

 Needs review.
 source: <20260702041759.51572-1-zhihao.yao@njit.edu>


* jk/hash-algo-leak-fixes (2026-07-02) 9 commits
  (merged to 'next' on 2026-07-09 at 7db7b74972)
 + hash: add platform-specific discard functions
 + hash: fix memory leak copying sha256 gcrypt handles
 + http: discard hash in dumb-http http_object_request
 + check_stream_oid(): discard hash on read error
 + patch-id: discard hash when done
 + csum-file: provide a function to release checkpoints
 + csum-file: always finalize or discard hash
 + hash: add discard primitive
 + csum-file: drop discard_hashfile()
 (this branch is used by jk/git-hash-cleanups.)

 Various code paths that initialize a cryptographic hash context but
 bail out or finish without calling 'git_hash_final()' have been taught
 to call 'git_hash_discard()' to release allocated resources, fixing
 memory leaks when Git is built with non-default backends like
 'OpenSSL' or 'libgcrypt'.

 Will merge to 'master'.
 cf. <aktIIKuReMxJmDsi@pks.im>
 source: <20260702075234.GA1548258@coredump.intra.peff.net>


* ml/t9811-replace-test-f (2026-07-02) 1 commit
 - t9811: replace 'test -f' and '! test -f' with 'test_path_*'

 The test script 't/t9811-git-p4-label-import.sh' has been
 modernized to use 'test_path_is_file' and 'test_path_is_missing'
 instead of raw 'test -f' and '! test -f' calls.

 Expecting a reroll.
 cf. <akdwp_a2EuhVoGVW@pks.im>
 cf. <CAO=vGZpMe3dxyzFVwR7BWBxaAZ-z9Kw3CqQ0kAe5ZZGSQszkzw@mail.gmail.com>
 source: <20260702140704.65805-1-marcelomlage@usp.br>


* ps/t-fixes-for-git-test-long (2026-07-05) 9 commits
  (merged to 'next' on 2026-07-09 at c5b13248c8)
 + gitlab-ci: enable "GIT_TEST_LONG"
 + gitlab-ci: disable RAM disk on macOS jobs
 + t: use `test_bool_env` to parse GIT_TEST_LONG
 + t7900: clean up large EXPENSIVE repository
 + t7508: skip EXPENSIVE test that is broken without SIZE_T_IS_64BIT
 + t5608: reduce maximum disk usage
 + t4141: fix inefficient use of dd(1)
 + t0021: skip EXPENSIVE test that is broken without SIZE_T_IS_64BIT
 + README: add GitLab CI badge to make it more discoverable

 Various test scripts have been updated to clean up large temporary
 files and repositories, reducing peak disk usage during testing.
 Also, expensive tests have been disabled on platforms that lack
 sufficient resources (like 32-bit platforms and Windows CI
 runners), and the long test suite has been enabled in GitLab CI.

 Will merge to 'master'.
 cf. <20260707043026.GB677056@coredump.intra.peff.net>
 source: <20260706-b4-pks-t-fixes-for-GIT-TEST-LONG-v3-0-4f6c5a37fd1f@pks.im>


* ih/precompose-flex-array (2026-07-04) 1 commit
  (merged to 'next' on 2026-07-09 at 737a87f65e)
 + precompose_utf8: use a flex array for d_name

 The UTF-8 precomposition wrapper on macOS has been updated to use a
 flexible array member to represent the name of a directory entry,
 preventing fortified libc checks from failing when the name is
 reallocated to be larger than 'NAME_MAX' bytes.

 Will merge to 'master'.
 cf. <20260703050800.GA29216@tb-raspi4>
 source: <20260704233724.16928-1-ihar.hrachyshka@gmail.com>


* sn/osxkeychain-rust-universal (2026-07-07) 3 commits
 - contrib: wire up osxkeychain in contrib/Makefile on macOS
 - Makefile: support universal macOS builds via RUST_TARGETS
 - Makefile: add $(RUST_LIB) prerequisite to osxkeychain

 The build system has been updated to support building universal macOS
 binaries when 'Rust' is enabled, by compiling separate static archives
 for each target triple listed in 'RUST_TARGETS' and combining them
 using the macOS 'lipo' tool. The 'git-credential-osxkeychain' helper
 has been updated to link against '$(RUST_LIB)' when 'Rust' is enabled.

 Will merge to 'next'?
 cf. <xmqq4ii9teym.fsf@gitster.g>
 source: <pull.2288.v8.git.git.1783480879.gitgitgadget@gmail.com>


* cl/conditional-config-on-worktree-path (2026-07-08) 3 commits
 . config: add "worktree" and "worktree/i" includeIf conditions
 . repository: keep a symlink-preserving copy of the worktree path
 . config: refactor include_by_gitdir() into include_by_path()

 The '[includeIf "condition"]' conditional inclusion facility for
 configuration files has been taught to use the location of the
 worktree in its condition.

 Needs review.
 cf. <ak9zWopOWpRVHmmS@pks.im>
 cf. <xmqqzezzkibu.fsf@gitster.g>
 source: <20260709-includeif-worktree-v7-0-e87e705e8df6@black-desk.cn>


* kk/commit-reach-find-all-fix (2026-06-29) 2 commits
 - commit-reach: guard !FIND_ALL early exit with generation ordering check
 - t6600: add test for merge-base early exit with clock skew

 The early-exit optimization in 'paint_down_to_common()' has been gated
 on the queue being generation-ordered. This fixes a bug where 'git
 merge-base' (without '--all') could return incorrect results on
 repositories with v1 commit graphs and clock skew.

 Will merge to 'next'.
 cf. <xmqqjyr5v1gu.fsf@gitster.g>
 source: <pull.2162.git.1782739162.gitgitgadget@gmail.com>


* bl/t7412-use-test-path-helpers (2026-06-29) 1 commit
 - submodule absorbgitdirs tests: use test_* helper functions

 't7412' that tests 'git submodule absorbgitdirs' has been modernized
 to use 'test_path_is_file', 'test_path_is_dir', and
 'test_path_is_missing' helper functions instead of raw 'test -[fde]'
 commands.

 Waiting for response(s) to review comment(s).
 cf. <akTKHfKPsP3-Rn31@pks.im>
 source: <20260630020220.1559190-1-bblima@usp.br>


* jk/format-patch-leakfix (2026-06-29) 2 commits
  (merged to 'next' on 2026-07-06 at 35aff0d609)
 + format-patch: fix leak of rev_info in prepare_bases()
 + t: move LSan errors from stdout to stderr

 A memory leak in the '--base' handling of 'git format-patch' has been
 plugged, and the leak-reporting of the test suite when running under a
 TAP harness has been improved.

 Will merge to 'master'.
 cf. <akOZy-BygZS8fqPM@pks.im>
 source: <20260630063944.GA3733670@coredump.intra.peff.net>


* ps/setup-split-discovery-and-setup (2026-07-07) 16 commits
 - setup: mark `set_git_work_tree()` as file-local
 - setup: pass worktree to `init_db()`
 - setup: drop redundant configuration of `startup_info->have_repository`
 - setup: make repository discovery self-contained
 - setup: propagate prefix via repository discovery
 - setup: drop static `cwd` variable
 - setup: move prefix into repository
 - setup: embed repository format in discovery
 - setup: introduce explicit repository discovery
 - setup: split up concerns of `setup_git_env_internal()`
 - setup: unify setup of shallow file
 - setup: mark bogus worktree in `apply_repository_format()`
 - setup: rename `check_repository_format_gently()`
 - Merge branch 'jk/repo-info-path-keys' into ps/setup-split-discovery-and-setup
 - Merge branch 'ps/setup-drop-global-state' into ps/setup-split-discovery-and-setup
 - Merge branch 'ps/refs-onbranch-fixes' into ps/setup-split-discovery-and-setup

 The repository discovery and repository configuration phases, which
 were previously intertwined in 'setup.c', have been split. Repository
 discovery has been updated to populate a 'struct repo_discovery'
 without modifying the repository state, which is then taken by
 repository configuration to initialize the repository, paving the way
 for clean unification of repository configuration.

 Will merge to 'next'.
 cf. <87h5m9om0j.fsf@emacs.iotcl.com>
 source: <20260707-pks-setup-split-discovery-and-setup-v2-0-aab372cd227c@pks.im>


* pw/rebase-drop-notes-with-commit (2026-06-30) 15 commits
 - amend! sequencer: simplify pick_one_commit()
 - amend! sequencer: remove unnecessary "or" in pick_one_commit()
 - fixup! sequencer: never reschedule on failed commit
 - fixup! sequencer: be more careful with external merge
 - sequencer: do not record dropped commits as rewritten
 - sequencer: use an enum to represent result of picking a commit
 - sequencer: return early from pick_one_commit() on success
 - sequencer: simplify pick_one_commit()
 - sequencer: remove unnecessary condition in pick_one_commit()
 - sequencer: simplify handing of fixup with conflicts
 - sequencer: remove unnecessary "or" in pick_one_commit()
 - sequencer: never reschedule on failed commit
 - sequencer: be more careful with external merge
 - sequencer: move definition of is_final_fixup()
 - t3400: restore coverage for note copying with apply backend

 The rebase post-rewrite notes-copying logic has been corrected. When a
 commit is dropped during rebase (e.g., because its changes are already
 upstream), it is no longer recorded as rewritten, preventing its notes
 from being copied to an unrelated commit.

 Expecting a reroll.
 cf. <dce74d17-eefd-40bb-82f3-f6b3179cc2b6@gmail.com>
 source: <cover.1782833268.git.phillip.wood@dunelm.org.uk>


* jk/bloom-leak-fixes (2026-06-30) 3 commits
  (merged to 'next' on 2026-07-08 at 3b9a1cda3f)
 + line-log: drop extra copy of range with bloom filters
 + revision: avoid leaking bloom keyvecs with multiple traversals
 + bloom: make bloom-filter slab initialization idempotent

 Various memory leaks in the Bloom-filter code paths that are exposed
 when running tests with the 'GIT_TEST_COMMIT_GRAPH_CHANGED_PATHS=1'
 environment variable have been plugged.

 Will merge to 'master'.
 cf. <b641aed4-ad52-477b-b1d8-9d8e470be46f@gmail.com>
 cf. <xmqqo6gqobrt.fsf@gitster.g>
 source: <20260701063538.GA2579765@coredump.intra.peff.net>


* js/ci-dockerized-pid-limit (2026-07-04) 1 commit
  (merged to 'next' on 2026-07-09 at cd80e673a5)
 + ci(dockerized): raise the PID limit for private repositories

 Dockerized CI jobs running in private GitHub repositories have been
 adjusted to use explicit process and file limits, preventing resource
 exhaustion errors on private runners.

 Will merge to 'master'.
 cf. <xmqqh5medmzh.fsf@gitster.g>
 source: <pull.2164.v2.git.1783155124926.gitgitgadget@gmail.com>


* js/coverity-fixes (2026-07-05) 12 commits
  (merged to 'next' on 2026-07-09 at 1823fe297c)
 + mingw: make `exit_process()` own the process handle on all paths
 + fsmonitor: plug token-data leak on early daemon-startup failures
 + reftable/table: release filter on error path
 + imap-send: avoid leaking the IMAP upload buffer
 + worktree: fix resource leaks when branch creation fails
 + submodule: fix cwd leak in `get_superproject_working_tree()`
 + dir: free allocations on parse-error paths in `read_one_dir()`
 + line-log: avoid redundant copy that leaks in process_ranges
 + run-command: avoid `close(-1)` in `start_command()` error paths
 + download_https_uri_to_file(): do not leak fd upon failure
 + loose: avoid closing invalid fd on error path
 + load_one_loose_object_map(): fix resource leak

 Various resource leaks, invalid file descriptor closures, and process
 handle ownership issues flagged by Coverity have been fixed.

 Will merge to 'master'.
 cf. <xmqqa4s238lg.fsf@gitster.g>
 source: <pull.2163.v2.git.1783239870.gitgitgadget@gmail.com>


* tb/repack-geometric-cruft (2026-06-28) 11 commits
 - SQUASH??? bare grep !???
 - repack: support combining '--geometric' with '--cruft'
 - pack-objects: support '--refs-snapshot' with 'follow-reachable'
 - pack-objects: introduce '--stdin-packs=follow-reachable'
 - pack-objects: extract `stdin_packs_add_all_pack_entries()`
 - repack-geometry: drop unused redundant-pack removal
 - repack: delete geometric packs via existing_packs
 - repack: teach MIDX retention about geometric rollups
 - repack: mark geometric progression of packs as retained
 - repack: extract `locate_existing_pack()` helper
 - repack: unconditionally exclude non-kept packs

 'git repack' has been taught to accept '--geometric' and '--cruft'
 together. When both are given, the geometric repack rolls up non-cruft
 packs as usual, while a separate cruft pack is written to collect
 unreachable objects.

 Waiting for response(s) to review comment(s).
 cf. <aj8cOhH6hGVZIFft@nand.local>
 source: <cover.1782500507.git.me@ttaylorr.com>


* jk/reftable-leakfix (2026-06-28) 1 commit
  (merged to 'next' on 2026-07-06 at 55ce81f2d5)
 + reftable: fix unlikely leak on API error

 A memory leak in the 'reftable_writer_new()' initialization function
 has been fixed by delaying the allocation of 'struct reftable_writer'
 until after input options are validated.

 Will merge to 'master'.
 cf. <akIPBJLtPqDjQt-A@pks.im>
 source: <20260628090314.GA661068@coredump.intra.peff.net>


* ad/gpg-strip-cr-before-lf (2026-06-24) 1 commit
  (merged to 'next' on 2026-07-06 at b099249efd)
 + gpg-interface: fix strip_cr_before_lf to only remove CR before LF

 The GPG and SSH signature parsing code has been corrected to strip
 carriage return characters only when they immediately precede line
 feeds, instead of unconditionally stripping all carriage returns.

 Will merge to 'master'.
 source: <20260624093618.17456-1-antonio.destefani08@gmail.com>


* jt/receive-pack-use-odb-transactions (2026-07-08) 11 commits
 - builtin/receive-pack: stage incoming objects via ODB transactions
 - builtin/receive-pack: drop redundant tmpdir env
 - odb/transaction: introduce ODB transaction flags
 - odb/transaction: add transaction env interface
 - odb/transaction: propagate commit errors
 - odb/transaction: propagate begin errors
 - object-file: propagate files transaction errors
 - object-file: drop check for inflight transactions
 - object-file: embed transaction flush logic in commit function
 - object-file: rename files transaction fsync function
 - object-file: rename files transaction prepare function

 'git receive-pack' has been refactored to use ODB transaction
 interfaces instead of directly managing 'tmp_objdir' for staging
 incoming objects, bringing it closer to being ODB backend agnostic.

 Expecting a reroll.
 cf. <ak-ntVKQ8XqMr6zv@denethor>
 source: <20260708235925.3992097-1-jltobler@gmail.com>


* ps/odb-drop-whence (2026-07-02) 7 commits
  (merged to 'next' on 2026-07-08 at f43ee51cc3)
 + odb: document object info fields
 + odb: drop `whence` field from object info
 + treewide: convert users of `whence` to the new source field
 + odb: add `source` field to struct object_info_source
 + odb: make backend-specific fields optional
 + packfile: thread odb_source_packed through packed_object_info()
 + Merge branch 'ps/odb-source-packed' into ps/odb-drop-whence
 (this branch is used by ps/odb-for-each-object-filter.)

 The 'whence' field in 'struct object_info' has been removed. The
 backend-specific object information retrieval has been refactored into
 an opt-in 'struct object_info_source' structure.

 Will merge to 'master'.
 cf. <xmqqv7b0rmt6.fsf@gitster.g>
 source: <20260702-b4-pks-odb-drop-whence-v2-0-b0af7468ad95@pks.im>


* ps/reftable-hardening (2026-07-03) 12 commits
 - reftable/table: fix OOB read on truncated table
 - reftable/table: fix NULL pointer access when seeking to bogus offsets
 - reftable/block: fix OOB read with bogus restart offset
 - reftable/block: fix use of uninitialized memory when binsearch fails
 - reftable/block: fix OOB read with bogus restart count
 - reftable/block: fix OOB read with bogus block size
 - reftable/block: fix OOB write with bogus inflated log size
 - t/unit-tests: introduce test helper to write reftable blocks
 - reftable/record: don't abort when decoding invalid ref value type
 - reftable/basics: fix OOB read on binary search of empty range
 - oss-fuzz: add fuzzer for parsing reftables
 - meson: support building fuzzers with libFuzzer

 The reftable code has been hardened against corrupted tables by
 fixing out-of-bounds writes, out-of-bounds reads, and abort calls
 during parsing.

 Will merge to 'next'?
 cf. <877bn5obz9.fsf@emacs.iotcl.com>
 source: <20260703-pks-reftable-hardening-v3-0-b87c555b9920@pks.im>


* hn/branch-push-slip-advice (2026-06-27) 2 commits
  (merged to 'next' on 2026-07-06 at acdff65ac5)
 + push: suggest <remote> <branch> for a slash slip
 + branch: suggest <remote>/<branch> on upstream slip

 When 'git push origin/main' or 'git branch origin main' is run, the
 command is now recognized as a potential typo, and advice has been
 added to offer a typofix.

 Will merge to 'master'.
 cf. <xmqqfr272lq7.fsf@gitster.g>
 source: <pull.2331.v3.git.git.1782583345.gitgitgadget@gmail.com>


* jc/history-message-prep-fix (2026-06-29) 1 commit
  (merged to 'next' on 2026-07-06 at 00534a21ce)
 + history: streamline message preparation and plug file stream leak

 A write file stream resource leak has been fixed as part of a code
 cleanup.

 Will merge to 'master'.
 cf. <akO1mhi2u2PntLbt@pks.im>
 source: <xmqqmrwdxrat.fsf@gitster.g>


* ty/migrate-excludes-file (2026-07-09) 9 commits
 - environment: move object_creation_mode into repo_config_values
 - environment: move autorebase into repo_config_values
 - environment: move push_default into repo_config_values
 - environment: migrate apply_default_whitespace and apply_default_ignorewhitespace
 - environment: move askpass_program into repo_config_values
 - environment: move pager_program into repo_config_values
 - environment: move editor_program into repo_config_values
 - environment: move excludes_file into repo_config_values
 - repository: introduce repo_config_values_clear()

 The 'excludes_file' and various other global configuration variables
 (including 'editor_program', 'pager_program', 'askpass_program', and
 'push_default') have been migrated into the per-repository structure.

 Needs review.
 source: <20260709161145.13349-1-cat@malon.dev>


* dk/meson-enable-use-nsec-build (2026-06-20) 1 commit
 - meson: wire up USE_NSEC build knob

 The 'USE_NSEC' build knob, which enables support for sub-second file
 timestamp resolution, has been wired up to the Meson build system.

 Expecting a reroll.
 cf. <CALnO6CDm74rCBQu6Q0djsvtuw5U14V=PApptcZTgP+pic1f_AA@mail.gmail.com>
 cf. <ajjuoS5Qc3K0nCRl@pks.im>
 cf. <akIL6oJgUv8J8SB2@pks.im>
 source: <c4c5ade901ff95b0f95939ea818870e4f3d59da1.1781971201.git.ben.knoble+github@gmail.com>


* ps/libgit-in-subdir (2026-06-30) 3 commits
 - Move libgit.a sources into separate "lib/" directory
 - t/helper: prepare "test-example-tap.c" for introduction of "lib/"
 - Merge branch 'ps/odb-source-packed' into ps/libgit-in-subdir

 The source files for 'libgit.a' have been moved into a new 'lib/'
 directory to clean up the top-level directory and clearly separate
 library code.

 Needs review.
 cf. <akX1TMoRr87Id8Ss@pks.im>
 source: <20260701-pks-libgit-in-subdir-v3-0-5e4860056094@pks.im>


* ps/odb-generalize-prepare (2026-06-22) 3 commits
  (merged to 'next' on 2026-07-06 at 6132517517)
 + odb: introduce `odb_prepare()`
 + odb/source: generalize `reprepare()` callback
 + Merge branch 'ps/odb-source-packed' into ps/odb-generalize-prepare

 The 'reprepare()' callback for object database sources has been
 generalized into a 'prepare()' callback with an optional flush cache
 flag, and a new 'odb_prepare()' wrapper has been introduced to allow
 pre-opening object database sources.

 Will merge to 'master'.
 cf. <87ik704f1j.fsf@emacs.iotcl.com>
 source: <20260622-b4-pks-odb-generalize-prepare-v1-0-d2a5c5d13144@pks.im>


* ty/migrate-ignorecase (2026-06-19) 2 commits
 - config: use repo_ignore_case() to access core.ignorecase
 - environment: move ignore_case into repo_config_values

 The global configuration variable 'ignore_case' (representing the
 'core.ignorecase' configuration) has been migrated into 'struct
 repo_config_values' to tie it to a specific repository instance.

 Waiting for comments from Johannes.
 cf. <xmqqzf0mzc7j.fsf@gitster.g>
 source: <20260619155152.642760-1-cat@malon.dev>


* mm/line-log-limited-ops (2026-06-27) 7 commits
 - diffcore-pickaxe: scope -G to the -L tracked range
 - diff: support --check with -L line ranges
 - line-log: support diff stat formats with -L
 - diff: extract a line-range diff helper for reuse
 - diff: emit -L hunk headers via xdiff's formatter
 - diff: simplify the line-range filter by classifying removals immediately
 - diff: rename and group the line-range filter for clarity

 The 'git log -L<range>:<path>' command has been taught to limit
 various 'diff' operations, such as '--stat', '--check', and '-G', to
 the specified range:path.

 Needs review.
 source: <pull.2152.v2.git.1782581342.gitgitgadget@gmail.com>


* hn/history-squash (2026-07-06) 5 commits
 - history: re-edit a squash with every message
 - sequencer: extract helpers for the squash message markers
 - history: add squash subcommand to fold a range
 - history: give commit_tree_ext a message template
 - history: extract helper for a commit's parent tree

 The experimental 'git history' command has been taught a new 'squash'
 subcommand to fold a range of commits into a single commit, replaying
 any descendants on top.

 Waiting for response(s) to review comment(s).
 cf. <38493ca6-8fdd-4b6c-9972-5145f3bf0aa4@gmail.com>
 source: <pull.2337.v7.git.git.1783327849.gitgitgadget@gmail.com>


* ps/refs-writing-subcommands (2026-07-06) 5 commits
  (merged to 'next' on 2026-07-08 at f001147283)
 + builtin/refs: add "rename" subcommand
 + builtin/refs: add "create" subcommand
 + builtin/refs: add "update" subcommand
 + builtin/refs: add "delete" subcommand
 + builtin/refs: drop `the_repository`
 (this branch is used by ps/refs-wo-the-repository.)

 The 'git refs' toolbox has been extended with new 'create', 'delete',
 'update', and 'rename' subcommands to create, delete, update, and
 rename references, respectively.

 Will merge to 'master'.
 source: <20260706-pks-refs-writing-subcommands-v4-0-d51f6ce7f830@pks.im>


* wy/doc-myfirstcontribution-trim-quotes (2026-06-11) 1 commit
 - MyFirstContribution: mention trimming quoted text in replies

 The contributor guide has been updated to advise new contributors to
 trim irrelevant quoted text when replying to review comments, matching
 the existing advice given to reviewers.

 Will merge to 'next'?
 cf. <xmqqcxxwljue.fsf@gitster.g>
 source: <080402ff0ac8127b654dccea59a1bf643df62a5c.1781186476.git.wy@wyuan.org>


* tb/midx-incremental-custom-base (2026-06-12) 3 commits
 - midx-write: include packs above custom incremental base
 - midx: pass custom '--base' through incremental writes
 - t5334: expose shared `nth_line()` helper

 The 'git multi-pack-index write --incremental' command has been
 corrected to properly honor the '--base' option. Previously, the
 custom base was ignored by the normal write path, and the pack
 exclusion logic incorrectly skipped packs from layers above the
 selected base, breaking reachability closure for bitmaps.

 Needs review.
 source: <cover.1781294771.git.me@ttaylorr.com>


* mm/test-grep-lint (2026-07-05) 6 commits
 - t: add greplint to detect bare grep assertions
 - t: convert grep assertions to test_grep
 - t: fix Lexer line count for $() inside double-quoted strings
 - t: extract chainlint's parser into shared module
 - t: fix grep assertions missing file arguments
 - t/README: document test_grep helper

 The test suite has been updated to use the 'test_grep' helper instead
 of bare 'grep' for test assertions, allowing file contents to be
 printed on failure for easier debugging. A new 'greplint' linter has
 been introduced to detect and prevent new bare 'grep' assertions from
 being added to the test suite.

 Will merge to 'next'.
 cf. <xmqqtsqedxmt.fsf@gitster.g>
 source: <pull.2135.v4.git.1783314119.gitgitgadget@gmail.com>


* kk/prio-queue-get-put-fusion (2026-06-08) 2 commits
  (merged to 'next' on 2026-07-06 at aa748c4564)
 + prio-queue: fold lazy_queue into prio_queue for automatic get+put fusion
 + prio-queue: rename .nr to .nr_ and add accessor helpers
 (this branch is used by kk/prio-queue-cascade-sift.)

 The lazy priority queue optimization pattern (deferring actual removal
 in 'prio_queue_get()' to allow get+put fusion) has been folded
 directly into 'prio_queue' itself, speeding up commit traversal
 workflows and simplifying callers.

 Will merge to 'master'.
 cf. <xmqqh5mjrbgq.fsf@gitster.g>
 source: <pull.2140.v4.git.1780945851.gitgitgadget@gmail.com>


* td/ref-filter-memoize-contains (2026-06-12) 3 commits
 - commit-reach: die on contains walk errors
 - ref-filter: memoize --contains with generations
 - commit-reach: reject cycles in contains walk

 'git branch --contains' and 'git for-each-ref --contains' have
 been optimized to use the memoized commit traversal previously
 used only by 'git tag --contains', significantly speeding up
 connectivity checks across many candidate refs with shared
 history.

 Needs review.
 cf. <xmqqqzlpulkp.fsf@gitster.g>
 source: <20260612-ref-filter-memoized-contains-v4-0-5ed39fd001dd@gmail.com>


* tc/replay-linearize (2026-07-07) 3 commits
  (merged to 'next' on 2026-07-09 at 371c2e9c3b)
 + replay: offer an option to linearize the commit topology
 + replay: resolve the replay base outside pick_regular_commit()
 + replay: add helper to put entry into replayed_commits

 The 'git replay' command has been taught the '--linearize' option to
 drop merge commits and linearize the replayed history, mimicking 'git
 rebase --no-rebase-merges'.

 Will merge to 'master'.
 cf. <xmqq5x2qz42z.fsf@gitster.g>
 source: <20260707-toon-git-replay-drop-merges-v7-0-808ab9b4afa6@iotcl.com>


* ps/cat-file-remote-object-info (2026-07-01) 13 commits
 - cat-file: make remote-object-info allow-list dynamic
 - cat-file: validate remote atoms with an allow-list
 - cat-file: add remote-object-info to batch-command
 - transport: add client support for object-info
 - serve: advertise object-info feature
 - fetch-pack: move fetch initialization
 - connect: make `write_fetch_command_and_capabilities()` more generic
 - fetch-pack: move `write_fetch_command_and_capabilities()` to connect.c
 - fetch-pack: drop static `advertise_sid` variable
 - t1006: split test utility functions into new 'lib-cat-file.sh'
 - cat-file: declare loop counter inside for()
 - git-compat-util: add `strtoumax_szt()` with error handling
 - transport-helper: fix memory leak of helper on disconnect

 The 'remote-object-info' command has been added to 'git cat-file
 --batch-command', allowing clients to request object metadata
 (currently size) from a remote server via protocol v2 without
 downloading the entire object. Format placeholders are dynamically
 filtered on the client based on server-advertised capabilities,
 returning empty strings for inapplicable or unsupported fields.

 Expecting a reroll.
 cf. <CAN5EUNQ=2qtKXSJvxQiNLYqx0N0m6sfyBGLLXm4FB1kwtOsdbQ@mail.gmail.com>
 source: <20260701-ps-eric-work-rebase-v15-0-c88a43b63917@gmail.com>


* mm/diff-process-hunks (2026-06-14) 6 commits
 - blame: consult diff process for no-hunk detection
 - diff: bypass diff process with --no-ext-diff and in format-patch
 - diff: add long-running diff process via diff.<driver>.process
 - sub-process: separate process lifecycle from hashmap management
 - userdiff: add diff.<driver>.process config
 - xdiff: support external hunks via xpparam_t

 A new 'diff.<driver>.process' configuration has been introduced to
 allow a long-running external process to act as a hunk provider,
 allowing external tools to control which lines Git considers changed
 while leaving all output formatting (word diff, color, blame, etc.) to
 Git's standard pipeline.

 Expecting a reroll.
 cf. <CAC2Qwm+P=fZOtpfMPeMiSXf3Afk6OLYpTP8Br78_PRA8WNL1Wg@mail.gmail.com>
 source: <pull.2120.v4.git.1781463564.gitgitgadget@gmail.com>


* ty/migrate-trust-executable-bit (2026-06-19) 3 commits
 - environment: move trust_executable_bit into repo_config_values
 - read-cache: move 'ce_mode_from_stat()' to 'read-cache.c'
 - read-cache: remove redundant extern declarations

 The 'trust_executable_bit' (coming from 'core.filemode'
 configuration) has been migrated into 'repo_config_values' to tie it
 to a specific repository instance.

 Needs review.
 cf. <xmqqcxx9ukvw.fsf@gitster.g>
 source: <20260619162105.648495-1-cat@malon.dev>


* kk/prio-queue-cascade-sift (2026-07-08) 3 commits
 - prio-queue: use cascade for unfused gets
 - prio-queue: extract sift_up() from prio_queue_put()
 - Merge branch 'kk/prio-queue-get-put-fusion' into kk/prio-queue-cascade-sift
 (this branch uses kk/prio-queue-get-put-fusion.)

 'prio_queue_get()' has been optimized by using a cascade-down approach
 (promoting the smaller child at each level and sifting up the last
 element from the leaf vacancy), which halves the number of comparisons
 per extract-min operation in the common case.

 Needs review.
 source: <pull.2132.v3.git.1783532989.gitgitgadget@gmail.com>


* ps/history-drop (2026-07-01) 11 commits
  (merged to 'next' on 2026-07-08 at 6fb84708a4)
 + builtin/history: implement "drop" subcommand
 + builtin/history: split handling of ref updates into two phases
 + replay: expose `replay_result_queue_update()`
 + reset: stop assuming that the caller passes in a clean index
 + reset: allow the caller to specify the current HEAD object
 + reset: introduce ability to skip updating HEAD
 + reset: introduce dry-run mode
 + reset: modernize flags passed to `reset_working_tree()`
 + reset: rename `reset_head()`
 + reset: drop `USE_THE_REPOSITORY_VARIABLE`
 + read-cache: split out function to drop unmerged entries to stage 0

 The experimental 'git history' command has been taught a new 'drop'
 subcommand to remove a commit and replay its descendants onto its
 parent.

 Will merge to 'master'.
 cf. <xmqq1pdmprbk.fsf@gitster.g>
 cf. <CAP8UFD3OAktVQsLuqBNFH2uhEO31PH8ZF3ZT1ZW8k++XE8YLPw@mail.gmail.com>
 source: <20260701-b4-pks-history-drop-v8-0-19b5cdf1facd@pks.im>


* za/completion-hide-dotfiles (2026-06-20) 2 commits
 - completion: hide dotfiles by default for path completion
 - completion: hide dotfiles for selected path completion

 Path completion for commands like 'git rm' and 'git mv' has been
 updated to hide dotfiles by default unless the user explicitly starts
 the path with a dot, matching standard shell-completion behavior.

 Waiting for response(s) to review comment(s).
 cf. <xmqqik71t3nr.fsf@gitster.g>
 source: <pull.2311.v3.git.git.1781978156.gitgitgadget@gmail.com>


* ec/commit-fixup-options (2026-05-26) 2 commits
 - commit: allow -c/-C for all kinds of --fixup
 - commit: allow -m/-F for all kinds of --fixup

 Support for '-m', '-F', '-c', or '-C' options to supply a commit log
 message from outside the editor has been added for all 'git commit
 --fixup' variations.

 Needs review.
 source: <cover.1779792311.git.erik@cervined.in>


* kh/doc-replay-config (2026-06-05) 4 commits
 - doc: replay: move “default” to the right-hand side
 - doc: replay: use a nested description list
 - doc: replay: improve config description
 - doc: link to config for git-replay(1)

 Documentation for 'git replay' has been updated to refer to its
 configuration variables.

 Waiting for response(s) to review comment(s).
 cf. <87cxwxofgv.fsf@emacs.iotcl.com>
 source: <V3_CV_doc_replay_config.780@msgid.xyz>


* hn/branch-delete-merged (2026-06-24) 7 commits
 - branch: add --dry-run for --delete-merged
 - branch: add branch.<name>.deleteMerged opt-out
 - branch: add --delete-merged <branch>
 - branch: prepare delete_branches for a bulk caller
 - branch: let delete_branches skip unmerged branches on bulk refusal
 - branch: convert delete_branches() to a flags argument
 - branch: add --forked filter for --list mode

 The 'git branch' command has been taught the '--delete-merged' option
 to remove local branches that are already merged to their tracked
 remote-tracking branches.

 Needs review.
 source: <pull.2285.v18.git.git.1782338106.gitgitgadget@gmail.com>


* hn/checkout-track-fetch (2026-06-24) 2 commits
 - checkout: extend --track with a "fetch" mode to refresh start-point
 - branch: expose helpers for finding the remote owning a tracking ref

 The 'git checkout --track=...' command has been taught to optionally
 fetch the branch from the remote the new branch will work with.

 Waiting for response(s) to review comment(s).
 cf. <xmqq5x37h6fj.fsf@gitster.g>
 source: <pull.2281.v15.git.git.1782338098.gitgitgadget@gmail.com>


* ps/shift-root-in-graph (2026-07-04) 3 commits
 - graph: indent visual root in graph
 - graph: add a 2 commit buffer for lookahead
 - lib-log-graph: move check_graph function

 'git log --graph' has been modified to visually distinguish parentless
 'root' commits (and commits that become roots due to history
 simplification) by indenting them, preventing them from appearing
 falsely related to unrelated commits rendered immediately above them.

 Expecting a reroll.
 cf. <CAN5EUNQoLtJ9cGwe8RNJTTdngM=qoak2=5F+yc7TH94TmQn7uw@mail.gmail.com>
 source: <20260704-ps-pre-commit-indent-v7-0-a94706cc8376@gmail.com>


* kk/merge-base-exhaustion (2026-07-01) 10 commits
 . commit-reach: remove commit-date ordering fallback
 . commit-reach: move min_generation check into paint_queue_get()
 . commit-reach: terminate merge-base walk when one paint side is exhausted
 . commit-reach: introduce struct paint_state with per-side counters
 . t6600: add clock-skew topologies and step counts for edge cases
 . commit-reach: add trace2 instrumentation to paint_down_to_common()
 . t6099, t6600: add side-exhaustion regression tests
 . t6600: add test cases for side-exhaustion edge cases
 . test-lib-functions: improve diagnostic output for trace2 data assertions
 . Documentation/technical: add paint-down-to-common doc

 The merge-base computation has been optimized by stopping the walk
 early when one side's exclusive commits in the queue are exhausted,
 yielding significant speedups for queries with one-sided histories.

 Expecting a reroll.
 cf. <CAL71e4PgcZDK-gJziJa_yjEqX9TE+PFMwZn0xbjAUzuUDDDBYA@mail.gmail.com>
 source: <pull.2149.v5.git.1782923832.gitgitgadget@gmail.com>

^ permalink raw reply

* Re: [PATCH] gpg-interface: still print ssh signatures when allowed signers file is not set
From: Grayson Tinker @ 2026-07-09 23:50 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Elijah Newren, Fabian Stelzer,
	Jeff King, René Scharfe
In-Reply-To: <20260625194330.3711-1-graysontinker@gmail.com>

Friendly ping on this :)

^ permalink raw reply

* [PATCH v1 3/3] worktree: run post-worktree-remove hook when pruning
From: Domen Kožar @ 2026-07-09 23:36 UTC (permalink / raw)
  To: git
  Cc: Eric Sunshine, Patrick Steinhardt,
	Ævar Arnfjörð Bjarmason, Caleb White,
	Junio C Hamano, Domen Kožar, Claude Fable 5
In-Reply-To: <20260709233542.628628-1-domen@cachix.org>

A working tree can also disappear via "git worktree prune", e.g.
after the user deleted the working tree directory manually. Tooling
that tears down per-worktree state wants to observe those deletions
the same way as an explicit "git worktree remove".

Run the post-worktree-remove hook once for each administrative entry
that "git worktree prune" removes, including duplicate entries pruned
during deduplication. The hook is not run with --dry-run, and a
failing hook is reflected in the exit status of the command.

should_prune_worktree() so far returned the path of the worktree's
.git file only for entries that are kept. Also return it when pruning
an entry whose gitdir file points to a location that no longer
exists, which is the common case of a manually deleted working tree,
so that the hook can be given the path. For entries whose path cannot
be determined at all (missing or corrupt gitdir file), the hook
receives an empty string instead. The one other caller of
should_prune_worktree() already frees the path unconditionally.

Signed-off-by: Domen Kožar <domen@cachix.org>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
 Documentation/githooks.adoc | 23 +++++-----
 builtin/worktree.c          | 48 ++++++++++++++------
 t/t2401-worktree-prune.sh   | 88 +++++++++++++++++++++++++++++++++++++
 worktree.c                  |  1 -
 worktree.h                  |  6 +--
 5 files changed, 139 insertions(+), 27 deletions(-)

diff --git a/Documentation/githooks.adoc b/Documentation/githooks.adoc
index 22b3263ff7..28fab7ccbe 100644
--- a/Documentation/githooks.adoc
+++ b/Documentation/githooks.adoc
@@ -239,16 +239,19 @@ post-worktree-remove
 ~~~~~~~~~~~~~~~~~~~~
 
 This hook is invoked by linkgit:git-worktree[1] after a working tree
-has been deleted by `git worktree remove`. The hook is given two
-parameters: the absolute path of the removed working tree and its
-identifier (the name of its former administrative directory in
-`$GIT_DIR/worktrees/`).
-
-The working tree no longer exists when the hook runs.
-
-This hook cannot affect the outcome of `git worktree remove`, other
-than that the hook's exit status becomes the exit status of the
-command.
+has been deleted by `git worktree remove`, and once for each working
+tree pruned by `git worktree prune`. The hook is given two parameters:
+the absolute path of the removed working tree and its identifier (the
+name of its former administrative directory in `$GIT_DIR/worktrees/`).
+
+The working tree no longer exists when the hook runs. For working
+trees pruned by `git worktree prune`, the first parameter may be the
+empty string if the path could not be determined from the leftover
+administrative files.
+
+This hook cannot affect the outcome of `git worktree remove` or
+`git worktree prune`, other than that the hook's exit status becomes
+the exit status of the command.
 
 This hook can be used to tear down per-worktree development
 environments or to unregister the working tree from external tools.
diff --git a/builtin/worktree.c b/builtin/worktree.c
index 01b62ed2fc..e2cdbef8bb 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -176,12 +176,27 @@ static int run_post_worktree_remove_hook(const char *path, const char *id)
 	return run_hooks_opt(the_repository, "post-worktree-remove", &hook_opt);
 }
 
-static void prune_worktree(const char *id, const char *reason)
+static int prune_worktree(const char *id, const char *dotgit,
+			  const char *reason)
 {
+	struct strbuf path = STRBUF_INIT;
+	int ret;
+
 	if (show_only || verbose)
 		fprintf_ln(stderr, _("Removing %s/%s: %s"), "worktrees", id, reason);
-	if (!show_only)
-		delete_git_dir(id);
+	if (show_only)
+		return 0;
+
+	delete_git_dir(id);
+
+	/* path stays empty when the worktree path cannot be determined */
+	if (dotgit) {
+		strbuf_addstr(&path, dotgit);
+		strbuf_strip_suffix(&path, "/.git");
+	}
+	ret = run_post_worktree_remove_hook(path.buf, id);
+	strbuf_release(&path);
+	return ret;
 }
 
 static int prune_cmp(const void *a, const void *b)
@@ -206,18 +221,22 @@ static int prune_cmp(const void *a, const void *b)
 	return strcmp(x->util, y->util);
 }
 
-static void prune_dups(struct string_list *l)
+static int prune_dups(struct string_list *l)
 {
 	int i;
+	int ret = 0;
 
 	QSORT(l->items, l->nr, prune_cmp);
 	for (i = 1; i < l->nr; i++) {
 		if (!fspathcmp(l->items[i].string, l->items[i - 1].string))
-			prune_worktree(l->items[i].util, "duplicate entry");
+			ret |= prune_worktree(l->items[i].util,
+					      l->items[i].string,
+					      "duplicate entry");
 	}
+	return ret;
 }
 
-static void prune_worktrees(void)
+static int prune_worktrees(void)
 {
 	struct strbuf reason = STRBUF_INIT;
 	struct strbuf main_path = STRBUF_INIT;
@@ -225,19 +244,22 @@ static void prune_worktrees(void)
 	char *path;
 	DIR *dir;
 	struct dirent *d;
+	int ret = 0;
 
 	path = repo_git_path(the_repository, "worktrees");
 	dir = opendir(path);
 	free(path);
 	if (!dir)
-		return;
+		return 0;
 	while ((d = readdir_skip_dot_and_dotdot(dir)) != NULL) {
 		char *path;
 		strbuf_reset(&reason);
-		if (should_prune_worktree(d->d_name, &reason, &path, expire))
-			prune_worktree(d->d_name, reason.buf);
-		else if (path)
+		if (should_prune_worktree(d->d_name, &reason, &path, expire)) {
+			ret |= prune_worktree(d->d_name, path, reason.buf);
+			free(path);
+		} else if (path) {
 			string_list_append_nodup(&kept, path)->util = xstrdup(d->d_name);
+		}
 	}
 	closedir(dir);
 
@@ -245,12 +267,13 @@ static void prune_worktrees(void)
 	/* massage main worktree absolute path to match 'gitdir' content */
 	strbuf_strip_suffix(&main_path, "/.");
 	string_list_append_nodup(&kept, strbuf_detach(&main_path, NULL));
-	prune_dups(&kept);
+	ret |= prune_dups(&kept);
 	string_list_clear(&kept, 1);
 
 	if (!show_only)
 		delete_worktrees_dir_if_empty();
 	strbuf_release(&reason);
+	return ret;
 }
 
 static int prune(int ac, const char **av, const char *prefix,
@@ -269,8 +292,7 @@ static int prune(int ac, const char **av, const char *prefix,
 			   0);
 	if (ac)
 		usage_with_options(git_worktree_prune_usage, options);
-	prune_worktrees();
-	return 0;
+	return prune_worktrees();
 }
 
 static char *junk_work_tree;
diff --git a/t/t2401-worktree-prune.sh b/t/t2401-worktree-prune.sh
index f8f28c76ee..74a80c1a8d 100755
--- a/t/t2401-worktree-prune.sh
+++ b/t/t2401-worktree-prune.sh
@@ -119,6 +119,94 @@ test_expect_success 'prune duplicate (main/linked)' '
 	test_path_is_missing .git/worktrees/wt
 '
 
+test_expect_success 'prune invokes post-worktree-remove hook' '
+	test_hook post-worktree-remove <<-\EOF &&
+	echo $* >hook.actual
+	EOF
+	git worktree add --detach flushed &&
+	rm -rf flushed &&
+	git worktree prune &&
+	echo $(pwd)/flushed flushed >hook.expect &&
+	test_cmp hook.expect hook.actual
+'
+
+test_expect_success 'prune invokes post-worktree-remove hook once per worktree' '
+	test_hook post-worktree-remove <<-\EOF &&
+	echo $* >>hook.actual
+	EOF
+	git worktree add --detach first &&
+	git worktree add --detach second &&
+	rm -rf first second hook.actual &&
+	git worktree prune &&
+	{
+		echo $(pwd)/first first &&
+		echo $(pwd)/second second
+	} >hook.expect &&
+	sort hook.actual >hook.sorted &&
+	test_cmp hook.expect hook.sorted
+'
+
+test_expect_success 'prune --dry-run does not invoke post-worktree-remove hook' '
+	git worktree add --detach dry &&
+	rm -rf dry &&
+	test_when_finished "git worktree prune" &&
+	test_hook post-worktree-remove <<-\EOF &&
+	>hook.ran
+	EOF
+	git worktree prune --dry-run &&
+	test_path_is_missing hook.ran
+'
+
+test_expect_success 'pruned entry with unknown path gives empty hook argument' '
+	test_hook post-worktree-remove <<-\EOF &&
+	echo "[$1][$2]" >hook.actual
+	EOF
+	mkdir -p .git/worktrees/broken &&
+	: >.git/worktrees/broken/gitdir &&
+	git worktree prune &&
+	echo "[][broken]" >hook.expect &&
+	test_cmp hook.expect hook.actual
+'
+
+test_expect_success 'failing post-worktree-remove hook fails prune' '
+	test_hook post-worktree-remove <<-\EOF &&
+	exit 1
+	EOF
+	git worktree add --detach doomed &&
+	rm -rf doomed &&
+	test_must_fail git worktree prune &&
+	test_path_is_missing .git/worktrees/doomed
+'
+
+test_expect_success 'prune duplicate invokes post-worktree-remove hook' '
+	test_when_finished rm -fr .git/worktrees w1 w2 &&
+	test_hook post-worktree-remove <<-\EOF &&
+	echo $* >>hook.actual
+	EOF
+	rm -f hook.actual &&
+	git worktree add --detach w1 &&
+	git worktree add --detach w2 &&
+	sed "s/w2/w1/" .git/worktrees/w2/gitdir >.git/worktrees/w2/gitdir.new &&
+	mv .git/worktrees/w2/gitdir.new .git/worktrees/w2/gitdir &&
+	git worktree prune &&
+	echo $(pwd)/w1 w2 >hook.expect &&
+	test_cmp hook.expect hook.actual
+'
+
+test_expect_success 'post-worktree-remove hook gets absolute path with relative worktrees' '
+	test_when_finished "rm -rf relhook" &&
+	git init relhook &&
+	test_commit -C relhook base &&
+	test_hook -C relhook post-worktree-remove <<-\EOF &&
+	echo $* >hook.actual
+	EOF
+	git -C relhook worktree add --relative-paths --detach wt &&
+	rm -rf relhook/wt &&
+	git -C relhook worktree prune &&
+	echo $(pwd)/relhook/wt wt >hook.expect &&
+	test_cmp hook.expect relhook/hook.actual
+'
+
 test_expect_success 'not prune proper worktrees inside linked worktree with relative paths' '
 	test_when_finished rm -rf repo wt_ext &&
 	git init repo &&
diff --git a/worktree.c b/worktree.c
index 30125827fd..6a9d943874 100644
--- a/worktree.c
+++ b/worktree.c
@@ -1004,7 +1004,6 @@ int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath,
 		if (stat(file.buf, &st) || st.st_mtime <= expire) {
 			strbuf_addstr(reason, _("gitdir file points to non-existent location"));
 			rc = 1;
-			goto done;
 		}
 	}
 	*wtpath = strbuf_detach(&dotgit, NULL);
diff --git a/worktree.h b/worktree.h
index 1075409f9a..dde8fc2be4 100644
--- a/worktree.h
+++ b/worktree.h
@@ -105,9 +105,9 @@ const char *worktree_prune_reason(struct worktree *wt, timestamp_t expire);
 
 /*
  * Return true if worktree entry should be pruned, along with the reason for
- * pruning. Otherwise, return false and the worktree's path in `wtpath`, or
- * NULL if it cannot be determined. Caller is responsible for freeing
- * returned path.
+ * pruning. Otherwise, return false. In both cases the path of the
+ * worktree's `.git` file is returned in `wtpath`, or NULL if it cannot
+ * be determined. Caller is responsible for freeing returned path.
  *
  * `expire` defines a grace period to prune the worktree when its path
  * does not exist.
-- 
2.54.0

^ permalink raw reply related

* [PATCH v1 0/3] worktree: add post-worktree-add and post-worktree-remove hooks
From: Domen Kožar @ 2026-07-09 23:36 UTC (permalink / raw)
  To: git
  Cc: Eric Sunshine, Patrick Steinhardt,
	Ævar Arnfjörð Bjarmason, Caleb White,
	Junio C Hamano, Domen Kožar

Hi everyone,

I maintain devenv, a developer environment manager, and lately the
workflow we see most is people letting AI coding agents loose on a
repository, one linked worktree per task, created and discarded at a
pace no human would type. Each of those worktrees expects a working
environment: processes, sockets, and stateful services such as a
database seeded from a dump.

Today there is no reliable trigger to set that up when a worktree
appears: post-checkout does not fire for --no-checkout or --orphan
and cannot be told apart from a plain checkout. Nothing at all fires
when a worktree goes away, so stale databases and services pile up
after "git worktree remove" or a manual rm followed by "git worktree
prune". Wrapping the worktree commands only helps when every tool,
human or agent, goes through the wrapper.

Patch 1 adds a post-worktree-add hook that fires after the working
tree is fully set up. Patch 2 adds post-worktree-remove for "git
worktree remove". Patch 3 extends it to "git worktree prune" so that
manually deleted worktrees are also observed.

Two design points I would especially appreciate feedback on:

 * post-worktree-add runs after post-checkout and is skipped when
   post-checkout fails. An argument could be made that it should run
   whenever the worktree was created, regardless of the earlier
   hook's exit status, since tooling registering worktrees would
   otherwise miss one that does exist.

 * for entries pruned because their gitdir file points to a location
   that no longer exists, the hook receives the recorded path; when
   the path cannot be determined at all (missing or corrupt gitdir
   file) it receives an empty string.

Thanks,
Domen

Domen Kožar (3):
  worktree: add post-worktree-add hook
  worktree: add post-worktree-remove hook
  worktree: run post-worktree-remove hook when pruning

 Documentation/githooks.adoc |  41 +++++++++++++
 builtin/worktree.c          |  73 ++++++++++++++++++-----
 t/t2400-worktree-add.sh     | 113 ++++++++++++++++++++++++++++++++++++
 t/t2401-worktree-prune.sh   |  88 ++++++++++++++++++++++++++++
 t/t2403-worktree-move.sh    |  44 ++++++++++++++
 worktree.c                  |   1 -
 worktree.h                  |   6 +-
 7 files changed, 347 insertions(+), 19 deletions(-)


base-commit: f85a7e662054a7b0d9070e432508831afa214b47
-- 
2.54.0

^ permalink raw reply

* [PATCH v1 1/3] worktree: add post-worktree-add hook
From: Domen Kožar @ 2026-07-09 23:36 UTC (permalink / raw)
  To: git
  Cc: Eric Sunshine, Patrick Steinhardt,
	Ævar Arnfjörð Bjarmason, Caleb White,
	Junio C Hamano, Domen Kožar, Claude Fable 5
In-Reply-To: <20260709233542.628628-1-domen@cachix.org>

Tools that manage per-worktree state, such as development environment
managers or IDEs, have no way to react when a new working tree is
created. The only hook that fires during "git worktree add" is
post-checkout, which is skipped when --no-checkout or --orphan is used
and cannot be distinguished from a plain checkout.

Introduce a post-worktree-add hook that runs after the working tree
has been fully set up, including with --no-checkout and --orphan. The
hook runs inside the new working tree with GIT_DIR and GIT_WORK_TREE
cleared, mirroring the existing post-checkout invocation, and is given
the absolute path of the new working tree and its identifier as
arguments. Anything else, such as the checked-out branch, can be
queried by running git from the hook's working directory.

Like post-checkout, the hook cannot affect the outcome of the command:
a failing hook does not delete the already-created working tree, but
its exit status becomes the exit status of "git worktree add". The
hook runs after post-checkout and is skipped if that hook fails.

Documenting the new hook in githooks(5) also registers its name in the
generated hook-list.h, so "git hook run" and hook.*.event recognize it
without further changes.

Signed-off-by: Domen Kožar <domen@cachix.org>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
 Documentation/githooks.adoc |  20 +++++++
 builtin/worktree.c          |  15 ++++-
 t/t2400-worktree-add.sh     | 113 ++++++++++++++++++++++++++++++++++++
 3 files changed, 146 insertions(+), 2 deletions(-)

diff --git a/Documentation/githooks.adoc b/Documentation/githooks.adoc
index ed045940d1..2778f73f30 100644
--- a/Documentation/githooks.adoc
+++ b/Documentation/githooks.adoc
@@ -215,6 +215,26 @@ This hook can be used to perform repository validity checks, auto-display
 differences from the previous HEAD if different, or set working dir metadata
 properties.
 
+post-worktree-add
+~~~~~~~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-worktree[1] after `git worktree add`
+has created and set up a new working tree. The hook is given two
+parameters: the absolute path of the new working tree and its identifier
+(the name of its administrative directory in `$GIT_DIR/worktrees/`).
+
+The hook runs inside the new working tree, so further details, such as
+the checked-out branch, can be queried by running `git` from the hook's
+current directory. Unlike the `post-checkout` hook, it is also run when
+`--no-checkout` or `--orphan` is used.
+
+This hook cannot affect the outcome of `git worktree add`, other than
+that the hook's exit status becomes the exit status of the command. It
+runs after the `post-checkout` hook, and is skipped if that hook fails.
+
+This hook can be used to set up per-worktree development environments
+or to register the new working tree with external tools.
+
 post-merge
 ~~~~~~~~~~
 
diff --git a/builtin/worktree.c b/builtin/worktree.c
index d21c43fde3..7b9d337234 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -605,8 +605,9 @@ static int add_worktree(const char *path, const char *refname,
 	}
 
 	/*
-	 * Hook failure does not warrant worktree deletion, so run hook after
-	 * is_junk is cleared, but do return appropriate code when hook fails.
+	 * Hook failures do not warrant worktree deletion, so run hooks after
+	 * is_junk is cleared, but do return appropriate code when a hook
+	 * fails.
 	 */
 	if (!ret && opts->checkout && !opts->orphan) {
 		struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_FORCE_SERIAL;
@@ -622,6 +623,16 @@ static int add_worktree(const char *path, const char *refname,
 		ret = run_hooks_opt(the_repository, "post-checkout", &opt);
 	}
 
+	if (!ret) {
+		struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_FORCE_SERIAL;
+
+		strvec_pushl(&opt.env, "GIT_DIR", "GIT_WORK_TREE", NULL);
+		strvec_pushl(&opt.args, wt->path, wt->id, NULL);
+		opt.dir = path;
+
+		ret = run_hooks_opt(the_repository, "post-worktree-add", &opt);
+	}
+
 	strvec_clear(&child_env);
 	strbuf_release(&sb);
 	strbuf_release(&symref);
diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh
index 58b4445cc4..3754559a98 100755
--- a/t/t2400-worktree-add.sh
+++ b/t/t2400-worktree-add.sh
@@ -1132,6 +1132,119 @@ test_expect_success '"add" in bare repo invokes post-checkout hook' '
 	test_cmp hook.expect goozy/hook.actual
 '
 
+# Install a post-worktree-add hook and write the output expected for
+# adding worktree $1; the hook is installed in repo $2 (default ".git").
+post_worktree_add_hook () {
+	test_when_finished "rm -rf .git/hooks" &&
+	mkdir .git/hooks &&
+	test_hook -C "$2" post-worktree-add <<-\EOF &&
+	{
+		echo $*
+		git rev-parse --git-dir --show-toplevel
+	} >hook.actual
+	EOF
+	{
+		echo $(pwd)/$1 $1 &&
+		echo $(pwd)/${2:-.git}/worktrees/$1 &&
+		echo $(pwd)/$1
+	} >hook.expect
+}
+
+test_expect_success '"add" invokes post-worktree-add hook' '
+	post_worktree_add_hook wanda &&
+	git worktree add wanda &&
+	test_cmp hook.expect wanda/hook.actual
+'
+
+test_expect_success '"add" in other worktree invokes post-worktree-add hook' '
+	post_worktree_add_hook wilbur &&
+	git -C wanda worktree add ../wilbur &&
+	test_cmp hook.expect wilbur/hook.actual
+'
+
+test_expect_success '"add --no-checkout" still invokes post-worktree-add hook' '
+	post_worktree_add_hook wendy &&
+	git worktree add --no-checkout wendy &&
+	test_cmp hook.expect wendy/hook.actual
+'
+
+test_expect_success '"add --orphan" invokes post-worktree-add hook' '
+	post_worktree_add_hook winnie &&
+	git worktree add --orphan winnie &&
+	test_cmp hook.expect winnie/hook.actual
+'
+
+test_expect_success '"add" in bare repo invokes post-worktree-add hook' '
+	rm -rf bare2 &&
+	git clone --bare . bare2 &&
+	post_worktree_add_hook willow bare2 &&
+	git -C bare2 worktree add --detach ../willow &&
+	test_cmp hook.expect willow/hook.actual
+'
+
+test_expect_success '"add" runs post-worktree-add after post-checkout' '
+	test_when_finished "rm -rf .git/hooks" &&
+	mkdir .git/hooks &&
+	test_hook post-checkout <<-\EOF &&
+	echo post-checkout >>hooks.actual
+	EOF
+	test_hook post-worktree-add <<-\EOF &&
+	echo post-worktree-add >>hooks.actual
+	EOF
+	test_write_lines post-checkout post-worktree-add >hooks.expect &&
+	git worktree add wobble &&
+	test_cmp hooks.expect wobble/hooks.actual
+'
+
+test_expect_success 'failing post-checkout hook suppresses post-worktree-add hook' '
+	test_when_finished "rm -rf .git/hooks" &&
+	mkdir .git/hooks &&
+	test_hook post-checkout <<-\EOF &&
+	exit 1
+	EOF
+	test_hook post-worktree-add <<-\EOF &&
+	>post-worktree-add.ran
+	EOF
+	test_must_fail git worktree add wozzle &&
+	test_path_is_missing wozzle/post-worktree-add.ran
+'
+
+test_expect_success 'failing post-worktree-add hook leaves worktree in place' '
+	test_when_finished "rm -rf .git/hooks" &&
+	mkdir .git/hooks &&
+	test_hook post-worktree-add <<-\EOF &&
+	exit 1
+	EOF
+	test_must_fail git worktree add wilma &&
+	git worktree list --porcelain >out &&
+	grep -F "worktree $(pwd)/wilma" out
+'
+
+test_expect_success 'failed "add" does not invoke post-worktree-add hook' '
+	test_when_finished "rm -rf .git/hooks occupied" &&
+	mkdir .git/hooks &&
+	test_hook post-worktree-add <<-\EOF &&
+	>hook.ran
+	EOF
+	mkdir occupied &&
+	: >occupied/blocker &&
+	test_must_fail git worktree add occupied &&
+	test_path_is_missing occupied/hook.ran &&
+	test_path_is_missing hook.ran
+'
+
+test_expect_success 'post-worktree-add hook gets absolute path with relative worktrees' '
+	test_when_finished "rm -rf relhook" &&
+	git init relhook &&
+	test_commit -C relhook base &&
+	test_hook -C relhook post-worktree-add <<-\EOF &&
+	echo $* >hook.actual
+	EOF
+	git -C relhook worktree add --relative-paths --detach wt &&
+	echo $(pwd)/relhook/wt wt >hook.expect &&
+	test_cmp hook.expect relhook/wt/hook.actual
+'
+
 test_expect_success '"add" an existing but missing worktree' '
 	git worktree add --detach pneu &&
 	test_must_fail git worktree add --detach pneu &&
-- 
2.54.0

^ permalink raw reply related

* [PATCH v1 2/3] worktree: add post-worktree-remove hook
From: Domen Kožar @ 2026-07-09 23:36 UTC (permalink / raw)
  To: git
  Cc: Eric Sunshine, Patrick Steinhardt,
	Ævar Arnfjörð Bjarmason, Caleb White,
	Junio C Hamano, Domen Kožar, Claude Fable 5
In-Reply-To: <20260709233542.628628-1-domen@cachix.org>

External tooling has no way to learn that a working tree is gone:
"git worktree remove" deletes both the working tree and its
administrative directory without running any hook.

Introduce a post-worktree-remove hook that runs after "git worktree
remove" has deleted a working tree. It is given the former absolute
path of the working tree and its identifier as arguments. The hook
also runs when only the administrative entry is deleted because the
working tree directory itself had already disappeared, since the
worktree is deregistered either way.

Because the working tree no longer exists at that point, no special
working directory or environment is set up; the hook runs wherever
the command ran, like other post-command hooks.

The hook runs once deletion is underway even if parts of it fail,
since there is no going back at that point, but it does not run when
the removal is refused (locked or dirty working tree, failed
validation). It cannot affect the outcome of the command other than
its exit status being reflected in the exit status of "git worktree
remove".

Signed-off-by: Domen Kožar <domen@cachix.org>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
 Documentation/githooks.adoc | 18 +++++++++++++++
 builtin/worktree.c          | 10 +++++++++
 t/t2403-worktree-move.sh    | 44 +++++++++++++++++++++++++++++++++++++
 3 files changed, 72 insertions(+)

diff --git a/Documentation/githooks.adoc b/Documentation/githooks.adoc
index 2778f73f30..22b3263ff7 100644
--- a/Documentation/githooks.adoc
+++ b/Documentation/githooks.adoc
@@ -235,6 +235,24 @@ runs after the `post-checkout` hook, and is skipped if that hook fails.
 This hook can be used to set up per-worktree development environments
 or to register the new working tree with external tools.
 
+post-worktree-remove
+~~~~~~~~~~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-worktree[1] after a working tree
+has been deleted by `git worktree remove`. The hook is given two
+parameters: the absolute path of the removed working tree and its
+identifier (the name of its former administrative directory in
+`$GIT_DIR/worktrees/`).
+
+The working tree no longer exists when the hook runs.
+
+This hook cannot affect the outcome of `git worktree remove`, other
+than that the hook's exit status becomes the exit status of the
+command.
+
+This hook can be used to tear down per-worktree development
+environments or to unregister the working tree from external tools.
+
 post-merge
 ~~~~~~~~~~
 
diff --git a/builtin/worktree.c b/builtin/worktree.c
index 7b9d337234..01b62ed2fc 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -168,6 +168,14 @@ static void delete_worktrees_dir_if_empty(void)
 	free(path);
 }
 
+static int run_post_worktree_remove_hook(const char *path, const char *id)
+{
+	struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_FORCE_SERIAL;
+
+	strvec_pushl(&hook_opt.args, path, id, NULL);
+	return run_hooks_opt(the_repository, "post-worktree-remove", &hook_opt);
+}
+
 static void prune_worktree(const char *id, const char *reason)
 {
 	if (show_only || verbose)
@@ -1437,6 +1445,8 @@ static int remove_worktree(int ac, const char **av, const char *prefix,
 	ret |= delete_git_dir(wt->id);
 	delete_worktrees_dir_if_empty();
 
+	ret |= run_post_worktree_remove_hook(wt->path, wt->id);
+
 	free_worktrees(worktrees);
 	return ret;
 }
diff --git a/t/t2403-worktree-move.sh b/t/t2403-worktree-move.sh
index 0bb33e8b1b..b94f00e426 100755
--- a/t/t2403-worktree-move.sh
+++ b/t/t2403-worktree-move.sh
@@ -246,6 +246,50 @@ test_expect_success 'not remove a repo with initialized submodule' '
 	)
 '
 
+test_expect_success '"remove" invokes post-worktree-remove hook' '
+	test_hook post-worktree-remove <<-\EOF &&
+	echo $* >hook.actual
+	EOF
+	git worktree add --detach wt-hooked &&
+	git worktree remove wt-hooked &&
+	echo $(pwd)/wt-hooked wt-hooked >hook.expect &&
+	test_cmp hook.expect hook.actual
+'
+
+test_expect_success '"remove" of missing worktree invokes post-worktree-remove hook' '
+	test_when_finished "rm -rf wt-moved-away" &&
+	test_hook post-worktree-remove <<-\EOF &&
+	echo $* >hook.actual
+	EOF
+	rm -f hook.actual &&
+	git worktree add --detach wt-elsewhere &&
+	mv wt-elsewhere wt-moved-away &&
+	git worktree remove wt-elsewhere &&
+	echo $(pwd)/wt-elsewhere wt-elsewhere >hook.expect &&
+	test_cmp hook.expect hook.actual
+'
+
+test_expect_success 'refused "remove" does not invoke post-worktree-remove hook' '
+	git worktree add --detach wt-kept &&
+	test_when_finished "git worktree remove --force --force wt-kept || :" &&
+	test_hook post-worktree-remove <<-\EOF &&
+	>hook.ran
+	EOF
+	git worktree lock wt-kept &&
+	test_must_fail git worktree remove wt-kept &&
+	test_path_is_missing hook.ran
+'
+
+test_expect_success 'failing post-worktree-remove hook fails "remove", worktree is gone' '
+	test_hook post-worktree-remove <<-\EOF &&
+	exit 1
+	EOF
+	git worktree add --detach wt-doomed &&
+	test_must_fail git worktree remove wt-doomed &&
+	test_path_is_missing wt-doomed &&
+	test_path_is_missing .git/worktrees/wt-doomed
+'
+
 test_expect_success 'move worktree with absolute path to relative path' '
 	test_config worktree.useRelativePaths false &&
 	git worktree add ./absolute &&
-- 
2.54.0

^ permalink raw reply related

* Re: [PATCH 1/2] git-subtree: Bail out if we find output from Rust rewrite [and 1 more messages]
From: Colin Stagner @ 2026-07-09 22:43 UTC (permalink / raw)
  To: Ian Jackson; +Cc: git, Johannes Schindelin
In-Reply-To: <27215.27575.968985.583226@chiark.greenend.org.uk>

On 7/9/26 04:36, Ian Jackson wrote:

> Colin Stagner writes ("Re: [PATCH 1/2] git-subtree: Bail out if we find output from Rust rewrite"):
>
>> I think that subtree merge should only test the top-level project, as
>> this patch does now.
>
> By "top-level" I think you mean what I've taken to calling the
> "downstream": the project where the subtree is in a subdir, and whose
> top-level has other stuff.  In which case I agree.

Yes, I think we're talking about the same thing.

In retrospect, "top-level" is ambiguous. "Upstream" and "downstream" may 
be as well. Within git-branch(1), the phrase "upstream" refers to the 
remote tracking branch set by

     git branch --set-upstream-to=<upstream>

git-merge(1) is consistent with this.

     "If no commit is given from the command line, merge the
      remote-tracking branches that the current branch is
      configured to use as its upstream."

git-subtree.sh doesn't really deal in "upstreams" in the git-branch or 
git-merge sense.

Less ambiguous language is available:

For merge commits, there is the "first parent" and "second parent" (or 
3rd or higher parents).

For trees, there is the "root tree" and "sub-trees," like `git ls-tree -r`

     -r     Recurse into sub-trees.

Both of these deliberately ignore the dependency relationship between 
the various projects and branches in question, which can potentially get 
messy.

>>> +	if git rev-parse --verify -q "$rev:$config"; then
>>
>> For subtree split, should we also test for this file in tree you are
>> splitting: i.e., "$dir/$config"? The answer might be no.
> 
> You're right that we should consider this question.  The answer is:
> no, we should not.  Briefly, whether to use the new or old algorithms
> depends on whether the downstream has adopted the new git-subtree, not
> on whether the upstream has added some optional config.

Very well-reasoned; I like it.

Let me ask this question in a slightly different way: does RIIR subtree 
honor config files in locations other than the one you test for above? 
That's

     ${rev}:.git-subtree/config

which is `.git-subtree/config` within the root tree of the rev that is 
being manipulated?

If this is the only config file RIIR subtree honors, the patch is 
probably correct. If RIIR subtree honors config from other places, such as

* the working tree
* HEAD:.git-subtree/config
* HEAD:./.git-subtree/config

then consider testing for those if appropriate.

>> Subtree merges can be performed without git-subtree, via the `-X
>> subtree` merge strategy option.
> 
> This is what I'm calling an "unmarked subtree merge".  My rewrite is
> not going to support this user behaviour.  The problem is that it is
> not possible to reliably determine whetheer something is an unmarked
> subtree merge.

Thanks for looking at this.

> Combining manual -X subtree merges with git-subtree --squash merges
> could easily produce quite weird and wrong results in the tree

I haven't tried it, but I think if --squash is in use, then attempting 
an unmarked subtree merge will probably die with "unrelated history" 
warnings.

Looking forward to v2,

Colin



^ permalink raw reply

* Re: [PATCH 6/7] odb: introduce object filters to `odb_for_each_object()`
From: Justin Tobler @ 2026-07-09 21:43 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git
In-Reply-To: <20260709-pks-odb-for-each-object-filter-v1-6-82fe014b12b3@pks.im>

On 26/07/09 10:35AM, Patrick Steinhardt wrote:
> The function `for_each_bitmapped_object()` can be used to iterate
> through all objects covered by a bitmap. The benefit of this function is
> that it allows the caller to efficiently handle some object filters. For
> example, this can be used to filter out objects of a specific type with
> some simple bitmap operations. But callers are currently required to
> manually wire up the use of bitmaps though, and to do so they have to
> reach into internals of a given object database source.
> 
> Introduce a new `struct odb_for_each_object_options::filter` field so
> that the interface becomes generic. When set, then a backend may
> optionally use the filter to skip some objects that it would have
> otherwise yielded.
> 
> Note that the respective backends are free to ignore this field if they
> cannot meaningfully optimize for a given filter, and consequently
> callers need to verify whether they actually want the returned objects.
> While annoying, we cannot easily lift this restriction anyway as the
> object filter infrastructure supports some filters that cannot be
> answered by the object database alone.

Huh, this feels rather awkward. So callers will always still have to
ensure correctness by filtering the result a second time? IIUC, the idea
is that the backend may be able to more efficiently process object
filtering so we would want it to attempt the first pass.

Is there a subset of object filters that we should expect any backend to
be able to answer? If so, maybe we should define a separate list of
object filter options specific to this interface? Any filtering not
supported would have to be deligated to the caller then.

-Justin

^ permalink raw reply

* Re: [PATCH 4/7] pack-bitmap: iterate object sources when opening bitmaps
From: Justin Tobler @ 2026-07-09 21:08 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git
In-Reply-To: <20260709-pks-odb-for-each-object-filter-v1-4-82fe014b12b3@pks.im>

On 26/07/09 10:35AM, Patrick Steinhardt wrote:
> When opening a bitmap for a repository we perform two steps:
> 
>   - We first look for a multi-pack index bitmap in any of the object
>     sources connected to the repository.
> 
>   - We then look for a packfile bitmap in any of the packfiles of any of
>     the object sources.

So IIUC, we generally stop searching for a bitmap once we find one.

> Both of these steps thus iterate through object sources themselves, one
> via `odb_prepare_alternates()` and one via `repo_for_each_pack()`. This
> layout makes it hard to introduce a way to open the bitmap of one
> specific object source, which is functionality that we'll require in a
> subsequent commit.
> 
> Reverse the loop so that we instead loop through all sources in the
> outer loop, and then for each source we try to load its bitmap via
> either the multi-pack index or via a packfile.

Conceptually, I think this is a lot easier to follow too which is nice.

> Note that this changes the precedence of bitmaps in one specific edge
> case: when an earlier object source only has a packfile bitmap, but a
> later source has a multi-pack index bitmap, we now pick the packfile
> bitmap of the earlier source. Previously, a multi-pack index bitmap from
> any source would have taken precedence over all packfile bitmaps. Given
> that object sources are ordered such that the local source comes first,
> this arguably is an improvement, as we now prefer local bitmaps over
> bitmaps in alternates. Furthermore, we already warn about repositories
> that have multiple bitmaps, so this setup is broken and thus arguably
> not worth worrying about too much.

I agree that the change in bitmap precedent is probably not a big deal.
Having multiple bitmaps in a repository is already something we warn
against so I think this should be fine.

> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
>  pack-bitmap.c | 65 ++++++++++++++++++++++++++---------------------------------
>  1 file changed, 29 insertions(+), 36 deletions(-)
> 
> diff --git a/pack-bitmap.c b/pack-bitmap.c
> index eda38a5433..0e3e18a557 100644
> --- a/pack-bitmap.c
> +++ b/pack-bitmap.c
> @@ -680,60 +680,53 @@ static int load_bitmap(struct repository *r, struct bitmap_index *bitmap_git,
>  	return 0;
>  }
>  
> -static int open_pack_bitmap(struct repository *r,
> -			    struct bitmap_index *bitmap_git)
> +static int open_bitmap_for_source(struct odb_source_packed *source,
> +				  struct bitmap_index *bitmap_git)
>  {
> -	struct packed_git *p;
> +	struct multi_pack_index *midx = get_multi_pack_index(source);
> +	struct packfile_list_entry *e;
>  	int ret = -1;
>  
> -	repo_for_each_pack(r, p) {
> -		if (open_pack_bitmap_1(bitmap_git, p) == 0) {
> -			ret = 0;
> -			/*
> -			 * The only reason to keep looking is to report
> -			 * duplicates.
> -			 */
> -			if (!trace2_is_enabled())
> -				break;
> -		}
> +	if (midx && !open_midx_bitmap_1(bitmap_git, midx))
> +		ret = 0;

Ok, open_midx_bitmap_1() returns 0 if it find a MIDX and -1 otherwise.
Probably just a matter of preference, but I think writing out like below
is a little bit easier on the eyes:

  if (midx)
    ret = open_midx_bitmap_1(bitmap_git, midx);

it might just be that I find the return values a bit confusing though.
Maybe we could instead use `found` like a bit later in this patch.

> +
> +	for (e = packfile_store_get_packs(source); e; e = e->next) {
> +		/*
> +		 * When tracing is enabled we want to keep looking to report
> +		 * duplicates even if we have already found a bitmap.
> +		 */
> +		if (!ret && !trace2_is_enabled())
> +			break;

So if have already found a bitmap from the MIDX and tracing is not
enabled, we don't continue searching for bitmaps in this source. 

> +
> +		if (open_pack_bitmap_1(bitmap_git, e->pack))
> +			continue;
> +		ret = 0;
>  	}
>  
>  	return ret;
>  }
>  
> -static int open_midx_bitmap(struct repository *r,
> -			    struct bitmap_index *bitmap_git)
> +static int open_bitmap(struct repository *r,
> +		       struct bitmap_index *bitmap_git)
>  {
>  	struct odb_source *source;
> -	int ret = -1;
> +	int found = 0;
>  
>  	assert(!bitmap_git->map);
>  
>  	odb_prepare_alternates(r->objects);
>  	for (source = r->objects->sources; source; source = source->next) {
>  		struct odb_source_files *files = odb_source_files_downcast(source);
> -		struct multi_pack_index *midx = get_multi_pack_index(files->packed);
> -		if (midx && !open_midx_bitmap_1(bitmap_git, midx))
> -			ret = 0;
> -	}
> -	return ret;
> -}
> -
> -static int open_bitmap(struct repository *r,
> -		       struct bitmap_index *bitmap_git)
> -{
> -	int found;
>  
> -	assert(!bitmap_git->map);
> +		found |= !open_bitmap_for_source(files->packed, bitmap_git);
>  
> -	found = !open_midx_bitmap(r, bitmap_git);
> -
> -	/*
> -	 * these will all be skipped if we opened a midx bitmap; but run it
> -	 * anyway if tracing is enabled to report the duplicates
> -	 */
> -	if (!found || trace2_is_enabled())
> -		found |= !open_pack_bitmap(r, bitmap_git);
> +		/*
> +		 * The only reason to keep looking after having found a bitmap
> +		 * is to report duplicates.
> +		 */
> +		if (found && !trace2_is_enabled())
> +			break;
> +	}

Ok, we only advance to the next source if tracing is enabled to print
warnings for multiple bitmaps. Makes sense.

Overall I quite like the direction of this patch.

-Justin

^ permalink raw reply

* Re: [PATCH v7 0/3] includeIf: add "worktree" condition for matching working tree path
From: Junio C Hamano @ 2026-07-09 20:40 UTC (permalink / raw)
  To: Chen Linxuan via B4 Relay
  Cc: git, Kristoffer Haugsbakk, Patrick Steinhardt, Chen Linxuan,
	Phillip Wood
In-Reply-To: <20260709-includeif-worktree-v7-0-e87e705e8df6@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.

Thanks.

This seems to break t1305 when merged to 'seen', even though all of
them pass standalone.  I did not have time to figure out what
interactions with which other topic are causing the breakages.

^ permalink raw reply

* Re: [PATCH 0/7] refs: remove use of `the_repository`
From: Junio C Hamano @ 2026-07-09 20:39 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git
In-Reply-To: <20260709-pks-refs-wo-the-repository-v1-0-1ad6f27529c9@pks.im>

Patrick Steinhardt <ps@pks.im> writes:

> The series is built on top of f85a7e6620 (Start Git 2.56 cycle,
> 2026-07-06) with ps/refs-writing-subcommands at 002fe677ca
> (builtin/refs: add "rename" subcommand, 2026-07-06) merged into it.
> Despite that, there's a small set of conflicts with "seen" that can be
> merged like this:

Thanks for a heads-up.

This seems to break so many tests when merged to either 'jch' or
'seen', even though all of them pass standalone.  I did not have
time to figure out what interactions with which other topic are
causing the breakages.

^ permalink raw reply

* Re: [PATCH 3/7] pack-bitmap: allow aborting iteration of bitmapped objects
From: Justin Tobler @ 2026-07-09 20:19 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git
In-Reply-To: <20260709-pks-odb-for-each-object-filter-v1-3-82fe014b12b3@pks.im>

On 26/07/09 10:35AM, Patrick Steinhardt wrote:
> In a subsequent commit we'll lift iteration of bitmapped objects into
> the "packed" backend and make it accessible via `odb_for_each_object()`.
> The calling convention for that function is that the callback may return
> a non-zero exit code, and if so we'll abort iteration. This is currently
> impossible to realize though, as `for_each_bitmapped_object()` will
> ignore any return value and just churn through all objects completely.

Ok.

> This doesn't matter to the callers of `for_each_bitmapped_object()`, as
> there's only one of them in git-cat-file(1), and the callbacks we pass
> always return zero. But once we move the logic into the generic
> infrastructure it becomes a latent bug waiting to happen.
> 
> Refactor the code so that the return value of the `show_reach` callback
> is not ignored anymore. Instead, returning a non-zero value will cause
> us to abort iteration in both `show_objects_for_type()` and in
> `for_each_bitmapped_object()`.

Make sense. We want to ensure that the `show_reach` callback can
properly signal back to `for_each_bitmapped_object()` to abort.

> Note though that there's a second user of `show_objects_for_type()` with
> `traverse_bitmap_commit_list()`, and that function does indeed invoke
> callbacks that may return non-zero. This non-zero return value never had
> any effect at all though, and the callbacks that return non-zero values
> are only ever invoked via `traverse_bitmap_commit_list()`. Consequently,
> we adapt them to always return 0.
> 
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
>  builtin/pack-objects.c |  2 +-
>  builtin/rev-list.c     |  2 +-
>  pack-bitmap.c          | 31 +++++++++++++++++++++----------
>  pack-bitmap.h          |  3 ++-
>  4 files changed, 25 insertions(+), 13 deletions(-)
> 
> diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c
> index ea5eab4cf8..8ff92c5272 100644
> --- a/builtin/pack-objects.c
> +++ b/builtin/pack-objects.c
> @@ -1909,7 +1909,7 @@ static int add_object_entry_from_bitmap(const struct object_id *oid,
>  		return 0;
>  
>  	create_object_entry(oid, type, name_hash, 0, 0, pack, offset);
> -	return 1;
> +	return 0;

I wonder why this was even returning 1 to begin with? As you mentioned,
the return value appears to be ignored anyways. I'm assuming it was
signal that an object entry was created?

>  }
>  
>  struct pbase_tree_cache {
> diff --git a/builtin/rev-list.c b/builtin/rev-list.c
> index 8f63003709..02818b81c6 100644
> --- a/builtin/rev-list.c
> +++ b/builtin/rev-list.c
> @@ -486,7 +486,7 @@ static int show_object_fast(
>  	void *payload UNUSED)
>  {
>  	fprintf(stdout, "%s\n", oid_to_hex(oid));
> -	return 1;
> +	return 0;

Also curious about this one too. It probably doesn't matter though.

>  }
>  
>  static void print_disk_usage(off_t size)
> diff --git a/pack-bitmap.c b/pack-bitmap.c
> index a47c231632..eda38a5433 100644
> --- a/pack-bitmap.c
> +++ b/pack-bitmap.c
> @@ -1695,7 +1695,7 @@ static void init_type_iterator(struct ewah_or_iterator *it,
>  	}
>  }
>  
> -static void show_objects_for_type(
> +static int show_objects_for_type(
>  	struct bitmap_index *bitmap_git,
>  	struct bitmap *objects,
>  	enum object_type object_type,
> @@ -1704,6 +1704,7 @@ static void show_objects_for_type(
>  {
>  	size_t i = 0;
>  	uint32_t offset;
> +	int ret;
>  
>  	struct ewah_or_iterator it;
>  	eword_t filter;
> @@ -1749,11 +1750,17 @@ static void show_objects_for_type(
>  
>  			hash = bitmap_name_hash(bitmap_git, index_pos);
>  
> -			show_reach(&oid, object_type, 0, hash, pack, ofs, payload);
> +			ret = show_reach(&oid, object_type, 0, hash, pack, ofs, payload);
> +			if (ret)
> +				goto out;

The show_reach callback now wires back its return code.

>  		}
>  	}
>  
> +	ret = 0;
> +
> +out:
>  	ewah_or_iterator_release(&it);
> +	return ret;
>  }
>  
>  static int in_bitmapped_pack(struct bitmap_index *bitmap_git,
> @@ -2062,6 +2069,12 @@ int for_each_bitmapped_object(struct bitmap_index *bitmap_git,
>  			      show_reachable_fn show_reach,
>  			      void *payload)
>  {
> +	const enum object_type types[] = {
> +		OBJ_COMMIT,
> +		OBJ_TREE,
> +		OBJ_BLOB,
> +		OBJ_TAG,
> +	};
>  	struct bitmap *filtered_bitmap = NULL;
>  	uint32_t objects_nr;
>  	size_t full_word_count;
> @@ -2086,14 +2099,12 @@ int for_each_bitmapped_object(struct bitmap_index *bitmap_git,
>  		goto out;
>  	}
>  
> -	show_objects_for_type(bitmap_git, filtered_bitmap,
> -			      OBJ_COMMIT, show_reach, payload);
> -	show_objects_for_type(bitmap_git, filtered_bitmap,
> -			      OBJ_TREE, show_reach, payload);
> -	show_objects_for_type(bitmap_git, filtered_bitmap,
> -			      OBJ_BLOB, show_reach, payload);
> -	show_objects_for_type(bitmap_git, filtered_bitmap,
> -			      OBJ_TAG, show_reach, payload);
> +	for (size_t i = 0; i < ARRAY_SIZE(types); i++) {
> +		ret = show_objects_for_type(bitmap_git, filtered_bitmap,
> +					    types[i], show_reach, payload);
> +		if (ret)
> +			goto out;
> +	}

`for_each_bitmapped_object()` now has access to the underlying return
code and can abort. Looks good.

-Justin

^ 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