# Worktree

> Linked worktrees, ignore rules, attributes, and pathspec helpers under the worktree domain.

Working-tree behavior lives under [`worktree`](https://docs.rs/grit-lib/latest/grit_lib/worktree/index.html): ignore/attributes (`worktree::ignore`, `worktree::attributes`), pathspecs (`worktree::pathspec`), and checkout helpers. Porcelain commands (`status`, `add`, `checkout`, …) compose these modules with the index and ODB.

## Reftable interoperability

Linked worktree creation and branch switching use the repository's selected ref backend.
In a reftable repository, each worktree has its own `reftable/tables.list` stack for
`HEAD`, while branch tips remain in the shared stack. The admin directory's `HEAD`
file is a discovery placeholder; the symbolic branch or detached commit is stored
in reftable. Worktrees created by either grit or system Git can be reopened and
switched by the other tool without converting ref storage.

`AddLinkedWorktreeOptions::no_checkout` creates the same ref metadata while leaving
the index and tracked files absent. Git can resolve that worktree's `HEAD` immediately;
its status shows the missing tracked files until checkout materializes them.

### Add and switch benchmark

Local Linux x86_64 measurements against system Git 2.55.0, with release-profile
Grit builds before and after the HEAD fix. Each repository has 2,000 files, 2,000
refs and 31 commits. Add creates a detached worktree; switch alternates attached
branches across distinct commits in a Git-created linked worktree. Reftable stacks
are compacted before each build's run. Values are medians of 15 runs after two
warmups: wall milliseconds / peak RSS in KiB (`/usr/bin/time -v`). Compaction
compares `grit gc pack-refs` with `git pack-refs --all`.

| Operation | Git | Grit before | Grit after |
| --- | ---: | ---: | ---: |
| Files: add | 95.34 / 5,964 | 93.81 / 15,812 | 92.55 / 17,028 |
| Files: switch | 6.83 / 6,048 | 7.75 / 15,108 | 7.28 / 16,364 |
| Reftable: add | 98.57 / 5,980 | 93.51 / 16,224 | 94.87 / 17,192 |
| Reftable: switch | 7.69 / 6,332 | 12.18 / 15,672 | 11.89 / 16,932 |
| Reftable: compact | 2.33 / 4,540 | 9.57 / 13,048 | 9.41 / 13,384 |

The original reftable add/switch timings produced incorrect Git-visible HEAD
state. Correct initialization writes a per-worktree table, adding about 1.4 ms to
add in this fixture; switch and compaction are slightly faster. The release build's
measured peak RSS rises by about 1.0–1.2 MiB for add/switch and 0.3 MiB for compaction.

## Matching paths against pathspecs

To filter a path list by user-typed pathspecs, resolve each one with [`Repository::resolve_worktree_pathspec`](https://docs.rs/grit-lib/latest/grit_lib/repo/struct.Repository.html#method.resolve_worktree_pathspec), then build a [`PathspecMatcher`](https://docs.rs/grit-lib/latest/grit_lib/pathspec/struct.PathspecMatcher.html) once with [`Repository::pathspec_matcher`](https://docs.rs/grit-lib/latest/grit_lib/repo/struct.Repository.html#method.pathspec_matcher) and ask it about each path. It applies Git's list rules (any positive spec, no `:(exclude)` spec) and every magic word, including `:(attr:...)`. Attributes come from the repository's own `.gitattributes` stack (or `info/attributes` in a bare repository). `[attr]` macros are expanded, and `core.ignorecase` makes attribute patterns case-insensitive, as in Git. Attributes are only loaded when a spec asks for them, once per matcher rather than once per path. Building the matcher returns a `Result`: a malformed `attr:` clause, such as an empty `:(attr:)` or two `attr:` clauses in one spec, fails with [`Error::InvalidAttrPathspec`](https://docs.rs/grit-lib/latest/grit_lib/error/enum.Error.html#variant.InvalidAttrPathspec) carrying an [`AttrPathspecError`](https://docs.rs/grit-lib/latest/grit_lib/error/enum.AttrPathspecError.html). `matches_change` also checks the old path of a rename. [`Repository::paths_match_pathspecs`](https://docs.rs/grit-lib/latest/grit_lib/repo/struct.Repository.html#method.paths_match_pathspecs) is the one-path shorthand. The free functions in `worktree::pathspec` (`matches_pathspec_list` and friends) have no attributes, so they never match a positive `:(attr:...)` spec. Use [`pathspec_has_attr_magic`](https://docs.rs/grit-lib/latest/grit_lib/pathspec/fn.pathspec_has_attr_magic.html) to detect those specs, or pass your own rules to [`matches_pathspec_list_for_object`](https://docs.rs/grit-lib/latest/grit_lib/pathspec/fn.matches_pathspec_list_for_object.html).

## Ignore rules

[`IgnoreMatcher::from_repository`](https://docs.rs/grit-lib/latest/grit_lib/worktree/ignore/struct.IgnoreMatcher.html#method.from_repository) loads `.git/info/exclude` and `core.excludesfile`, then reads each directory's `.gitignore` the first time a path under it is queried. It applies Git's precedence: a nested `.gitignore` beats its parents, and the last matching line wins. [`is_path_ignored`](https://docs.rs/grit-lib/latest/grit_lib/worktree/ignore/struct.IgnoreMatcher.html#method.is_path_ignored) answers yes or no for hot walks, and [`check_path`](https://docs.rs/grit-lib/latest/grit_lib/worktree/ignore/struct.IgnoreMatcher.html#method.check_path) also returns the deciding pattern. Status, staging, auto-stage commits and clean all use the same matcher. With `core.ignorecase=true`, patterns and nested `.gitignore` directories match without regard to ASCII case, as in Git: `Build/` ignores `build/` and `*.LOG` ignores `debug.log`. [`with_ignore_case`](https://docs.rs/grit-lib/latest/grit_lib/worktree/ignore/struct.IgnoreMatcher.html#method.with_ignore_case) overrides the setting. Tracked paths are also found without regard to case there, so status, clean and staging never treat `build/` as ignored while `Build/t` is tracked. The matcher's own tracked-path exemption stays exact-case, like `git check-ignore`.

## Quoted attribute patterns

Double-quoted patterns in `.gitattributes` accept C-style escapes for tabs, newlines,
quotes, backslashes and other control characters, plus three-digit octal byte escapes
(`\000` through `\377`). Octal escapes can encode the bytes of UTF-8 names.
These patterns work when attributes are read from the worktree, index or a commit tree.

## Linked worktrees (handle)

`Repository::worktrees` returns `RepositoryWorktrees`: list main and linked checkouts, add/remove/move/repair/lock linked worktrees with the same on-disk layout Git uses (`worktrees/` admin files, `gitdir` links). [`add_linked_worktree`](https://docs.rs/grit-lib/latest/grit_lib/worktree/fn.add_linked_worktree.html) runs the `post-checkout` hook from the new worktree after a successful checkout (null old id, start commit, flag `1`), matching Git; `--no-checkout` and orphan branches skip it. [`AddLinkedWorktreeOptions`](https://docs.rs/grit-lib/latest/grit_lib/worktree/struct.AddLinkedWorktreeOptions.html) uses [`WorktreeBranch`](https://docs.rs/grit-lib/latest/grit_lib/worktree/enum.WorktreeBranch.html) for branch modes (`Create`, `CreateOrReset`, `Existing`, `Detach`), optional lock-before-checkout, `--no-checkout`, and `--track`. [`move_linked_worktree`](https://docs.rs/grit-lib/latest/grit_lib/worktree/fn.move_linked_worktree.html) and [`repair_worktrees`](https://docs.rs/grit-lib/latest/grit_lib/worktree/fn.repair_worktrees.html) rewrite gitfile links atomically; repairs return [`WorktreeRepair`](https://docs.rs/grit-lib/latest/grit_lib/worktree/struct.WorktreeRepair.html) rows. [`prune_linked_worktrees`](https://docs.rs/grit-lib/latest/grit_lib/worktree/fn.prune_linked_worktrees.html) accepts [`PruneWorktreesOptions`](https://docs.rs/grit-lib/latest/grit_lib/worktree/struct.PruneWorktreesOptions.html) and returns [`PrunedWorktreeEntry`](https://docs.rs/grit-lib/latest/grit_lib/worktree/struct.PrunedWorktreeEntry.html) rows with a reason string.

Add, list and remove also work on a repository opened with an injected ref store or object store ([`RepositoryBuilder`](https://docs.rs/grit-lib/latest/grit_lib/repo/struct.RepositoryBuilder.html) or `with_ref_store`). Branches are created, read and rolled back in the injected store. The linked worktree's own `HEAD` lives in its admin directory (`worktrees/<id>/HEAD`), because the store holds only the main worktree's `HEAD`. Its branch tip is still resolved through the store, so `WorktreeEntry::head` matches what a files repository reports. Bareness comes from [`Repository::is_bare`](https://docs.rs/grit-lib/latest/grit_lib/repo/struct.Repository.html#method.is_bare), so a git directory kept outside the work tree is not mistaken for a bare repository.

To work in a linked worktree on the same injected stores, call [`Repository::open_linked_worktree`](https://docs.rs/grit-lib/latest/grit_lib/repo/struct.Repository.html#method.open_linked_worktree) on the main handle with the entry's `admin_dir` and path, or open the admin directory with `RepositoryBuilder::open(<git dir>/worktrees/<id>, Some(<linked path>))` and the same stores. Either handle reads and writes the worktree's own `HEAD` (`worktrees/<id>/HEAD`) and `HEAD` reflog (`worktrees/<id>/logs/HEAD`); every other ref and reflog stays in the injected store. A commit there moves the branch that file names, a detach or switch rewrites only that file, and the main worktree's branch, `HEAD` and `HEAD` reflog are left alone, as in a files repository. `with_ref_store` on a linked handle keeps the same split.

Refusals are typed. Worktree failures arrive as [`Error::Worktree`](https://docs.rs/grit-lib/latest/grit_lib/error/enum.Error.html#variant.Worktree) carrying a [`WorktreeError`](https://docs.rs/grit-lib/latest/grit_lib/worktree/enum.WorktreeError.html): `NotRegistered`, `MainWorktree` (with the refused [`WorktreeOperation`](https://docs.rs/grit-lib/latest/grit_lib/worktree/enum.WorktreeOperation.html)), `Locked`, `AlreadyLocked`, `NotLocked`, `HasLocalChanges`, `HasSubmodules`, `PathRegistered`, `PathRegisteredMissing`, `PathExists`, `BareRepository` and so on. Branch problems during add use the same [`BranchError`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/branch_error/enum.BranchError.html) variants as branch creation (`InvalidName`, `NotFound`, `AlreadyExists`). A branch that is already checked out in another worktree is reported as `Error::BranchCheckedOutInWorktree`, and a start point that is not a commit as `Error::UnexpectedObjectKind`.

## Example

This program lists every worktree path grit sees for an open repository:

```rust
//! List linked worktrees and match Git's worktree admin output.
//!
//! Source for the library guide "Worktree" page (included in the docs site).

use grit_lib::config::environment::RepositoryOptions;
use grit_lib::repo::Repository;
use std::path::{Path, PathBuf};

fn open_repo(root: &Path) -> Result<Repository, grit_lib::error::Error> {
    let git_dir = if root.join(".git").is_dir() {
        root.join(".git")
    } else {
        root.to_path_buf()
    };
    let work_tree = if root.join(".git").is_dir() {
        Some(root)
    } else {
        None
    };
    let mut opts = RepositoryOptions::from_process();
    if let Some(wt) = work_tree {
        opts = opts.with_work_tree(wt);
    }
    Repository::open(&git_dir, &opts)
}

fn main() -> Result<(), grit_lib::error::Error> {
    let root = std::env::args()
        .nth(1)
        .map(PathBuf::from)
        .or_else(|| std::env::current_dir().ok())
        .ok_or_else(|| grit_lib::error::Error::Message("missing repository path".into()))?;
    let repo = open_repo(&root)?;

    let rows = repo.worktrees().list()?;
    println!("worktree_count={}", rows.len());
    for entry in rows {
        println!("worktree {}", entry.path.display());
    }

    Ok(())
}
```

Create a linked worktree with Git, then compare counts:

```bash
git worktree add ../linked -b feature
cargo run --bin guide_worktree .
git worktree list
```

The integration test adds a linked worktree with system Git and compares `worktree_count` to `git worktree list --porcelain`.
