Create cece_git CLI for repo status checks (#106)

Implements a standalone Python CLI tool that provides instant git repo
status from any terminal without dependencies on Warp or QLM.

Features:
- Two modes: 'status' (detailed) and 'summary' (compact one-liner)
- Shows local vs remote HEAD comparison (origin/main)
- Reports ahead/behind counts for branch drift
- Indicates dirty/clean working tree state
- Works from any directory with --path/-C flag
- No external dependencies (stdlib only)

Usage examples:
python -m cece_git status python -m cece_git summary python -m cece_git
status --path /path/to/repo

This is the foundation for Operator awareness of repo reality. Future
enhancements can add optional QLM event logging and integration with the
GitConnector.

# Pull Request

## Description

<!-- Provide a brief description of the changes in this PR -->

## Type of Change

<!-- Mark the relevant option with an 'x' -->

- [ ] 📝 Documentation update
- [ ] 🧪 Tests only
- [ ] 🏗️ Scaffolding/stubs
- [ ]  New feature
- [ ] 🐛 Bug fix
- [ ] ♻️ Refactoring
- [ ] ⚙️ Infrastructure/CI
- [ ] 📦 Dependencies update
- [ ] 🔒 Security fix
- [ ] 💥 Breaking change

## Checklist

<!-- Mark completed items with an 'x' -->

- [ ] Code follows the project's style guidelines
- [ ] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes

## Auto-Merge Eligibility

<!-- This section helps determine if this PR qualifies for auto-merge
-->

**Eligible for auto-merge?**
- [ ] Yes - This is a docs-only, tests-only, or small AI-generated PR
- [ ] No - Requires human review

**Reason for auto-merge eligibility:**
- [ ] Docs-only (Tier 1)
- [ ] Tests-only (Tier 2)
- [ ] Scaffolding < 200 lines (Tier 3)
- [ ] AI-generated < 500 lines (Tier 4)
- [ ] Dependency patch/minor (Tier 5)

**If not auto-merge eligible, why?**
- [ ] Breaking change
- [ ] Security-related
- [ ] Infrastructure changes
- [ ] Requires discussion
- [ ] Large PR (> 500 lines)

## Related Issues

<!-- Link to related issues -->

Closes #
Related to #

## Test Plan

<!-- Describe how you tested these changes -->

## Screenshots (if applicable)

<!-- Add screenshots for UI changes -->

---

**Note**: This PR will be automatically labeled based on files changed.
See `GITHUB_AUTOMATION_RULES.md` for details.

If this PR meets auto-merge criteria (see `AUTO_MERGE_POLICY.md`), it
will be automatically approved and merged after checks pass.

For questions about the merge queue system, see `MERGE_QUEUE_PLAN.md`.
This commit is contained in:
Alexa Amundson
2025-11-18 04:43:59 -06:00
committed by GitHub

203
cece_git.py Executable file
View File

@@ -0,0 +1,203 @@
#!/usr/bin/env python3
"""
cece_git: Operator-friendly git reality helper for BlackRoad-Operating-Systems.
Usage examples (from repo root):
python -m cece_git status
python -m cece_git summary
This CLI does NOT require Warp and does NOT depend on QLM.
It simply reports git state in a way that makes sense to the Operator.
"""
from __future__ import annotations
import argparse
import subprocess
from dataclasses import dataclass
from typing import Optional
@dataclass
class GitHead:
ref: str
sha: str
subject: str
@dataclass
class GitStatusSummary:
local: Optional[GitHead]
remote: Optional[GitHead]
branch: Optional[str]
ahead: int
behind: int
dirty: bool
repo_path: str
def _run(cmd: list[str], cwd: str = ".") -> str:
"""Run a shell command and return stdout, or raise RuntimeError."""
try:
out = subprocess.check_output(cmd, cwd=cwd, stderr=subprocess.STDOUT)
return out.decode("utf-8").strip()
except subprocess.CalledProcessError as e:
raise RuntimeError(f"Command failed: {' '.join(cmd)}\n{e.output.decode('utf-8')}") from e
def _get_current_branch(cwd: str = ".") -> Optional[str]:
try:
return _run(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=cwd)
except RuntimeError:
return None
def _get_head(ref: str, cwd: str = ".") -> Optional[GitHead]:
"""
Get the HEAD info for a given ref (e.g. 'HEAD' or 'origin/main').
Returns None if the ref doesn't exist.
"""
try:
fmt = "%H|%s"
out = _run(["git", "log", "-1", f"--pretty=format:{fmt}", ref], cwd=cwd)
sha, subject = out.split("|", 1)
return GitHead(ref=ref, sha=sha, subject=subject)
except Exception:
return None
def _get_ahead_behind(branch: str, upstream: str, cwd: str = ".") -> tuple[int, int]:
"""
Return (ahead, behind) between branch and upstream.
If upstream doesn't exist, returns (0, 0).
"""
try:
out = _run(["git", "rev-list", "--left-right", "--count", f"{upstream}...{branch}"], cwd=cwd)
behind_str, ahead_str = out.split()
behind = int(behind_str)
ahead = int(ahead_str)
return ahead, behind
except Exception:
return (0, 0)
def _is_dirty(cwd: str = ".") -> bool:
"""True if working tree has uncommitted changes."""
try:
out = _run(["git", "status", "--porcelain"], cwd=cwd)
return bool(out.strip())
except RuntimeError:
return False
def get_git_status_summary(cwd: str = ".") -> GitStatusSummary:
"""Collect a summary of git reality for the current repo."""
branch = _get_current_branch(cwd=cwd)
local_head = _get_head("HEAD", cwd=cwd)
remote_head = _get_head("origin/main", cwd=cwd)
dirty = _is_dirty(cwd=cwd)
ahead = behind = 0
if branch and remote_head is not None:
ahead, behind = _get_ahead_behind(branch, "origin/main", cwd=cwd)
return GitStatusSummary(
local=local_head,
remote=remote_head,
branch=branch,
ahead=ahead,
behind=behind,
dirty=dirty,
repo_path=cwd,
)
def cmd_status(args: argparse.Namespace) -> None:
summary = get_git_status_summary(cwd=args.path)
print(f"📂 Repo: {summary.repo_path}")
if summary.branch is None:
print("❌ Not a git repository or no current branch.")
return
print(f"🌿 Branch: {summary.branch}")
if summary.local:
print(f" Local HEAD: {summary.local.sha[:7]} {summary.local.subject}")
else:
print(" Local HEAD: (unavailable)")
if summary.remote:
print(f" Remote HEAD: {summary.remote.sha[:7]} {summary.remote.subject}")
else:
print(" Remote HEAD: (origin/main not found)")
if summary.ahead == 0 and summary.behind == 0 and summary.remote:
print("📡 Sync: local is in sync with origin/main")
else:
if summary.remote is None:
print("📡 Sync: no origin/main to compare (or fetch first).")
else:
print(f"📡 Sync: ahead {summary.ahead}, behind {summary.behind}")
print(f"🧼 Working tree: {'DIRTY' if summary.dirty else 'CLEAN'}")
def cmd_summary(args: argparse.Namespace) -> None:
"""Shorter output, more like a single-line Operator ping."""
summary = get_git_status_summary(cwd=args.path)
if summary.branch is None:
print("❌ cece_git: not a git repo here.")
return
parts = []
parts.append(f"{summary.branch}")
if summary.local:
parts.append(f"local {summary.local.sha[:7]}")
if summary.remote:
parts.append(f"origin {summary.remote.sha[:7]}")
drift = []
if summary.ahead:
drift.append(f"{summary.ahead}")
if summary.behind:
drift.append(f"{summary.behind}")
if drift:
parts.append(f"({', '.join(drift)})")
parts.append("DIRTY" if summary.dirty else "clean")
print("cece_git:", " | ".join(parts))
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="cece_git",
description="Cece-flavored git reality helper for BlackRoad repos.",
)
parser.add_argument(
"--path",
"-C",
default=".",
help="Repo path (default: current directory)",
)
subparsers = parser.add_subparsers(dest="command", required=True)
p_status = subparsers.add_parser("status", help="Show detailed local vs origin/main status")
p_status.set_defaults(func=cmd_status)
p_summary = subparsers.add_parser("summary", help="Show a compact status summary")
p_summary.set_defaults(func=cmd_summary)
return parser
def main(argv: Optional[list[str]] = None) -> None:
parser = build_parser()
args = parser.parse_args(argv)
args.func(args)
if __name__ == "__main__":
main()