{"record":{"id":"65d89944613be169","repo":"gitbutlerapp/gitbutler","slug":"worktree-state-must-be-read-from-the-main-worktree","errorCode":null,"errorMessage":"worktree state must be read from the main worktree - a linked-worktree context has its own database, letting adoption and archived state diverge","messagePattern":"worktree state must be read from the main worktree - a linked-worktree context has its own database, letting adoption and archived state diverge","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/but-db/src/worktrees.rs","lineNumber":120,"sourceCode":"/// archived until explicitly unarchived.\n///\n/// Worktrees whose checkout is gone from disk (prunable) are never returned.\n/// Entries are identity only - whether a worktree has a usable `HEAD` (readable,\n/// born, not the workspace ref) is resolved freshly by [`worktree_head()`]\n/// wherever a consumer actually needs it.\n///\n/// Errors when `repo` is itself a linked worktree: such a repository stores its\n/// database in the worktree's private git dir, so adoption and archived state\n/// would silently diverge from the main worktree's database.\npub fn worktrees_with_state(\n    repo: &gix::Repository,\n    db: &mut DbHandle,\n) -> Result<Vec<WorktreeEntry>> {\n    // The `commondir` redirect only exists in linked-worktree git dirs; unlike\n    // `Kind::LinkedWorkTree`, which is a path heuristic requiring a literal\n    // `.git` component, this also catches worktrees of bare repositories.\n    if repo.git_dir() != repo.common_dir() {\n        anyhow::bail!(\n            \"worktree state must be read from the main worktree - \\\n             a linked-worktree context has its own database, letting adoption \\\n             and archived state diverge\"\n        );\n    }\n    let (all_names, mut worktrees) = enumerate_worktrees(repo)?;\n\n    let archived = adopt_and_read_archived(db, &all_names)?;\n\n    for wt in &mut worktrees {\n        wt.archived = archived.contains(&wt.name);\n    }\n    Ok(worktrees)\n}\n\n/// Enumerate the linked worktrees of `repo`, returning the names of ALL of them\n/// (for adoption - a worktree that is unusable today must still be adopted today,\n/// not when it becomes usable) along with the entries whose checkout still exists","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-db/src/worktrees.rs#L102-L138","documentation":"`worktrees_with_state()` refuses repositories where `git_dir() != common_dir()` — i.e. a repository opened inside a linked worktree, whose database (adoption/archived state) lives in the worktree's private git dir. Reading state there would silently diverge from the main worktree's database, so the function bails instead. The check uses the commondir redirect, which also catches worktrees of bare repositories that path heuristics miss.","triggerScenarios":"Passing a `gix::Repository` discovered from inside a linked worktree (`git worktree add`-created) to `worktrees_with_state()` instead of one opened on the main worktree.","commonSituations":"CLI run from a linked-worktree directory; tools that discover 'the repo' from cwd and land in `.../wt-1/.git` file pointing at the private git dir; tests using worktree fixtures.","solutions":["Open the repository from the main worktree (or the bare repo) instead: resolve `repo.common_dir()` and re-open a repository rooted there before calling.","Change cwd to the main worktree before discovery, or pass an explicit main-worktree path to the CLI.","Guard callers: compare `repo.git_dir()` and `repo.common_dir()` and route to the main worktree before invoking this API."],"exampleFix":"// before\nlet entries = but_db::worktrees::worktrees_with_state(&repo, &mut db)?; // repo from linked worktree\n\n// after\nlet main_git_dir = repo.common_dir().to_path_buf();\nlet main_repo = gix::open(main_git_dir)?;\nlet entries = but_db::worktrees::worktrees_with_state(&main_repo, &mut db)?;","handlingStrategy":"validation","validationCode":"// Rust — ensure the repo handle is the main worktree before reading worktree state\nfn main_worktree_repo(repo: &gix::Repository) -> anyhow::Result<gix::Repository> {\n    if repo.git_dir() == repo.common_dir() {\n        Ok(repo.clone_shallow()) // already main\n    } else {\n        gix::open(repo.common_dir()).map_err(Into::into)\n    }\n}","typeGuard":"fn is_main_worktree(repo: &gix::Repository) -> bool {\n    repo.git_dir() == repo.common_dir()\n}","tryCatchPattern":"let repo = main_worktree_repo(&repo)?; // normalize before the call\nlet entries = but_db::worktrees::worktrees_with_state(&repo, &mut db)?;","preventionTips":["Discover repositories from a configured main-worktree path, not from cwd, in tools that touch worktree state.","In CLIs run from linked worktrees, resolve `common_dir()` and re-open before DB-backed queries.","Unit-test this guard with a `git worktree add` fixture."],"tags":["worktree","linked-worktree","database","precondition"],"backgroundTag":"linked-worktree-context","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}