- Parallel development in tmux* with git worktrees
+ Parallel development in tmux* with git worktrees & jj workspaces
@@ -20,7 +20,7 @@
---
Giga opinionated zero-friction workflow tool for managing
-[git worktrees](https://git-scm.com/docs/git-worktree) and tmux windows as
+[git worktrees](https://git-scm.com/docs/git-worktree) (or [jj workspaces](https://jj-vcs.github.io/jj/latest/working-copy/#workspaces)) and tmux windows as
isolated development environments. Perfect for running multiple AI agents in
parallel without conflict.
@@ -1920,12 +1920,16 @@ entire process and pairs each worktree with a dedicated tmux window, creating
fully isolated development environments. See
[Before and after](#before-and-after) for how workmux streamlines this workflow.
+**Using jj?** workmux also supports [jj (Jujutsu)](https://jj-vcs.github.io/jj/) natively. jj workspaces provide the same parallel development benefits. workmux auto-detects your VCS backend.
+
## Git worktree caveats
While powerful, git worktrees have nuances that are important to understand.
workmux is designed to automate solutions to these, but awareness of the
underlying mechanics helps.
+> **Note:** These caveats are specific to git worktrees. If you're using jj, some (like ignored files and conflicts) still apply to jj workspaces, while git-specific ones (like `.git/info/exclude`) do not.
+
- [Gitignored files require configuration](#gitignored-files-require-configuration)
- [Conflicts](#conflicts)
- [Package manager considerations (pnpm, yarn)](#package-manager-considerations-pnpm-yarn)
@@ -2242,7 +2246,7 @@ workmux completions fish | source
## Requirements
- Rust (for building)
-- Git 2.5+ (for worktree support)
+- Git 2.5+ (for worktree support) or [jj](https://jj-vcs.github.io/jj/) (Jujutsu)
- tmux (or an alternative backend)
### Alternative backends
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
index 47f5d99a..3b77eef9 100644
--- a/docs/.vitepress/config.mts
+++ b/docs/.vitepress/config.mts
@@ -117,7 +117,7 @@ export default defineConfig({
{ text: "Session mode", link: "/guide/session-mode" },
{ text: "direnv", link: "/guide/direnv" },
{ text: "Monorepos", link: "/guide/monorepos" },
- { text: "Git worktree caveats", link: "/guide/git-worktree-caveats" },
+ { text: "Worktree caveats", link: "/guide/git-worktree-caveats" },
{ text: "Nix", link: "/guide/nix" },
],
},
diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md
index a9d7a9ee..d48730f7 100644
--- a/docs/guide/configuration.md
+++ b/docs/guide/configuration.md
@@ -140,7 +140,7 @@ windows:
### File operations
-New worktrees are clean checkouts with no gitignored files (`.env`, `node_modules`, etc.). Use `files` to automatically copy or symlink what each worktree needs:
+New worktrees are clean checkouts with no ignored files (`.env`, `node_modules`, etc.). Use `files` to automatically copy or symlink what each worktree needs:
```yaml
files:
diff --git a/docs/guide/git-worktree-caveats.md b/docs/guide/git-worktree-caveats.md
index 1e698ca2..73bbbd2d 100644
--- a/docs/guide/git-worktree-caveats.md
+++ b/docs/guide/git-worktree-caveats.md
@@ -1,14 +1,16 @@
---
-description: Common git worktree pitfalls and how workmux handles them
+description: Common worktree pitfalls and how workmux handles them
---
-# Git worktree caveats
+# Worktree caveats
-While powerful, git worktrees have nuances that are important to understand. workmux is designed to automate solutions to these, but awareness of the underlying mechanics helps.
+While powerful, worktrees (git worktrees and jj workspaces) have nuances that are important to understand. workmux is designed to automate solutions to these, but awareness of the underlying mechanics helps.
-## Gitignored files require configuration
+> **Note:** Most caveats below apply to both git worktrees and jj workspaces. Sections that are git-specific are marked as such.
-When `git worktree add` creates a new working directory, it's a clean checkout. Files listed in your `.gitignore` (e.g., `.env` files, `node_modules`, IDE configuration) will not exist in the new worktree by default. Your application will be broken in the new worktree until you manually create or link these necessary files.
+## Ignored files require configuration
+
+When a new worktree is created (via `git worktree add` or `jj workspace add`), it's a clean checkout. Ignored files (e.g., `.env` files, `node_modules`, IDE configuration) will not exist in the new worktree by default. Your application will be broken in the new worktree until you manually create or link these necessary files.
This is a primary feature of workmux. Use the `files` section in your `.workmux.yaml` to automatically copy or symlink these files on creation:
@@ -36,9 +38,9 @@ panes:
## Conflicts
-Worktrees isolate your filesystem, but they do not prevent merge conflicts. If you modify the same area of code on two different branches (in two different worktrees), you will still have a conflict when you merge one into the other.
+Worktrees isolate your filesystem, but they do not prevent merge conflicts. If you modify the same area of code on two different branches (in two different worktrees), you will still have a conflict when you merge one into the other. This applies to both git and jj.
-The best practice is to work on logically separate features in parallel worktrees. When conflicts are unavoidable, use standard git tools to resolve them. You can also leverage an AI agent within the worktree to assist with the conflict resolution.
+The best practice is to work on logically separate features in parallel worktrees. When conflicts are unavoidable, use standard VCS tools to resolve them (`git` conflict resolution or `jj resolve`). You can also leverage an AI agent within the worktree to assist with the conflict resolution.
## Package manager considerations (pnpm, yarn)
@@ -69,7 +71,7 @@ rustc-wrapper = "sccache"
This caches compiled dependencies globally, so new worktrees benefit from cached artifacts without any lock contention.
-## Symlinks and `.gitignore` trailing slashes
+## Symlinks and `.gitignore` trailing slashes (git-specific)
If your `.gitignore` uses a trailing slash to ignore directories (e.g., `tests/venv/`), symlinks to that path in the created worktree will **not** be ignored and will show up in `git status`. This is because `venv/` only matches directories, not files (symlinks).
@@ -80,7 +82,7 @@ To ignore both directories and symlinks, remove the trailing slash:
+ tests/venv
```
-## Local git ignores are not shared
+## Local git ignores are not shared (git-specific)
The local git ignore file, `.git/info/exclude`, is specific to the main worktree's git directory and is not respected in other worktrees. Personal ignore patterns for your editor or temporary files may not apply in new worktrees, causing them to appear in `git status`.
diff --git a/docs/guide/index.md b/docs/guide/index.md
index 7cc87fc6..c6e93de5 100644
--- a/docs/guide/index.md
+++ b/docs/guide/index.md
@@ -4,7 +4,7 @@ description: A workflow tool for managing git worktrees and tmux windows as isol
# What is workmux?
-workmux is a giga opinionated zero-friction workflow tool for managing [git worktrees](https://git-scm.com/docs/git-worktree) and tmux windows as isolated development environments. Also supports [kitty](/guide/kitty), [WezTerm](/guide/wezterm), and [Zellij](/guide/zellij) (experimental). Perfect for running multiple AI agents in parallel without conflict.
+workmux is a giga opinionated zero-friction workflow tool for managing [git worktrees](https://git-scm.com/docs/git-worktree) (or [jj workspaces](https://jj-vcs.github.io/jj/latest/working-copy/#workspaces)) and tmux windows as isolated development environments. Also supports [kitty](/guide/kitty), [WezTerm](/guide/wezterm), and [Zellij](/guide/zellij) (experimental). Perfect for running multiple AI agents in parallel without conflict.
**Philosophy:** Do one thing well, then compose. Your terminal handles windowing and layout, git handles branches and worktrees, your agent executes, and workmux ties it all together.
@@ -122,7 +122,7 @@ state, editor session, dev server, and AI agent. Context switching is switching
## Features
-- Create git worktrees with matching tmux windows (or kitty/WezTerm/Zellij tabs) in a single command (`add`)
+- Create worktrees (git) or workspaces (jj) with matching tmux windows (or kitty/WezTerm/Zellij tabs) in a single command (`add`)
- Merge branches and clean up everything (worktree, tmux window, branches) in one command (`merge`)
- [Dashboard](/guide/dashboard/) for monitoring agents, reviewing changes, and sending commands
- [Delegate tasks to worktree agents](/guide/skills#-worktree) with a `/worktree` skill
@@ -186,9 +186,11 @@ workmux merge
In a standard Git setup, switching branches disrupts your flow by requiring a clean working tree. Worktrees remove this friction. `workmux` automates the entire process and pairs each worktree with a dedicated tmux window, creating fully isolated development environments.
+**Using jj?** workmux also supports [jj (Jujutsu)](https://jj-vcs.github.io/jj/) natively. jj workspaces provide the same parallel development benefits. workmux auto-detects your VCS backend.
+
## Requirements
-- Git 2.5+ (for worktree support)
+- Git 2.5+ (for worktree support) or [jj](https://jj-vcs.github.io/jj/) (Jujutsu)
- tmux (or [WezTerm](/guide/wezterm), [kitty](/guide/kitty), or [Zellij](/guide/zellij))
## Inspiration and related tools
diff --git a/docs/guide/quick-start.md b/docs/guide/quick-start.md
index 4f8154d6..a214f5b2 100644
--- a/docs/guide/quick-start.md
+++ b/docs/guide/quick-start.md
@@ -32,7 +32,7 @@ workmux add new-feature
This will:
-- Create a git worktree at `/../__worktrees/new-feature`
+- Create a worktree (git) or workspace (jj) at `/../__worktrees/new-feature`
- Copy config files and symlink dependencies (if [configured](/guide/configuration#file-operations))
- Run any [`post_create`](/guide/configuration#lifecycle-hooks) setup commands
- Create a tmux window named `wm-new-feature` (the prefix is configurable)
diff --git a/docs/guide/workflows.md b/docs/guide/workflows.md
index bd2042df..6ad24757 100644
--- a/docs/guide/workflows.md
+++ b/docs/guide/workflows.md
@@ -117,6 +117,9 @@ git push -u origin feature-123
gh pr create
```
+> For jj users, push with `jj git push` instead.
+
+
Once your PR is merged on GitHub, use `workmux remove` to clean up:
```bash
@@ -127,4 +130,4 @@ workmux remove feature-123
workmux rm --gone
```
-The `--gone` flag is particularly useful - it automatically finds worktrees whose upstream branches no longer exist (because the PR was merged and the branch was deleted on GitHub) and removes them.
+The `--gone` flag is particularly useful - it automatically finds worktrees whose upstream branches no longer exist (because the PR was merged and the branch was deleted on GitHub) and removes them. For jj repos, workmux detects gone branches via `jj git fetch` and bookmark tracking.
diff --git a/docs/index.md b/docs/index.md
index 614fedf3..953f9f13 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,6 +1,6 @@
---
layout: home
-description: The zero-friction workflow for git worktrees and tmux, kitty, WezTerm, or Zellij
+description: The zero-friction workflow for git worktrees, jj workspaces, and tmux, kitty, WezTerm, or Zellij
---
@@ -61,7 +61,7 @@ description: The zero-friction workflow for git worktrees and tmux, kitty, WezTe
Worktree pain points, solved
-
Git worktrees are powerful, but managing them manually is painful. workmux automates the rough edges.
+
Git worktrees (and jj workspaces) are powerful, but managing them manually is painful. workmux automates the rough edges.
"You need to reinstall everything"
diff --git a/docs/reference/commands/add.md b/docs/reference/commands/add.md
index b295da46..c47a83a6 100644
--- a/docs/reference/commands/add.md
+++ b/docs/reference/commands/add.md
@@ -1,10 +1,10 @@
---
-description: Create git worktrees and tmux windows, with support for AI prompts and parallel generation
+description: Create worktrees and tmux windows, with support for AI prompts and parallel generation
---
# add
-Creates a new git worktree with a matching tmux window and switches you to it immediately. If the branch doesn't exist, it will be created automatically.
+Creates a new worktree with a matching tmux window and switches you to it immediately. If the branch doesn't exist, it will be created automatically.
```bash
workmux add [flags]
@@ -48,7 +48,7 @@ These options allow you to skip expensive setup steps when they're not needed (e
## What happens
1. Determines the **handle** for the worktree by slugifying the branch name (e.g., `feature/auth` becomes `feature-auth`). This can be overridden with the `--name` flag.
-2. Creates a git worktree at `/` (the `worktree_dir` is configurable and defaults to a sibling directory of your project)
+2. Creates a worktree (via `git worktree add` or `jj workspace add`) at `/` (the `worktree_dir` is configurable and defaults to a sibling directory of your project)
3. Runs any configured file operations (copy/symlink)
4. Executes `post_create` commands if defined (runs before the tmux window/session opens, so keep them fast)
5. Creates a new tmux window named `` (e.g., `wm-feature-auth` with `window_prefix: wm-`). With `--session`, the window is created in its own dedicated session instead of the current session.
diff --git a/docs/reference/commands/list.md b/docs/reference/commands/list.md
index 66470eb9..db6e328c 100644
--- a/docs/reference/commands/list.md
+++ b/docs/reference/commands/list.md
@@ -1,10 +1,10 @@
---
-description: List all git worktrees with their agent, window, and merge status
+description: List all worktrees with their agent, window, and merge status
---
# list
-Lists all git worktrees with their agent status, multiplexer window status, and merge status. Alias: `ls`
+Lists all worktrees with their agent status, multiplexer window status, and merge status. Alias: `ls`
```bash
workmux list [options] [worktree-or-branch...]
diff --git a/docs/reference/commands/merge.md b/docs/reference/commands/merge.md
index e575d401..7abee0dc 100644
--- a/docs/reference/commands/merge.md
+++ b/docs/reference/commands/merge.md
@@ -35,8 +35,8 @@ If your workflow uses pull requests, the merge happens on the remote after revie
By default, `workmux merge` performs a standard merge commit (configurable via `merge_strategy`). You can override the configured behavior with these mutually exclusive flags:
-- `--rebase`: Rebase the feature branch onto the target before merging (creates a linear history via fast-forward merge). If conflicts occur, you'll need to resolve them manually in the worktree and run `git rebase --continue`.
-- `--squash`: Squash all commits from the feature branch into a single commit on the target. You'll be prompted to provide a commit message in your editor.
+- `--rebase`: Rebase the feature branch onto the target before merging (creates a linear history via fast-forward merge). If conflicts occur, you'll need to resolve them manually in the worktree and run `git rebase --continue`. For jj repos, this uses `jj rebase`.
+- `--squash`: Squash all commits from the feature branch into a single commit on the target. You'll be prompted to provide a commit message in your editor. For jj repos, this uses `jj squash`.
If you don't want to have merge commits in your main branch, use the `rebase` merge strategy, which does `--rebase` by default.
diff --git a/docs/reference/commands/remove.md b/docs/reference/commands/remove.md
index 38b19f18..88b6d781 100644
--- a/docs/reference/commands/remove.md
+++ b/docs/reference/commands/remove.md
@@ -19,7 +19,7 @@ workmux remove [name]... [flags]
| Flag | Description |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--all` | Remove all worktrees at once (except the main worktree). Prompts for confirmation unless `--force` is used. Safely skips worktrees with uncommitted changes or unmerged commits. |
-| `--gone` | Remove worktrees whose upstream remote branch has been deleted (e.g., after a PR is merged on GitHub). Automatically runs `git fetch --prune` first. |
+| `--gone` | Remove worktrees whose upstream remote branch has been deleted (e.g., after a PR is merged on GitHub). Automatically runs `git fetch --prune` (or `jj git fetch` for jj repos) first. |
| `--force, -f` | Skip confirmation prompt and ignore uncommitted changes. |
| `--keep-branch, -k` | Remove only the worktree and tmux window while keeping the local branch. |
diff --git a/skills/merge/SKILL.md b/skills/merge/SKILL.md
index b646496c..d306b668 100644
--- a/skills/merge/SKILL.md
+++ b/skills/merge/SKILL.md
@@ -24,6 +24,17 @@ This command finishes work on the current branch by:
2. Rebasing onto the base branch
3. Running `workmux merge` to merge and clean up
+## Step 0: Detect VCS
+
+Determine the VCS backend:
+- If `.jj/` directory exists at or above the current directory → jj mode
+- Otherwise → git mode
+
+For jj mode, adapt the steps below:
+- Step 1: Use `jj describe` instead of `git commit`
+- Step 2: Use `jj rebase -d ` instead of `git rebase`
+- For conflicts: Use `jj resolve` and inspect with `jj diff`
+
## Step 1: Commit
If there are staged changes, commit them. Use lowercase, imperative mood, no conventional commit prefixes. Skip if nothing is staged.
diff --git a/skills/rebase/SKILL.md b/skills/rebase/SKILL.md
index 59697952..a4d3d5a7 100644
--- a/skills/rebase/SKILL.md
+++ b/skills/rebase/SKILL.md
@@ -11,6 +11,13 @@ Rebase the current branch.
Arguments: $ARGUMENTS
+## Step 0: Detect VCS
+
+If `.jj/` exists at or above the current directory → use jj commands:
+- `jj git fetch` instead of `git fetch`
+- `jj rebase -d ` instead of `git rebase`
+- For conflicts: `jj resolve` instead of manual conflict resolution + `git rebase --continue`
+
Behavior:
- No arguments: rebase on local main
diff --git a/skills/worktree/SKILL.md b/skills/worktree/SKILL.md
index e1366d3b..97cbcf2c 100644
--- a/skills/worktree/SKILL.md
+++ b/skills/worktree/SKILL.md
@@ -1,11 +1,11 @@
---
name: worktree
-description: Launch one or more tasks in new git worktrees using workmux.
+description: Launch one or more tasks in new worktrees using workmux.
disable-model-invocation: true
allowed-tools: Bash, Write
---
-Launch one or more tasks in new git worktrees using workmux.
+Launch one or more tasks in new worktrees using workmux.
Tasks: $ARGUMENTS
diff --git a/src/cli.rs b/src/cli.rs
index 9644ca02..5d47c1e9 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -1,5 +1,5 @@
use crate::command::args::{MultiArgs, PromptArgs, RescueArgs, SetupFlags};
-use crate::{claude, command, config, git, nerdfont};
+use crate::{claude, command, config, nerdfont, vcs};
use anyhow::{Context, Result};
use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::{Shell, generate};
@@ -13,18 +13,19 @@ impl WorktreeBranchParser {
}
fn get_branches(&self) -> Vec {
- // Don't attempt completions if not in a git repo.
- if !git::is_git_repo().unwrap_or(false) {
- return Vec::new();
- }
+ // Don't attempt completions if not in a VCS repo.
+ let vcs = match vcs::try_detect_vcs() {
+ Some(v) => v,
+ None => return Vec::new(),
+ };
- let worktrees = match git::list_worktrees() {
+ let worktrees = match vcs.list_workspaces() {
Ok(wt) => wt,
// Fail silently on completion; don't disrupt the user's shell.
Err(_) => return Vec::new(),
};
- let main_branch = git::get_default_branch().ok();
+ let main_branch = vcs.get_default_branch().ok();
worktrees
.into_iter()
@@ -70,18 +71,19 @@ impl WorktreeHandleParser {
}
fn get_handles() -> Vec {
- // Don't attempt completions if not in a git repo.
- if !git::is_git_repo().unwrap_or(false) {
- return Vec::new();
- }
+ // Don't attempt completions if not in a VCS repo.
+ let vcs = match vcs::try_detect_vcs() {
+ Some(v) => v,
+ None => return Vec::new(),
+ };
- let worktrees = match git::list_worktrees() {
+ let worktrees = match vcs.list_workspaces() {
Ok(wt) => wt,
// Fail silently on completion; don't disrupt the user's shell.
Err(_) => return Vec::new(),
};
- let main_worktree_root = git::get_main_worktree_root().ok();
+ let main_worktree_root = vcs.get_main_workspace_root().ok();
worktrees
.into_iter()
@@ -130,13 +132,14 @@ impl GitBranchParser {
}
fn get_branches() -> Vec {
- // Don't attempt completions if not in a git repo.
- if !git::is_git_repo().unwrap_or(false) {
- return Vec::new();
- }
+ // Don't attempt completions if not in a VCS repo.
+ let vcs = match vcs::try_detect_vcs() {
+ Some(v) => v,
+ None => return Vec::new(),
+ };
// Fail silently on completion; don't disrupt the user's shell.
- git::list_checkout_branches().unwrap_or_default()
+ vcs.list_checkout_branches().unwrap_or_default()
}
}
diff --git a/src/command/add.rs b/src/command/add.rs
index 30d39e65..8cec99c7 100644
--- a/src/command/add.rs
+++ b/src/command/add.rs
@@ -10,7 +10,7 @@ use crate::template::{
use crate::workflow::SetupOptions;
use crate::workflow::pr::detect_remote_branch;
use crate::workflow::prompt_loader::{PromptLoadArgs, load_prompt, parse_prompt_with_frontmatter};
-use crate::{config, git, workflow};
+use crate::{config, vcs, workflow};
use anyhow::{Context, Result, anyhow, bail};
use serde_json::Value;
use std::collections::BTreeMap;
@@ -90,11 +90,11 @@ fn read_stdin_lines() -> Result> {
/// Check preconditions for the add command (git repo and multiplexer session).
/// Returns Ok(()) if all preconditions are met, or an error listing all failures.
fn check_preconditions() -> Result<()> {
- let is_git = git::is_git_repo()?;
+ let is_repo = vcs::try_detect_vcs().is_some();
let mux = create_backend(detect_backend());
let is_mux_running = mux.is_running()?;
- if is_git && is_mux_running {
+ if is_repo && is_mux_running {
return Ok(());
}
@@ -103,8 +103,8 @@ fn check_preconditions() -> Result<()> {
if !is_mux_running {
errors.push(format!("{} is not running.", mux.name()));
}
- if !is_git {
- errors.push("Current directory is not a git repository.".to_string());
+ if !is_repo {
+ errors.push("Current directory is not a git or jj repository.".to_string());
}
// Add blank line before suggestions
@@ -113,8 +113,8 @@ fn check_preconditions() -> Result<()> {
if !is_mux_running {
errors.push(format!("Please start a {} session first.", mux.name()));
}
- if !is_git {
- errors.push("Please run this command from within a git repository.".to_string());
+ if !is_repo {
+ errors.push("Please run this command from within a git or jj repository.".to_string());
}
Err(anyhow!(errors.join("\n")))
diff --git a/src/command/capture.rs b/src/command/capture.rs
index 32065084..13fb455c 100644
--- a/src/command/capture.rs
+++ b/src/command/capture.rs
@@ -6,7 +6,8 @@ use crate::workflow;
pub fn run(name: &str, lines: u16) -> Result<()> {
let mux = create_backend(detect_backend());
- let (_path, agent) = workflow::resolve_worktree_agent(name, mux.as_ref())?;
+ let vcs = crate::vcs::detect_vcs()?;
+ let (_path, agent) = workflow::resolve_worktree_agent(name, mux.as_ref(), vcs.as_ref())?;
let output = mux
.capture_pane(&agent.pane_id, lines)
diff --git a/src/command/close.rs b/src/command/close.rs
index 3e83fa48..889104ce 100644
--- a/src/command/close.rs
+++ b/src/command/close.rs
@@ -1,6 +1,6 @@
use crate::multiplexer::handle::mode_label;
use crate::multiplexer::{MuxHandle, create_backend, detect_backend};
-use crate::{config, git, sandbox};
+use crate::{config, sandbox, vcs};
use anyhow::{Context, Result, anyhow};
pub fn run(name: Option<&str>) -> Result<()> {
@@ -8,34 +8,28 @@ pub fn run(name: Option<&str>) -> Result<()> {
let mux = create_backend(detect_backend());
let prefix = config.window_prefix();
- // Resolve the handle first. When the user passes a branch name that differs
- // from the worktree directory name, find_worktree resolves through both handle
- // and branch lookups, then we extract the true handle from the path basename.
+ // Resolve the handle first to determine target mode
let resolved_handle = match name {
- Some(n) => {
- let (path, _branch) = git::find_worktree(n).with_context(|| {
- format!(
- "No worktree found with name '{}'. Use 'workmux list' to see available worktrees.",
- n
- )
- })?;
- path.file_name()
- .ok_or_else(|| anyhow!("Invalid worktree path: no directory name"))?
- .to_string_lossy()
- .to_string()
- }
+ Some(h) => h.to_string(),
None => super::resolve_name(None)?,
};
// Determine if this worktree was created as a session or window
- let mode = git::get_worktree_mode(&resolved_handle);
+ let vcs = vcs::detect_vcs()?;
+ let mode = vcs.get_workspace_mode(&resolved_handle);
// When no name is provided, prefer the current window/session name
// This handles duplicate windows/sessions (e.g., wm:feature-2) correctly
let (full_target_name, is_current_target) = match name {
- Some(_) => {
- // Explicit name provided - worktree already validated above
- let target = MuxHandle::new(mux.as_ref(), mode, prefix, &resolved_handle);
+ Some(handle) => {
+ // Explicit name provided - validate the worktree exists
+ vcs.find_workspace(handle).with_context(|| {
+ format!(
+ "No worktree found with name '{}'. Use 'workmux list' to see available worktrees.",
+ handle
+ )
+ })?;
+ let target = MuxHandle::new(mux.as_ref(), mode, prefix, handle);
let full = target.full_name();
let current = target.current_name()?;
let is_current = current.as_deref() == Some(full.as_str());
diff --git a/src/command/dashboard/app.rs b/src/command/dashboard/app.rs
index ff454681..14098544 100644
--- a/src/command/dashboard/app.rs
+++ b/src/command/dashboard/app.rs
@@ -10,7 +10,7 @@ use std::sync::{Arc, mpsc};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::config::Config;
-use crate::git::{self, GitStatus};
+use crate::vcs::{self, VcsStatus};
use crate::github::PrSummary;
use crate::multiplexer::{AgentPane, AgentStatus, Multiplexer};
use crate::state::StateStore;
@@ -69,11 +69,11 @@ pub struct App {
/// Height of the preview area (updated during rendering)
pub preview_height: u16,
/// Git status for each worktree path
- pub git_statuses: HashMap,
+ pub git_statuses: HashMap,
/// Channel receiver for git status updates from background thread
- git_rx: mpsc::Receiver<(PathBuf, GitStatus)>,
+ git_rx: mpsc::Receiver<(PathBuf, VcsStatus)>,
/// Channel sender for git status updates (cloned for background threads)
- git_tx: mpsc::Sender<(PathBuf, GitStatus)>,
+ git_tx: mpsc::Sender<(PathBuf, VcsStatus)>,
/// Last time git status was fetched (to throttle background fetches)
last_git_fetch: std::time::Instant,
/// Flag to track if a git fetch is in progress (prevents thread pile-up)
@@ -124,7 +124,9 @@ impl App {
let palette = ThemePalette::from_theme(config.theme);
let sort_mode = SortMode::load();
- let git_statuses = git::load_status_cache();
+ let git_statuses = vcs::try_detect_vcs()
+ .map(|v| v.load_status_cache())
+ .unwrap_or_default();
let pr_statuses = crate::github::load_pr_cache();
let hide_stale = load_hide_stale();
let last_pane_id = load_last_pane_id();
@@ -204,7 +206,7 @@ impl App {
.into_iter()
.map(|path| {
std::thread::spawn(move || {
- let root = git::get_repo_root_for(&path).ok();
+ let root = vcs::try_detect_vcs().and_then(|v| v.get_repo_root_for(&path).ok());
(path, root)
})
})
@@ -322,7 +324,9 @@ impl App {
let _reset = ResetFlag(is_fetching);
for path in agent_paths {
- let status = git::get_git_status(&path);
+ let status = vcs::try_detect_vcs()
+ .map(|v| v.get_status(&path))
+ .unwrap_or_default();
// Ignore send errors (receiver dropped means app is shutting down)
let _ = tx.send((path, status));
}
diff --git a/src/command/dashboard/mod.rs b/src/command/dashboard/mod.rs
index c89b1044..574d49e2 100644
--- a/src/command/dashboard/mod.rs
+++ b/src/command/dashboard/mod.rs
@@ -45,8 +45,8 @@ use ratatui::backend::CrosstermBackend;
use std::io;
use std::time::Duration;
-use crate::git;
use crate::github;
+use crate::vcs;
use crate::multiplexer::{create_backend, detect_backend};
use self::actions::apply_action;
@@ -233,7 +233,9 @@ pub fn run(cli_preview_size: Option, open_diff: bool) -> Result<()> {
}
// Save git status cache before exiting
- git::save_status_cache(&app.git_statuses);
+ if let Some(v) = vcs::try_detect_vcs() {
+ v.save_status_cache(&app.git_statuses);
+ };
// Save PR status cache before exiting
github::save_pr_cache(app.pr_statuses());
diff --git a/src/command/dashboard/ui/format.rs b/src/command/dashboard/ui/format.rs
index 9b291024..97d43acd 100644
--- a/src/command/dashboard/ui/format.rs
+++ b/src/command/dashboard/ui/format.rs
@@ -2,7 +2,7 @@
use ratatui::style::{Color, Modifier, Style};
-use crate::git::GitStatus;
+use crate::vcs::VcsStatus;
use crate::github::{CheckState, PrSummary};
use crate::nerdfont;
@@ -13,7 +13,7 @@ use super::theme::ThemePalette;
/// Format: "→branch +N -M +X -Y ↑A ↓B"
/// When there are uncommitted changes that differ from total, branch totals are dimmed
pub fn format_git_status(
- status: Option<&GitStatus>,
+ status: Option<&VcsStatus>,
spinner_frame: u8,
palette: &ThemePalette,
) -> Vec<(String, Style)> {
diff --git a/src/command/list.rs b/src/command/list.rs
index 0140ae06..ff488032 100644
--- a/src/command/list.rs
+++ b/src/command/list.rs
@@ -112,7 +112,8 @@ fn format_agent_status(
pub fn run(show_pr: bool, filter: &[String]) -> Result<()> {
let config = config::Config::load(None)?;
let mux = create_backend(detect_backend());
- let worktrees = workflow::list(&config, mux.as_ref(), show_pr, filter)?;
+ let vcs = crate::vcs::detect_vcs()?;
+ let worktrees = workflow::list(&config, mux.as_ref(), vcs.as_ref(), show_pr, filter)?;
if worktrees.is_empty() {
println!("No worktrees found");
diff --git a/src/command/open.rs b/src/command/open.rs
index 575bb004..81d2457c 100644
--- a/src/command/open.rs
+++ b/src/command/open.rs
@@ -3,7 +3,7 @@ use crate::config::MuxMode;
use crate::multiplexer::{create_backend, detect_backend};
use crate::workflow::prompt_loader::{PromptLoadArgs, load_prompt};
use crate::workflow::{SetupOptions, WorkflowContext};
-use crate::{config, git, workflow};
+use crate::{config, workflow};
use anyhow::{Context, Result, bail};
pub fn run(
@@ -27,7 +27,7 @@ pub fn run(
let context = WorkflowContext::new(config, mux, config_location)?;
// Determine the target mode from stored metadata
- let stored_mode = git::get_worktree_mode(&resolved_name);
+ let stored_mode = context.vcs.get_workspace_mode(&resolved_name);
let target_type = match stored_mode {
MuxMode::Session => "session",
MuxMode::Window => "window",
diff --git a/src/command/path.rs b/src/command/path.rs
index 66858d58..6a482337 100644
--- a/src/command/path.rs
+++ b/src/command/path.rs
@@ -1,11 +1,12 @@
-use crate::git;
+use crate::vcs;
use anyhow::{Context, Result};
pub fn run(name: &str) -> Result<()> {
+ let vcs = vcs::detect_vcs()?;
// Smart resolution: try handle first, then branch name
- let (path, _branch) = git::find_worktree(name).with_context(|| {
+ let (path, _branch) = vcs.find_workspace(name).with_context(|| {
format!(
- "No worktree found with name '{}'. Use 'workmux list' to see available worktrees.",
+ "No workspace found with name '{}'. Use 'workmux list' to see available workspaces.",
name
)
})?;
diff --git a/src/command/remove.rs b/src/command/remove.rs
index 09f825cb..30019da2 100644
--- a/src/command/remove.rs
+++ b/src/command/remove.rs
@@ -1,6 +1,6 @@
use crate::multiplexer::{create_backend, detect_backend};
use crate::workflow::WorkflowContext;
-use crate::{config, git, spinner, workflow};
+use crate::{config, spinner, vcs, workflow};
use anyhow::{Context, Result, anyhow};
use std::io::{self, Write};
use std::path::PathBuf;
@@ -35,11 +35,13 @@ fn run_specified(names: Vec, force: bool, keep_branch: bool) -> Result<(
.collect::>>()?
};
+ let vcs = vcs::detect_vcs()?;
+
// 2. Resolve all targets and validate they exist
let mut candidates: Vec<(String, PathBuf, String)> = Vec::new();
for name in resolved_names {
- let (worktree_path, branch_name) = git::find_worktree(&name)
- .with_context(|| format!("No worktree found with name '{}'", name))?;
+ let (worktree_path, branch_name) = vcs.find_workspace(&name)
+ .with_context(|| format!("No workspace found with name '{}'", name))?;
let handle = worktree_path
.file_name()
@@ -83,13 +85,13 @@ fn run_specified(names: Vec, force: bool, keep_branch: bool) -> Result<(
for (handle, path, branch) in candidates {
// Check uncommitted (blocking)
- if path.exists() && git::has_uncommitted_changes(&path).unwrap_or(false) {
+ if path.exists() && vcs.has_uncommitted_changes(&path).unwrap_or(false) {
uncommitted.push(handle);
continue;
}
// Check unmerged (promptable), only if we're deleting the branch
- if !keep_branch && let Some(base) = is_unmerged(&branch)? {
+ if !keep_branch && let Some(base) = is_unmerged(vcs.as_ref(), &branch)? {
unmerged.push((handle, branch, base));
continue;
}
@@ -144,25 +146,25 @@ fn run_specified(names: Vec, force: bool, keep_branch: bool) -> Result<(
}
/// Check if a branch has unmerged commits. Returns Some(base) if unmerged, None otherwise.
-fn is_unmerged(branch: &str) -> Result