How pi-web Integrates with Git Worktrees for Branch-Based Development
pi-web treats each Git worktree as a separate project while unifying them in the UI through automatic project resolution and session grouping.
pi-web, an open-source development environment maintained in the agegr/pi-web repository, enables seamless branch-based development by leveraging Git worktrees to isolate branches while maintaining a unified interface. The application automatically detects worktree relationships through Git metadata, groups sessions by underlying repository root regardless of which worktree they originated from, and allows dynamic branch switching via a dedicated UI component.
Project Resolution via Git Metadata
At the heart of pi-web’s worktree integration sits resolveProject() in lib/worktree.ts. Whenever a session’s working directory is inspected, this function executes a targeted Git query to discover the common .git directory shared by all worktrees:
git rev-parse --git-common-dir
If the current working directory resides within a linked worktree, resolveProject() marks isWorktree as true, records the current branch name in the branch field, and identifies the main repository root. For directories outside Git repositories, it falls back to the cwd itself. To avoid the overhead of repeated Git spawns, results are cached on globalThis.__piProjectCache for 60 seconds.
Session Grouping by Repository Root
The listAllSessions() function in lib/session-reader.ts loads every session file and invokes resolveProject() once per unique cwd. It attaches the returned ProjectInfo to each SessionInfo object as projectRoot and, when applicable, as worktreeBranch. This metadata allows the sidebar component to group sessions that belong to the same underlying repository, regardless of whether they were created in the main working tree or a linked worktree.
REST API for Worktree Operations
The /api/worktrees endpoint in app/api/worktrees/route.ts exposes three core operations for branch management:
- GET – Returns the repository’s
projectRoot, boolean flags indicating whether the cwd is a Git repo and whether it is the repository top-level, plus a complete list of existing worktrees vialistWorktrees(). - POST – Invokes
addWorktree()to create a new directory under<repoRoot>-worktrees/<sanitized-branch>and immediately registers the path with the file-access allow-list. - DELETE – Calls
removeWorktree()to delete a worktree directory, optionally forcing removal of dirty worktrees with uncommitted changes.
File-Access Gating for Security
Worktree paths are added to the global allow-list through allowFileRoot()—implemented in lib/file-access.ts—both when created via addWorktree() and when enumerated via the GET endpoint. This gating ensures the file explorer can safely read and write files within any worktree without exposing unrelated parts of the host filesystem.
Branch-Aware UI Components
The interface adapts dynamically based on worktree context. The sidebar—rendered by components/SessionSidebar.tsx—displays a worktree switcher only when the current session resides at the repository top-level (project.isTopLevel). This component, along with components/BranchNavigator.tsx, lists each worktree’s path and branch obtained from listWorktrees().
Selecting a different worktree updates the session’s cwd, which automatically triggers resolveProject() to re-resolve the project context for the new branch.
Cache Invalidation on Worktree Changes
Mutating worktree state requires cache synchronization. Adding or removing a worktree invokes invalidateProjectCache(), clearing cached ProjectInfo entries so subsequent resolves reflect the updated repository structure. Additionally, invalidateSessionListCache() clears session-list caches to ensure sidebar grouping remains accurate after worktree modifications.
Code Examples
Resolve the project information for any directory to determine if it lives inside a worktree:
import { resolveProject } from "@/lib/worktree";
const cwd = "/home/alice/myrepo-feature";
const info = await resolveProject(cwd);
// info => { projectRoot: "/home/alice/myrepo", branch: "feature", isWorktree: true, isTopLevel: true }
List all existing worktrees for repository navigation:
import { listWorktrees } from "@/lib/worktree";
const worktrees = await listWorktrees("/home/alice/myrepo");
console.log(worktrees);
/* [
* { path: "/home/alice/myrepo-worktrees/feature", branch: "feature", isMain: true },
* { path: "/home/alice/myrepo-worktrees/bugfix", branch: "bugfix", isMain: false },
* ] */
Create a new worktree for a feature branch via the API:
await fetch("/api/worktrees", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ cwd: "/home/alice/myrepo", branch: "new-feature" })
});
// Returns: { path: "/home/alice/myrepo-worktrees/new-feature", branch: "new-feature" }
Remove a worktree, optionally forcing deletion of dirty trees:
await fetch("/api/worktrees", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
cwd: "/home/alice/myrepo",
path: "/home/alice/myrepo-worktrees/old-feature",
force: true
})
});
// Returns: { success: true }
Summary
- Automatic detection:
resolveProject()inlib/worktree.tsusesgit rev-parseto identify worktree relationships and cache results for 60 seconds. - Unified grouping:
lib/session-reader.tsattachesprojectRootandworktreeBranchto sessions, enabling the sidebar to group sessions by underlying repository. - Secure file access: The
allowFileRoot()mechanism inlib/file-access.tsgates filesystem access, registering worktree paths only when explicitly created or listed. - RESTful management:
app/api/worktrees/route.tsprovides endpoints to create, list, and delete worktrees under<repoRoot>-worktrees/<branch>. - Dynamic UI: Components like
BranchNavigator.tsxexpose branch switching only at the repository top-level, updating session cwd to trigger re-resolution.
Frequently Asked Questions
How does pi-web detect if a directory is a Git worktree?
pi-web detects worktrees through the resolveProject() function in lib/worktree.ts, which executes git rev-parse --git-common-dir to locate the shared .git directory. If the query reveals the cwd is a linked worktree rather than the main repository, the function sets isWorktree to true and extracts the current branch name.
Where does pi-web create new worktrees when adding a branch?
When calling the POST endpoint at /api/worktrees, pi-web’s addWorktree() function creates a new directory under <repoRoot>-worktrees/<sanitized-branch>. This path is immediately registered with allowFileRoot() in lib/file-access.ts to grant the file explorer access.
How does pi-web prevent unauthorized file access when browsing worktrees?
pi-web maintains a global allow-list of permitted file roots via allowFileRoot(). Worktree paths are added to this list only when explicitly created through addWorktree() or enumerated via the GET /api/worktrees endpoint, ensuring the UI cannot access arbitrary filesystem locations outside the repository structure.
Why does the worktree switcher only appear at the repository top-level?
The BranchNavigator.tsx component checks the isTopLevel property returned by resolveProject() before rendering the switcher. This design prevents nested worktree directories from displaying redundant navigation controls, as the root repository context is required to enumerate all available worktrees through listWorktrees().
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →