Bug Fixes (BREAKING)
-
preserve sparse-checkout state when adding worktrees
Looked at everything carefully, but was despite test refactoring, I went
through those more quickly. The benchmark I only skimmed.Adding a linked worktree from a sparse checkout copied its configuration
without its patterns, then materialized the entire target tree. Copy active
info/sparse-checkoutpatterns from the source worktree's private Git directory
during preparation, including when checkout is deferred. Missing patterns are
skipped, empty files are retained, and copy failures roll back registration.Expose pattern loading and full-index selection through
gix_worktree::sparse.
Initial checkout ingixreads the destination's current patterns and marks
excluded target-tree entries withSKIP_WORKTREEandEXTENDEDbefore writing
files and persisting the index. Support cone and non-cone patterns, case
folding, invalid-cone fallback, root entries, and directory pruning. Sparse
index generation and updates to existing checkouts remain outside this API.Reuse matching decisions for shared directory ancestors with storage bounded
by path depth, and skip index traversal for a validated full-cone selection.
Cover pattern matching with 18 Git-generated baseline scenarios, regression
tests for cache boundaries and ordering, and integration tests for main and
linked sources, deferred checkout, missing or disabled patterns, and rollback.
Add Criterion benchmarks for cone and non-cone selection on synthetic indices
with 20,000 to 200,000 entries, checking membership and flags before timing.Breaking change:
gix_worktree::add::prepare()now accepts the destination
first, followed by anadd::Contextcontaining the common Git directory and
an optional source private Git directory, then its existing options. Use
add::Context::new(common_dir)when pattern inheritance is not wanted. -
make bounded removal retries configurable
Went through the tests pretty quickly, as is increasingly common.
Expose
Options::retry_delayalongsidemax_retries. The default is 10 ms;
Duration::ZEROretries immediately, and callers can choose a longer delay for
longer (expected) write bursts. Only retryableDirectoryNotEmptyerrors with
attempts remaining wait, before restarting traversal. Successful first-pass
removal, zero-retry operation, and terminal failures incur no pause. The default
two retries request at most 20 ms of sleep per root; filesystem operations and
scheduler delays have no wall-clock bound.Remove the shrinking-entry-count guard. A writer can replace every deleted file
with a new one, leaving the scan count unchanged even though deletion succeeds
for every scanned entry. The old guard then stops before the configured retry
allowance is used. A deterministic regression writes one replacement after each
of the first two scans: the old loop fails on the second pass; the corrected
loop succeeds on the third, even with a zero delay. The retry count bounds
shrinking, equal, and growing workloads without trying to infer writer progress.Deletion duration or entry counts cannot tell when an independent writer will
finish. Keep the delay an explicit caller policy instead of introducing another
heuristic tied to tree size. No finite retry policy guarantees completion for
arbitrary finite or indefinite writers. Existing porcelainTarget::options()
and the re-exportedOptionsexpose this setting without another configuration
layer. Downstream exhaustiveOptionsliterals must addretry_delayor use
..Default::default().
Performance
-
cap automatic macOS removal at four workers
This is an APFS problem, which is also noticeable when scanning through
metadata, which now caps at 8 at least indua.On Linux Ext4 no such limits exist for all I know, so this commit is
a macOS specific fix to ensure decent performance.Resolve automatic removal concurrency to at most four available logical CPUs
on macOS.NoneandSome(0)stay automatic; positive explicit limits remain
unchanged, as do defaults on other platforms. Apply the cap in the shared
plumbing options so repository, proxy, and CLI removal inherit it.Sixteen concurrent unlink workers contend on native APFS. A dedicated comparison
of three and four workers favors four in all 12 matched pairs, including trees
created by Git, gix, and APFS cloning. Median wall seconds (range), four samples
per cell:Creator 3 workers 4 workers Less elapsed time Git 2.013 (1.878–2.059) 1.842 (1.820–1.856) 8.49% gix 2.873 (2.849–2.890) 2.597 (2.577–2.673) 9.59% APFS CoW 1.577 (1.571–1.594) 1.429 (1.420–1.432) 9.42% Median per-round 3/4 time ratios are 1.087×, 1.101×, and 1.107× respectively.
One additional single-worker control per creator took 3.258, 3.911, and 2.781 s:
the four-worker medians are 1.77×, 1.51×, and 1.95× faster. Those controls ran
last and are diagnostic, not paired repetitions.The preceding native worker sweep measured sixteen-worker medians of 5.018 s
for Git-created and 5.819 s for gix-created trees (two samples each). These are
separate measurements, not additional pairs in this comparison. GitButler's
image-derived one-worker override and the old all-core default both give up
native-volume throughput.Fixture and method
2026-10-09, Apple M4 Max, 16 logical CPUs, 64 GiB RAM, macOS 27.0.1 (26A434),
native APFS under/private/tmp; Rust 1.96.1 release probe and Apple Git 2.54.0.
The cached probe uses local Gitoxide00bb1af, withmax-performance,parallel,
both pack-cache variants, SHA-1, and worktree mutation. Explicit removal thread
limits exercise the same deletion path as the new automatic cap. The retry
pause only applies after an error; no concurrent writes were injected here.Borrow objects read-only from
/Users/byron/dev/git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux, commit
707df3375124b51048233625a7e1c801e8c8a7fd, using independent seed metadata.
The clean seed checks out tree176a057fce1932fe5057196e63c2a636e1a3cdab:
88,821 tracked files / 1,509,611,994 blob bytes. Native case-insensitive APFS
requires omitting thirteen later case-fold collisions ingit ls-tree -r
order: 15,749 bytes, 0.00104% of the source tree.Each of four rounds creates fresh Git, gix, and CoW destinations with both
removal counts; odd rounds reverse all case ordering. Git checkout uses one
worker, gix its default sixteen. CoW registers with Git without checkout,
usesclonefile()for regular files and index, and recreates symlinks. This is
a deletion fixture, not a timing of GitButler's entire CoW creation workflow.
A small byte/symlink self-check validates the fixture copier.Synchronize with
sync_volume_np(SYNC_VOLUME_WAIT)before creation and removal,
outside timers. Validate clean status before forced removal, excluding status
cost from the removal timer. All 27 samples removed both checkout and private
administration. No build or other benchmark overlapped measurements. One
0.24 s read-side inventory overlapped the second-round CoW/3 case; excluding
that pair leaves medians 1.577 vs 1.430 s, with four winning all remaining pairs.These are warm-cache timings on one native APFS machine. No new disk-space,
cold-cache, durable-write, or other-platform measurement was made. The old
sparse-image results are unsuitable for choosing native macOS concurrency.The updated automatic-resolution test failed with 16 instead of 4 before the
change. Removal and bounded late-write regressions pass, including explicit
limits above the cap. Keep results here; discard temporary fixtures and logs.
Bug Fixes
-
classify sparse gitlinks as directories on every host
Non-cone directory-only patterns should treat gitlinks as directories on
every host, including when the destination is initially empty. The original
matcher instead hardcoded them as non-directories, reproducing the result
of Git's little-endiance_to_dtype()bug.Expose
gix_index::entry::Mode::is_dir_for_git_matching()using decoded
modes directly, and document why Git's extrantohl()conversion is wrong.
Use this classification for sparse matching, so/*/includes an absent
gitlink and!/module/excludes it consistently across hosts.Keep Git-generated baselines while working around its conversion bug:
pre-create the empty gitlink directory only for the reference operation,
allowing Git's filesystem fallback to recover its intended classification.
The gix index-only regression and initial checkout still start without it.
Check included gitlinks as directories rather than reading them as files.
Commit Statistics
- 11 commits contributed to the release over the course of 14 calendar days.
- 14 days passed between releases.
- 4 commits were understood as conventional.
- 0 issues like '(#ID)' were seen in commit messages
Commit Details
view details
- Uncategorized
- Merge pull request #3066 from GitoxideLabs/check-commit-graph (7540f69)
- Merge pull request #3058 from GitoxideLabs/worktree-improvements (79f3880)
- Merge pull request #3056 from GitoxideLabs/worktree-sparse-checkout (ca9eb08)
- Classify sparse gitlinks as directories on every host (5bf2f95)
- Preserve sparse-checkout state when adding worktrees (d471c0b)
- Merge pull request #3053 from GitoxideLabs/various-improvements (2d8dcc4)
- Release gix-error v0.5.1, gix-utils v0.5.1 (6d1b686)
- Merge pull request #3052 from GitoxideLabs/various-improvements (2711f6a)
- Cap automatic macOS removal at four workers (a175395)
- Make bounded removal retries configurable (08da4d4)
- Merge pull request #3048 from GitoxideLabs/new-release (5fccccf)