Git repository operations for VCollab applications
Project description
vcti-git
Overview
VCollab applications often need to interact with Git repositories —
cloning template repositories, managing remotes, fetching updates,
and pushing changes. The vcti-git package provides a single
Repository class with a small, focused API for these operations.
The class is a thin façade over a configurable backend. Three backends are supported:
- pygit2 — libgit2 bindings via
a binary wheel. Fastest of the three, no
gitbinary required. Suitable for slim container images. - GitPython — pure-Python but
shells out to the system
gitbinary for many operations. Easy to develop with on any machine that already has Git installed. - dulwich — pure-Python
implementation of git. No native dependencies, no
gitbinary required. Slower than the other two but maximally portable (useful for AWS Lambda layers, Alpine images without build toolchains, etc.).
Backend-specific exceptions are wrapped in a GitError hierarchy so
callers do not depend on the underlying library.
All backends are optional dependencies. Install at least one extra:
pip install 'vcti-git[pygit2]' # pygit2 backend (recommended)
pip install 'vcti-git[gitpython]' # GitPython backend
pip install 'vcti-git[dulwich]' # dulwich backend
pip install 'vcti-git[pygit2,gitpython,dulwich]' # all three
If no backend is installed, constructing a Repository raises
GitError with the install instructions. If backend is not passed
explicitly, the constructor auto-detects in preference order:
pygit2 → gitpython → dulwich.
Status: This package is not yet published to PyPI. See STATUS.md for current status, alternatives considered (GitPython, pygit2, dulwich), and why we have not released yet.
When to use this package
Use vcti-git when your application needs to:
- Clone repositories from remote URLs
- Manage multiple remotes (add, remove, list)
- Fetch updates or push changes programmatically
- Pull (fetch + fast-forward only) to update a local branch
- Commit local changes
- Validate whether a directory is a Git repository
When NOT to use this package
This is a focused wrapper, not a full git client. Drop down to the underlying library (pygit2, GitPython, or dulwich) directly if you need any of:
- Branch management (create, switch, list, delete branches)
- Working tree inspection (
git status,git diff, dirty checks) - Commit history iteration (
git log) - Non-fast-forward
git pull,git merge,git rebase,git checkout(thepull()we provide is FF-only) - Tag creation, cherry-picking, stashing, submodule handling
- Authentication callbacks for private remotes (none of the backends' auth APIs are exposed by this wrapper yet)
Repository selection is also single-threaded by design — see Thread safety below.
Installation
A backend extra must be specified, otherwise vcti-git installs with no
git library and Repository(...) will raise at construction.
# pygit2 backend (latest main) — recommended
pip install "vcti-git[pygit2] @ git+https://github.com/vcollab/vcti-python-git.git"
# GitPython backend (latest main)
pip install "vcti-git[gitpython] @ git+https://github.com/vcollab/vcti-python-git.git"
# dulwich backend (latest main)
pip install "vcti-git[dulwich] @ git+https://github.com/vcollab/vcti-python-git.git"
# All three, pinned to a version
pip install "vcti-git[pygit2,gitpython,dulwich] @ git+https://github.com/vcollab/vcti-python-git.git@v1.2.0"
From a GitHub Release
Download the wheel from the Releases page and install with the chosen extra:
pip install "vcti_git-1.2.0-py3-none-any.whl[pygit2]"
In requirements.txt
vcti-git[pygit2] @ git+https://github.com/vcollab/vcti-python-git.git@v1.2.0
In pyproject.toml dependencies
dependencies = [
"vcti-git[pygit2] @ git+https://github.com/vcollab/vcti-python-git.git@v1.2.0",
]
Quick Start
Check if a directory is a Git repository
from vcti.git import Repository
repo = Repository("/path/to/repo")
if repo.is_valid():
print("Valid Git repository")
Clone a remote repository
from vcti.git import Repository
repo = Repository("/tmp/my-clone")
repo.clone("https://github.com/user/repo.git")
print(f"Cloned to: {repo.path}")
Select a backend
from vcti.git import Repository
# Auto-detect in preference order: pygit2 → gitpython → dulwich.
repo = Repository("/path/to/repo")
# Explicit selection.
repo = Repository("/path/to/repo", backend="pygit2")
repo = Repository("/path/to/repo", backend="gitpython")
repo = Repository("/path/to/repo", backend="dulwich")
Manage remotes
from vcti.git import Repository
repo = Repository("/path/to/repo")
# List all remotes
remotes = repo.list_remotes()
# {'origin': 'https://github.com/user/repo.git'}
# Add a new remote (fetches by default)
repo.add_remote("upstream", "https://github.com/org/repo.git")
# Add without fetching
repo.add_remote("backup", "https://backup.example.com/repo.git", fetch=False)
# Remove a remote
repo.remove_remote("backup")
Fetch and push
from vcti.git import Repository
repo = Repository("/path/to/repo")
# Fetch from origin (default)
repo.fetch()
# Fetch from upstream with options
repo.fetch("upstream", prune=True, tags=True)
# Pull (fetch + fast-forward) the current branch from origin.
# Raises GitError if the branch has diverged from the remote —
# pull never creates a merge commit.
repo.pull()
# Push the current branch to origin (auto-detects from HEAD).
# Raises GitError if HEAD is detached or unborn — pass an explicit
# branch name in that case.
repo.push()
# Push a specific branch with force
repo.push("origin", "feature-branch", force=True)
Commit changes
from vcti.git import Repository
repo = Repository("/path/to/repo")
# Commit whatever is already staged in the index
repo.commit("Update configuration")
# Stage all tracked + untracked changes, then commit
repo.commit("Snapshot working tree", add_all=True)
Handle errors
Every exception raised by Repository derives from GitError. Catch
the specific subclass you care about, or the base class for a blanket
handler:
from vcti.git import GitError, RemoteError, Repository, RepositoryError
repo = Repository("/path/to/repo")
try:
repo.fetch("origin")
except RemoteError as e:
# Clone/fetch/push or remote-config failed (often network/auth)
print(f"Remote operation failed: {e}")
except RepositoryError as e:
# Path isn't a valid repo, or unsafe to overwrite
print(f"Repository state invalid: {e}")
except GitError as e:
# Anything else (commit failure, backend not installed, etc.)
print(f"Other git failure: {e}")
Error messages are prefixed with the backend tag — [pygit2],
[gitpython], or [dulwich] — so logs from mixed deployments stay
unambiguous.
Release resources
Repository holds a cached handle to the underlying library's
repository object. Use it as a context manager to release that handle
deterministically:
with Repository("/path/to/repo") as repo:
repo.commit("done", add_all=True)
# handle released here
repo.close() is also exposed if you can't use a with block. After
close(), the instance must not be used again. Calling close() more
than once is safe.
Select a backend at deployment time
In addition to the backend= constructor argument, the
VCTI_GIT_BACKEND environment variable selects the backend without
touching code. Useful for switching between backends per environment
(e.g. gitpython locally where git is installed, pygit2 in containers,
dulwich in pure-Python serverless layers):
VCTI_GIT_BACKEND=pygit2 python my-app.py
Valid values: pygit2, gitpython, dulwich. The constructor
argument, if passed, takes precedence over the env var.
Public API
| Member | Purpose |
|---|---|
Repository(path, *, backend=None) |
Construct a repository handle. backend=None auto-detects (pygit2, then gitpython, then dulwich). |
.path |
The resolved local path |
.backend_name |
The name of the active backend |
.is_valid() |
Check if path contains a valid Git repo |
.clone(url, *, overwrite=False) |
Clone a remote repo to the local path |
.commit(message="Update", *, add_all=False) |
Commit staged changes; optionally stage everything first |
.list_remotes() |
List all remotes as {name: url} |
.add_remote(name, url, *, fetch=True) |
Add a new remote |
.remove_remote(name) |
Remove an existing remote |
.fetch(remote="origin", *, prune=False, tags=False) |
Fetch from a remote |
.pull(remote="origin", branch=None) |
Fetch and fast-forward the local branch. Raises if diverged. |
.push(remote="origin", branch=None, *, force=False) |
Push to a remote (defaults to current branch) |
GitError |
Base class for all errors raised by Repository |
RepositoryError |
Invalid repository state or unsafe overwrite |
RemoteError |
Remote operation failed (clone/fetch/push/config) |
Troubleshooting
All errors raised by this package include a backend tag ([pygit2],
[gitpython], or [dulwich]) and a descriptive message. Common ones
and what they mean:
| Message fragment | Cause | Fix |
|---|---|---|
No git backend is installed |
The package was installed without an extra, or the chosen extra is missing. | pip install 'vcti-git[pygit2]' (or [gitpython] / [dulwich]). |
Backend 'X' requires the 'X' extra |
backend="X" was passed but X is not importable. |
Install the matching extra. |
Invalid VCTI_GIT_BACKEND=... |
The env var was set to something other than pygit2 / gitpython / dulwich. |
Unset or correct the variable. |
Path already exists |
clone() was called on a non-empty target without overwrite=True. |
Choose a fresh path or pass overwrite=True. |
Refusing to overwrite <path>: directory is non-empty and not a git repository |
The safety guard: overwrite=True refuses to delete a directory that isn't empty and isn't already a git repo (prevents typo-induced data loss). |
Verify the path is correct; delete it manually if you really mean it. |
Not a valid Git repository: <path> |
An operation that needs an existing repo was called on a path that isn't one. | Initialize or clone first; check the path. |
Commit failed: user.name and user.email must be set in git config (pygit2) |
The pygit2 backend cannot construct a signature. | git config user.name "..." and git config user.email "..." (locally or globally). The gitpython and dulwich backends fall back to OS defaults and do not raise. |
Cannot push: HEAD is detached, no current branch to push (gitpython) / Cannot push: HEAD is detached or unborn (pygit2, dulwich) |
push() was called with no branch= while HEAD does not point at a branch (detached) or no commits yet (unborn). |
Pass branch= explicitly: repo.push("origin", "main"). |
Cannot pull: HEAD is detached, no current branch / Cannot pull: HEAD is detached or unborn |
Same situation for pull(). |
Pass branch= explicitly. |
Cannot fast-forward '<branch>' from '<remote>': local branch has diverged. |
pull() is fast-forward only. The local branch has commits the remote doesn't (or both have moved independently). |
Resolve manually using the underlying library: e.g. git pull --rebase, git merge, or hard-reset to remote. The wrapper deliberately won't do this for you. |
Remote 'X' does not exist. Available remotes: [...] |
fetch / push / remove_remote was given a remote name that isn't configured. |
Use the actual remote name (the error lists what's available) or repo.add_remote("X", "...") first. |
Push to '<remote>/<branch>' rejected: ... |
The remote rejected the push (non-fast-forward, server hook, etc.). | Pull and merge first, or pass force=True if you intentionally want to overwrite the remote. |
Clone failed: ... |
The clone failed (network error, invalid URL, authentication). | The wrapper does not handle authentication; private remotes require credentials configured at the OS / git-config level. |
Thread safety
A Repository instance is not thread-safe. Each instance lazily
caches an underlying library Repo / pygit2.Repository object and
the cache itself is unguarded. Use one of:
- A separate
Repositoryinstance per thread. - An external lock around any shared instance.
There is no goal to make Repository itself thread-safe — the
underlying libraries (GitPython, libgit2) have their own constraints
that would leak through any wrapper-level locking.
Documentation
- Design — Why wrap Git libraries, design decisions, backends
- Source Guide — File structure, dependency map, execution flows
- API Reference — Autodoc for all modules
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vcti_git-1.2.0.tar.gz.
File metadata
- Download URL: vcti_git-1.2.0.tar.gz
- Upload date:
- Size: 29.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6fcb4fe66fff277a39da8d861d701c271315f36b421b81562c988fc86a92c623
|
|
| MD5 |
6f1e0834fcb1a00b94e6a3c871fb5fa9
|
|
| BLAKE2b-256 |
4e411f928efc4e341f430fc858fb8f7f8fa462c4dfc68564e28dd517a85cc38b
|
Provenance
The following attestation bundles were made for vcti_git-1.2.0.tar.gz:
Publisher:
release.yml on vcollab/vcti-python-git
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vcti_git-1.2.0.tar.gz -
Subject digest:
6fcb4fe66fff277a39da8d861d701c271315f36b421b81562c988fc86a92c623 - Sigstore transparency entry: 1564219954
- Sigstore integration time:
-
Permalink:
vcollab/vcti-python-git@dce57a1f3e62fcff016a07ed7753c2cb81a99f8a -
Branch / Tag:
refs/tags/v1.2.0 - Owner: https://github.com/vcollab
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@dce57a1f3e62fcff016a07ed7753c2cb81a99f8a -
Trigger Event:
push
-
Statement type:
File details
Details for the file vcti_git-1.2.0-py3-none-any.whl.
File metadata
- Download URL: vcti_git-1.2.0-py3-none-any.whl
- Upload date:
- Size: 20.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6eda9a88f948990da9b29e397f913ef1b1245f4e2ad378e72c8b378d2b749866
|
|
| MD5 |
4979775ec428768dcccd69822569dc5b
|
|
| BLAKE2b-256 |
21d9388695446437fb9094289762e48e9c5e28d163c2bcb3d67ef6d6ac728870
|
Provenance
The following attestation bundles were made for vcti_git-1.2.0-py3-none-any.whl:
Publisher:
release.yml on vcollab/vcti-python-git
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vcti_git-1.2.0-py3-none-any.whl -
Subject digest:
6eda9a88f948990da9b29e397f913ef1b1245f4e2ad378e72c8b378d2b749866 - Sigstore transparency entry: 1564220087
- Sigstore integration time:
-
Permalink:
vcollab/vcti-python-git@dce57a1f3e62fcff016a07ed7753c2cb81a99f8a -
Branch / Tag:
refs/tags/v1.2.0 - Owner: https://github.com/vcollab
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@dce57a1f3e62fcff016a07ed7753c2cb81a99f8a -
Trigger Event:
push
-
Statement type: