VCS¶
Git/Mercurial/Jujutsu CLI wrapper -- zero dependencies, stdlib only, Python 3.10+.
Replaces:
GitPython,pygit2(high-level)
Overview¶
The VCS module provides a uniform Python interface to version-control systems by shelling out to their CLI binaries. Each backend implements the VCSBackend protocol, enabling VCS-agnostic tooling. Cross-platform binary discovery handles Linux, macOS, and Windows.
| File | Description | Dependencies |
|---|---|---|
vcs.py |
VCS CLI wrapper | None (stdlib only) |
The module supports three VCS backends:
| Backend | Binary | Coverage |
|---|---|---|
| Git | git |
Full (diff, status, log, blame, apply, merge-file, branch, workspace, commit) |
| Mercurial | hg |
Inspection only (diff, status, log, blame, branch) |
| Jujutsu | jj |
Full (diff, status, log, blame, merge-file, branch, workspace, commit) |
How to Use in Your Project¶
Just copy the single .py file into your project:
Then import directly:
API Reference¶
detect(path)¶
Auto-detect the VCS backend for a given directory.
Walks upward from path checking for .git/, .hg/, or .jj/ directories. Returns the corresponding backend instance if the binary is available, or None if no repository is found.
Example:
from vcs import detect
repo = detect(".")
if repo is not None:
print(repo.name) # "git", "hg", or "jj"
print(repo.current_branch())
VCSBackend Protocol¶
All backends implement this protocol:
class VCSBackend(Protocol):
name: str
# -- inspection --
def is_repo(self, path: str) -> bool: ...
def diff(self, *paths: str, staged: bool = False) -> str: ...
def diff_files(self, path_a: str, path_b: str) -> str: ...
def apply(self, patch: str) -> None: ...
def status(self) -> list[FileStatus]: ...
def log(self, n: int = 10) -> list[Commit]: ...
def blame(self, path: str) -> list[BlameLine]: ...
def current_branch(self) -> str: ...
def merge_file(self, base: str, ours: str, theirs: str) -> str: ...
# -- workspace lifecycle --
def workspace_add(self, path: str, *, branch: str | None = None, rev: str | None = None) -> str: ...
def workspace_remove(self, path: str, *, force: bool = False) -> None: ...
def workspace_list(self) -> list[WorkspaceInfo]: ...
# -- branch / commit --
def branches(self) -> list[str]: ...
def create_branch(self, name: str, *, rev: str | None = None) -> None: ...
def switch(self, target: str) -> None: ...
def commit(self, message: str, *, paths: list[str] | None = None) -> str: ...
def rev_parse(self, rev: str) -> str: ...
Git Backend¶
The most complete backend. Created by passing a repository path:
status()¶
Returns a list of FileStatus objects for the working tree.
Status codes: M (modified), A (added), D (deleted), R (renamed), ? (untracked).
diff(*paths, staged=False)¶
Returns unified diff text for working tree changes.
d = g.diff() # All unstaged changes
d = g.diff(staged=True) # Staged changes only
d = g.diff("src/main.py") # Specific file
diff_files(path_a, path_b)¶
Diff two arbitrary files (not necessarily tracked).
log(n=10)¶
Returns recent commits as Commit objects.
blame(path)¶
Returns per-line blame information as BlameLine objects.
lines = g.blame("README.md")
for bl in lines:
print(f"{bl.commit[:8]} {bl.author:>15} {bl.content}", end="")
apply(patch)¶
Apply a unified diff patch to the working tree.
current_branch()¶
Returns the current branch name, or the short commit hash for detached HEAD.
merge_file(base, ours, theirs)¶
Three-way merge of text content using git merge-file.
Workspace Lifecycle¶
These methods manage isolated workspaces (Git worktrees / Jujutsu workspaces). Mercurial raises NotImplementedError for all workspace and branch operations.
workspace_add(path, *, branch=None, rev=None)¶
Create a new isolated workspace at the given path.
- Git: runs
git worktree add [-b branch] <path> [rev] - Jujutsu: runs
jj workspace add <path> [-r rev], optionally creates a bookmark
workspace_remove(path, *, force=False)¶
Remove a workspace and clean up its directory.
g.workspace_remove("/tmp/feature-ws")
g.workspace_remove("/tmp/dirty-ws", force=True) # force even with uncommitted changes
workspace_list()¶
List all workspaces as WorkspaceInfo objects.
Branch Operations¶
branches()¶
List all branch names (Git) or bookmarks (Jujutsu).
create_branch(name, *, rev=None)¶
Create a new branch or bookmark.
switch(target)¶
Switch to a branch or revision.
- Git: uses
git switch, falls back togit switch --detachfor non-branch targets - Jujutsu: uses
jj newto create a new change on top of the target
Commit Operations¶
commit(message, *, paths=None)¶
Create a commit and return its full hash.
- Git: stages
paths(if given) then commits; otherwise commits what is already staged - Jujutsu: finalizes the current change;
pathsis ignored (jj auto-tracks all changes)
rev_parse(rev)¶
Resolve a revision string to a full commit hash.
Data Structures¶
FileStatus¶
Frozen dataclass representing a file's status.
path: str-- Relative path.status: str-- Single-character code ('M','A','D','R','?','!').original_path: str | None-- For renames, the original path.
Commit¶
Frozen dataclass for commit metadata.
hash: str-- Full commit hash.short_hash: str-- Abbreviated hash.author: str-- Author name.date: str-- ISO 8601 date string.message: str-- Commit message subject.
BlameLine¶
Frozen dataclass for per-line blame information.
commit: str-- Commit hash.author: str-- Author name.date: str-- Commit date.line_no: int-- 1-based line number.content: str-- Line content.
WorkspaceInfo¶
Frozen dataclass for workspace metadata.
path: str-- Absolute path to the workspace directory.head: str-- HEAD commit hash.branch: str | None-- Branch name (Git) or bookmark (Jujutsu),Nonefor detached HEAD.is_main: bool-- Whether this is the main/default workspace.
Exceptions¶
| Exception | When Raised |
|---|---|
VCSError |
Base class for all VCS errors. |
BinaryNotFoundError |
VCS binary not found on the system (with binary_name). |
CommandError |
VCS command exited with unexpected return code (with command, returncode, stderr). |
NotARepoError |
Path is not inside a repository (with path). |
Cross-Platform Binary Discovery¶
The module uses a three-tier strategy to locate VCS binaries:
- Environment variable override:
ZERODEP_GIT_PATH,ZERODEP_HG_PATH,ZERODEP_JJ_PATH shutil.which(): Standard PATH lookup (works on all platforms)- Windows fallback: Checks common install directories (e.g.
C:\Program Files\Git\bin\git.exe)
macOS Homebrew paths (/opt/homebrew/bin, /usr/local/bin) are included in the search.
Cross-Module Integration¶
The Mercurial and Jujutsu backends optionally use the sibling diff/diff.py module's merge3() function for merge_file(). If the diff module is not available, merge_file() raises NotImplementedError for those backends.
Notes and Caveats¶
Subprocess-Based
All VCS operations shell out to the CLI binary. This means the corresponding VCS tool must be installed on the system. The module does not embed or bundle any VCS implementation.
Windows Support
On Windows, subprocess calls use CREATE_NO_WINDOW to prevent console window flashing. Binary discovery includes common Windows installation paths.
- Python version: Requires Python 3.10+.
- Mercurial is inspection-only: Workspace, branch, commit, and rev-parse operations raise
NotImplementedErroron the Mercurial backend due to fundamental semantic differences. Git and Jujutsu have full lifecycle support.
Benchmark¶
No benchmark is provided for this module. All operations are subprocess-based, so performance is dominated by process startup and CLI execution time rather than Python wrapper code -- see VCS Benchmark for details.