Skip to content

Repository files navigation

git-sprout

Stop paying for the same tree twice.
A drop-in replacement for git worktree add that doesn't copy your tree.

Website · Sponsor

crates.io License Stars


Note

Checked on every commit. The contract below is compared against real git worktree add across 26 repository fixtures and 41 argument shapes, on macOS, Linux (btrfs, XFS, ext4) and Windows (NTFS, ReFS), plus the Linux kernel on two filesystems. The suite proves itself by detecting nineteen deliberately injected differences and finding none when git is compared against itself. The kernel figures are measured on the released binary; the smaller benchmark rows are still the research prototype's.

Get Started

brew install alltuner/tap/git-sprout

Or cargo install git-sprout, or a binary from the releases page.

git sprout add ../myrepo-feature -b feature     # or: git worktree-fast add

Two things have to be true. The filesystem needs block cloning: APFS on macOS; btrfs, XFS with reflinks, or bcachefs on Linux; a ReFS volume or a Windows 11 Dev Drive on Windows. Everywhere else, ext4 and NTFS included, it runs plain git worktree add.

And the repository has to be one that does not convert files on checkout, because a file can only be shared when checking it out would not rewrite its bytes. Git for Windows turns core.autocrlf on by default, and with that setting there is nothing to share, so on a typical Windows repository sprout passes straight through to git worktree add. The same goes for * text=auto eol=crlf on any platform. Repositories with no conversion attributes clone everything, which is the common case on macOS and Linux, the kernel included.

What is git-sprout?

Every worktree you create is a second full copy of your repository on disk. git-sprout materialises the new worktree with filesystem copy-on-write clones of a checkout you already have, instead of inflating every blob out of the object store into fresh blocks. The clone shares disk blocks with the source until something writes to them, so the second worktree costs almost nothing until it diverges.

The pitch is disk, not speed. This is not a speed-up: on a small repository it saves under a second, and on the kernel the two commands finish within half a second of each other. The disk saving is close to total at every size, and cost there scales with file count rather than with bytes.

The numbers

workload git worktree add git sprout add
Linux kernel, 95 299 files, 2.0 GB 1816 MB 36 MB
250 MB, 2000 files 0.85s · 251 MB 0.21s · ~0 MB
188 MB, 3000 files, source 6 commits behind 0.83s · 187 MB 0.15s · ~1.5 MB
btrfs, 188 MB 0.33s · 187 MB 0.05s · 0.1 MB
ext4 (no block cloning) 0.41s · 187 MB falls back, identical

One worktree of the Linux kernel: 50x less disk, and no meaningful difference in wall clock. That is 1.78 GB that never gets allocated every time anyone creates one. Ten engineers with five worktrees each is

90 GB of kernel checkouts on git, and about 2 GB on sprout.

The filesystem's own accounting, verbatim:

<!--bench:btrfs.du-->       Total   Exclusive  Set shared  Filename
   187.00MiB       0.00B   187.00MiB  repo/src        <- source
   187.00MiB       0.00B   187.00MiB  wt-sprout/src   <- git sprout add
   187.00MiB   187.00MiB       0.00B  wt-plain/src    <- git worktree add<!--/bench-->

Provisional figures. The kernel row is measured on the implementation itself, on a dedicated APFS image; the smaller rows are still the research prototype's. Machine:

Apple M2, 8 cores, macOS 26.6.1, git 2.55.0; Linux

figures on kernel 7.0.12, git 2.47.3, loopback btrfs and XFS. The harness in bench/ re-measures them and rewrites this table.

Measuring it yourself

One property will otherwise waste your afternoon. A repository that arrives as a copy accelerates nothing until its stat cache is rebuilt. git sprout add clones a file only when git already considers the source's copy unmodified, which it decides from the inode, size and mtime recorded in the index. A cp -r, an rsync, a container image layer, a restored CI cache or an unpacked tarball gives every file a new inode and a fresh mtime, so every entry looks modified even though every byte is identical. The plan comes back empty, the worktree is still correct, and nothing is shared.

Run this once in the source repository first, and the numbers appear:

git update-index --refresh

Nothing else about the tool depends on it — the worktree is correct either way, which is exactly why it is easy to miss. Ask for the counts if you want to be certain a run actually cloned:

SPROUT_STATS=1 git sprout add ../wt -b feature   # cloned=… on stderr

Compatibility

git sprout add is meant to be indistinguishable from git worktree add in every observable way except time, disk, and the two differences named below. That is the contract, and the differential suite in tests/differential/ is being written to prove it.

  • Same flags, same stdout, same exit codes. Same stderr too, apart from the progress meter noted below.
  • Same hooks, in the same order, with the same arguments.
  • Same files, same modes, same index, compared byte for byte against real git worktree add across a matrix that includes eol conversion, ident, custom filters, LFS, submodules, sparse checkout, split index, SHA-256 repositories and case-insensitive filesystems, where the correct answer is the same set of already-modified paths git itself leaves behind rather than a clean worktree.
  • Untracked and ignored files are not copied, exactly as git does not copy them.
  • Your repository's configuration is never modified.
  • On a filesystem without block cloning, or in a repository that converts files on checkout, it simply runs git worktree add.
  • Any flag or combination it does not fully understand is not an error. It hands the whole command to git and exits with git's status.

Two differences you can observe, both deliberate. Files that were cloned keep the timestamp they had in the checkout they came from, rather than the moment the worktree was created. Nothing git does depends on it, but make and anything else that reads modification times can see it.

And on a big repository git worktree add prints a progress meter while it writes the files out. sprout has almost no files left to write, so git never starts one: you see less output because less happened.

Beyond those two, and beyond time and disk, anything you can tell apart is a bug.

Make it automatic

There is no git setting that can redirect git worktree add: git ignores an alias that shadows a builtin, and builtins never go through GIT_EXEC_PATH or PATH. Both were tested. So there are two ways, and both are things you install deliberately.

1. A shell function, for what you type yourself. It only affects the interactive shell, and it matches only worktree add as the first two words.

# bash / zsh — ~/.bashrc, ~/.zshrc
git() {
  if [ "${1:-}" = worktree ] && [ "${2:-}" = add ]; then
    shift 2
    command git sprout add "$@"
  else
    command git "$@"
  fi
}
# fish — ~/.config/fish/config.fish
function git
    if test (count $argv) -ge 2; and test "$argv[1]" = worktree; and test "$argv[2]" = add
        command git sprout add $argv[3..]
    else
        command git $argv
    end
end
# PowerShell — $PROFILE
function git {
    $real = (Get-Command git -CommandType Application | Select-Object -First 1).Source
    if ($args.Count -ge 2 -and $args[0] -eq 'worktree' -and $args[1] -eq 'add') {
        $rest = @($args | Select-Object -Skip 2)
        & $real sprout add @rest
    } else {
        & $real @args
    }
}
# nushell — $nu.config-path
def --wrapped git [...args] {
    if ($args | length) >= 2 and $args.0 == "worktree" and $args.1 == "add" {
        ^git sprout add ...($args | skip 2)
    } else {
        ^git ...$args
    }
}

The bash, zsh, fish and nushell blocks were executed against a real git before release. The PowerShell one was reviewed line by line but has not been run on any machine yet, so treat it as unverified and say so if it misbehaves.

2. A git shim on PATH, for everything else. Editors, worktree managers, CI jobs and agent harnesses spawn git themselves, so a shell function never sees them. The shim is a small git wrapper in its own directory that you put ahead of the real git on PATH; it rewrites worktree add and passes every other command straight through to the real git.

git sprout install-shim      # prints the directory it wrote and the PATH line to add
git sprout uninstall-shim    # removes it

brew install never does this on its own. A wrapper in front of git is yours to opt into, and one command to undo.

Prior art

josharian/git-cow-worktree got here first, in Go. Two of its ideas are in sprout: choosing the source worktree by commit distance, and hashing clones in parallel to write real stat data into the index instead of leaning on core.checkStat. joeinnes/cow takes a looser approach to the same idea.

Development

git clone https://github.com/alltuner/git-sprout.git
cd git-sprout
just            # menu of dev tasks
just build      # build the binaries
just test       # run the workspace tests
just check      # fmt + clippy

License

MIT

Support the project

git-sprout is an open source project built by David Poblador i Garcia through All Tuner Labs.

If this project was useful to you, consider supporting its development.


Built by David Poblador i Garcia with the support of All Tuner Labs.
Made with ❤️ in Poblenou, Barcelona.

About

Drop-in replacement for git worktree add that shares disk blocks instead of copying your tree. On the Linux kernel, 1816 MB becomes 36 MB.

Topics

Resources

Code of conduct

Security policy

Stars

16 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages