DEV Community

Brad
Brad

Posted on

Python Git Automation: Commit, Deploy, and Manage Repos Without Touching the CLI

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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}"
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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)