* Re: [PATCH v2 1/4] t1517: skip svn tests if svn is not installed2sy
From: brian m. carlson @ 2026-07-02 14:47 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git, Jeff King
In-Reply-To: <xmqqzf0al51j.fsf@gitster.g>
[-- Attachment #1: Type: text/plain, Size: 1187 bytes --]
On 2026-07-01 at 22:27:04, Junio C Hamano wrote:
> "brian m. carlson" <sandals@crustytoothpaste.net> writes:
>
> > +test_lazy_prereq SVN '
> > + test_have_prereq PERL && test -n "$NO_SVN_TESTS" && perl -w -e "
> > + use SVN::Core;
> > + use SVN::Repos;
> > + \$SVN::Core::VERSION gt '1.1.0' or exit(42);
> > + "
> > +'
>
> If "have_prereq PERL" is not satisfied, SVN is not satisfied.
Correct.
> If NO_SVN_TESTS is an empty string (or unset), "test -n" fails, and
> SVN is not satisfied. Questionable---am I misreading this part of
> the logic???
I think that's reversed, yes.
> The perl script would not barf only if use SVN::* succeed and then
> SVN::Core::VERSION is strictly better than '1.1.0'. If not, i.e.,
> libsvn-perl is not available, or its version is older, then we fail
> with exit(42), and SVN is not satisfied.
Correct. And yes, this came in from `t/lib-git-svn.sh`. I'll probably
just simplify this to omit the version check since it's very unlikely
that anybody is using SVN 1.0 any more and, as Peff pointed out, this
doesn't actually work using a string comparison.
--
brian m. carlson (they/them)
Toronto, Ontario, CA
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 325 bytes --]
^ permalink raw reply
* Re: [PATCH] meson: restore hook-list.h to builtin_sources
From: Mike Gilbert @ 2026-07-02 17:03 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: Mike Gilbert, git, adrian.ratiu
In-Reply-To: <akZGJP1kVtjBFN_e@pks.im>
On Thu, Jul 2, 2026 at 7:06 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> On Wed, Jul 01, 2026 at 03:39:28PM -0400, Mike Gilbert wrote:
> > This fixes a racy build failure.
> >
> > ```
> > builtin/bugreport.c:12:10: fatal error: hook-list.h: No such file or directory
> > 12 | #include "hook-list.h"
> > | ^~~~~~~~~~~~~
> >
> > ```
> >
> > hook-list.h must be generated before builtin/bugreport.c is compiled.
>
> "hook-list.h" is required by both "hook.c" and by "builtin/bugreport.c".
> So you would expect that we indeed need the header generated for both of
> these, but right now we only explicitly list the dependency for our
> libgit sources, not to our builtin sources. And consequently the header
> may not be generated:
>
> $ meson setup build
> ...
> $ ninja -C build git.p/builtin_bugreport.c.o
> ...
> ../builtin/bugreport.c:12:10: fatal error: 'hook-list.h' file not found
> 12 | #include "hook-list.h"
> | ^~~~~~~~~~~~~
> 1 error generated.
>
> The fix is of course to explicitly list the header for both targets.
> And...
>
> > diff --git a/meson.build b/meson.build
> > index 3247697f74aa..bdc83843e8e0 100644
> > --- a/meson.build
> > +++ b/meson.build
> > @@ -278,7 +278,20 @@ compat_sources = [
> > 'compat/terminal.c',
> > ]
> >
> > +hook_list = custom_target(
> > + input: 'Documentation/githooks.adoc',
> > + output: 'hook-list.h',
> > + command: [
> > + shell,
> > + meson.current_source_dir() + '/tools/generate-hooklist.sh',
> > + meson.current_source_dir(),
> > + '@OUTPUT@',
> > + ],
> > + env: script_environment,
> > +)
> > +
> > libgit_sources = [
> > + hook_list,
> > 'abspath.c',
> > 'add-interactive.c',
> > 'add-patch.c',
> > @@ -566,19 +579,8 @@ libgit_sources += custom_target(
> > env: script_environment,
> > )
> >
> > -libgit_sources += custom_target(
> > - input: 'Documentation/githooks.adoc',
> > - output: 'hook-list.h',
> > - command: [
> > - shell,
> > - meson.current_source_dir() + '/tools/generate-hooklist.sh',
> > - meson.current_source_dir(),
> > - '@OUTPUT@',
> > - ],
> > - env: script_environment,
> > -)
> > -
> > builtin_sources = [
> > + hook_list,
> > 'builtin/add.c',
> > 'builtin/am.c',
> > 'builtin/annotate.c',
>
> ... that's exactly what you do. So this fix looks good to me, thanks!
Thank you for the review. This is my first contribution to the Git
project and I'm trying to follow the lengthy SubmittingPatches guide.
I believe we have "reached a consensus" and my next steps are as follows:
- Add Reviewed-by (or Acked-by?) for Patrick and Adrian.
- Send the patch to Junio with the list CCed.
Do I have that right?
^ permalink raw reply
* Re: [PATCH 3/9] t4141: fix inefficient use of dd(1)
From: SZEDER Gábor @ 2026-07-02 17:49 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git, Junio C Hamano, Johannes Schindelin
In-Reply-To: <20260702-b4-pks-t-fixes-for-GIT-TEST-LONG-v1-3-76b4d7bab3d0@pks.im>
On Thu, Jul 02, 2026 at 02:00:56PM +0200, Patrick Steinhardt wrote:
> In t4141 we generate a patch that is roughly 1GB in size to verify that
> git-apply(1) indeed rejects that patch. We generate that patch by
> prepending a patch header and then executing `test-tool genzeros`
> without a limit. This causes us to print infinitely many zeros, and we
> limit the overall amount of generated bytes via `test_copy_bytes`.
>
> This test setup is extremely expensive, as `test_copy_bytes` is
> implemented via `dd ibs=1 count="$1"`, which copies data one byte at a
> time. So as we write 1GB of data, we end up doing 1 billion reads and
> writes. This naturally takes a while: it takes 6 minutes on my system,
> and around 40 minutes in some CI jobs!
>
> We can do much better though, as genzeros already knows to handle an
> optional limit of how much data it is supposed to write, which allows us
> to remove the call to `test_copy_bytes`. Furthermore, it has already
> been optimized to generate the data fast.
>
> And indeed, doing this conversion drops the test execution to less than
> a second on my machine, so that we can drop the EXPENSIVE prerequisite.
EXPENSIVE is not only about execution time, but about resources in
general. While the modified test finishes quite fast indeed, 'git
apply' uses over 1GB of RSS. Therefore, the EXPENSIVE prerequisite
should be kept.
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> t/t4141-apply-too-large.sh | 7 +++----
> 1 file changed, 3 insertions(+), 4 deletions(-)
>
> diff --git a/t/t4141-apply-too-large.sh b/t/t4141-apply-too-large.sh
> index eac6f7e151..dad67779ed 100755
> --- a/t/t4141-apply-too-large.sh
> +++ b/t/t4141-apply-too-large.sh
> @@ -4,8 +4,7 @@ test_description='git apply with too-large patch'
>
> . ./test-lib.sh
>
> -test_expect_success EXPENSIVE 'git apply rejects patches that are too large' '
> - sz=$((1024 * 1024 * 1023)) &&
> +test_expect_success 'git apply rejects patches that are too large' '
> {
> cat <<-\EOF &&
> diff --git a/file b/file
> @@ -14,8 +13,8 @@ test_expect_success EXPENSIVE 'git apply rejects patches that are too large' '
> +++ b/file
> @@ -0,0 +1 @@
> EOF
> - test-tool genzeros
> - } | test_copy_bytes $sz | test_must_fail git apply 2>err &&
> + test-tool genzeros $((1024 * 1024 * 1023))
> + } | test_must_fail git apply 2>err &&
> grep "patch too large" err
> '
>
>
> --
> 2.55.0.795.g602f6c329a.dirty
>
^ permalink raw reply
* [PATCH v6 0/3] Teach git-replay(1) to linearize merge commits
From: Toon Claes @ 2026-07-02 17:58 UTC (permalink / raw)
To: git; +Cc: Elijah Newren, Toon Claes, Johannes Schindelin,
Johannes Schindelin
In-Reply-To: <20260626-toon-git-replay-drop-merges-v5-0-5e120738b9d0@iotcl.com>
As an alternative to dscho's patch series to replay merges[1], add
an option to git-replay(1) to linearize merges. This mimics what
git-rebase(1) does with --no-rebase-merges (the default).
The first two patches do some refactoring. The third patch implements
the actual change. This patch was kindly provided by Dscho, which I've
tweaked to be upstreamed.
The --linearize option is only added to git-replay(1) and not to
git-history(1) because in my opinion it doesn't make much sense to do
so, but I'm happy to hear if anyone disagrees.
This series might conflict with Kristoffer's series to make
documentation changes[2], but should be trivial to resolve. And I don't
think there's a conflict with Patrick's series on adding "drop" to
git-history(1)[3].
dscho's series to replay merges[1] needs a bit of rework to fit on top
of this, but I'm happy to help figuring that out. We've been discussing
to either name the option --flatten or --linearize, but I've decided on
"linearize" because the documentation of git-rebase(1) also mentions
"linearize".
[1]: <pull.2106.git.1778107405.gitgitgadget@gmail.com>
[2]: <V2_CV_doc_replay_config.767@msgid.xyz>
[3]: <20260603-b4-pks-history-drop-v2-0-742cb5b5176d@pks.im>
---
Changes in v6:
- Reworked the second commit that moves picking the base completely
outside pick_regular_commit(), instead of adding more explanation.
- Drastically extended the commit message on commit #3.
- Extended docs on flattening multiple revision ranges and how it's
different from git-rebase(1)'s --no-rebase-merges.
- Added a bunch of tests to cover various scenarios.
- Remove newline from BUG() message.
- Link to v5: https://patch.msgid.link/20260626-toon-git-replay-drop-merges-v5-0-5e120738b9d0@iotcl.com
Changes in v5:
- Dropped the enum->bool patch and instead added a patch that better
explains how pick_regular_commit() picks a base.
- Order of commits is shuffled.
- (BIGGEST CHANGE) When working on a refactor to undo the enum->bool
patch, I extended the code comments to explain how things work. This
made me realize the use of the "replayed_base" was incorrect when
multiple branches are rebased with --onto. This is fixed now and a
test is added for this scenario.
- Link to v4: https://patch.msgid.link/20260622-toon-git-replay-drop-merges-v4-0-ff257f534319@iotcl.com
Changes in v4:
- Use test_grep instead of a bare grep in the range-diff test, to
prepare for mm/test-grep-lint.
- Link to v3: https://patch.msgid.link/20260616-toon-git-replay-drop-merges-v3-0-153e9eb99ce1@iotcl.com
Changes in v3:
- Add --linearize to Documentation SYNOPSIS, and mention it's
incompatible with --revert.
- Small language change in help message for --linearize.
- Rephrase comment to include last_commit isn't modified when
linearizing merges.
- Remove test that was added in earlier versions, but actually is
a duplicate of 'replaying merge commits is not supported yet'.
- Add test to verify --revert and --linearize are incompatible.
- Properly test that replaying down to root with --linearize works.
- Add test for --linearize with --advance.
- Add test that uses git-range-diff(1) to verify the patches created by
--linearize are correct.
- Link to v2: https://patch.msgid.link/20260610-toon-git-replay-drop-merges-v2-0-5714a71c6d83@iotcl.com
Changes in v2:
- Restructured the conditions to detect merge commits and added a line
of comment why the loop continues.
- Rewrote tests to use the history from the setup step and added a few
test cases.
- Re-added Johannes's Signed-off-by trailer. Johannes gave me the
patches with this trailer, and if I understand correctly, I can keep
it. Please let me know if that wrong.
- Link to v1: https://patch.msgid.link/20260608-toon-git-replay-drop-merges-v1-0-e3ee71fce7b4@iotcl.com
---
Johannes Schindelin (1):
replay: offer an option to linearize the commit topology
Toon Claes (2):
replay: add helper to put entry into replayed_commits
replay: resolve the replay base outside pick_regular_commit()
Documentation/git-replay.adoc | 21 ++++++-
builtin/replay.c | 6 +-
replay.c | 81 ++++++++++++++++--------
replay.h | 5 ++
t/t3650-replay-basics.sh | 140 +++++++++++++++++++++++++++++++++++++++++-
5 files changed, 225 insertions(+), 28 deletions(-)
Range-diff versus v5:
1: b4512eb233 ! 1: b957989fd9 replay: add helper to put entry into mapped_commits
@@ Metadata
Author: Toon Claes <toon@iotcl.com>
## Commit message ##
- replay: add helper to put entry into mapped_commits
+ replay: add helper to put entry into replayed_commits
The function replay_revisions() in replay.c is rather lengthy. Extract
the logic to put a commit entry into mapped_commits into a helper
@@ replay.c: static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,
+
+ pos = kh_put_oid_map(replayed_commits, commit->object.oid, &ret);
+ if (ret == 0)
-+ BUG("Duplicate rewritten commit: %s\n",
++ BUG("Duplicate rewritten commit: %s",
+ oid_to_hex(&commit->object.oid));
+
+ kh_value(replayed_commits, pos) = new_commit;
2: 91ed61bafd < -: ---------- replay: better explain how pick_regular_commit() picks a base
-: ---------- > 2: 6d457e8c39 replay: resolve the replay base outside pick_regular_commit()
3: eb6a3b0d72 ! 3: af39c0ae44 replay: offer an option to linearize the commit topology
@@ Commit message
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 commit history into a sequence of the
- regular (single-parent) commits.
+ linearizes the history into a sequence of regular (single-parent)
+ commits.
- Add option `--linearize` to git-replay(1) to do the same.
+ Add option `--linearize` to git-replay(1) to do the same. 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.
+
+ git-replay(1) accepts multiple revision ranges, for example:
+
+ $ git replay --onto main topic1 topic2
+
+ Without `--linearize` this replays 'topic1' and 'topic2' onto 'main'
+ independently and updates both refs.
+
+ 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.
+
+ Replaying all revision ranges into one single linear history is
+ intentional and it's the only way to ensure predictable results. A user
+ who wants to linearize ranges independently is advised to use separate
+ git-replay(1) invocations.
+
+ 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.
Co-authored-by: Toon Claes <toon@iotcl.com>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
@@ Documentation/git-replay.adoc: incompatible with `--contained` (which is a modif
The default mode can be configured via the `replay.refAction` configuration variable.
+--linearize::
-+ In this mode, `git replay` imitates `git rebase --no-rebase-merges`,
-+ i.e. it cherry-picks only non-merge commits, each one on top of the
-+ previous one.
-+ This option is incompatible with `--revert`.
++ In this mode, each replayed commit is stacked on top of the
++ previously replayed one, so all replayed commits are flattened into
++ a single linear history.
+++
++When a merge commit is encountered, the behavior of git-rebase(1)'s
++option `--no-rebase-merges` is imitated. All commits in the range
++reachable from the merge commit are replayed into a linear history, and
++the merge commit itself is dropped. A ref that pointed to a merge commit
++is updated to the merge's last replayed ancestor.
+++
++This flattens the `<revision-range>` as a whole. When multiple revision
++ranges are given they are stacked on top of each other into one linear
++history. Each of their refs is updated to point to its position in that
++history. To linearize ranges separately, replay them in separate `git
++replay` invocations.
+++
++This option is incompatible with `--revert`.
+
<revision-range>::
Range of commits to replay; see "Specifying Ranges" in
@@ replay.c: int replay_revisions(struct rev_info *revs,
const struct name_decoration *decoration;
- /*
-- * pick_regular_commit() looks up the parent of `commit` in
-- * `replayed_commits` to determine the ancestor to replay onto.
-- * The `default_base` parameter is used when no ancestor is found,
-- * which happens for the first commit in the revision range.
-- * When reverting, commits are replayed in reverse order, so the
-- * lookup never succeeds, and we need to pass `last_commit`.
+- * Decide where to replay this commit on.
+- * If the parent commit was replayed already, the replayed result
+- * can be found in `replayed_commits`. Otherwise fall back to `onto`.
+- * When reverting, commits are replayed in reverse order and thus
+- * its parent isn't replayed yet. Therefore revert commits are
+- * always replayed onto `last_commit`.
- */
-- struct commit *base = onto;
+- struct commit *parent = commit->parents ? commit->parents->item : NULL;
+- struct commit *base = get_mapped_commit(replayed_commits, parent, onto);
+-
- if (mode == REPLAY_MODE_REVERT)
- base = last_commit;
-
@@ replay.c: int replay_revisions(struct rev_info *revs,
- die(_("replaying merge commits is not supported yet!"));
-
- last_commit = pick_regular_commit(revs->repo, commit, base,
-- replayed_commits,
-- &merge_opt, &result, mode, opts->empty);
+- &merge_opt, &result,
+- mode, opts->empty);
+ if (commit->parents && commit->parents->next) {
+ if (!opts->linearize)
+ die(_("replaying merge commits is not supported yet!"));
@@ replay.c: int replay_revisions(struct rev_info *revs,
+ * Drop the merge commit: do not pick it, leave
+ * `last_commit` unchanged, and fall through to the
+ * rest of the loop. As a result:
-+ * - the merge commit is mapped to `last_commit` in
-+ * `replayed_commits`, this will become the parent for
-+ * the child commits.
-+ * - refs previously pointing to the merge commit are
-+ * rewritten to point to the previous non-merge commit.
++ * - refs pointing to the merge commit will be updated
++ * to `last_commit`.
++ * - the next replayed commit uses `last_commit` as its
++ * `base`.
+ */
+ } else {
+ /*
-+ * pick_regular_commit() looks up the parent of `commit` in
-+ * `replayed_commits` to determine the ancestor to replay onto.
-+ * The `default_base` parameter is used when no ancestor is found,
-+ * which happens for the first commit in the revision range.
-+ * When reverting, commits are replayed in reverse order, so the
-+ * lookup never succeeds, and we need to pass `last_commit`.
++ * Decide where to replay this commit onto.
++ * If the parent commit was replayed already, the replayed result
++ * can be found in `replayed_commits`. Otherwise fall back to `onto`.
++ * When reverting, commits are replayed in reverse order and thus
++ * its parent isn't replayed yet. Therefore revert commits are
++ * always replayed onto `last_commit`.
++ * Also when opts->linearize is true, set the base to
++ * `last_commit` to create a single linear history.
+ */
-+ struct commit *base = onto;
-+ if (mode == REPLAY_MODE_REVERT)
++ struct commit *parent = commit->parents ? commit->parents->item : NULL;
++ struct commit *base = get_mapped_commit(replayed_commits, parent, onto);
++
++ if (opts->linearize || mode == REPLAY_MODE_REVERT)
+ base = last_commit;
+
+ last_commit = pick_regular_commit(revs->repo, commit, base,
-+ replayed_commits,
+ &merge_opt, &result,
+ mode, opts->empty);
+ }
@@ t/t3650-replay-basics.sh: test_expect_success '--onto with --ref rejects multipl
+ test_line_count = 3 out
+'
+
-+test_expect_success 'replay with --linearize to rebase multiple divergent branches' '
++test_expect_success 'replay with --linearize rebase multiple divergent branches into a single line' '
+ git replay --ref-action=print --linearize \
-+ --onto main ^B topic2 topic-with-merge >result &&
++ --onto main ^B topic2 topic3 topic4 >result &&
+
-+ test_line_count = 2 result &&
++ test_line_count = 3 result &&
+ cut -f 3 -d " " result >new-branch-tips &&
+
-+ git log --format=%s $(head -n 1 new-branch-tips) >actual &&
-+ test_write_lines E D C M L B A >expect &&
++ >expect &&
++ for i in 2 3 4
++ do
++ printf "update refs/heads/topic$i " >>expect &&
++ printf "%s " $(grep topic$i result | cut -f 3 -d " ") >>expect &&
++ git rev-parse topic$i >>expect || return 1
++ done &&
++
++ test_cmp expect result &&
++
++ test_write_lines E D C M L B A >expect2 &&
++ test_write_lines H G F E D C M L B A >expect3 &&
++ test_write_lines J I H G F E D C M L B A >expect4 &&
++
++ for i in 2 3 4
++ do
++ git log --format=%s $(grep topic$i result | cut -f 3 -d " ") >actual &&
++ test_cmp expect$i actual || return 1
++ done
++'
++
++test_expect_success 'replay with --linearize of a divergent merge keeps both sides' '
++ test_when_finished "git update-ref -d refs/heads/divergent-x" &&
++ test_when_finished "git update-ref -d refs/heads/divergent-y" &&
++
++ # Build a real merge of two commits that diverged from a common base:
++ #
++ # X - Z (divergent-x)
++ # / /
++ # M - Y (divergent-y)
++ #
++ git switch -c divergent-x main &&
++ test_commit X &&
++ git switch -c divergent-y main &&
++ test_commit Y &&
++ git switch divergent-x &&
++ test_merge Z divergent-y --no-ff &&
++
++ git replay --ref-action=print --linearize \
++ --onto main main..divergent-x >result &&
++ test_line_count = 1 result &&
++ tip=$(cut -f 3 -d " " result) &&
++
++ # The merge Z is dropped, but both X and Y are linearized onto main;
++ # neither side is lost.
++ git log --format=%s main..$tip >actual &&
++ test_write_lines Y X >expect &&
++ test_cmp expect actual
++'
++
++test_expect_success '--linearize with --contained updates contained refs' '
++ git replay --ref-action=print --linearize --contained \
++ --onto main ^B topic-with-merge >result &&
++
++ test_line_count = 2 result &&
++
++ git log --format=%s $(head -n 1 result | cut -f 3 -d " ") >actual &&
++ test_write_lines J I M L B A >expect &&
+ test_cmp expect actual &&
+
-+ git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
++ git log --format=%s $(tail -n 1 result | cut -f 3 -d " ") >actual &&
+ test_write_lines O N J I M L B A >expect &&
+ test_cmp expect actual
+'
---
base-commit: ab776a62a78576513ee121424adb19597fbb7613
change-id: 20260604-toon-git-replay-drop-merges-807fa008d395
^ permalink raw reply
* [PATCH v6 1/3] replay: add helper to put entry into replayed_commits
From: Toon Claes @ 2026-07-02 17:58 UTC (permalink / raw)
To: git; +Cc: Elijah Newren, Toon Claes, Johannes Schindelin
In-Reply-To: <20260702-toon-git-replay-drop-merges-v6-0-78a07cdd0382@iotcl.com>
The function replay_revisions() in replay.c is rather lengthy. Extract
the logic to put a commit entry into mapped_commits into a helper
function put_mapped_commit().
While at it, rename mapped_commit() to get_mapped_commit() to pair with
this new function.
Signed-off-by: Toon Claes <toon@iotcl.com>
---
replay.c | 31 ++++++++++++++++++++-----------
1 file changed, 20 insertions(+), 11 deletions(-)
diff --git a/replay.c b/replay.c
index da531d5bc6..b9f8fc47ce 100644
--- a/replay.c
+++ b/replay.c
@@ -250,9 +250,9 @@ static void set_up_replay_mode(struct repository *repo,
strset_clear(&rinfo.positive_refs);
}
-static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,
- struct commit *commit,
- struct commit *fallback)
+static struct commit *get_mapped_commit(kh_oid_map_t *replayed_commits,
+ struct commit *commit,
+ struct commit *fallback)
{
khint_t pos;
if (!commit)
@@ -263,6 +263,21 @@ static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,
return kh_value(replayed_commits, pos);
}
+static void put_mapped_commit(kh_oid_map_t *replayed_commits,
+ struct commit *commit,
+ struct commit *new_commit)
+{
+ khint_t pos;
+ int ret;
+
+ pos = kh_put_oid_map(replayed_commits, commit->object.oid, &ret);
+ if (ret == 0)
+ BUG("Duplicate rewritten commit: %s",
+ oid_to_hex(&commit->object.oid));
+
+ kh_value(replayed_commits, pos) = new_commit;
+}
+
static struct commit *pick_regular_commit(struct repository *repo,
struct commit *pickme,
kh_oid_map_t *replayed_commits,
@@ -283,7 +298,7 @@ static struct commit *pick_regular_commit(struct repository *repo,
base_tree = lookup_tree(repo, repo->hash_algo->empty_tree);
}
- replayed_base = mapped_commit(replayed_commits, base, onto);
+ replayed_base = get_mapped_commit(replayed_commits, base, onto);
replayed_base_tree = repo_get_commit_tree(repo, replayed_base);
pickme_tree = repo_get_commit_tree(repo, pickme);
@@ -423,8 +438,6 @@ int replay_revisions(struct rev_info *revs,
replayed_commits = kh_init_oid_map();
while ((commit = get_revision(revs))) {
const struct name_decoration *decoration;
- khint_t pos;
- int hr;
if (commit->parents && commit->parents->next)
die(_("replaying merge commits is not supported yet!"));
@@ -436,11 +449,7 @@ int replay_revisions(struct rev_info *revs,
break;
/* Record commit -> last_commit mapping */
- pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);
- if (hr == 0)
- BUG("Duplicate rewritten commit: %s\n",
- oid_to_hex(&commit->object.oid));
- kh_value(replayed_commits, pos) = last_commit;
+ put_mapped_commit(replayed_commits, commit, last_commit);
/* Update any necessary branches */
if (ref)
--
2.53.0.1323.g189a785ab5
^ permalink raw reply related
* [PATCH v6 2/3] replay: resolve the replay base outside pick_regular_commit()
From: Toon Claes @ 2026-07-02 17:58 UTC (permalink / raw)
To: git; +Cc: Elijah Newren, Toon Claes, Johannes Schindelin
In-Reply-To: <20260702-toon-git-replay-drop-merges-v6-0-78a07cdd0382@iotcl.com>
Depending on what gets passed into the function pick_regular_commit(),
it decides the new base for the replayed commit. It first tries to find
the replayed results of `pickme`'s parent in the `replayed_commits` map.
If not found, it falls back to `onto`.
When using git-replay(1) with --onto, the fallback is the revision
passed in with this option, but when using --revert, the fallback is
`last_commit`.
It's rather confusing the base is decided partly inside
pick_regular_commit() and partly by its caller.
Move the base selection completely into the caller: replay_revisions().
This bundles all the logic of deciding on the base together. Also, this
reduces the number of parameters of pick_regular_commit(), making it's
interface cleaner.
This refactoring doesn't bring any behavior changes.
Signed-off-by: Toon Claes <toon@iotcl.com>
---
replay.c | 34 +++++++++++++++++++++-------------
1 file changed, 21 insertions(+), 13 deletions(-)
diff --git a/replay.c b/replay.c
index b9f8fc47ce..5aee0eafbc 100644
--- a/replay.c
+++ b/replay.c
@@ -280,25 +280,19 @@ static void put_mapped_commit(kh_oid_map_t *replayed_commits,
static struct commit *pick_regular_commit(struct repository *repo,
struct commit *pickme,
- kh_oid_map_t *replayed_commits,
- struct commit *onto,
+ struct commit *replayed_base,
struct merge_options *merge_opt,
struct merge_result *result,
enum replay_mode mode,
enum replay_empty_commit_action empty)
{
- struct commit *base, *replayed_base;
struct tree *pickme_tree, *base_tree, *replayed_base_tree;
- if (pickme->parents) {
- base = pickme->parents->item;
- base_tree = repo_get_commit_tree(repo, base);
- } else {
- base = NULL;
+ if (pickme->parents)
+ base_tree = repo_get_commit_tree(repo, pickme->parents->item);
+ else
base_tree = lookup_tree(repo, repo->hash_algo->empty_tree);
- }
- replayed_base = get_mapped_commit(replayed_commits, base, onto);
replayed_base_tree = repo_get_commit_tree(repo, replayed_base);
pickme_tree = repo_get_commit_tree(repo, pickme);
@@ -439,12 +433,26 @@ int replay_revisions(struct rev_info *revs,
while ((commit = get_revision(revs))) {
const struct name_decoration *decoration;
+ /*
+ * Decide where to replay this commit on.
+ * If the parent commit was replayed already, the replayed result
+ * can be found in `replayed_commits`. Otherwise fall back to `onto`.
+ * When reverting, commits are replayed in reverse order and thus
+ * its parent isn't replayed yet. Therefore revert commits are
+ * always replayed onto `last_commit`.
+ */
+ struct commit *parent = commit->parents ? commit->parents->item : NULL;
+ struct commit *base = get_mapped_commit(replayed_commits, parent, onto);
+
+ if (mode == REPLAY_MODE_REVERT)
+ base = last_commit;
+
if (commit->parents && commit->parents->next)
die(_("replaying merge commits is not supported yet!"));
- last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
- mode == REPLAY_MODE_REVERT ? last_commit : onto,
- &merge_opt, &result, mode, opts->empty);
+ last_commit = pick_regular_commit(revs->repo, commit, base,
+ &merge_opt, &result,
+ mode, opts->empty);
if (!last_commit)
break;
--
2.53.0.1323.g189a785ab5
^ permalink raw reply related
* [PATCH v6 3/3] replay: offer an option to linearize the commit topology
From: Toon Claes @ 2026-07-02 17:58 UTC (permalink / raw)
To: git; +Cc: Elijah Newren, Toon Claes, Johannes Schindelin,
Johannes Schindelin
In-Reply-To: <20260702-toon-git-replay-drop-merges-v6-0-78a07cdd0382@iotcl.com>
From: Johannes Schindelin <Johannes.Schindelin@gmx.de>
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. 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.
git-replay(1) accepts multiple revision ranges, for example:
$ git replay --onto main topic1 topic2
Without `--linearize` this replays 'topic1' and 'topic2' onto 'main'
independently and updates both refs.
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.
Replaying all revision ranges into one single linear history is
intentional and it's the only way to ensure predictable results. A user
who wants to linearize ranges independently is advised to use separate
git-replay(1) invocations.
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.
Co-authored-by: Toon Claes <toon@iotcl.com>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Signed-off-by: Toon Claes <toon@iotcl.com>
---
Documentation/git-replay.adoc | 21 ++++++-
builtin/replay.c | 6 +-
replay.c | 54 ++++++++++------
replay.h | 5 ++
t/t3650-replay-basics.sh | 140 +++++++++++++++++++++++++++++++++++++++++-
5 files changed, 203 insertions(+), 23 deletions(-)
diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index a32f72aead..cc1d2bd251 100644
--- a/Documentation/git-replay.adoc
+++ b/Documentation/git-replay.adoc
@@ -10,7 +10,7 @@ SYNOPSIS
--------
[verse]
(EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)
- [--ref=<ref>] [--ref-action=<mode>] <revision-range>
+ [--ref=<ref>] [--ref-action=<mode>] [--linearize] <revision-range>
DESCRIPTION
-----------
@@ -88,6 +88,25 @@ incompatible with `--contained` (which is a modifier for `--onto` only).
+
The default mode can be configured via the `replay.refAction` configuration variable.
+--linearize::
+ In this mode, each replayed commit is stacked on top of the
+ previously replayed one, so all replayed commits are flattened into
+ a single linear history.
++
+When a merge commit is encountered, the behavior of git-rebase(1)'s
+option `--no-rebase-merges` is imitated. All commits in the range
+reachable from the merge commit are replayed into a linear history, and
+the merge commit itself is dropped. A ref that pointed to a merge commit
+is updated to the merge's last replayed ancestor.
++
+This flattens the `<revision-range>` as a whole. When multiple revision
+ranges are given they are stacked on top of each other into one linear
+history. Each of their refs is updated to point to its position in that
+history. To linearize ranges separately, replay them in separate `git
+replay` invocations.
++
+This option is incompatible with `--revert`.
+
<revision-range>::
Range of commits to replay; see "Specifying Ranges" in
linkgit:git-rev-parse[1]. In `--advance=<branch>` or
diff --git a/builtin/replay.c b/builtin/replay.c
index 39e3a86f6c..62962c73c7 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -85,7 +85,7 @@ int cmd_replay(int argc,
const char *const replay_usage[] = {
N_("(EXPERIMENTAL!) git replay "
"([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n"
- "[--ref=<ref>] [--ref-action=<mode>] <revision-range>"),
+ "[--ref=<ref>] [--ref-action=<mode>] [--linearize] <revision-range>"),
NULL
};
struct option replay_options[] = {
@@ -111,6 +111,8 @@ int cmd_replay(int argc,
N_("mode"),
N_("control ref update behavior (update|print)"),
PARSE_OPT_NONEG),
+ OPT_BOOL(0, "linearize", &opts.linearize,
+ N_("drop merge commits, replaying only non-merge commits")),
OPT_END()
};
@@ -132,6 +134,8 @@ int cmd_replay(int argc,
opts.contained, "--contained");
die_for_incompatible_opt2(!!opts.ref, "--ref",
!!opts.contained, "--contained");
+ die_for_incompatible_opt2(!!opts.revert, "--revert",
+ opts.linearize, "--linearize");
/* Parse ref action mode from command line or config */
ref_mode = get_ref_action_mode(repo, ref_action);
diff --git a/replay.c b/replay.c
index 5aee0eafbc..bd1f3bb898 100644
--- a/replay.c
+++ b/replay.c
@@ -433,26 +433,40 @@ int replay_revisions(struct rev_info *revs,
while ((commit = get_revision(revs))) {
const struct name_decoration *decoration;
- /*
- * Decide where to replay this commit on.
- * If the parent commit was replayed already, the replayed result
- * can be found in `replayed_commits`. Otherwise fall back to `onto`.
- * When reverting, commits are replayed in reverse order and thus
- * its parent isn't replayed yet. Therefore revert commits are
- * always replayed onto `last_commit`.
- */
- struct commit *parent = commit->parents ? commit->parents->item : NULL;
- struct commit *base = get_mapped_commit(replayed_commits, parent, onto);
-
- if (mode == REPLAY_MODE_REVERT)
- base = last_commit;
-
- if (commit->parents && commit->parents->next)
- die(_("replaying merge commits is not supported yet!"));
-
- last_commit = pick_regular_commit(revs->repo, commit, base,
- &merge_opt, &result,
- mode, opts->empty);
+ if (commit->parents && commit->parents->next) {
+ if (!opts->linearize)
+ die(_("replaying merge commits is not supported yet!"));
+ /*
+ * Drop the merge commit: do not pick it, leave
+ * `last_commit` unchanged, and fall through to the
+ * rest of the loop. As a result:
+ * - refs pointing to the merge commit will be updated
+ * to `last_commit`.
+ * - the next replayed commit uses `last_commit` as its
+ * `base`.
+ */
+ } else {
+ /*
+ * Decide where to replay this commit onto.
+ * If the parent commit was replayed already, the replayed result
+ * can be found in `replayed_commits`. Otherwise fall back to `onto`.
+ * When reverting, commits are replayed in reverse order and thus
+ * its parent isn't replayed yet. Therefore revert commits are
+ * always replayed onto `last_commit`.
+ * Also when opts->linearize is true, set the base to
+ * `last_commit` to create a single linear history.
+ */
+ struct commit *parent = commit->parents ? commit->parents->item : NULL;
+ struct commit *base = get_mapped_commit(replayed_commits, parent, onto);
+
+ if (opts->linearize || mode == REPLAY_MODE_REVERT)
+ base = last_commit;
+
+ last_commit = pick_regular_commit(revs->repo, commit, base,
+ &merge_opt, &result,
+ mode, opts->empty);
+ }
+
if (!last_commit)
break;
diff --git a/replay.h b/replay.h
index faf95c7459..64f42b6512 100644
--- a/replay.h
+++ b/replay.h
@@ -62,6 +62,11 @@ struct replay_revisions_options {
* Defaults to REPLAY_EMPTY_COMMIT_DROP.
*/
enum replay_empty_commit_action empty;
+
+ /*
+ * Whether to linearize the commits (i.e. drop merge commits).
+ */
+ int linearize;
};
/* This struct is used as an out-parameter by `replay_revisions()`. */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 3353bc4a4d..e832e2c93d 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -52,8 +52,12 @@ test_expect_success 'setup' '
test_merge P O --no-ff &&
git switch main &&
+ git switch --orphan unrelated &&
+ test_commit unrelated-root &&
+
git switch -c conflict B &&
- test_commit C.conflict C.t conflict
+ test_commit C.conflict C.t conflict &&
+ git branch -D unrelated
'
test_expect_success 'setup bare' '
@@ -97,6 +101,12 @@ test_expect_success '--advance and --contained cannot be used together' '
test_grep "cannot be used together" actual
'
+test_expect_success '--revert and --linearize cannot be used together' '
+ test_must_fail git replay --revert=main --linearize \
+ topic1..topic2 2>actual &&
+ test_grep "cannot be used together" actual
+'
+
test_expect_success 'cannot advance target ... ordering would be ill-defined' '
echo "fatal: ${SQ}--advance${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined" >expect &&
test_must_fail git replay --advance=main main topic1 topic2 2>actual &&
@@ -565,4 +575,132 @@ test_expect_success '--onto with --ref rejects multiple revision ranges' '
test_grep "cannot be used with multiple revision ranges" err
'
+test_expect_success 'replay to rebase merge commit with --linearize' '
+ git replay --ref-action=print --linearize \
+ --onto main I..topic-with-merge >result &&
+
+ test_line_count = 1 result &&
+
+ git log --format=%s $(cut -f 3 -d " " result) >actual &&
+ test_write_lines O N J M L B A >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success 'replay to rebase merge commit with --linearize down to the root commit' '
+ git replay --ref-action=print --linearize \
+ --onto unrelated-root topic-with-merge >result &&
+
+ test_line_count = 1 result &&
+
+ git log --format=%s $(cut -f 3 -d " " result) >actual &&
+ test_write_lines O N J I B A unrelated-root >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success 'replay to cherry-pick merge commit with --linearize' '
+ git replay --ref-action=print --linearize \
+ --advance main I..topic-with-merge >result &&
+
+ test_line_count = 1 result &&
+
+ git log --format=%s $(cut -f 3 -d " " result) >actual &&
+ test_write_lines O N J M L B A >expect &&
+ test_cmp expect actual &&
+
+ printf "update refs/heads/main " >expect &&
+ printf "%s " $(cut -f 3 -d " " result) >>expect &&
+ git rev-parse main >>expect &&
+ test_cmp expect result
+'
+
+test_expect_success 'replay --linearize produces the same patches' '
+ git replay --ref-action=print --linearize \
+ --onto main I..topic-with-merge >result &&
+
+ test_line_count = 1 result &&
+ tip=$(cut -f 3 -d " " result) &&
+
+ # range-diff does not care about the dropped merge,
+ # so the original commits (I..topic-with-merge)
+ # and the replayed chain (main..tip) must produce identical patches.
+ git range-diff I..topic-with-merge main..$tip >out &&
+ test_file_not_empty out &&
+ test_grep ! -v "=" out &&
+
+ git log --oneline main..$tip >out &&
+ test_line_count = 3 out
+'
+
+test_expect_success 'replay with --linearize rebase multiple divergent branches into a single line' '
+ git replay --ref-action=print --linearize \
+ --onto main ^B topic2 topic3 topic4 >result &&
+
+ test_line_count = 3 result &&
+ cut -f 3 -d " " result >new-branch-tips &&
+
+ >expect &&
+ for i in 2 3 4
+ do
+ printf "update refs/heads/topic$i " >>expect &&
+ printf "%s " $(grep topic$i result | cut -f 3 -d " ") >>expect &&
+ git rev-parse topic$i >>expect || return 1
+ done &&
+
+ test_cmp expect result &&
+
+ test_write_lines E D C M L B A >expect2 &&
+ test_write_lines H G F E D C M L B A >expect3 &&
+ test_write_lines J I H G F E D C M L B A >expect4 &&
+
+ for i in 2 3 4
+ do
+ git log --format=%s $(grep topic$i result | cut -f 3 -d " ") >actual &&
+ test_cmp expect$i actual || return 1
+ done
+'
+
+test_expect_success 'replay with --linearize of a divergent merge keeps both sides' '
+ test_when_finished "git update-ref -d refs/heads/divergent-x" &&
+ test_when_finished "git update-ref -d refs/heads/divergent-y" &&
+
+ # Build a real merge of two commits that diverged from a common base:
+ #
+ # X - Z (divergent-x)
+ # / /
+ # M - Y (divergent-y)
+ #
+ git switch -c divergent-x main &&
+ test_commit X &&
+ git switch -c divergent-y main &&
+ test_commit Y &&
+ git switch divergent-x &&
+ test_merge Z divergent-y --no-ff &&
+
+ git replay --ref-action=print --linearize \
+ --onto main main..divergent-x >result &&
+ test_line_count = 1 result &&
+ tip=$(cut -f 3 -d " " result) &&
+
+ # The merge Z is dropped, but both X and Y are linearized onto main;
+ # neither side is lost.
+ git log --format=%s main..$tip >actual &&
+ test_write_lines Y X >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success '--linearize with --contained updates contained refs' '
+ git replay --ref-action=print --linearize --contained \
+ --onto main ^B topic-with-merge >result &&
+
+ test_line_count = 2 result &&
+
+ git log --format=%s $(head -n 1 result | cut -f 3 -d " ") >actual &&
+ test_write_lines J I M L B A >expect &&
+ test_cmp expect actual &&
+
+ git log --format=%s $(tail -n 1 result | cut -f 3 -d " ") >actual &&
+ test_write_lines O N J I M L B A >expect &&
+ test_cmp expect actual
+'
+
test_done
--
2.53.0.1323.g189a785ab5
^ permalink raw reply related
* Re: [PATCH 1/9] csum-file: drop discard_hashfile()
From: Junio C Hamano @ 2026-07-02 18:19 UTC (permalink / raw)
To: Jeff King; +Cc: git, Patrick Steinhardt
In-Reply-To: <20260702075744.GA2029434@coredump.intra.peff.net>
Jeff King <peff@peff.net> writes:
> So now we have two functions, discard_hashfile() and free_hashfile(),
> and we only need one. Which one do we want to keep?
>
> The only difference between them is that the discard variant also closes
> the descriptors held in the struct. Let's look at the three callers:
> ...
> Note that I said "descriptors" plural above. Those callers all care
> about the "fd" member of the struct. But discard_hashfile() also closes
> check_fd. That is only used if the struct is initialized with
> hashfd_check(), and neither of its two callers call either discard or
> free (they always "finalize" instead). So closing it is irrelevant for
> the current callers.
>
> I think we're better off sticking with the simpler free_hashfile()
> interface, and the handful of callers can decide how to handle the
> descriptors themselves.
Sonds good.
Our resident naming czar (already Cc'ed) may have preference about
the names and word order, though ;-)
^ permalink raw reply
* [ANNOUNCE] Git Rev News edition 136
From: Christian Couder @ 2026-07-02 19:38 UTC (permalink / raw)
To: git
Cc: Junio C Hamano, Jakub Narebski, Markus Jansen, Kaartic Sivaraam,
Štěpán Němec, Taylor Blau,
Johannes Schindelin, Derrick Stolee, Elijah Newren, Toon Claes,
Paulo Gomes, Phillip Wood, lwn
Hi everyone,
The 136th edition of Git Rev News is now published:
https://git.github.io/rev_news/2026/06/30/edition-136/
Thanks a lot to Toon Claes, Štěpán Němec and Paulo Gomes who helped this month!
Enjoy,
Christian, Jakub, Markus and Kaartic.
PS: An issue for the next edition is already opened and contributions
are welcome:
https://github.com/git/git.github.io/issues/853
^ permalink raw reply
* Re: [PATCH v5 0/4] history: add squash subcommand to fold a range
From: Junio C Hamano @ 2026-07-02 20:28 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: phillip.wood, Matt Hunter, Harald Nordgren,
Harald Nordgren via GitGitGadget, git
In-Reply-To: <akZfm-igZKeHaDST@pks.im>
Patrick Steinhardt <ps@pks.im> writes:
> Yeah, agreed. I think that the reflog is insufficient for a lot of Git's
> operations and that it is way too hard to reason about.
As the reflog has never been about undoing, this is understandable.
Things like @{-N} notation and "git push --force-if-includes" make
good use of the "what was this ref pointing at historically?"
information, so they give us a proof that it is possible to
programmatically go back and find the necessary state, though.
^ permalink raw reply
* Re: [PATCH 1/9] csum-file: drop discard_hashfile()
From: Jeff King @ 2026-07-02 21:06 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git, Patrick Steinhardt
In-Reply-To: <xmqqik6xl0fb.fsf@gitster.g>
On Thu, Jul 02, 2026 at 11:19:04AM -0700, Junio C Hamano wrote:
> Jeff King <peff@peff.net> writes:
>
> > So now we have two functions, discard_hashfile() and free_hashfile(),
> > and we only need one. Which one do we want to keep?
> >
> > The only difference between them is that the discard variant also closes
> > the descriptors held in the struct. Let's look at the three callers:
> > ...
> > Note that I said "descriptors" plural above. Those callers all care
> > about the "fd" member of the struct. But discard_hashfile() also closes
> > check_fd. That is only used if the struct is initialized with
> > hashfd_check(), and neither of its two callers call either discard or
> > free (they always "finalize" instead). So closing it is irrelevant for
> > the current callers.
> >
> > I think we're better off sticking with the simpler free_hashfile()
> > interface, and the handful of callers can decide how to handle the
> > descriptors themselves.
>
> Sonds good.
>
> Our resident naming czar (already Cc'ed) may have preference about
> the names and word order, though ;-)
Heh, yes, it should be hashfile_free() but that would require changing
the whole interface. We could do that on top, which might also be a good
time to do s/free/discard/ without worrying about a subtle behavior
change.
-Peff
^ permalink raw reply
* Re: [PATCH 3/9] t4141: fix inefficient use of dd(1)
From: Jeff King @ 2026-07-02 21:16 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git, Junio C Hamano, Johannes Schindelin
In-Reply-To: <20260702-b4-pks-t-fixes-for-GIT-TEST-LONG-v1-3-76b4d7bab3d0@pks.im>
On Thu, Jul 02, 2026 at 02:00:56PM +0200, Patrick Steinhardt wrote:
> This test setup is extremely expensive, as `test_copy_bytes` is
> implemented via `dd ibs=1 count="$1"`, which copies data one byte at a
> time. So as we write 1GB of data, we end up doing 1 billion reads and
> writes. This naturally takes a while: it takes 6 minutes on my system,
> and around 40 minutes in some CI jobs!
>
> We can do much better though, as genzeros already knows to handle an
> optional limit of how much data it is supposed to write, which allows us
> to remove the call to `test_copy_bytes`. Furthermore, it has already
> been optimized to generate the data fast.
Seems like a good fix for this case, where we can skip the extra process
entirely.
It feels like test_copy_bytes should be able to do much better in
general. The obvious thing to reach for is "head -c", but the function
was originally added because that wasn't portable. The "-c" option is
not in POSIX, though the original comment claims IRIX was the problem,
so I wonder if "head -c" is de facto portable these days.
I'd use perl of course. ;) The history here is somewhat amusing. We
originally did use dd, but that changed in 4de0bbd898 (t9300: use perl
"head -c" clone in place of "dd bs=1 count=16000" kluge, 2010-12-13)
because dd was slow. The code moved to test-lib.sh in 48860819e8 (t9300:
factor out portable "head -c" replacement, 2016-06-30), where I rejected
the dd solution because it was slow. And then the perl turned back into
dd in 01486b5de8 (t: adapt `test_copy_bytes()` to not use Perl,
2025-04-03), becoming slow again.
Chesterton's fence at work?
-Peff
^ permalink raw reply
* Re: [PATCH 5/9] t7508: skip EXPENSIVE test that is broken without SIZE_T_IS_32BIT
From: Jeff King @ 2026-07-02 21:22 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git, Junio C Hamano, Johannes Schindelin
In-Reply-To: <20260702-b4-pks-t-fixes-for-GIT-TEST-LONG-v1-5-76b4d7bab3d0@pks.im>
On Thu, Jul 02, 2026 at 02:00:58PM +0200, Patrick Steinhardt wrote:
> This test has also been blowing up in the "linux32" CI job in GitHub
> Workflows since 7a094d68a2 (ci: run expensive tests on push builds to
> integration branches, 2026-05-08). But that job doesn't only fail, it
> also hangs, and that has been concealing the failure.
One thing I don't understand about this and a few other patches in this
series: I've been getting passing GitHub Actions runs, including
linux32, even after that commit turned on the expensive jobs.
From your description it sounds like it should _never_ work, but it does
for me. It's possible there's something going on in my CI builds that
would cause the expensive tests not to run, but I don't think so. Am I
misunderstanding the problem? Or is there something missing from the
analysis?
-Peff
^ permalink raw reply
* Re: [PATCH 6/9] t7900: clean up large EXPENSIVE repository
From: Jeff King @ 2026-07-02 21:30 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git, Junio C Hamano, Johannes Schindelin
In-Reply-To: <20260702-b4-pks-t-fixes-for-GIT-TEST-LONG-v1-6-76b4d7bab3d0@pks.im>
On Thu, Jul 02, 2026 at 02:00:59PM +0200, Patrick Steinhardt wrote:
> One of the tests in t7900 is marked with EXPENSIVE because we create a
> repository with 2GB of data that we end up repacking. We never clean up
> that repository though, so we occupy the full 2GB of data until the end
> of the test suite. Besides clogging our disk, it also means that all
> subsequent tests may have to repack this data multiple times.
Hmm, I hoped this would drop the time to run t7900 with --long, but it
takes about 1m40s both before and after your patch (vs ~6s without
--long). Just looking at the script, I'd guess that it's because the
subsequent repacks are mostly incremental or geometric, so they don't
need to write the big pack.
Oh well. It still seems like an obvious improvement, though, both in
terms of peak disk usage and avoiding unwanted surprises when more tests
are added later.
-Peff
^ permalink raw reply
* Re: [PATCH 5/9] t7508: skip EXPENSIVE test that is broken without SIZE_T_IS_32BIT
From: Jeff King @ 2026-07-02 22:18 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git, Junio C Hamano, Johannes Schindelin
In-Reply-To: <20260702212235.GC2051171@coredump.intra.peff.net>
On Thu, Jul 02, 2026 at 05:22:35PM -0400, Jeff King wrote:
> On Thu, Jul 02, 2026 at 02:00:58PM +0200, Patrick Steinhardt wrote:
>
> > This test has also been blowing up in the "linux32" CI job in GitHub
> > Workflows since 7a094d68a2 (ci: run expensive tests on push builds to
> > integration branches, 2026-05-08). But that job doesn't only fail, it
> > also hangs, and that has been concealing the failure.
>
> One thing I don't understand about this and a few other patches in this
> series: I've been getting passing GitHub Actions runs, including
> linux32, even after that commit turned on the expensive jobs.
>
> From your description it sounds like it should _never_ work, but it does
> for me. It's possible there's something going on in my CI builds that
> would cause the expensive tests not to run, but I don't think so. Am I
> misunderstanding the problem? Or is there something missing from the
> analysis?
Ah, nevermind. It _is_ my setup. We run the expensive tests only on pull
requests, or when pushing to some specific branches, none of which match
the name of my particular integration branch.
Given all of the headaches I'm hesitant to "fix" my setup to run them,
but I probably should. ;)
-Peff
^ permalink raw reply
* [PATCH v3 0/2] Makefile: link osxkeychain helper against Rust
From: Shardul Natu via GitGitGadget @ 2026-07-02 22:22 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, Shnatu, Koji Nakamaru
In-Reply-To: <pull.2288.v2.git.git.1782943303219.gitgitgadget@gmail.com>
Shardul Natu (2):
Makefile: add $(RUST_LIB) prerequisite to osxkeychain
Makefile: support universal macOS builds via RUST_TARGETS
Makefile | 45 ++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 40 insertions(+), 5 deletions(-)
base-commit: 602f6c329a7d99df269d382df353b4e1bbbbd8aa
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2288%2Fkiranani%2Fnext-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2288/kiranani/next-v3
Pull-Request: https://github.com/git/git/pull/2288
Range-diff vs v2:
-: ---------- > 1: 41de7d391a Makefile: add $(RUST_LIB) prerequisite to osxkeychain
1: 6a11aff909 ! 2: 257f5ef42f Makefile: link osxkeychain & support universal Rust
@@
## Metadata ##
-Author: Shnatu <snatu@google.com>
+Author: Shardul Natu <snatu@google.com>
## Commit message ##
- Makefile: link osxkeychain & support universal Rust
+ Makefile: support universal macOS builds via RUST_TARGETS
- When Rust is enabled, ensure that the git-credential-osxkeychain
- helper is linked with the necessary Rust libraries.
+ On macOS, Universal Binaries contain native executable code for
+ multiple architectures (such as Intel x86_64 and Apple Silicon arm64)
+ bundled into a single file. This is standard practice for macOS
+ distribution and CI packaging (such as internal distribution packages
+ or tooling like Burrito/Homebrew), allowing a single build artifact
+ to run natively across all Macs without Rosetta emulation or
+ maintaining separate packages.
- Also, introduce native support for macOS Universal Binaries
- (multi-architecture builds) in the Git build system by allowing
- the user to specify a list of target triples in the RUST_TARGETS
- environment variable.
+ When building Git C code for multiple architectures on macOS, the
+ Apple toolchain (clang) natively supports universal builds via
+ CFLAGS/LDFLAGS. When "-arch x86_64 -arch arm64" is passed, clang
+ automatically compiles and links universal binaries for all C object
+ files and executables out of the box.
- To implement this cleanly without complex shell scripting in recipes:
- 1. We introduce a declarative Make pattern rule (target/%/...) to
- compile each target-specific library slice (e.g.,
- target/aarch64-apple-darwin/...).
- 2. We update the $(RUST_LIB) recipe to depend on the list of
- compiled target-specific member libraries ($(RUST_MEMBER_LIBS)).
- 3. On macOS, if multiple targets are specified, we use lipo to
- combine them into a single Universal static library at
- target/release/libgitcore.a.
- 4. If only one target is specified, we copy it to the standard
- path.
- 5. We enforce that building for multiple targets requires macOS
- (as lipo is only available there), raising a clear make error
- on other platforms.
+ Cargo and rustc, however, do not support multiple "-arch" flags or
+ emitting universal binaries in a single invocation. Instead, Cargo
+ requires invoking each target triple independently (e.g., passing
+ "--target x86_64-apple-darwin" and "--target aarch64-apple-darwin").
- This is a highly elegant and native Makefile solution that avoids
- complex shell scripting in recipes and fully supports macOS Universal
- Binaries.
+ To bridge this gap when Rust is enabled:
+ 1. Allow specifying space-separated target triples in RUST_TARGETS.
+ 2. Introduce declarative pattern rules (target/%/...) to compile
+ each target-specific library slice via Cargo.
+ 3. On macOS, if multiple targets are specified, use "lipo" (part of
+ the mandatory Xcode Command Line Tools) to combine the resulting
+ static libraries into target/release/libgitcore.a.
+ 4. Ensure target directory creation before invoking lipo via
+ mkdir_p_parent_template.
+
+ Once $(RUST_LIB) is compiled into a universal static archive, the
+ standard C linker seamlessly links it with the C object files to
+ produce universal Git executables.
Signed-off-by: Shardul Natu <snatu@google.com>
@@ Makefile: include shared.mak
# == SHA-1 and SHA-256 defines ==
#
# === SHA-1 backend ===
-@@ Makefile: TEST_SHELL_PATH = $(SHELL_PATH)
-
- LIB_FILE = libgit.a
+@@ Makefile: LIB_FILE = libgit.a
-+ifndef NO_RUST
+ ifndef NO_RUST
ifdef DEBUG
-RUST_TARGET_DIR = target/debug
+RUST_BUILD_CONFIG = debug
@@ Makefile: TEST_SHELL_PATH = $(SHELL_PATH)
else
-RUST_LIB = $(RUST_TARGET_DIR)/libgitcore.a
+RUST_LIB_NAME = libgitcore.a
-+endif
+ endif
+RUST_LIB = target/$(RUST_BUILD_CONFIG)/$(RUST_LIB_NAME)
endif
GITLIBS = common-main.o $(LIB_FILE)
-@@ Makefile: scalar$X: scalar.o GIT-LDFLAGS $(GITLIBS)
- $(LIB_FILE): $(LIB_OBJS)
+@@ Makefile: $(LIB_FILE): $(LIB_OBJS)
$(QUIET_AR)$(RM) $@ && $(AR) $(ARFLAGS) $@ $^
-+ifndef NO_RUST
+ ifndef NO_RUST
+ifeq ($(RUST_TARGETS),)
$(RUST_LIB): Cargo.toml $(RUST_SOURCES) $(LIB_FILE)
$(QUIET_CARGO)cargo build $(CARGO_ARGS)
@@ Makefile: scalar$X: scalar.o GIT-LDFLAGS $(GITLIBS)
+ $(QUIET_CARGO)cargo build $(CARGO_ARGS) --target $*
+
+$(RUST_LIB): $(RUST_MEMBER_LIBS)
++ @$(call mkdir_p_parent_template)
+ $(QUIET_GEN)\
+ if [ $(words $(RUST_TARGETS)) -gt 1 ]; then \
+ lipo -create $^ -output $@; \
@@ Makefile: scalar$X: scalar.o GIT-LDFLAGS $(GITLIBS)
.PHONY: rust
rust: $(RUST_LIB)
-+endif
-
- export DEFAULT_EDITOR DEFAULT_PAGER
-
-@@ Makefile: $(LIBGIT_HIDDEN_EXPORT): $(LIBGIT_PARTIAL_EXPORT)
- contrib/libgit-sys/libgitpub.a: $(LIBGIT_HIDDEN_EXPORT)
- $(AR) $(ARFLAGS) $@ $^
-
--contrib/credential/osxkeychain/git-credential-osxkeychain: contrib/credential/osxkeychain/git-credential-osxkeychain.o $(LIB_FILE) GIT-LDFLAGS
-+# When Rust is enabled, git-credential-osxkeychain depends on Rust symbols in $(RUST_LIB)
-+contrib/credential/osxkeychain/git-credential-osxkeychain: contrib/credential/osxkeychain/git-credential-osxkeychain.o $(LIB_FILE) $(RUST_LIB) GIT-LDFLAGS
- $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \
- $(filter %.o,$^) $(LIBS) -framework Security -framework CoreFoundation
-
--
gitgitgadget
^ permalink raw reply
* [PATCH v3 1/2] Makefile: add $(RUST_LIB) prerequisite to osxkeychain
From: Shardul Natu via GitGitGadget @ 2026-07-02 22:22 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, Shnatu, Koji Nakamaru, Shardul Natu
In-Reply-To: <pull.2288.v3.git.git.1783030971.gitgitgadget@gmail.com>
From: Shardul Natu <snatu@google.com>
When Rust is enabled, the git-credential-osxkeychain helper depends on
Rust symbols compiled into $(RUST_LIB). While commit 522ea8ef7d
("osxkeychain: fix build with Rust") updated the linker command line to
use $(LIBS), it omitted $(RUST_LIB) from the target prerequisite list.
Without this prerequisite, running a parallel build ("make -j") from a
clean working tree can fail because Make does not know to invoke Cargo
to build libgitcore.a before linking git-credential-osxkeychain.
Add $(RUST_LIB) as a prerequisite dependency to the
git-credential-osxkeychain target.
Additionally, wrap the definitions of $(RUST_LIB) and the "rust" build
target in "ifndef NO_RUST". This ensures that when NO_RUST=1 is
specified, $(RUST_LIB) evaluates to empty, making the Rust dependency a
clean no-op without needing intermediate variables.
Signed-off-by: Shardul Natu <snatu@google.com>
---
Makefile | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/Makefile b/Makefile
index 1f3f099f5c..7db38ecce9 100644
--- a/Makefile
+++ b/Makefile
@@ -939,6 +939,7 @@ TEST_SHELL_PATH = $(SHELL_PATH)
LIB_FILE = libgit.a
+ifndef NO_RUST
ifdef DEBUG
RUST_TARGET_DIR = target/debug
else
@@ -950,6 +951,7 @@ RUST_LIB = $(RUST_TARGET_DIR)/gitcore.lib
else
RUST_LIB = $(RUST_TARGET_DIR)/libgitcore.a
endif
+endif
GITLIBS = common-main.o $(LIB_FILE)
EXTLIBS =
@@ -3019,11 +3021,13 @@ scalar$X: scalar.o GIT-LDFLAGS $(GITLIBS)
$(LIB_FILE): $(LIB_OBJS)
$(QUIET_AR)$(RM) $@ && $(AR) $(ARFLAGS) $@ $^
+ifndef NO_RUST
$(RUST_LIB): Cargo.toml $(RUST_SOURCES) $(LIB_FILE)
$(QUIET_CARGO)cargo build $(CARGO_ARGS)
.PHONY: rust
rust: $(RUST_LIB)
+endif
export DEFAULT_EDITOR DEFAULT_PAGER
@@ -4074,7 +4078,8 @@ $(LIBGIT_HIDDEN_EXPORT): $(LIBGIT_PARTIAL_EXPORT)
contrib/libgit-sys/libgitpub.a: $(LIBGIT_HIDDEN_EXPORT)
$(AR) $(ARFLAGS) $@ $^
-contrib/credential/osxkeychain/git-credential-osxkeychain: contrib/credential/osxkeychain/git-credential-osxkeychain.o $(LIB_FILE) GIT-LDFLAGS
+# When Rust is enabled, git-credential-osxkeychain depends on Rust symbols in $(RUST_LIB)
+contrib/credential/osxkeychain/git-credential-osxkeychain: contrib/credential/osxkeychain/git-credential-osxkeychain.o $(LIB_FILE) $(RUST_LIB) GIT-LDFLAGS
$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \
$(filter %.o,$^) $(LIBS) -framework Security -framework CoreFoundation
--
gitgitgadget
^ permalink raw reply related
* [PATCH v3 2/2] Makefile: support universal macOS builds via RUST_TARGETS
From: Shardul Natu via GitGitGadget @ 2026-07-02 22:22 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, Shnatu, Koji Nakamaru, Shardul Natu
In-Reply-To: <pull.2288.v3.git.git.1783030971.gitgitgadget@gmail.com>
From: Shardul Natu <snatu@google.com>
On macOS, Universal Binaries contain native executable code for
multiple architectures (such as Intel x86_64 and Apple Silicon arm64)
bundled into a single file. This is standard practice for macOS
distribution and CI packaging (such as internal distribution packages
or tooling like Burrito/Homebrew), allowing a single build artifact
to run natively across all Macs without Rosetta emulation or
maintaining separate packages.
When building Git C code for multiple architectures on macOS, the
Apple toolchain (clang) natively supports universal builds via
CFLAGS/LDFLAGS. When "-arch x86_64 -arch arm64" is passed, clang
automatically compiles and links universal binaries for all C object
files and executables out of the box.
Cargo and rustc, however, do not support multiple "-arch" flags or
emitting universal binaries in a single invocation. Instead, Cargo
requires invoking each target triple independently (e.g., passing
"--target x86_64-apple-darwin" and "--target aarch64-apple-darwin").
To bridge this gap when Rust is enabled:
1. Allow specifying space-separated target triples in RUST_TARGETS.
2. Introduce declarative pattern rules (target/%/...) to compile
each target-specific library slice via Cargo.
3. On macOS, if multiple targets are specified, use "lipo" (part of
the mandatory Xcode Command Line Tools) to combine the resulting
static libraries into target/release/libgitcore.a.
4. Ensure target directory creation before invoking lipo via
mkdir_p_parent_template.
Once $(RUST_LIB) is compiled into a universal static archive, the
standard C linker seamlessly links it with the C object files to
produce universal Git executables.
Signed-off-by: Shardul Natu <snatu@google.com>
---
Makefile | 38 ++++++++++++++++++++++++++++++++++----
1 file changed, 34 insertions(+), 4 deletions(-)
diff --git a/Makefile b/Makefile
index 7db38ecce9..e01f989cd0 100644
--- a/Makefile
+++ b/Makefile
@@ -500,6 +500,14 @@ include shared.mak
#
# Building Rust code requires Cargo.
#
+# Define RUST_TARGETS if you want to cross-compile. If left unspecified, it uses
+# the default rust target on the system.
+#
+# On macOS, this supports specifying multiple targets, separated by a space.
+# This will produce a Universal static library using `lipo`.
+#
+# Example: RUST_TARGETS="aarch64-apple-darwin x86_64-apple-darwin"
+#
# == SHA-1 and SHA-256 defines ==
#
# === SHA-1 backend ===
@@ -941,16 +949,17 @@ LIB_FILE = libgit.a
ifndef NO_RUST
ifdef DEBUG
-RUST_TARGET_DIR = target/debug
+RUST_BUILD_CONFIG = debug
else
-RUST_TARGET_DIR = target/release
+RUST_BUILD_CONFIG = release
endif
ifeq ($(uname_S),Windows)
-RUST_LIB = $(RUST_TARGET_DIR)/gitcore.lib
+RUST_LIB_NAME = gitcore.lib
else
-RUST_LIB = $(RUST_TARGET_DIR)/libgitcore.a
+RUST_LIB_NAME = libgitcore.a
endif
+RUST_LIB = target/$(RUST_BUILD_CONFIG)/$(RUST_LIB_NAME)
endif
GITLIBS = common-main.o $(LIB_FILE)
@@ -3022,8 +3031,29 @@ $(LIB_FILE): $(LIB_OBJS)
$(QUIET_AR)$(RM) $@ && $(AR) $(ARFLAGS) $@ $^
ifndef NO_RUST
+ifeq ($(RUST_TARGETS),)
$(RUST_LIB): Cargo.toml $(RUST_SOURCES) $(LIB_FILE)
$(QUIET_CARGO)cargo build $(CARGO_ARGS)
+else
+ifneq ($(words $(RUST_TARGETS)),1)
+ifneq ($(uname_S),Darwin)
+$(error Building universal Rust libraries requires macOS (lipo is not available on $(uname_S)))
+endif
+endif
+
+RUST_MEMBER_LIBS = $(foreach target,$(RUST_TARGETS),target/$(target)/$(RUST_BUILD_CONFIG)/$(RUST_LIB_NAME))
+$(RUST_MEMBER_LIBS): target/%/$(RUST_BUILD_CONFIG)/$(RUST_LIB_NAME): Cargo.toml $(RUST_SOURCES) $(LIB_FILE)
+ $(QUIET_CARGO)cargo build $(CARGO_ARGS) --target $*
+
+$(RUST_LIB): $(RUST_MEMBER_LIBS)
+ @$(call mkdir_p_parent_template)
+ $(QUIET_GEN)\
+ if [ $(words $(RUST_TARGETS)) -gt 1 ]; then \
+ lipo -create $^ -output $@; \
+ else \
+ cp $< $@; \
+ fi
+endif
.PHONY: rust
rust: $(RUST_LIB)
--
gitgitgadget
^ permalink raw reply related
* Re: [PATCH v2] Makefile: link osxkeychain & support universal Rust
From: Shardul Natu @ 2026-07-02 22:30 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: Shardul Natu via GitGitGadget, git, Kristoffer Haugsbakk, Shnatu,
Koji Nakamaru
In-Reply-To: <akZQmDYe9MtTdGM2@pks.im>
> "Shardul Natu via GitGitGadget" <gitgitgadget@gmail.com> writes:
>
> > From: Shnatu <snatu@google.com>
> > Signed-off-by: Shardul Natu <snatu@google.com>
>
> You'd want to make sure these two match.
Good catch. Done!
> > This is a highly elegant and native Makefile solution that avoids
> > complex shell scripting in recipes and fully supports macOS Universal
> > Binaries.
>
> You're the second person on this list I saw who calls their own
> creation "elegant" ;-).
Removed! It was AI generated description
> Do we know that leading directories to $(RUST_LIB) target has
> already been created at this point? If not, we may want to have
>
> $(RUST_LIB): $(RUST_MEMBER_LIBS)
> + $(call mkdir_p_parent_template)
> $(QUIET_GEN)\
> if [ $(words $(RUST_TARGETS)) -gt 1 ]; then \
> lipo -create $^ -output $@; \
>
> on top.
Added $(call mkdir_p_parent_template).
> > When Rust is enabled, ensure that the git-credential-osxkeychain
> > helper is linked with the necessary Rust libraries.
> >
> > Also, introduce native support for macOS Universal Binaries
> > (multi-architecture builds) in the Git build system by allowing
> > the user to specify a list of target triples in the RUST_TARGETS
> > environment variable.
>
> These are fundamentally unrelated things, aren't they? So I'd argue they
> should be split up into two commits.
You're right; these address two fundamentally different
problems. In v3, I have split this into a two commits:
1. Makefile: add $(RUST_LIB) prerequisite to osxkeychain
2. Makefile: support universal macOS builds via RUST_TARGETS
> I think we could also use an explanation here what the universal binary
> buys us for those who are not deeply familiar with the macOS platform.
> What are they, and why do we want/need to support them?
I have added this background to the relevant commit.
> Can we assume lipo to be generally available on macOS? Also, is it
> sufficient to just do this for the library? I would have expected that
> binaries would also need some treatment there.
>
> In other words: what does it help us to have the Rust treated this way
> if the rest isn't?
Yes, "lipo" is part of the Apple Xcode CLT, which
is already a hard prerequisite for invoking clang or make on macOS.
The reason only Rust needs special treatment in the Makefile is due to
how the respective toolchains handle multi-architecture builds:
1. Apple's C toolchain (clang) natively supports universal builds via
CFLAGS and LDFLAGS. When "-arch x86_64 -arch arm64" is passed, clang
automatically compiles and links universal binaries for all C object
files and executables out of the box.
2. Cargo and rustc, however, do not support multiple "-arch" flags or
emitting universal binaries in a single invocation. Instead, Cargo must
be invoked separately for each target triple ("--target x86_64-apple-darwin"
and "--target aarch64-apple-darwin").
By using "lipo" to combine those target-specific Rust static libraries
into a single universal archive at "target/release/libgitcore.a", we
bridge this gap. Once $(RUST_LIB) is a universal archive, the standard C
linker seamlessly links it with the C object files to produce the final
universal Git executables.
On Thu, Jul 2, 2026 at 4:57 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> On Wed, Jul 01, 2026 at 10:01:43PM +0000, Shardul Natu via GitGitGadget wrote:
> > From: Shnatu <snatu@google.com>
> >
> > When Rust is enabled, ensure that the git-credential-osxkeychain
> > helper is linked with the necessary Rust libraries.
> >
> > Also, introduce native support for macOS Universal Binaries
> > (multi-architecture builds) in the Git build system by allowing
> > the user to specify a list of target triples in the RUST_TARGETS
> > environment variable.
>
> These are fundamentally unrelated things, aren't they? So I'd argue they
> should be split up into two commits.
>
> I think we could also use an explanation here what the universal binary
> buys us for those who are not deeply familiar with the macOS platform.
> What are they, and why do we want/need to support them?
>
> > To implement this cleanly without complex shell scripting in recipes:
> > 1. We introduce a declarative Make pattern rule (target/%/...) to
> > compile each target-specific library slice (e.g.,
> > target/aarch64-apple-darwin/...).
> > 2. We update the $(RUST_LIB) recipe to depend on the list of
> > compiled target-specific member libraries ($(RUST_MEMBER_LIBS)).
> > 3. On macOS, if multiple targets are specified, we use lipo to
> > combine them into a single Universal static library at
> > target/release/libgitcore.a.
> > 4. If only one target is specified, we copy it to the standard
> > path.
> > 5. We enforce that building for multiple targets requires macOS
> > (as lipo is only available there), raising a clear make error
> > on other platforms.
> >
> > This is a highly elegant and native Makefile solution that avoids
> > complex shell scripting in recipes and fully supports macOS Universal
> > Binaries.
>
> As Junio already pointed out this self-praise reads quite weird. I'm
> just going to assume that this is AI-generated fluff.
>
> > diff --git a/Makefile b/Makefile
> > index 1f3f099f5c..8d49ecc897 100644
> > --- a/Makefile
> > +++ b/Makefile
> > @@ -3019,11 +3030,33 @@ scalar$X: scalar.o GIT-LDFLAGS $(GITLIBS)
> > $(LIB_FILE): $(LIB_OBJS)
> > $(QUIET_AR)$(RM) $@ && $(AR) $(ARFLAGS) $@ $^
> >
> > +ifndef NO_RUST
> > +ifeq ($(RUST_TARGETS),)
> > $(RUST_LIB): Cargo.toml $(RUST_SOURCES) $(LIB_FILE)
> > $(QUIET_CARGO)cargo build $(CARGO_ARGS)
> > +else
> > +ifneq ($(words $(RUST_TARGETS)),1)
> > +ifneq ($(uname_S),Darwin)
> > +$(error Building universal Rust libraries requires macOS (lipo is not available on $(uname_S)))
> > +endif
> > +endif
> > +
> > +RUST_MEMBER_LIBS = $(foreach target,$(RUST_TARGETS),target/$(target)/$(RUST_BUILD_CONFIG)/$(RUST_LIB_NAME))
> > +$(RUST_MEMBER_LIBS): target/%/$(RUST_BUILD_CONFIG)/$(RUST_LIB_NAME): Cargo.toml $(RUST_SOURCES) $(LIB_FILE)
> >
> > + $(QUIET_CARGO)cargo build $(CARGO_ARGS) --target $*
> > +
> > +$(RUST_LIB): $(RUST_MEMBER_LIBS)
> > + $(QUIET_GEN)\
> > + if [ $(words $(RUST_TARGETS)) -gt 1 ]; then \
> > + lipo -create $^ -output $@; \
>
> Can we assume lipo to be generally available on macOS? Also, is it
> sufficient to just do this for the library? I would have expected that
> binaries would also need some treatment there.
>
> In other words: what does it help us to have the Rust treated this way
> if the rest isn't?
>
> Thanks!
>
> Patrick
>
^ permalink raw reply
* [PATCH] precompose_utf8: use a flex array for d_name
From: Ihar Hrachyshka @ 2026-07-03 2:35 UTC (permalink / raw)
To: git; +Cc: Ihar Hrachyshka
On macOS, git status may abort while reading a directory entry
whose UTF-8 name grows past NAME_MAX bytes:
__chk_fail_overflow
__strlcpy_chk
precompose_utf8_readdir
read_directory_recursive
wt_status_collect
cmd_status
The precompose wrapper already reallocates dirent_prec_psx for
long names, but d_name is declared as char[NAME_MAX + 1]. A
fortified libc can still see that declared object size and reject a
larger strlcpy bound, even though the allocation was grown.
Make d_name a FLEX_ARRAY and size allocations from offsetof(). That
matches the actual object layout with the dynamic allocation, so the
fortified copy sees a destination whose size can grow with max_name_len.
Add a regression test that creates a 261-byte non-ASCII basename and
runs status with core.precomposeunicode enabled.
Signed-off-by: Ihar Hrachyshka <ihar.hrachyshka@gmail.com>
---
compat/precompose_utf8.c | 12 ++++++++----
compat/precompose_utf8.h | 9 +++++----
t/t3910-mac-os-precompose.sh | 15 +++++++++++++++
3 files changed, 28 insertions(+), 8 deletions(-)
diff --git a/compat/precompose_utf8.c b/compat/precompose_utf8.c
index 1711794..8077f62 100644
--- a/compat/precompose_utf8.c
+++ b/compat/precompose_utf8.c
@@ -19,6 +19,11 @@ typedef char *iconv_ibp;
static const char *repo_encoding = "UTF-8";
static const char *path_encoding = "UTF-8-MAC";
+static size_t dirent_prec_psx_size(size_t max_name_len)
+{
+ return st_add(offsetof(dirent_prec_psx, d_name), max_name_len);
+}
+
static size_t has_non_ascii(const char *s, size_t maxlen, size_t *strlen_c)
{
const uint8_t *ptr = (const uint8_t *)s;
@@ -114,8 +119,8 @@ const char *precompose_argv_prefix(int argc, const char **argv, const char *pref
PREC_DIR *precompose_utf8_opendir(const char *dirname)
{
PREC_DIR *prec_dir = xmalloc(sizeof(PREC_DIR));
- prec_dir->dirent_nfc = xmalloc(sizeof(dirent_prec_psx));
- prec_dir->dirent_nfc->max_name_len = sizeof(prec_dir->dirent_nfc->d_name);
+ prec_dir->dirent_nfc = xmalloc(dirent_prec_psx_size(NAME_MAX + 1));
+ prec_dir->dirent_nfc->max_name_len = NAME_MAX + 1;
prec_dir->dirp = opendir(dirname);
if (!prec_dir->dirp) {
@@ -145,8 +150,7 @@ struct dirent_prec_psx *precompose_utf8_readdir(PREC_DIR *prec_dir)
int ret_errno = errno;
if (new_maxlen > prec_dir->dirent_nfc->max_name_len) {
- size_t new_len = sizeof(dirent_prec_psx) + new_maxlen -
- sizeof(prec_dir->dirent_nfc->d_name);
+ size_t new_len = dirent_prec_psx_size(new_maxlen);
prec_dir->dirent_nfc = xrealloc(prec_dir->dirent_nfc, new_len);
prec_dir->dirent_nfc->max_name_len = new_maxlen;
diff --git a/compat/precompose_utf8.h b/compat/precompose_utf8.h
index fea06cf..c7c3cc2 100644
--- a/compat/precompose_utf8.h
+++ b/compat/precompose_utf8.h
@@ -14,11 +14,12 @@ typedef struct dirent_prec_psx {
/*
* See http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/dirent.h.html
- * NAME_MAX + 1 should be enough, but some systems have
- * NAME_MAX=255 and strlen(d_name) may return 508 or 510
- * Solution: allocate more when needed, see precompose_utf8_readdir()
+ * Start with room for NAME_MAX + 1 bytes, but keep d_name as a
+ * flexible array. Some systems have NAME_MAX=255 while strlen(d_name)
+ * from readdir() may return 508 or 510 bytes. Grow the allocation as
+ * needed in precompose_utf8_readdir().
*/
- char d_name[NAME_MAX+1];
+ char d_name[FLEX_ARRAY];
} dirent_prec_psx;
diff --git a/t/t3910-mac-os-precompose.sh b/t/t3910-mac-os-precompose.sh
index 6d5918c..fda4a76 100755
--- a/t/t3910-mac-os-precompose.sh
+++ b/t/t3910-mac-os-precompose.sh
@@ -207,6 +207,21 @@ test_expect_success "Add long precomposed filename" '
git commit -m "Long filename"
'
+test_expect_success "status with long non-ASCII filename" '
+ test_when_finished "rm -rf long-utf8-status" &&
+ git init long-utf8-status &&
+ (
+ cd long-utf8-status &&
+ test "$(git config --bool core.precomposeunicode)" = true &&
+ long_utf8_name=$(
+ perl -e "print q(a) x 249, qq(\342\200\224) x 3, q(.md)"
+ ) &&
+ test "$(printf "%s" "$long_utf8_name" | wc -c | tr -d " ")" = 261 &&
+ printf "content\n" >"$long_utf8_name" &&
+ git status --porcelain=v1 >actual
+ )
+'
+
test_expect_failure 'handle existing decomposed filenames' '
echo content >"verbatim.$Adiarnfd" &&
git -c core.precomposeunicode=false add "verbatim.$Adiarnfd" &&
base-commit: e9019fcafe0040228b8631c30f97ae1adb61bcdc
--
2.54.0
^ permalink raw reply related
* Re: [PATCH v2 05/12] t/unit-tests: introduce test helper to write reftable blocks
From: Junio C Hamano @ 2026-07-03 2:52 UTC (permalink / raw)
To: Christian Couder; +Cc: Patrick Steinhardt, git, oxsignal, Christian Couder
In-Reply-To: <CAP8UFD3d4e_OOQrNUXU5iVavwhuCZfiNUuE-hH=hwV84xN+pEg@mail.gmail.com>
Christian Couder <christian.couder@gmail.com> writes:
> On Mon, Jun 29, 2026 at 11:02 AM Patrick Steinhardt <ps@pks.im> wrote:
>> ...
>> +static int cl_reftable_write_block(struct reftable_buf *buf,
>> + uint8_t block_type,
>> + struct reftable_record *recs,
>> + size_t nrecs)
>
> Yeah, I suggested:
>
> int cl_reftable_write_block(struct reftable_buf *buf, uint8_t block_type,
> size_t block_size, uint32_t header_off,
> struct reftable_record *recs, size_t nrecs)
>
> which accepts `size_t block_size` and `uint32_t header_off` as
> arguments, so that more existing tests could be refactored using
> cl_reftable_write_block().
>
> Your choice to not have these extra arguments is reasonable though, as
> they are not needed for the code that your series adds, and they make
> the implementation of cl_reftable_write_block() a bit more complex.
>
> Also they can still be added in the future if we really want to clean
> up more existing tests.
>
> This version of your series looks good to me now.
Thanks, both.
^ permalink raw reply
* Re: [PATCH v5 00/10] commit-reach: terminate merge-base walk when one side is exhausted
From: Junio C Hamano @ 2026-07-03 2:54 UTC (permalink / raw)
To: Kristofer Karlsson; +Cc: Kristofer Karlsson via GitGitGadget, git
In-Reply-To: <CAL71e4PgcZDK-gJziJa_yjEqX9TE+PFMwZn0xbjAUzuUDDDBYA@mail.gmail.com>
Kristofer Karlsson <krka@spotify.com> writes:
> In the meantime, there are still some aspects of this v5 that would
> benefit from some discussion and feedback -- specifically the new
> test diagnostic helper (patch 2) and the commit-date ordering
> fallback removal (patch 10). Both are new in this version and could
> be seen as optional.
Sure, review comment on this iterations are welcome, of course, but
I'll punt on integrating it in 'seen'.
Thanks.
^ permalink raw reply
* [PATCH v6 0/2] includeIf: add "worktree" condition for matching working tree path
From: Chen Linxuan via B4 Relay @ 2026-07-03 3:13 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, Junio C Hamano, Patrick Steinhardt,
Chen Linxuan, Phillip Wood
The `includeIf` mechanism already supports matching on the `.git`
directory path (`gitdir`) and the currently checked out branch
(`onbranch`). But in multi-worktree setups the `.git` directory of a
linked worktree points into the main repository's `.git/worktrees/`
area, which makes `gitdir` patterns cumbersome when one wants to
include config based on the working tree's checkout path instead.
Introduce two new condition keywords:
- `worktree:<pattern>` matches the realpath of the current worktree's
working directory against a glob pattern.
- `worktree/i:<pattern>` is the case-insensitive variant.
Supported pattern features: glob wildcards, `**/` and `/**`, `~`
expansion, `./` relative paths, and trailing-`/` prefix matching.
The condition never matches in a bare repository.
Signed-off-by: Chen Linxuan <me@black-desk.cn>
---
Changes in v6:
- Rebase onto current `master` at Git 2.55.
- Add an in-code comment explaining why the non-repository worktree
tests use the loose `**.path` pattern (suggested by Junio C Hamano).
- Link to v5: https://lore.kernel.org/r/20260525-includeif-worktree-v5-0-1efe525d025a@black-desk.cn
Changes in v5:
- Fix Windows CI failure: use `**` glob pattern instead of `/` in the
"worktree without repository" tests, since `/` as a path pattern is
Unix-specific and does not match Windows paths.
Github CI pass: https://github.com/black-desk/git/actions/runs/26380466288
- Add a test verifying case-sensitive matching by default, with the
`!CASE_INSENSITIVE_FS` prerequisite (suggested by Patrick Steinhardt).
- Link to v4: https://lore.kernel.org/r/20260513-includeif-worktree-v4-0-f8e6212d1fba@black-desk.cn
Changes in v4:
- Deduplicate the worktree pattern documentation by referencing the
gitdir syntax instead of repeating the full pattern description
(suggested by Patrick Steinhardt).
- Add documentation comparing includeIf "worktree:" with
extensions.worktreeConfig, including a concrete use case example
(suggested by Phillip Wood, Junio C Hamano).
- Add a test verifying that the worktree condition does not match
during early config reading (suggested by Patrick Steinhardt).
- Add tests for the non-repository (nongit) scenario (suggested by
Patrick Steinhardt).
- Add a test for the case-insensitive "worktree/i" variant
- Link to v3: https://lore.kernel.org/r/20260403-includeif-worktree-v3-0-109ce5782b03@black-desk.cn
Changes in v3:
- Apply Junio's suggestion.
- Link to v2: https://lore.kernel.org/r/20260402-includeif-worktree-v2-0-36e339b898d7@black-desk.cn
Changes in v2:
- Add missing signed-off-by lines.
- Link to v1: https://lore.kernel.org/r/20260401-includeif-worktree-v1-0-906db69f2c79@black-desk.cn
---
Chen Linxuan (2):
config: refactor include_by_gitdir() into include_by_path()
config: add "worktree" and "worktree/i" includeIf conditions
Documentation/config.adoc | 48 +++++++++++++++++
config.c | 25 +++++----
t/t1305-config-include.sh | 128 ++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 190 insertions(+), 11 deletions(-)
---
base-commit: e9019fcafe0040228b8631c30f97ae1adb61bcdc
change-id: 20260401-includeif-worktree-fcb64950dfba
Best regards,
--
Chen Linxuan <me@black-desk.cn>
^ permalink raw reply
* [PATCH v6 1/2] config: refactor include_by_gitdir() into include_by_path()
From: Chen Linxuan via B4 Relay @ 2026-07-03 3:13 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, Junio C Hamano, Patrick Steinhardt,
Chen Linxuan, Phillip Wood
In-Reply-To: <20260703-includeif-worktree-v6-0-a13893ad9a7f@black-desk.cn>
From: Chen Linxuan <me@black-desk.cn>
The include_by_gitdir() function matches the realpath of a given
path against a glob pattern, but its interface is tightly coupled to
the gitdir condition: it takes a struct config_options *opts and
extracts opts->git_dir internally.
Refactor it into a more generic include_by_path() helper that takes
a const char *path parameter directly, and update the gitdir and
gitdir/i callers to pass opts->git_dir explicitly. No behavior
change, just preparing for the addition of a new worktree condition
that will reuse the same path-matching logic with a different path.
Signed-off-by: Chen Linxuan <me@black-desk.cn>
---
config.c | 19 ++++++++-----------
1 file changed, 8 insertions(+), 11 deletions(-)
diff --git a/config.c b/config.c
index 6a0de86e3ae9..00eeeea370c9 100644
--- a/config.c
+++ b/config.c
@@ -235,23 +235,20 @@ static int prepare_include_condition_pattern(const struct key_value_info *kvi,
return 0;
}
-static int include_by_gitdir(const struct key_value_info *kvi,
- const struct config_options *opts,
- const char *cond, size_t cond_len, int icase)
+static int include_by_path(const struct key_value_info *kvi,
+ const char *path,
+ const char *cond, size_t cond_len, int icase)
{
struct strbuf text = STRBUF_INIT;
struct strbuf pattern = STRBUF_INIT;
size_t prefix;
int ret = 0;
- const char *git_dir;
int already_tried_absolute = 0;
- if (opts->git_dir)
- git_dir = opts->git_dir;
- else
+ if (!path)
goto done;
- strbuf_realpath(&text, git_dir, 1);
+ strbuf_realpath(&text, path, 1);
strbuf_add(&pattern, cond, cond_len);
ret = prepare_include_condition_pattern(kvi, &pattern, &prefix);
if (ret < 0)
@@ -284,7 +281,7 @@ static int include_by_gitdir(const struct key_value_info *kvi,
* which'll do the right thing
*/
strbuf_reset(&text);
- strbuf_add_absolute_path(&text, git_dir);
+ strbuf_add_absolute_path(&text, path);
already_tried_absolute = 1;
goto again;
}
@@ -400,9 +397,9 @@ static int include_condition_is_true(const struct key_value_info *kvi,
const struct config_options *opts = inc->opts;
if (skip_prefix_mem(cond, cond_len, "gitdir:", &cond, &cond_len))
- return include_by_gitdir(kvi, opts, cond, cond_len, 0);
+ return include_by_path(kvi, opts->git_dir, cond, cond_len, 0);
else if (skip_prefix_mem(cond, cond_len, "gitdir/i:", &cond, &cond_len))
- return include_by_gitdir(kvi, opts, cond, cond_len, 1);
+ return include_by_path(kvi, opts->git_dir, cond, cond_len, 1);
else if (skip_prefix_mem(cond, cond_len, "onbranch:", &cond, &cond_len))
return include_by_branch(inc, cond, cond_len);
else if (skip_prefix_mem(cond, cond_len, "hasconfig:remote.*.url:", &cond,
--
2.53.0
^ permalink raw reply related
* [PATCH v6 2/2] config: add "worktree" and "worktree/i" includeIf conditions
From: Chen Linxuan via B4 Relay @ 2026-07-03 3:13 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, Junio C Hamano, Patrick Steinhardt,
Chen Linxuan, Phillip Wood
In-Reply-To: <20260703-includeif-worktree-v6-0-a13893ad9a7f@black-desk.cn>
From: Chen Linxuan <me@black-desk.cn>
The includeIf mechanism already supports matching on the .git
directory path (gitdir) and the currently checked out branch
(onbranch). But in multi-worktree setups the .git directory of a
linked worktree points into the main repository's .git/worktrees/
area, which makes gitdir patterns cumbersome when one wants to
include config based on the working tree's checkout path instead.
Introduce two new condition keywords:
- worktree:<pattern> matches the realpath of the current worktree's
working directory (i.e. repo_get_work_tree()) against a glob
pattern. This is the path returned by git rev-parse
--show-toplevel.
- worktree/i:<pattern> is the case-insensitive variant.
The implementation reuses the include_by_path() helper introduced in
the previous commit, passing the worktree path in place of the
gitdir. The condition never matches in bare repositories (where
there is no worktree) or during early config reading (where no
repository is available).
Add documentation describing the new conditions, including a comparison
with extensions.worktreeConfig. Add tests covering bare repositories,
multiple worktrees, symlinked worktree paths, case-sensitive and
case-insensitive matching, early config reading, and non-repository
scenarios.
Signed-off-by: Chen Linxuan <me@black-desk.cn>
---
Documentation/config.adoc | 48 +++++++++++++++++
config.c | 6 +++
t/t1305-config-include.sh | 128 ++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 182 insertions(+)
diff --git a/Documentation/config.adoc b/Documentation/config.adoc
index 15b1a4d59347..c153da986e4a 100644
--- a/Documentation/config.adoc
+++ b/Documentation/config.adoc
@@ -146,6 +146,46 @@ refer to linkgit:gitignore[5] for details. For convenience:
This is the same as `gitdir` except that matching is done
case-insensitively (e.g. on case-insensitive file systems)
+`worktree`::
+ The data that follows the keyword `worktree` and a colon is used as a
+ glob pattern. If the working directory of the current worktree matches
+ the pattern, the include condition is met.
++
+The worktree location is the path where files are checked out (as returned
+by `git rev-parse --show-toplevel`). This is different from `gitdir`, which
+matches the `.git` directory path. In a linked worktree, the worktree path
+is the directory where that worktree's files are located, not the main
+repository's `.git` directory.
++
+The pattern uses the same glob syntax as `gitdir` (including `~/`, `./`,
+`**/`, and trailing-`/` prefix matching). This condition will never match
+in a bare repository (which has no worktree).
++
+This is useful when you want to apply configuration based on where the
+working tree is located on the filesystem. For example, a contributor who
+works on the same project both personally and as an employee can use
+different `user.name` and `user.email` values depending on which directory
+the worktree is checked out under:
++
+----
+[includeIf "worktree:/home/user/work/"]
+ path = ~/.config/git/work.inc
+[includeIf "worktree:/home/user/personal/"]
+ path = ~/.config/git/personal.inc
+----
++
+While `extensions.worktreeConfig` (see linkgit:git-worktree[1]) also supports
+per-worktree configuration, it stores the config inside each repository's
+`.git/config.worktree` file and requires running `git config --worktree`
+inside each worktree individually. In contrast, `includeIf "worktree:..."`
+can be set once in a global or system-level configuration file (e.g.
+`~/.config/git/config`) and applies to all repositories at once based on
+their worktree location.
+
+`worktree/i`::
+ This is the same as `worktree` except that matching is done
+ case-insensitively (e.g. on case-insensitive file systems)
+
`onbranch`::
The data that follows the keyword `onbranch` and a colon is taken to be a
pattern with standard globbing wildcards and two additional
@@ -244,6 +284,14 @@ Example
[includeIf "gitdir:~/to/group/"]
path = /path/to/foo.inc
+; include if the worktree is at /path/to/project-build
+[includeIf "worktree:/path/to/project-build"]
+ path = build-config.inc
+
+; include for all worktrees inside /path/to/group
+[includeIf "worktree:/path/to/group/"]
+ path = group-config.inc
+
; relative paths are always relative to the including
; file (if the condition is true); their location is not
; affected by the condition
diff --git a/config.c b/config.c
index 00eeeea370c9..9d6d7872d76c 100644
--- a/config.c
+++ b/config.c
@@ -400,6 +400,12 @@ static int include_condition_is_true(const struct key_value_info *kvi,
return include_by_path(kvi, opts->git_dir, cond, cond_len, 0);
else if (skip_prefix_mem(cond, cond_len, "gitdir/i:", &cond, &cond_len))
return include_by_path(kvi, opts->git_dir, cond, cond_len, 1);
+ else if (skip_prefix_mem(cond, cond_len, "worktree:", &cond, &cond_len))
+ return include_by_path(kvi, inc->repo ? repo_get_work_tree(inc->repo) : NULL,
+ cond, cond_len, 0);
+ else if (skip_prefix_mem(cond, cond_len, "worktree/i:", &cond, &cond_len))
+ return include_by_path(kvi, inc->repo ? repo_get_work_tree(inc->repo) : NULL,
+ cond, cond_len, 1);
else if (skip_prefix_mem(cond, cond_len, "onbranch:", &cond, &cond_len))
return include_by_branch(inc, cond, cond_len);
else if (skip_prefix_mem(cond, cond_len, "hasconfig:remote.*.url:", &cond,
diff --git a/t/t1305-config-include.sh b/t/t1305-config-include.sh
index f3892578e4ff..4e840dfdb35b 100755
--- a/t/t1305-config-include.sh
+++ b/t/t1305-config-include.sh
@@ -396,4 +396,132 @@ test_expect_success 'onbranch without repository but explicit nonexistent Git di
test_must_fail nongit git --git-dir=nonexistent config get foo.bar
'
+# worktree: conditional include tests
+
+test_expect_success 'conditional include, worktree bare repo' '
+ git init --bare wt-bare &&
+ (
+ cd wt-bare &&
+ echo "[includeIf \"worktree:/\"]path=bar-bare" >>config &&
+ echo "[test]wtbare=1" >bar-bare &&
+ test_must_fail git config test.wtbare
+ )
+'
+
+test_expect_success 'conditional include, worktree multiple worktrees' '
+ git init wt-multi &&
+ (
+ cd wt-multi &&
+ test_commit initial &&
+ git worktree add -b linked-branch ../wt-linked HEAD &&
+ git worktree add -b prefix-branch ../wt-prefix/linked HEAD
+ ) &&
+ wt_main="$(cd wt-multi && pwd)" &&
+ wt_linked="$(cd wt-linked && pwd)" &&
+ wt_prefix_parent="$(cd wt-prefix && pwd)" &&
+ cat >>wt-multi/.git/config <<-EOF &&
+ [includeIf "worktree:$wt_main"]
+ path = main-config
+ [includeIf "worktree:$wt_linked"]
+ path = linked-config
+ [includeIf "worktree:$wt_prefix_parent/"]
+ path = prefix-config
+ EOF
+ echo "[test]mainvar=main" >wt-multi/.git/main-config &&
+ echo "[test]linkedvar=linked" >wt-multi/.git/linked-config &&
+ echo "[test]prefixvar=prefix" >wt-multi/.git/prefix-config &&
+ echo main >expect &&
+ git -C wt-multi config test.mainvar >actual &&
+ test_cmp expect actual &&
+ test_must_fail git -C wt-multi config test.linkedvar &&
+ test_must_fail git -C wt-multi config test.prefixvar &&
+ echo linked >expect &&
+ git -C wt-linked config test.linkedvar >actual &&
+ test_cmp expect actual &&
+ test_must_fail git -C wt-linked config test.mainvar &&
+ test_must_fail git -C wt-linked config test.prefixvar &&
+ echo prefix >expect &&
+ git -C wt-prefix/linked config test.prefixvar >actual &&
+ test_cmp expect actual &&
+ test_must_fail git -C wt-prefix/linked config test.mainvar &&
+ test_must_fail git -C wt-prefix/linked config test.linkedvar
+'
+
+test_expect_success SYMLINKS 'conditional include, worktree resolves symlinks' '
+ mkdir real-wt &&
+ ln -s real-wt link-wt &&
+ git init link-wt/repo &&
+ (
+ cd link-wt/repo &&
+ # repo->worktree resolves symlinks, so use real path in pattern
+ echo "[includeIf \"worktree:**/real-wt/repo\"]path=bar-link" >>.git/config &&
+ echo "[test]wtlink=2" >.git/bar-link &&
+ echo 2 >expect &&
+ git config test.wtlink >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success !CASE_INSENSITIVE_FS 'conditional include, worktree, case sensitive' '
+ git init wt-case &&
+ (
+ cd wt-case &&
+ test_commit initial &&
+ wt_path="$(pwd)" &&
+ wt_upper=$(echo "$wt_path" | tr a-z A-Z) &&
+ echo "[includeIf \"worktree:$wt_upper\"]path=case-inc" >>.git/config &&
+ echo "[test]wtcase=1" >.git/case-inc &&
+ test_must_fail git config test.wtcase
+ )
+'
+
+test_expect_success 'conditional include, worktree, icase' '
+ git init wt-icase &&
+ (
+ cd wt-icase &&
+ test_commit initial &&
+ wt_path="$(pwd)" &&
+ wt_upper=$(echo "$wt_path" | tr a-z A-Z) &&
+ echo "[includeIf \"worktree/i:$wt_upper\"]path=icase-inc" >>.git/config &&
+ echo "[test]wticase=1" >.git/icase-inc &&
+ echo 1 >expect &&
+ git config test.wticase >actual &&
+ test_cmp expect actual
+ )
+'
+
+# The "worktree" condition cannot match during early config reading
+# because the repository object is not yet fully initialized and
+# repo_get_work_tree() returns NULL.
+test_expect_success 'conditional include, worktree does not match in early config' '
+ git init wt-early &&
+ (
+ cd wt-early &&
+ test_commit initial &&
+ wt_path="$(pwd)" &&
+ echo "[includeIf \"worktree:$wt_path\"]path=early-inc" >>.git/config &&
+ echo "[test]wtearly=1" >.git/early-inc &&
+ test-tool config read_early_config test.wtearly >actual &&
+ test_must_be_empty actual
+ )
+'
+
+# Use a loose pattern so the "present in non-worktree cases" check works
+# for Unix-style absolute paths and Windows paths like D:/a/git/...
+test_expect_success 'conditional include, worktree without repository' '
+ test_when_finished "rm -f .gitconfig config.inc" &&
+ git config set -f .gitconfig "includeIf.worktree:**.path" config.inc &&
+ git config set -f config.inc foo.bar baz &&
+ git config get foo.bar &&
+ test_must_fail nongit git config get foo.bar
+'
+
+test_expect_success 'conditional include, worktree without repository but explicit nonexistent Git directory' '
+ test_when_finished "rm -f .gitconfig config.inc" &&
+ git config set -f .gitconfig "includeIf.worktree:**.path" config.inc &&
+ git config set -f config.inc foo.bar baz &&
+ git config get foo.bar &&
+ test_must_fail nongit git --git-dir=nonexistent config get foo.bar
+'
+
test_done
--
2.53.0
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox