Skip to content

API Reference

The repoenv Python package is primarily an implementation detail of the renv CLI. Symbols and module layout may change before v1.0 — pin your dependency if you integrate programmatically.

Direct CLI users can skip this section.

repoenv

repoenv

repo-env: git-worktree environments across many repositories.

Public command is renv; import package is repoenv.

repoenv.domain

repoenv.domain

Pure domain layer: models and logic with no IO.

Environment

Bases: BaseModel

A named collection of git worktrees.

Source code in src/repoenv/domain/models.py
class Environment(BaseModel):
    """A named collection of git worktrees."""

    model_config = ConfigDict(extra="forbid")

    schema_version: int = SCHEMA_VERSION
    name: StrictStr
    alias: StrictStr | None = None
    path: Path
    source: Path
    base_branch: StrictStr | None = None
    created_at: datetime = Field(default_factory=_utcnow)
    updated_at: datetime = Field(default_factory=_utcnow)
    repos: list[RepoEntry] = Field(default_factory=list)

    def touch(self) -> None:
        """Update the ``updated_at`` timestamp in place."""
        self.updated_at = _utcnow()

    def repo_names(self) -> list[str]:
        """Return the repo names contained in this environment."""
        return [entry.repo for entry in self.repos]

touch()

Update the updated_at timestamp in place.

Source code in src/repoenv/domain/models.py
def touch(self) -> None:
    """Update the ``updated_at`` timestamp in place."""
    self.updated_at = _utcnow()

repo_names()

Return the repo names contained in this environment.

Source code in src/repoenv/domain/models.py
def repo_names(self) -> list[str]:
    """Return the repo names contained in this environment."""
    return [entry.repo for entry in self.repos]

RepoEntry

Bases: BaseModel

A single repository's worktree within an environment.

Source code in src/repoenv/domain/models.py
class RepoEntry(BaseModel):
    """A single repository's worktree within an environment."""

    model_config = ConfigDict(extra="forbid")

    repo: StrictStr
    worktree_path: Path
    remote: StrictStr = StrictStr("origin")
    base: StrictStr
    branch: StrictStr
    branch_created: bool = False
    source_sha: StrictStr | None = None
    status: RepoStatus = RepoStatus.PENDING
    note: StrictStr | None = None

RepoStatus

Bases: str, Enum

Per-repo state within an environment.

Source code in src/repoenv/domain/models.py
class RepoStatus(str, Enum):
    """Per-repo state within an environment."""

    OK = "ok"
    FAILED = "failed"
    SKIPPED = "skipped"
    STALE = "stale"
    PENDING = "pending"

RunResult

Bases: BaseModel

Outcome of running a command in one repository's worktree.

Source code in src/repoenv/domain/models.py
class RunResult(BaseModel):
    """Outcome of running a command in one repository's worktree."""

    model_config = ConfigDict(extra="forbid")

    repo: StrictStr
    worktree_path: Path
    exit_code: int
    duration_s: float
    stdout: str = ""
    stderr: str = ""
    skipped: bool = False

    @property
    def ok(self) -> bool:
        """True when the command succeeded (or the repo was skipped)."""
        return self.skipped or self.exit_code == 0

ok property

True when the command succeeded (or the repo was skipped).

SetOp

Bases: str, Enum

Set operations for combining environments in merge.

Source code in src/repoenv/domain/selection.py
class SetOp(str, Enum):
    """Set operations for combining environments in ``merge``."""

    UNION = "union"
    INTERSECT = "intersect"
    DIFFERENCE = "difference"

RunSummary dataclass

Aggregate counts for a batch run.

Source code in src/repoenv/domain/summary.py
@dataclass(frozen=True)
class RunSummary:
    """Aggregate counts for a batch run."""

    total: int
    succeeded: int
    failed: int
    skipped: int

    @classmethod
    def from_results(cls, results: list[RunResult]) -> "RunSummary":
        """Build a summary from per-repo results."""
        succeeded = sum(1 for r in results if not r.skipped and r.exit_code == 0)
        failed = sum(1 for r in results if not r.skipped and r.exit_code != 0)
        skipped = sum(1 for r in results if r.skipped)
        return cls(total=len(results), succeeded=succeeded, failed=failed, skipped=skipped)

from_results(results) classmethod

Build a summary from per-repo results.

Source code in src/repoenv/domain/summary.py
@classmethod
def from_results(cls, results: list[RunResult]) -> "RunSummary":
    """Build a summary from per-repo results."""
    succeeded = sum(1 for r in results if not r.skipped and r.exit_code == 0)
    failed = sum(1 for r in results if not r.skipped and r.exit_code != 0)
    skipped = sum(1 for r in results if r.skipped)
    return cls(total=len(results), succeeded=succeeded, failed=failed, skipped=skipped)

resolve_selection(candidates, *, include=None, exclude=None)

Resolve a selection of names from candidates.

  • include globs are OR-ed; if omitted, all candidates are included.
  • exclude globs are applied after include and take precedence.
  • Order is preserved from candidates; duplicates are removed.
  • Patterns may contain ** to match across path separators.
  • Comma-separated values within a single pattern string are split.
Source code in src/repoenv/domain/selection.py
def resolve_selection(
    candidates: list[str],
    *,
    include: list[str] | None = None,
    exclude: list[str] | None = None,
) -> list[str]:
    """Resolve a selection of names from ``candidates``.

    - ``include`` globs are OR-ed; if omitted, all candidates are included.
    - ``exclude`` globs are applied after include and take precedence.
    - Order is preserved from ``candidates``; duplicates are removed.
    - Patterns may contain ``**`` to match across path separators.
    - Comma-separated values within a single pattern string are split.
    """
    include = split_csv(include or ["*"])
    exclude = split_csv(exclude or [])

    seen: set[str] = set()
    result: list[str] = []
    for name in candidates:
        if name in seen:
            continue
        if not any(_match_pattern(name, pat) for pat in include):
            continue
        if any(_match_pattern(name, pat) for pat in exclude):
            continue
        seen.add(name)
        result.append(name)
    return result

set_combine(left, right, op)

Combine two ordered name lists with a set operation, preserving order.

Order follows left first, then any new names from right (for union).

Source code in src/repoenv/domain/selection.py
def set_combine(left: list[str], right: list[str], op: SetOp) -> list[str]:
    """Combine two ordered name lists with a set operation, preserving order.

    Order follows ``left`` first, then any new names from ``right`` (for union).
    """
    right_set = set(right)
    left_set = set(left)

    if op is SetOp.UNION:
        ordered = list(left)
        ordered.extend(name for name in right if name not in left_set)
        return ordered
    if op is SetOp.INTERSECT:
        return [name for name in left if name in right_set]
    if op is SetOp.DIFFERENCE:
        return [name for name in left if name not in right_set]
    raise ValueError(f"unknown set operation: {op!r}")

aggregate_exit_code(results)

Map a batch of results to a single process exit code.

  • no results -> NOTHING_MATCHED
  • all ok/skipped -> OK
  • some ok, some failed -> PARTIAL
  • all failed -> GENERIC
Source code in src/repoenv/domain/summary.py
def aggregate_exit_code(results: list[RunResult]) -> ExitCode:
    """Map a batch of results to a single process exit code.

    - no results          -> NOTHING_MATCHED
    - all ok/skipped       -> OK
    - some ok, some failed -> PARTIAL
    - all failed           -> GENERIC
    """
    if not results:
        return ExitCode.NOTHING_MATCHED

    summary = RunSummary.from_results(results)
    if summary.failed == 0:
        return ExitCode.OK
    if summary.succeeded == 0 and summary.skipped == 0:
        return ExitCode.GENERIC
    return ExitCode.PARTIAL

repoenv.services

repoenv.services

Orchestration services: use-cases that coordinate domain + adapters.

repoenv.adapters

repoenv.adapters

IO-boundary adapters (mockable): config, state, and git.

repoenv.errors

repoenv.errors

Typed exception hierarchy mapped to stable process exit codes.

Exit-code contract (from the safety spec): 0 ok 1 generic error 2 usage error 3 partial failure (some repos failed) 4 nothing matched 5 config / state error

ExitCode

Bases: IntEnum

Stable process exit codes. Do not renumber.

Source code in src/repoenv/errors.py
class ExitCode(IntEnum):
    """Stable process exit codes. Do not renumber."""

    OK = 0
    GENERIC = 1
    USAGE = 2
    PARTIAL = 3
    NOTHING_MATCHED = 4
    CONFIG = 5

RepoEnvError

Bases: Exception

Base class for all repo-env errors.

Carries an :class:ExitCode plus an optional user-facing hint that the UI layer renders as a "next step" line.

Source code in src/repoenv/errors.py
class RepoEnvError(Exception):
    """Base class for all repo-env errors.

    Carries an :class:`ExitCode` plus an optional user-facing ``hint`` that the
    UI layer renders as a "next step" line.
    """

    exit_code: ExitCode = ExitCode.GENERIC

    def __init__(self, message: str, *, hint: str | None = None) -> None:
        super().__init__(message)
        self.message = message
        self.hint = hint

UsageError

Bases: RepoEnvError

Invalid invocation / arguments.

Source code in src/repoenv/errors.py
class UsageError(RepoEnvError):
    """Invalid invocation / arguments."""

    exit_code = ExitCode.USAGE

NothingMatchedError

Bases: RepoEnvError

A selector or glob matched no repositories.

Source code in src/repoenv/errors.py
class NothingMatchedError(RepoEnvError):
    """A selector or glob matched no repositories."""

    exit_code = ExitCode.NOTHING_MATCHED

PartialFailureError

Bases: RepoEnvError

Some repositories in a batch operation failed.

Source code in src/repoenv/errors.py
class PartialFailureError(RepoEnvError):
    """Some repositories in a batch operation failed."""

    exit_code = ExitCode.PARTIAL

ConfigError

Bases: RepoEnvError

Configuration or persisted-state problem.

Source code in src/repoenv/errors.py
class ConfigError(RepoEnvError):
    """Configuration or persisted-state problem."""

    exit_code = ExitCode.CONFIG

GitError

Bases: RepoEnvError

A git subprocess failed.

Source code in src/repoenv/errors.py
class GitError(RepoEnvError):
    """A git subprocess failed."""

    exit_code = ExitCode.GENERIC