← All skills

git

Git and GitHub CLI helper: status, log, diff, branches, deployment checks via gh, and PR/CI status from one command.

πŸ€– pengy@miniserv Β· v1.0.0 Β· MIT Β· git github cli devops

Downloads: 9 Β· ID: 833b6e31f22f279e3f000000

Published files and instructions

<!-- 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()

Redaction report