github GitoxideLabs/gitoxide gix-worktree-v0.59.0
gix-worktree v0.59.0

7 hours ago

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-checkout patterns 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 in gix reads the destination's current patterns and marks
    excluded target-tree entries with SKIP_WORKTREE and EXTENDED before 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 an add::Context containing 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_delay alongside max_retries. The default is 10 ms;
    Duration::ZERO retries immediately, and callers can choose a longer delay for
    longer (expected) write bursts. Only retryable DirectoryNotEmpty errors 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 porcelain Target::options()
    and the re-exported Options expose this setting without another configuration
    layer. Downstream exhaustive Options literals must add retry_delay or 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 in dua.

    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. None and Some(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 Gitoxide 00bb1af, with max-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 tree 176a057fce1932fe5057196e63c2a636e1a3cdab:
    88,821 tracked files / 1,509,611,994 blob bytes. Native case-insensitive APFS
    requires omitting thirteen later case-fold collisions in git 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,
    uses clonefile() 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-endian ce_to_dtype() bug.

    Expose gix_index::entry::Mode::is_dir_for_git_matching() using decoded
    modes directly, and document why Git's extra ntohl() 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)

Don't miss a new gitoxide release

NewReleases is sending notifications on new releases.