# How pi‑web Handles Git Worktrees and Resolves Shared Project Roots Across Multiple Checkouts

> Discover how pi-web seamlessly manages Git worktrees, resolving shared project roots and unifying multiple checkouts into a single logical project for efficient development.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: internals
- Published: 2026-08-16

---

**pi‑web treats Git worktrees as a single logical project by detecting linked worktrees via `git rev-parse` commands, resolving the shared project root through path manipulation, and grouping all sessions under one project entry regardless of which checkout they originate from.**

When working with Git worktrees, developers often maintain multiple checkouts of the same repository—each on a different branch—in separate directories. Managing these as isolated projects creates fragmentation. The **pi‑web** codebase solves this by unifying worktrees under a shared project root, enabling seamless session grouping and branch switching across checkouts.

## Detecting Linked Worktrees

The foundation of pi‑web's worktree handling is determining whether a directory belongs to a linked worktree versus a standard Git repository or non‑Git folder.

In **[`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts)**, the `isLinkedWorktree(cwd)` function executes:

```bash
git rev-parse --git-common-dir

```

This command returns the path to the main repository's `.git` directory. When the "common" directory points to the main repo's `.git` rather than a local `.git` file or directory, the current working directory is identified as a linked worktree [[source]](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts#L23-L26). The UI uses this distinction to conditionally hide the worktree switcher for plain directories.

## Resolving the Shared Project Root

Once a worktree is detected, pi‑web must locate the **project root**—the canonical directory shared by all worktrees.

The `resolveProject(cwd)` function in [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts) implements this logic:

1. Runs `git rev-parse --show-toplevel` to obtain the top‑level checkout path
2. For linked worktrees, computes the main repo root by stripping the `-worktrees` suffix from the parent directory [[source]](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts#L72-L80)

The function returns a `ProjectInfo` object containing:

| Property | Description |
|----------|-------------|
| `projectRoot` | The shared root directory for all worktrees |
| `isWorktree` | Boolean flag indicating worktree status |
| `branch` | The worktree's branch name (if applicable) |

```typescript
import { resolveProject } from "@/lib/worktree";

const info = await resolveProject("/home/user/my-repo-worktrees/feature-x");
console.log(info);
/*
{
  projectRoot: "/home/user/my-repo",
  isWorktree: true,
  branch: "feature-x"
}
*/

```

## Managing Worktrees Programmatically

The [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts) module provides full CRUD operations for worktree manipulation.

### Listing Worktrees

`listWorktrees(cwd)` executes `git worktree list --porcelain` and parses the structured output into `WorktreeInfo` objects:

```typescript
import { listWorktrees } from "@/lib/worktree";

const cwd = "/home/user/my-repo-worktrees/feature-x";
const worktrees = await listWorktrees(cwd);
// Returns: { path, branch, isMain, isPrunable }[]

```

### Creating Worktrees

`addWorktree(cwd, branch)` creates new worktrees under the convention `<repoRoot>-worktrees/<branch>`:

```typescript
import { addWorktree } from "@/lib/worktree";

const cwd = "/home/user/my-repo";
const { path, branch } = await addWorktree(cwd, "feature-y");
console.log(`Worktree created at ${path} on branch ${branch}`);

```

After creation, `allowFileRoot()` registers the new path so the file‑browser can access it.

### Removing Worktrees

`removeWorktree(cwd, worktreePath, force)` handles deletion with safety guards:

```typescript
import { removeWorktree } from "@/lib/worktree";

await removeWorktree(
  "/home/user/my-repo",               // repository root
  "/home/user/my-repo-worktrees/bugfix",
  true                                // force removal even if dirty
);

```

The function refuses to delete the main worktree and surfaces Git's `--force` requirement for dirty worktrees.

## Session Integration and UI Grouping

Worktree awareness propagates through pi‑web's session system via **[`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts)**. This module enriches session metadata with `worktreeBranch` [[source]](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts#L30-L34), enabling the system to:

- Group all sessions from the same repository under one project entry
- Switch active worktrees via the **Worktrees** sidebar UI
- Re‑associate sessions with the main repository root when their worktree is deleted [[AGENTS.md]](https://github.com/agegr/pi-web/blob/main/AGENTS.md#worktrees-and-project-grouping)

The API endpoint in **[`app/api/worktrees/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/worktrees/route.ts)** exposes these capabilities to the frontend:

```typescript
// GET /api/worktrees?cwd=/home/user/my-repo
const response = await fetch("/api/worktrees?cwd=" + encodeURIComponent(cwd));
// Returns: { projectRoot, projectKey, isGit, isTopLevel, currentWorktreePath, worktrees }

```

## Key Implementation Files

| File | Purpose |
|------|---------|
| [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts) | Core utilities for worktree detection, project root resolution, and manipulation |
| [`app/api/worktrees/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/worktrees/route.ts) | HTTP API wiring utilities for the frontend |
| [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) | Injects `worktreeBranch` into session metadata |
| [`lib/file-access.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-access.ts) | Grants read access to the shared project root |
| [`AGENTS.md`](https://github.com/agegr/pi-web/blob/main/AGENTS.md) | Design documentation for worktree grouping behavior |

## Summary

- **Detection**: `isLinkedWorktree()` uses `git rev-parse --git-common-dir` to identify linked worktrees
- **Resolution**: `resolveProject()` combines `git rev-parse --show-toplevel` with path manipulation to find the shared project root
- **Convention**: Worktrees are created under `<repoRoot>-worktrees/<branch>` for predictable organization
- **Integration**: Session metadata carries `worktreeBranch`, enabling unified project grouping across checkouts
- **API**: REST endpoints in [`app/api/worktrees/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/worktrees/route.ts) expose operations to the frontend

## Frequently Asked Questions

### How does pi‑web distinguish between a regular Git repository and a linked worktree?

pi‑web runs `git rev-parse --git-common-dir` via `isLinkedWorktree()`. If the common directory points to the main repository's `.git` folder rather than a local `.git` file or directory, the path is identified as a linked worktree. This check determines whether the worktree switcher appears in the UI.

### What happens to sessions when a worktree is deleted?

Sessions that pointed to a deleted worktree are automatically re‑associated with the main repository root. This graceful fallback prevents broken references and maintains project continuity, as documented in [`AGENTS.md`](https://github.com/agegr/pi-web/blob/main/AGENTS.md) under "Worktrees and project grouping".

### Why does pi‑web use a `-worktrees` suffix convention for new worktrees?

The suffix creates a predictable, namespaced location (`<repoRoot>-worktrees/<branch>`) that simplifies path manipulation when resolving the shared project root. The `resolveProject()` function strips this suffix to locate the canonical repository directory, enabling accurate project grouping without additional configuration.

### Can pi‑web's worktree detection handle nested or non‑standard directory structures?

The current implementation assumes the `-worktrees` convention for resolving shared roots. For directories outside this pattern, `git rev-parse --show-toplevel` still provides accurate top‑level detection, though the automatic main repo root derivation relies on the expected suffix structure.