Skip to content

Worktree

worktree

GitWorktreeTool — add, remove, and list git worktrees.

GitWorktreeTool

Bases: AXMTool

Add, remove, or list git worktrees.

Registered as git_worktree via axm.tools entry point.

Source code in packages/axm-git/src/axm_git/tools/worktree.py
Python
class GitWorktreeTool(AXMTool):
    """Add, remove, or list git worktrees.

    Registered as ``git_worktree`` via axm.tools entry point.
    """

    @property
    def name(self) -> str:
        """Tool name used for MCP registration."""
        return "git_worktree"

    def execute(  # type: ignore[override]  # noqa: PLR0913
        self,
        *,
        action: str,
        path: str = ".",
        worktree_path: str | None = None,
        branch: str | None = None,
        base: str | None = None,
        force: bool = False,
        **kwargs: object,
    ) -> ToolResult:
        """Manage git worktrees.

        Args:
            action: One of ``add``, ``remove``, ``list``.
            path: Repository path. Git-root resolution and
                  ``resolve_default_branch`` run here — so a fresh
                  worktree (a sibling directory that does not exist yet)
                  is created correctly.
            worktree_path: For ``add``/``remove``: the worktree location
                  (the future or existing worktree dir). When omitted,
                  *path* doubles as the worktree location (legacy form,
                  which only works when that dir already lives inside a
                  repo).
            branch: Branch name for ``add`` action.
            base: Base ref for ``add`` (default: the repo's resolved
                  default branch).
            force: Force removal for ``remove`` action.

        Returns:
            ToolResult with worktree data on success.
        """
        resolved = Path(path).resolve()
        # When worktree_path is given, *path* is the repo (an existing
        # dir) and *worktree_path* is the target. Otherwise legacy: the
        # sole path is both repo-locator and worktree target.
        wt_target = Path(worktree_path).resolve() if worktree_path else resolved
        repo_locator = resolved if worktree_path else wt_target

        match action:
            case "list":
                return self._list(resolved)
            case "add":
                try:
                    effective_base = base or resolve_default_branch(repo_locator)
                except FileNotFoundError:
                    return _not_a_repo_result(repo_locator)
                return self._add(
                    repo_locator,
                    wt_target,
                    branch=branch,
                    base=effective_base,
                )
            case "remove":
                return self._remove(repo_locator, wt_target, force=force)
            case _:
                error = f"Invalid action {action!r}. Use 'add', 'remove', or 'list'."
                return ToolResult(
                    success=False,
                    error=error,
                    text=render_failure_text(error=error, data=None),
                )

    def _list(self, path: Path) -> ToolResult:
        """List all worktrees."""
        git_root = find_git_root(path)
        if git_root is None:
            return _not_a_repo_result(path)

        try:
            result = run_git(["worktree", "list", "--porcelain"], git_root)
        except subprocess.TimeoutExpired as exc:
            return timeout_error_result(exc)
        if result.returncode != 0:
            return _git_error_result(result)

        worktrees = _parse_worktree_porcelain(result.stdout)
        data: dict[str, object] = {"worktrees": worktrees}
        return ToolResult(success=True, data=data, text=render_list_text(data))

    def _add(
        self,
        repo: Path,
        wt_target: Path,
        *,
        branch: str | None,
        base: str,
    ) -> ToolResult:
        """Add a new worktree at *wt_target*, resolving git from *repo*."""
        git_root = find_git_root(repo)
        if git_root is None:
            return _not_a_repo_result(repo)

        cmd: list[str] = ["worktree", "add"]
        if branch:
            cmd.extend(["-b", branch])
        cmd.append(str(wt_target))
        cmd.append(base)

        try:
            result = run_git(cmd, git_root)
        except subprocess.TimeoutExpired as exc:
            return timeout_error_result(exc)
        if result.returncode != 0:
            return _git_error_result(result)

        data: dict[str, object] = {
            "path": str(wt_target),
            "branch": branch or base,
            "base": base,
        }
        return ToolResult(success=True, data=data, text=render_add_text(data))

    def _remove(self, repo: Path, wt_target: Path, *, force: bool) -> ToolResult:
        """Remove worktree *wt_target*, resolving git from *repo*."""
        git_root = find_git_root(repo)
        if git_root is None:
            return _not_a_repo_result(repo)

        cmd: list[str] = ["worktree", "remove", str(wt_target)]
        if force:
            cmd.append("--force")

        try:
            result = run_git(cmd, git_root)
        except subprocess.TimeoutExpired as exc:
            return timeout_error_result(exc)
        if result.returncode != 0:
            return _git_error_result(result)

        data: dict[str, object] = {"removed": str(wt_target)}
        return ToolResult(success=True, data=data, text=render_remove_text(data))
name property

Tool name used for MCP registration.

execute(*, action, path='.', worktree_path=None, branch=None, base=None, force=False, **kwargs)

Manage git worktrees.

Parameters:

Name Type Description Default
action str

One of add, remove, list.

required
path str

Repository path. Git-root resolution and resolve_default_branch run here — so a fresh worktree (a sibling directory that does not exist yet) is created correctly.

'.'
worktree_path str | None

For add/remove: the worktree location (the future or existing worktree dir). When omitted, path doubles as the worktree location (legacy form, which only works when that dir already lives inside a repo).

None
branch str | None

Branch name for add action.

None
base str | None

Base ref for add (default: the repo's resolved default branch).

None
force bool

Force removal for remove action.

False

Returns:

Type Description
ToolResult

ToolResult with worktree data on success.

Source code in packages/axm-git/src/axm_git/tools/worktree.py
Python
def execute(  # type: ignore[override]  # noqa: PLR0913
    self,
    *,
    action: str,
    path: str = ".",
    worktree_path: str | None = None,
    branch: str | None = None,
    base: str | None = None,
    force: bool = False,
    **kwargs: object,
) -> ToolResult:
    """Manage git worktrees.

    Args:
        action: One of ``add``, ``remove``, ``list``.
        path: Repository path. Git-root resolution and
              ``resolve_default_branch`` run here — so a fresh
              worktree (a sibling directory that does not exist yet)
              is created correctly.
        worktree_path: For ``add``/``remove``: the worktree location
              (the future or existing worktree dir). When omitted,
              *path* doubles as the worktree location (legacy form,
              which only works when that dir already lives inside a
              repo).
        branch: Branch name for ``add`` action.
        base: Base ref for ``add`` (default: the repo's resolved
              default branch).
        force: Force removal for ``remove`` action.

    Returns:
        ToolResult with worktree data on success.
    """
    resolved = Path(path).resolve()
    # When worktree_path is given, *path* is the repo (an existing
    # dir) and *worktree_path* is the target. Otherwise legacy: the
    # sole path is both repo-locator and worktree target.
    wt_target = Path(worktree_path).resolve() if worktree_path else resolved
    repo_locator = resolved if worktree_path else wt_target

    match action:
        case "list":
            return self._list(resolved)
        case "add":
            try:
                effective_base = base or resolve_default_branch(repo_locator)
            except FileNotFoundError:
                return _not_a_repo_result(repo_locator)
            return self._add(
                repo_locator,
                wt_target,
                branch=branch,
                base=effective_base,
            )
        case "remove":
            return self._remove(repo_locator, wt_target, force=force)
        case _:
            error = f"Invalid action {action!r}. Use 'add', 'remove', or 'list'."
            return ToolResult(
                success=False,
                error=error,
                text=render_failure_text(error=error, data=None),
            )