Python Git Automation: Commit, Deploy, and Manage Repos Without Touching the CLI
Tired of repetitive git workflows? Here's how to automate commits, branches, deployments, and repository management with Python.
GitPython: The Essential Library
# pip install gitpython
import git
from pathlib import Path
from datetime import datetime
import os
class GitAutomation:
"""Automate common git operations with Python."""
def __init__(self, repo_path: str):
try:
self.repo = git.Repo(repo_path)
self.path = Path(repo_path)
except git.InvalidGitRepositoryError:
raise ValueError(f"{repo_path} is not a git repository")
@classmethod
def init_repo(cls, path: str, initial_branch: str = 'main') -> 'GitAutomation':
"""Initialize a new git repository."""
repo = git.Repo.init(path)
# Set initial branch
repo.head.reference = repo.create_head(initial_branch)
return cls(path)
@classmethod
def clone(cls, url: str, local_path: str, branch: str = None) -> 'GitAutomation':
"""Clone a remote repository."""
kwargs = {}
if branch:
kwargs['branch'] = branch
git.Repo.clone_from(url, local_path, **kwargs)
return cls(local_path)
def status(self) -> dict:
"""Get repository status."""
return {
'branch': self.repo.active_branch.name,
'modified': [item.a_path for item in self.repo.index.diff(None)],
'untracked': self.repo.untracked_files,
'staged': [item.a_path for item in self.repo.index.diff('HEAD')] if not self.repo.head.is_detached else [],
'is_dirty': self.repo.is_dirty()
}
def commit_all(self, message: str, author_name: str = None, author_email: str = None) -> str:
"""Stage all changes and create a commit."""
if not self.repo.is_dirty(untracked_files=True):
print("Nothing to commit")
return None
# Stage all changes
self.repo.git.add('--all')
# Configure author if provided
kwargs = {}
if author_name and author_email:
author = git.Actor(author_name, author_email)
kwargs['author'] = author
# Create commit
commit = self.repo.index.commit(message, **kwargs)
print(f"Committed: {commit.hexsha[:8]} {message}")
return commit.hexsha
def create_branch(self, branch_name: str, checkout: bool = True) -> None:
"""Create a new branch."""
new_branch = self.repo.create_head(branch_name)
if checkout:
new_branch.checkout()
print(f"Created branch: {branch_name}")
def merge_branch(self, source_branch: str, message: str = None) -> bool:
"""Merge a branch into the current branch."""
current = self.repo.active_branch.name
try:
self.repo.git.merge(source_branch, '--no-ff',
m=message or f"Merge {source_branch} into {current}")
print(f"Merged {source_branch} into {current}")
return True
except git.GitCommandError as e:
print(f"Merge conflict: {e}")
return False
def push(self, remote: str = 'origin', branch: str = None) -> bool:
"""Push to remote repository."""
branch = branch or self.repo.active_branch.name
try:
self.repo.git.push(remote, branch)
print(f"Pushed {branch} to {remote}")
return True
except git.GitCommandError as e:
print(f"Push failed: {e}")
return False
Auto-Commit on File Changes
# pip install watchdog
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
import threading
class AutoCommitHandler(FileSystemEventHandler):
def __init__(self, git_auto: GitAutomation, exclude_patterns: list = None):
self.git_auto = git_auto
self.exclude_patterns = exclude_patterns or ['.git', '__pycache__', '*.pyc', 'node_modules']
self._pending_commit = None
self._commit_timer = None
def _should_ignore(self, path: str) -> bool:
for pattern in self.exclude_patterns:
if pattern in path:
return True
return False
def on_any_event(self, event):
if event.is_directory or self._should_ignore(event.src_path):
return
# Debounce: wait 5 seconds after last change before committing
if self._commit_timer:
self._commit_timer.cancel()
self._commit_timer = threading.Timer(5.0, self._do_commit)
self._commit_timer.start()
def _do_commit(self):
message = f"Auto-commit: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
self.git_auto.commit_all(message)
def watch_and_commit(repo_path: str):
git_auto = GitAutomation(repo_path)
handler = AutoCommitHandler(git_auto)
observer = Observer()
observer.schedule(handler, repo_path, recursive=True)
observer.start()
print(f"Auto-committing changes in {repo_path}...")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()
Deployment Pipeline
class DeploymentPipeline:
def __init__(self, repos: dict):
"""
repos: {
'staging': GitAutomation('/path/to/staging'),
'production': GitAutomation('/path/to/production')
}
"""
self.repos = repos
def deploy_to_staging(self, source_repo: GitAutomation, branch: str = 'develop') -> bool:
"""Deploy a branch to staging environment."""
print(f"\n=== Deploying to Staging ===")
staging = self.repos.get('staging')
if not staging:
print("No staging repo configured")
return False
# Update staging
staging.repo.git.fetch('origin')
staging.repo.git.checkout(branch)
staging.repo.git.pull('origin', branch)
print("✓ Staging updated")
return True
def deploy_to_production(self, version_tag: str) -> bool:
"""Deploy a tagged version to production."""
print(f"\n=== Deploying {version_tag} to Production ===")
prod = self.repos.get('production')
if not prod:
return False
try:
prod.repo.git.fetch('--tags')
prod.repo.git.checkout(version_tag)
print(f"✓ Production updated to {version_tag}")
return True
except git.GitCommandError as e:
print(f"Deployment failed: {e}")
return False
def create_release(self, repo: GitAutomation, version: str, notes: str) -> str:
"""Create a release tag."""
tag = repo.repo.create_tag(
f"v{version}",
message=notes,
ref=repo.repo.head
)
repo.repo.remote('origin').push(f"refs/tags/v{version}")
print(f"Created and pushed release tag: v{version}")
return f"v{version}"
Git Analytics
def analyze_repo_activity(repo_path: str, days: int = 30) -> dict:
"""Analyze repository activity for the past N days."""
repo = git.Repo(repo_path)
since = datetime.now() - timedelta(days=days)
commits = list(repo.iter_commits(since=since.isoformat()))
# Author stats
author_commits = {}
for commit in commits:
author = commit.author.email
if author not in author_commits:
author_commits[author] = 0
author_commits[author] += 1
# File change stats
changed_files = {}
for commit in commits:
for stat_file in commit.stats.files:
if stat_file not in changed_files:
changed_files[stat_file] = 0
changed_files[stat_file] += 1
top_files = sorted(changed_files.items(), key=lambda x: x[1], reverse=True)[:10]
return {
'total_commits': len(commits),
'unique_authors': len(author_commits),
'author_activity': sorted(author_commits.items(), key=lambda x: x[1], reverse=True),
'most_changed_files': top_files,
'avg_commits_per_day': len(commits) / days
}
# Usage
stats = analyze_repo_activity(".", days=30)
print(f"Last 30 days: {stats['total_commits']} commits by {stats['unique_authors']} authors")
Want More DevOps Automation Scripts?
This git automation toolkit is part of my Python automation collection.
👉 Get 50+ Python automation scripts — git tools, deployment pipelines, file organizers, email automators, database managers, and more.
Automate your entire development workflow, not just git.
Top comments (0)