| # fx worktree |
| |
| `fx worktree` manages a pool of reusable, multi-repository Fuchsia checkouts for |
| parallel development. |
| |
|  |
| |
| ## Overview |
| |
| Fuchsia developers and automated coding agents frequently work on multiple tasks |
| at the same time. Running parallel builds or switching branches inside a single |
| Fuchsia checkout invalidates incremental **GN** (Generate Ninja) and Ninja build |
| caches inside `out/`. Creating and deleting full checkouts from scratch for each |
| task also wastes disk I/O and discards compiled build artifacts when the task |
| finishes. |
| |
| `fx worktree` solves this problem by combining `jiri worktree` multi-repository |
| checkouts with a persistent, lease-based worktree pool. Instead of destroying a |
| checkout after a task completes, `fx worktree` detaches the task alias and |
| restores baseline GN build arguments. The tool then returns the physical |
| checkout and its populated `out/` build directories to the pool for immediate |
| reuse. |
| |
| ## Quick Start |
| |
| You can create, build inside, inspect, and release a leased worktree in five |
| commands. Follow these steps to complete a full task lifecycle: |
| |
| 1. Create and lease a worktree for your task (auto-provisioning a new pool slot |
| if none are free): |
| |
| ```bash |
| fx worktree add my-feature |
| ``` |
| |
| 2. Navigate to the leased worktree directory using `fx worktree locate`: |
| |
| ```bash |
| cd "$(fx worktree locate my-feature)" |
| ``` |
| |
| 3. Configure a build directory (if the pool slot does not already have one) and |
| run a build: |
| |
| ```bash |
| fx set fuchsia.arm64 |
| fx build |
| ``` |
| |
| 4. List all currently leased worktrees, their sync status, and their build |
| directories: |
| |
| ```bash |
| fx worktree list |
| ``` |
| |
| 5. Release the worktree back to the pool when your task is complete: |
| |
| ```bash |
| fx worktree remove my-feature |
| ``` |
| |
| ## Usage and Workflows |
| |
| ### Everyday Task Lifecycle |
| |
| Top-level `fx worktree` subcommands mirror standard `git worktree` ergonomics |
| while operating on pooled checkouts under the hood. When you run |
| `fx worktree add <name>`, the tool claims an available free checkout from |
| `.jiri_root/worktrees/` and backs up existing `args.gn` files. The command then |
| runs `jiri worktree sync` and creates a relative symlink at |
| `.jiri_root/worktrees/<name>`: |
| |
| ```bash |
| # Claim a free slot (or auto-provision a new slot) as 'bugfix-123' |
| fx worktree add bugfix-123 |
| |
| # Optionally claim a specific free physical slot from the pool by name |
| fx worktree add bugfix-123 --pool-name warm-lagoon |
| |
| # Print the resolved physical path to the worktree |
| fx worktree locate bugfix-123 |
| |
| # Inspect active leased worktrees, git sync counts, and build directories |
| fx worktree list |
| |
| # Release 'bugfix-123' back to the pool while preserving its out/ directory |
| fx worktree remove bugfix-123 |
| ``` |
| |
| Updating the main Fuchsia checkout with `jiri update` leaves existing worktrees |
| on their previous manifest revision. Run `jiri worktree sync` to re-synchronize |
| an active worktree with the main checkout's manifest: |
| |
| ```bash |
| # Run from inside the leased worktree directory |
| jiri worktree sync |
| |
| # Or specify the worktree path explicitly from any directory |
| jiri worktree sync "$(fx worktree locate bugfix-123)" |
| ``` |
| |
| ### Pool Administration |
| |
| You rarely need to manage the worktree pool manually because `fx worktree add` |
| automatically provisions a new physical checkout whenever the pool has no free |
| slots. Manual pool administration via `fx worktree pool` helps when you want to |
| pre-warm slots with `fx set` configurations or share the main checkout's |
| `local/` directory. You can also use `fx worktree pool remove` to reclaim disk |
| space by permanently deleting unused physical checkouts: |
| |
| ```bash |
| # Provision a randomly named slot (e.g., 'warm-lagoon') with two build dirs |
| fx worktree pool add \ |
| --set "fuchsia.arm64 --auto-dir" \ |
| --set "fuchsia.x64 --auto-dir" \ |
| --symlink-local |
| |
| # List all physical slots in the pool (both free and leased) |
| fx worktree pool list |
| |
| # Permanently delete a free physical worktree and its out/ dirs from disk |
| fx worktree pool remove warm-lagoon |
| |
| # Force removal of a physical worktree even when an active task holds a lease |
| fx worktree pool remove warm-lagoon --force |
| ``` |
| |
| ### Machine-Readable JSON Output |
| |
| Automated workflows and AI coding agents can pass `--json` to `add`, `list`, and |
| `pool list` to receive structured output on standard output. Passing `--json` to |
| `fx worktree add <name>` emits a JSON object containing the task identifier and |
| symlink path: |
| |
| ```json |
| { |
| "worktree_id": "agent-refactor", |
| "path": "/home/user/fuchsia/.jiri_root/worktrees/agent-refactor" |
| } |
| ``` |
| |
| Passing `--json` to `fx worktree list` or `fx worktree pool list` emits a JSON |
| array of worktree status objects. In `list --json`, `name` holds the active task |
| alias; in `pool list --json`, `name` always holds the physical slot name and |
| `task` is `null` when the slot is free: |
| |
| ```json |
| [ |
| { |
| "name": "warm-lagoon", |
| "task": "agent-refactor", |
| "sync": { |
| "behind": 0, |
| "new": 2 |
| }, |
| "build_dirs": [ |
| { |
| "path": "out/fuchsia.arm64", |
| "active": true, |
| "config": "fuchsia.arm64", |
| "last_build": "5m ago" |
| } |
| ] |
| } |
| ] |
| ``` |
| |
| ### Shell Tab Completion |
| |
| `fx worktree` integrates with Bash (`//scripts/fx-env.sh`) and Zsh |
| (`//scripts/zsh-completion/_fx_worktree`) tab completion through the internal |
| `fx worktree _complete <filter>` subcommand. The completion scripts filter |
| candidate worktree names based on the active subcommand context: |
| |
| - `fx worktree remove <TAB>` completes only active leased task aliases. |
| - `fx worktree pool remove <TAB>` completes only registered physical slot names. |
| - `fx worktree add --pool-name <TAB>` completes only free physical slot names. |
| - `fx worktree locate <TAB>` completes both active task aliases and physical |
| slot names. |
| |
| ## Architecture |
| |
| ### Relationship with jiri worktree |
| |
| `fx worktree` acts as a higher-level lifecycle and build-directory manager on |
| top of `jiri worktree`. The two tools divide responsibilities across distinct |
| layers: |
| |
| - **`jiri worktree` (Lower-Level Multi-Repo Checkout Layer)**: Manages the |
| physical creation, synchronization, and deletion of git worktrees across all |
| repositories in the Fuchsia Jiri manifest. Specifically, |
| `jiri worktree add <path>` provisions git worktrees, links **CIPD** (Chrome |
| Infrastructure Package Deployment) prebuilts, and registers `<path>` in |
| `.jiri_root/worktrees_registry`. Meanwhile, `jiri worktree sync [<path>]` |
| aligns repositories with the parent manifest, and |
| `jiri worktree remove [-force] <path>` deletes and unregisters the checkout. |
| - **`fx worktree` (Higher-Level Pooling, Leasing, and Build Layer)**: Wraps |
| `jiri worktree` to prevent destructive teardown of build caches. `fx worktree` |
| introduces a pool of reusable physical slots (`WorktreePool`), atomic lease |
| tracking (`lease.json`), task aliasing via relative symlinks, and automatic |
| backup and restoration of GN build arguments (`BuildDir`). |
| |
| ### Physical Worktree Pool and Task Aliasing |
| |
| `WorktreePool` (`worktree_pool.py`) resolves the primary Fuchsia checkout root |
| (`FUCHSIA_DIR`) and reads `.jiri_root/worktrees_registry` to discover all |
| provisioned physical worktrees. When you invoke `fx worktree` from inside a |
| worktree, `WorktreePool` follows the `<worktree><parent_root>` entry that jiri |
| writes to the worktree's `.jiri_root/config` to locate the main checkout. When |
| provisioning a new slot without an explicit name, `WorktreePool` generates a |
| random `<adjective>-<noun>` identifier (such as `warm-lagoon` or |
| `amber-badger`). |
| |
| A `Worktree` instance (`worktree.py`) represents each physical checkout rooted |
| at `.jiri_root/worktrees/<slot-name>`. `Worktree` manages two filesystem |
| artifacts to coordinate task leases: |
| |
| - **Atomic Lease File (`<worktree>/.jiri_root/lease.json`)**: Determines whether |
| a physical slot is in the `FREE` or `LEASED` state. `Worktree.acquire_lease()` |
| opens `.jiri_root/lease.json` with `os.O_CREAT | os.O_EXCL` to prevent race |
| conditions when multiple processes claim worktrees concurrently. The file |
| stores a JSON object containing `worktree_id` (the physical slot name), `pid`, |
| `timestamp_sec`, `path`, and `task_id` (the user-facing task name). |
| - **Task Alias Symlink (`.jiri_root/worktrees/<task-name>`)**: Claiming a slot |
| with `fx worktree add <task-name>` creates a relative symlink |
| `.jiri_root/worktrees/<task-name>` pointing to `<slot-name>`. Running |
| `fx worktree remove <task-name>` unlinks the symlink and deletes `lease.json`. |
| |
| IMPORTANT: `fx worktree add` intentionally keeps the worktree on a detached git |
| `HEAD` (synchronized to the parent checkout's `JIRI_HEAD` via |
| `jiri worktree sync`) rather than checking out a named git branch. Avoiding |
| unnecessary branch switches preserves the modification timestamp on |
| `.git/worktrees/<slot>/HEAD`, which prevents Ninja from invalidating build |
| stamps across pooled builds. When `fx worktree remove` releases a slot, |
| `release_lease()` runs `git checkout --detach` so the slot returns to a detached |
| `HEAD` even if the developer manually switched branches during the task. |
| |
| `fx worktree` stores pool metadata, task symlinks, lease locks, and build |
| backups under `.jiri_root/`. The following directory tree shows how |
| `fx worktree` organizes physical slots and task symlinks on disk: |
| |
| ```text |
| <fuchsia_root>/ |
| └── .jiri_root/ |
| ├── worktrees_registry # Paths of registered physical slots |
| └── worktrees/ |
| ├── my-feature -> warm-lagoon |
| └── warm-lagoon/ # Physical worktree slot directory |
| ├── .fx-build-dir # Active build dir (out/fuchsia.arm64) |
| ├── .jiri_root/ |
| │ └── lease.json # Active lease metadata (task_id, pid) |
| └── out/ |
| ├── default -> fuchsia.arm64 |
| └── fuchsia.arm64/ |
| ├── args.gn # Active GN build arguments |
| ├── args.gn.ref |
| ├── build.ninja |
| ├── .ninja_log # Used for last-build elapsed time |
| └── last_ninja_build_success.stamp |
| ``` |
| |
| The pool directory defaults to `.jiri_root/worktrees/`. To keep the pool |
| elsewhere, for example so that file watchers such as Watchman treat each |
| worktree as its own project root instead of ignoring it under `.jiri_root/`, |
| configure it in the main checkout with jiri. A relative path is relative to the |
| checkout root: |
| |
| ```bash |
| jiri init -worktrees-dir=../fuchsia-worktrees |
| ``` |
| |
| `WorktreePool` reads the setting from `<worktrees_dir>` in the main checkout's |
| `.jiri_root/config`. Only slots directly inside the configured directory belong |
| to the pool, so remove existing slots with `fx worktree pool remove` before |
| changing it. |
| |
| ### Build Directory and GN Arguments Lifecycle |
| |
| Each physical worktree retains its `out/` build directories across leases so |
| subsequent tasks can perform incremental builds. `Worktree.build_dirs()` and |
| `BuildDir` (`build_dir.py`) manage the discovery, status inspection, and |
| configuration lifecycle of `out/` directories inside each pooled checkout: |
| |
| - **Build Directory Discovery**: `Worktree.build_dirs()` reads `.fx-build-dir` |
| (falling back to `out/default` if `.fx-build-dir` does not exist) and scans |
| all immediate subdirectories of `out/` that contain `args.gn` or |
| `build.ninja`. `Worktree.build_dirs()` resolves symlinks and filters out any |
| directory literally named `default` so that `out/default` symlinks do not |
| appear as duplicate entries alongside the real build directory. |
| - **GN Arguments Backup and Restore**: When a task acquires a lease on a |
| worktree, `BuildDir.backup_args()` copies `args.gn` to `args.gn.ref` in every |
| discovered build directory. If a developer or agent modifies GN arguments |
| during the task, `BuildDir.restore_args()` copies `args.gn.ref` back over |
| `args.gn` and removes `args.gn.ref` when `fx worktree remove` releases the |
| lease. Restoring the backup guarantees that the next task claiming the slot |
| inherits a clean baseline build configuration. |
| - **Configuration and Build Health Inspection**: `BuildDir.get_build_config()` |
| parses `args.gn` for `build_info_product` / `build_info_board` assignments or |
| `//products/<name>.gni` / `//boards/<name>.gni` imports (including |
| `//vendor/...` paths) to format `<product>.<board>`. |
| `BuildDir.get_build_status()` inspects `args.gn`, `.ninja_errors.json`, and |
| `last_ninja_build_success.stamp` to classify the directory as |
| `Not Configured`, `Configured`, `Built`, `Dirty`, or `Build Failed`. Finally, |
| `BuildDir.get_build_time_ago_sec()` reads the modification time of |
| `.ninja_log` to calculate how long ago the last build ran. |
| |
| ### Lifecycle State Transitions |
| |
| A physical worktree slot transitions between `FREE` and `LEASED` states across |
| multiple tasks before eventual removal from the pool. The following diagram |
| illustrates the state transitions of a physical worktree slot from creation to |
| deletion: |
| |
| ```mermaid |
| stateDiagram-v2 |
| [*] --> Free: fx worktree pool add |
| Free --> Leased: fx worktree add <task> |
| Leased --> Leased: jiri worktree sync |
| Leased --> Free: fx worktree remove <task> |
| Free --> [*]: fx worktree pool remove <slot> |
| Leased --> [*]: fx worktree pool remove <slot> --force |
| ``` |
| |
| Each CLI operation transitions a physical slot between states. The following |
| table summarizes the state transitions for each command: |
| |
| | Operation | State Transition | |
| | :------------------------------- | :-------------------------- | |
| | `fx worktree pool add [<slot>]` | Unprovisioned → `FREE` | |
| | `fx worktree add <task>` | `FREE` → `LEASED` | |
| | `fx worktree remove <task>` | `LEASED` → `FREE` | |
| | `fx worktree pool remove <slot>` | `FREE` / `LEASED` → Deleted | |
| |
| During each transition, `fx worktree` mutates specific metadata files and |
| symlinks inside `.jiri_root/`. The following list details the mechanical |
| operations performed at each stage: |
| |
| - **Provisioning (`fx worktree pool add [<slot>]`)**: Invokes |
| `jiri worktree add .jiri_root/worktrees/<slot>`, appends the path to |
| `.jiri_root/worktrees_registry`, optionally links or copies `local/`, and |
| optionally runs `fx set`. |
| - **Claiming (`fx worktree add <task>`)**: Selects a free slot (or runs |
| `pool add` if none exist), atomically writes `.jiri_root/lease.json`, copies |
| `args.gn` → `args.gn.ref` across `out/*`, runs `jiri worktree sync`, and |
| creates the relative symlink `.jiri_root/worktrees/<task>` → `<slot>`. |
| - **Releasing (`fx worktree remove <task>`)**: Restores `args.gn.ref` → |
| `args.gn` across `out/*`, runs `git checkout --detach`, removes |
| `.jiri_root/lease.json`, and unlinks `.jiri_root/worktrees/<task>`. |
| - **Deleting (`fx worktree pool remove <slot>`)**: Verifies no active lease |
| exists (unless you pass `--force`), invokes `jiri worktree remove`, deletes |
| `.jiri_root/worktrees/<slot>`, and removes the path from |
| `.jiri_root/worktrees_registry`. |
| |
| ## Testing |
| |
| You can run the unit test suite either directly with Python's `unittest` runner |
| or through the Fuchsia build system. Run the following commands to execute the |
| tests: |
| |
| ```bash |
| # Fast local execution without a full build |
| python3 -m unittest discover -s tools/devshell/worktree/tests -p "test_*.py" |
| |
| # Host test execution via fx test |
| fx test //tools/devshell/worktree:tests |
| ``` |