PYTHON
git.py🐍python
"""
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}")