PYTHONPython

git

real world projects / cli tool / devtools / commands

PYTHON
git.py🐍
"""
Git helper commands.
"""

import subprocess
from pathlib import Path
import click
from rich.console import Console
from rich.table import Table

console = Console()


@click.group()
def git():
    """Git helper utilities."""
    pass


def _run_git(*args) -> tuple[str, int]:
    """Run a git command and return output."""
    try:
        result = subprocess.run(
            ['git'] + list(args),
            capture_output=True,
            text=True
        )
        return result.stdout, result.returncode
    except FileNotFoundError:
        return "Git not found", 1


@git.command()
def status():
    """Show enhanced git status."""
    output, code = _run_git('status', '--porcelain', '-b')
    
    if code != 0:
        console.print("[red]Not a git repository[/red]")
        return
    
    lines = output.strip().split('\n')
    if not lines or lines == ['']:
        console.print("[green]✓ Working directory clean[/green]")
        return
    
    # Parse branch
    branch_line = lines[0]
    if branch_line.startswith('##'):
        branch = branch_line[3:].split('...')[0]
        console.print(f"[bold]Branch:[/bold] [cyan]{branch}[/cyan]")
    
    # Parse changes
    staged = []
    modified = []
    untracked = []
    
    for line in lines[1:]:
        if not line:
            continue
        status = line[:2]
        filename = line[3:]
        
        if status[0] in 'MADRC':
            staged.append((status[0], filename))
        if status[1] == 'M':
            modified.append(filename)
        elif status[1] == '?':
            untracked.append(filename)
    
    if staged:
        console.print(f"\n[green]Staged ({len(staged)}):[/green]")
        for status, name in staged:
            console.print(f"  {status} {name}")
    
    if modified:
        console.print(f"\n[yellow]Modified ({len(modified)}):[/yellow]")
        for name in modified:
            console.print(f"  M {name}")
    
    if untracked:
        console.print(f"\n[red]Untracked ({len(untracked)}):[/red]")
        for name in untracked[:10]:  # Limit output
            console.print(f"  ? {name}")
        if len(untracked) > 10:
            console.print(f"  ... and {len(untracked) - 10} more")


@git.command('log')
@click.option('--count', '-n', default=10, help='Number of commits')
def log_pretty(count: int):
    """Show pretty git log."""
    output, code = _run_git(
        'log', f'-{count}',
        '--pretty=format:%h|%s|%an|%ar'
    )
    
    if code != 0:
        console.print("[red]Not a git repository[/red]")
        return
    
    table = Table(title="Recent Commits")
    table.add_column("Hash", style="yellow", width=8)
    table.add_column("Message", style="white")
    table.add_column("Author", style="cyan")
    table.add_column("When", style="green")
    
    for line in output.strip().split('\n'):
        if line:
            parts = line.split('|', 3)
            if len(parts) == 4:
                table.add_row(*parts)
    
    console.print(table)


@git.command('branches')
@click.option('--all', '-a', is_flag=True, help='Show remote branches too')
def branches(all: bool):
    """List branches with info."""
    args = ['branch', '-v']
    if all:
        args.append('-a')
    
    output, code = _run_git(*args)
    
    if code != 0:
        console.print("[red]Not a git repository[/red]")
        return
    
    table = Table(title="Branches")
    table.add_column("", width=2)
    table.add_column("Branch", style="cyan")
    table.add_column("Last Commit", style="yellow")
    table.add_column("Message")
    
    for line in output.strip().split('\n'):
        if not line:
            continue
        
        current = "→" if line.startswith('*') else ""
        line = line.lstrip('* ')
        parts = line.split(None, 2)
        
        if len(parts) >= 2:
            branch = parts[0]
            commit = parts[1]
            message = parts[2] if len(parts) > 2 else ""
            table.add_row(current, branch, commit, message[:50])
    
    console.print(table)


@git.command('cleanup')
@click.option('--dry-run', '-n', is_flag=True, help='Show what would be deleted')
def branch_cleanup(dry_run: bool):
    """Remove merged branches."""
    # Get merged branches
    output, code = _run_git('branch', '--merged', 'main')
    
    if code != 0:
        # Try master if main doesn't exist
        output, code = _run_git('branch', '--merged', 'master')
    
    if code != 0:
        console.print("[red]Could not determine merged branches[/red]")
        return
    
    branches = [
        b.strip() for b in output.strip().split('\n')
        if b.strip() and not b.strip().startswith('*')
        and b.strip() not in ('main', 'master', 'develop')
    ]
    
    if not branches:
        console.print("[green]No merged branches to clean up[/green]")
        return
    
    console.print(f"Found {len(branches)} merged branches:")
    for branch in branches:
        console.print(f"  [yellow]{branch}[/yellow]")
    
    if dry_run:
        console.print("\n[blue](dry run - no branches deleted)[/blue]")
    else:
        if click.confirm("\nDelete these branches?"):
            for branch in branches:
                _run_git('branch', '-d', branch)
                console.print(f"  [red]Deleted[/red] {branch}")
PreviousNext