git
Git and GitHub CLI helper: status, log, diff, branches, deployment checks via gh, and PR/CI status from one command.
Downloads: 9 Β· ID: 833b6e31f22f279e3f000000
Git and GitHub CLI helper: status, log, diff, branches, deployment checks via gh, and PR/CI status from one command.
Downloads: 9 Β· ID: 833b6e31f22f279e3f000000
<!-- FILE: git_skill.md -->
# Git Skill β Repository & Deployment Management
Wraps `git` and `gh` (GitHub CLI) for structured repo operations and deployment status checks.
## Quick Start
```bash
# All commands via the helper script:
uv run git_ops.py <command> [args...]
```
Or use `run_bash` with raw `git`/`gh` commands (the script just provides structured output).
## Commands
### Status β current branch + dirty files + ahead/behind
```bash
uv run git_ops.py status /path/to/repo
```
### Log β recent commits (default: 10)
```bash
uv run git_ops.py log /path/to/repo [--count 20]
```
### Diff β unstaged changes summary
```bash
uv run git_ops.py diff /path/to/repo [--staged]
```
### Branches β list local + remote branches
```bash
uv run git_ops.py branches /path/to/repo
```
### Deployment β check GitHub Actions deployment status
```bash
uv run git_ops.py deploy <owner/repo> [--branch main] [--limit 5]
```
### PR status β open PRs for a repo
```bash
uv run git_ops.py prs <owner/repo> [--limit 10]
```
### CI status β latest workflow run status
```bash
uv run git_ops.py ci <owner/repo> [--branch main]
```
### Quick summary β all of the above in one shot
```bash
uv run git_ops.py summary /path/to/repo
```
## Environment
- `GH_TOKEN` or `GITHUB_TOKEN` β for `gh`-based commands (deploy, prs, ci)
- Otherwise uses `gh`'s built-in auth (oauth token stored by `gh auth login`)
## Notes
- For local repos, provide the filesystem path.
- For GitHub queries (deploy, prs, ci), provide `owner/repo` slug.
- The script auto-detects whether a path looks like a local directory vs a GitHub slug.
- When in doubt, use `summary` β it runs status + log + branches in one call.
<!-- FILE: git_ops.py -->
#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.10"
# dependencies = []
# ///
"""Git & GitHub CLI helper β structured output for common repo operations.
Usage:
uv run git_ops.py status <path>
uv run git_ops.py log <path> [--count N]
uv run git_ops.py diff <path> [--staged]
uv run git_ops.py branches <path>
uv run git_ops.py deploy <owner/repo> [--branch main] [--limit N]
uv run git_ops.py prs <owner/repo> [--limit N]
uv run git_ops.py ci <owner/repo> [--branch main]
uv run git_ops.py summary <path>
"""
import argparse, json, os, subprocess, sys
from pathlib import Path
from datetime import datetime
def _ensure_uv():
try:
subprocess.run(["uv", "--version"], capture_output=True, check=True, timeout=5)
except (FileNotFoundError, subprocess.CalledProcessError):
print("ERROR: 'uv' (https://docs.astral.sh/uv/) is required.", file=sys.stderr)
print("Install: curl -LsSf https://astral.sh/uv/install.sh | sh", file=sys.stderr)
sys.exit(1)
except subprocess.TimeoutExpired:
pass
_ensure_uv()
def _run(cmd, cwd=None, timeout=30):
"""Run a command, return (stdout, stderr, returncode)."""
try:
r = subprocess.run(cmd, capture_output=True, text=True,
cwd=cwd, timeout=timeout)
return r.stdout.strip(), r.stderr.strip(), r.returncode
except FileNotFoundError:
tool = cmd[0]
if tool == "gh":
return "", (
f"Error: 'gh' (GitHub CLI) not found.\n\n"
f"Install it:\n"
f" sudo apt install gh\n\n"
f"Then authenticate:\n"
f" gh auth login\n\n"
f"Or set GH_TOKEN or GITHUB_TOKEN in your environment."
), -1
if tool == "git":
return "", (
f"Error: 'git' not found.\n\n"
f"Install it:\n"
f" sudo apt install git"
), -1
return "", f"Error: '{tool}' not found. Is it installed?", -1
except subprocess.TimeoutExpired:
return "", f"Error: command timed out after {timeout}s", -1
def _check_gh():
"""Verify gh is installed and authenticated. Returns error string or None."""
# Check installed
_, err, rc = _run(["gh", "--version"])
if rc != 0:
return err # _run already produced a good error message for 'gh' not found
# Check authenticated
out, err, rc = _run(["gh", "auth", "status"])
if rc != 0:
return (
f"β GitHub CLI is installed but not authenticated.\n\n"
f"Run this on the machine:\n"
f" gh auth login\n\n"
f"Or set the GH_TOKEN or GITHUB_TOKEN environment variable."
)
return None
def cmd_status(args):
"""Show current branch, dirty files, ahead/behind."""
path = args.path
# Current branch
branch, err, rc = _run(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=path)
if rc != 0:
return f"β Not a git repository: {path}\n{err}"
lines = [f"π Branch: {branch}"]
# Dirty status
dirty, _, _ = _run(["git", "status", "--porcelain"], cwd=path)
if dirty:
files = dirty.split("\n")
lines.append(f"π Uncommitted changes: {len(files)} file(s)")
for f in files[:20]:
flag = f[:2].strip()
name = f[3:]
lines.append(f" {flag} {name}")
if len(files) > 20:
lines.append(f" ... and {len(files)-20} more")
else:
lines.append("β
Working tree clean")
# Ahead/behind
ahead, _, _ = _run(["git", "rev-list", "--count", f"origin/{branch}..HEAD"], cwd=path)
behind, _, _ = _run(["git", "rev-list", "--count", f"HEAD..origin/{branch}"], cwd=path)
a = int(ahead) if ahead.isdigit() else 0
b = int(behind) if behind.isdigit() else 0
if a or b:
parts = []
if a: parts.append(f"{a} ahead")
if b: parts.append(f"{b} behind")
lines.append(f"π origin/{branch}: {', '.join(parts)}")
else:
lines.append(f"β
Up to date with origin/{branch}")
return "\n".join(lines)
def cmd_log(args):
"""Show recent commits."""
count = args.count
path = args.path
fmt = "--format=%hβ%anβ%arβ%s"
out, err, rc = _run(
["git", "log", f"-{count}", fmt, "--no-merges"], cwd=path)
if rc != 0:
return f"β Error: {err}"
if not out:
return "No commits found."
lines = [f"π Last {count} commits:"]
for commit in out.split("\n"):
parts = commit.split("β", 3)
if len(parts) == 4:
lines.append(f" {parts[0]} {parts[3]}")
lines.append(f" by {parts[1]}, {parts[2]}")
return "\n".join(lines)
def cmd_diff(args):
"""Show diff summary."""
path = args.path
cmd = ["git", "diff", "--stat"]
if args.staged:
cmd.append("--cached")
out, err, rc = _run(cmd, cwd=path)
if rc != 0:
return f"β Error: {err}"
if not out:
return "No changes."
header = "π Staged changes:" if args.staged else "π Unstaged changes:"
return f"{header}\n{out}"
def cmd_branches(args):
"""List branches."""
path = args.path
out, err, rc = _run(
["git", "branch", "-a", "--sort=-committerdate"], cwd=path)
if rc != 0:
return f"β Error: {err}"
if not out:
return "No branches."
current, _, _ = _run(
["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=path)
lines = ["πΏ Branches:"]
for b in out.split("\n"):
b = b.strip()
if b == current:
lines.append(f" * {b} β current")
else:
lines.append(f" {b}")
return "\n".join(lines)
def _gh_run(args_list, timeout=30):
"""Run gh command, return (stdout, stderr, rc)."""
return _run(["gh"] + args_list, timeout=timeout)
def cmd_deploy(args):
"""Check deployment status via gh."""
err = _check_gh()
if err:
return err
repo = args.repo
branch = args.branch
limit = args.limit
out, err, rc = _gh_run([
"run", "list", "--repo", repo,
"--branch", branch,
"--event", "push",
f"--limit={limit}",
"--json", "databaseId,conclusion,headBranch,displayTitle,createdAt,url,status"
])
if rc != 0:
return f"β gh error: {err}\n\nMake sure you're authenticated: `gh auth status`"
try:
runs = json.loads(out)
except json.JSONDecodeError:
return f"β Could not parse gh output:\n{out}"
if not runs:
return f"No workflow runs found for {repo} on branch '{branch}'."
lines = [f"π Deployments for {repo} ({branch}):"]
for r in runs:
status = r.get("conclusion", r.get("status", "unknown"))
created = r.get("createdAt", "")
title = r.get("displayTitle", "")
url = r.get("url", "")
# Format timestamp
try:
dt = datetime.fromisoformat(created.replace("Z", "+00:00"))
created = dt.strftime("%b %d %H:%M")
except:
pass
icon = {"success": "β
", "failure": "β", "cancelled": "βΉοΈ",
"in_progress": "π", "pending": "β³", "skipped": "βοΈ",
"timed_out": "β°"}.get(status, "β")
lines.append(f" {icon} {status} β {title}")
lines.append(f" {created}")
lines.append(f" {url}")
return "\n".join(lines)
def cmd_prs(args):
"""List open PRs."""
err = _check_gh()
if err:
return err
repo = args.repo
limit = args.limit
out, err, rc = _gh_run([
"pr", "list", "--repo", repo,
f"--limit={limit}",
"--json", "number,title,author,headRefName,baseRefName,createdAt,state,url"
])
if rc != 0:
return f"β gh error: {err}"
try:
prs = json.loads(out)
except json.JSONDecodeError:
return f"β Could not parse gh output:\n{out}"
if not prs:
return f"No open PRs for {repo}."
lines = [f"π Open PRs for {repo}:"]
for pr in prs:
author = pr.get("author", {}).get("login", "?")
title = pr.get("title", "?")
num = pr.get("number", "?")
head = pr.get("headRefName", "?")
base = pr.get("baseRefName", "?")
lines.append(f" #{num} {title}")
lines.append(f" {head} β {base} by {author}")
return "\n".join(lines)
def cmd_ci(args):
"""Latest CI status."""
err = _check_gh()
if err:
return err
repo = args.repo
branch = args.branch
out, err, rc = _gh_run([
"run", "list", "--repo", repo,
"--branch", branch,
"--limit=1",
"--json", "conclusion,displayTitle,createdAt,url,status"
])
if rc != 0:
return f"β gh error: {err}"
try:
runs = json.loads(out)
except json.JSONDecodeError:
return f"β Could not parse gh output:\n{out}"
if not runs:
return f"No CI runs found for {repo} on {branch}."
r = runs[0]
status = r.get("conclusion", r.get("status", "unknown"))
title = r.get("displayTitle", "")
url = r.get("url", "")
created = r.get("createdAt", "")
try:
dt = datetime.fromisoformat(created.replace("Z", "+00:00"))
created = dt.strftime("%b %d %H:%M")
except:
pass
icon = {"success": "β
", "failure": "β", "cancelled": "βΉοΈ",
"in_progress": "π", "pending": "β³"}.get(status, "β")
return f"π Latest CI: {icon} {status}\n {title}\n {created}\n {url}"
def cmd_summary(args):
"""Run status + log + branches in one shot."""
parts = []
parts.append("=" * 50)
parts.append("π GIT SUMMARY")
parts.append("=" * 50)
parts.append("")
parts.append(cmd_status(args))
parts.append("")
parts.append(cmd_log(args))
parts.append("")
parts.append(cmd_branches(args))
return "\n".join(parts)
def main():
p = argparse.ArgumentParser(description="Git & GitHub CLI helper")
sub = p.add_subparsers(dest="command", required=True)
# status
sp = sub.add_parser("status", help="Current branch + dirty files + ahead/behind")
sp.add_argument("path", help="Path to git repo")
sp.set_defaults(func=cmd_status)
# log
sp = sub.add_parser("log", help="Recent commits")
sp.add_argument("path", help="Path to git repo")
sp.add_argument("--count", type=int, default=10, help="Number of commits")
sp.set_defaults(func=cmd_log)
# diff
sp = sub.add_parser("diff", help="Diff summary")
sp.add_argument("path", help="Path to git repo")
sp.add_argument("--staged", action="store_true", help="Show staged changes")
sp.set_defaults(func=cmd_diff)
# branches
sp = sub.add_parser("branches", help="List branches")
sp.add_argument("path", help="Path to git repo")
sp.set_defaults(func=cmd_branches)
# deploy
sp = sub.add_parser("deploy", help="Check deployment status (gh)")
sp.add_argument("repo", help="owner/repo slug")
sp.add_argument("--branch", default="main", help="Branch to check")
sp.add_argument("--limit", type=int, default=5, help="Runs to show")
sp.set_defaults(func=cmd_deploy)
# prs
sp = sub.add_parser("prs", help="List open PRs (gh)")
sp.add_argument("repo", help="owner/repo slug")
sp.add_argument("--limit", type=int, default=10)
sp.set_defaults(func=cmd_prs)
# ci
sp = sub.add_parser("ci", help="Latest CI status (gh)")
sp.add_argument("repo", help="owner/repo slug")
sp.add_argument("--branch", default="main")
sp.set_defaults(func=cmd_ci)
# summary
sp = sub.add_parser("summary", help="Full repo summary")
sp.add_argument("path", help="Path to git repo")
sp.set_defaults(func=cmd_summary)
a = p.parse_args()
result = a.func(a)
print(result)
if __name__ == "__main__":
main()