Skip to main content

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 git binary required. Suitable for slim container images.
  • GitPython — pure-Python but shells out to the system git binary 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 git binary 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: pygit2gitpythondulwich.

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
  • 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)
  • git pull, git merge, git rebase, git checkout
  • 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.1.2"

From a GitHub Release

Download the wheel from the Releases page and install with the chosen extra:

pip install "vcti_git-1.1.2-py3-none-any.whl[pygit2]"

In requirements.txt

vcti-git[pygit2] @ git+https://github.com/vcollab/vcti-python-git.git@v1.1.2

In pyproject.toml dependencies

dependencies = [
    "vcti-git[pygit2] @ git+https://github.com/vcollab/vcti-python-git.git@v1.1.2",
]

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)

# 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
.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").
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 Repository instance 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

vcti_git-1.1.2.tar.gz (27.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vcti_git-1.1.2-py3-none-any.whl (19.4 kB view details)

Uploaded Python 3

File details

Details for the file vcti_git-1.1.2.tar.gz.

File metadata

  • Download URL: vcti_git-1.1.2.tar.gz
  • Upload date:
  • Size: 27.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for vcti_git-1.1.2.tar.gz
Algorithm Hash digest
SHA256 77200e5616ed0a39f925c774d50c1cea417269c736acff532e8c6975d6ae4736
MD5 cad0b6cbb30dbaa0e90906c542714204
BLAKE2b-256 e0ae8debc7c5bf86d8a6dcbbfad992b4c5b925dec5e4951a82a39ea31a34f18b

See more details on using hashes here.

Provenance

The following attestation bundles were made for vcti_git-1.1.2.tar.gz:

Publisher: release.yml on vcollab/vcti-python-git

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file vcti_git-1.1.2-py3-none-any.whl.

File metadata

  • Download URL: vcti_git-1.1.2-py3-none-any.whl
  • Upload date:
  • Size: 19.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for vcti_git-1.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 9bfcdb61f1433d6ff2ac978ec10298fdda6d88903c7310e9690d5168ba86fc75
MD5 eeade0cba493ac70ff157bcb58c8f39f
BLAKE2b-256 6a46face8aff1b71d0e6057a15a9642ee821055f808085426091148d42514838

See more details on using hashes here.

Provenance

The following attestation bundles were made for vcti_git-1.1.2-py3-none-any.whl:

Publisher: release.yml on vcollab/vcti-python-git

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page