200 lines
6.4 KiB
Python
200 lines
6.4 KiB
Python
"""flow sync — check git sync status of all projects."""
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
from flow.core.config import FlowContext
|
|
|
|
|
|
def register(subparsers):
|
|
p = subparsers.add_parser("sync", help="Git sync tools for projects")
|
|
sub = p.add_subparsers(dest="sync_command")
|
|
|
|
check = sub.add_parser("check", help="Check all projects status")
|
|
check.add_argument(
|
|
"--fetch",
|
|
dest="fetch",
|
|
action="store_true",
|
|
help="Run git fetch before checking (default)",
|
|
)
|
|
check.add_argument(
|
|
"--no-fetch",
|
|
dest="fetch",
|
|
action="store_false",
|
|
help="Skip git fetch",
|
|
)
|
|
check.set_defaults(fetch=True)
|
|
check.set_defaults(handler=run_check)
|
|
|
|
fetch = sub.add_parser("fetch", help="Fetch all project remotes")
|
|
fetch.set_defaults(handler=run_fetch)
|
|
|
|
summary = sub.add_parser("summary", help="Quick overview of project status")
|
|
summary.set_defaults(handler=run_summary)
|
|
|
|
p.set_defaults(handler=lambda ctx, args: p.print_help())
|
|
|
|
|
|
def _git(repo: str, *cmd, capture: bool = True) -> subprocess.CompletedProcess:
|
|
return subprocess.run(
|
|
["git", "-C", repo] + list(cmd),
|
|
capture_output=capture, text=True,
|
|
)
|
|
|
|
|
|
def _check_repo(repo_path: str, do_fetch: bool = True):
|
|
"""Check a single repo, return (name, issues list)."""
|
|
name = os.path.basename(repo_path)
|
|
git_dir = os.path.join(repo_path, ".git")
|
|
if not os.path.isdir(git_dir):
|
|
return name, None # Not a git repo
|
|
|
|
issues = []
|
|
|
|
if do_fetch:
|
|
fetch_result = _git(repo_path, "fetch", "--all", "--quiet")
|
|
if fetch_result.returncode != 0:
|
|
issues.append("git fetch failed")
|
|
|
|
# Current branch
|
|
result = _git(repo_path, "rev-parse", "--abbrev-ref", "HEAD")
|
|
branch = result.stdout.strip() if result.returncode == 0 else "HEAD"
|
|
|
|
# Uncommitted changes
|
|
diff_result = _git(repo_path, "diff", "--quiet")
|
|
cached_result = _git(repo_path, "diff", "--cached", "--quiet")
|
|
if diff_result.returncode != 0 or cached_result.returncode != 0:
|
|
issues.append("uncommitted changes")
|
|
else:
|
|
untracked = _git(repo_path, "ls-files", "--others", "--exclude-standard")
|
|
if untracked.stdout.strip():
|
|
issues.append("untracked files")
|
|
|
|
# Unpushed commits
|
|
upstream_check = _git(repo_path, "rev-parse", "--abbrev-ref", f"{branch}@{{u}}")
|
|
if upstream_check.returncode == 0:
|
|
unpushed = _git(repo_path, "rev-list", "--oneline", f"{branch}@{{u}}..{branch}")
|
|
if unpushed.stdout.strip():
|
|
count = len(unpushed.stdout.strip().split("\n"))
|
|
issues.append(f"{count} unpushed commit(s) on {branch}")
|
|
else:
|
|
issues.append(f"no upstream for {branch}")
|
|
|
|
# Unpushed branches
|
|
branches_result = _git(repo_path, "for-each-ref", "--format=%(refname:short)", "refs/heads")
|
|
for b in branches_result.stdout.strip().split("\n"):
|
|
if not b or b == branch:
|
|
continue
|
|
up = _git(repo_path, "rev-parse", "--abbrev-ref", f"{b}@{{u}}")
|
|
if up.returncode == 0:
|
|
ahead = _git(repo_path, "rev-list", "--count", f"{b}@{{u}}..{b}")
|
|
if ahead.stdout.strip() != "0":
|
|
issues.append(f"branch {b}: {ahead.stdout.strip()} ahead")
|
|
else:
|
|
issues.append(f"branch {b}: no upstream")
|
|
|
|
return name, issues
|
|
|
|
|
|
def run_check(ctx: FlowContext, args):
|
|
projects_dir = os.path.expanduser(ctx.config.projects_dir)
|
|
if not os.path.isdir(projects_dir):
|
|
ctx.console.error(f"Projects directory not found: {projects_dir}")
|
|
sys.exit(1)
|
|
|
|
rows = []
|
|
needs_action = []
|
|
not_git = []
|
|
checked = 0
|
|
|
|
for entry in sorted(os.listdir(projects_dir)):
|
|
repo_path = os.path.join(projects_dir, entry)
|
|
if not os.path.isdir(repo_path):
|
|
continue
|
|
|
|
name, issues = _check_repo(repo_path, do_fetch=args.fetch)
|
|
if issues is None:
|
|
not_git.append(name)
|
|
continue
|
|
checked += 1
|
|
if issues:
|
|
needs_action.append(name)
|
|
rows.append([name, "; ".join(issues)])
|
|
else:
|
|
rows.append([name, "clean and synced"])
|
|
|
|
if checked == 0:
|
|
ctx.console.info("No git repositories found in projects directory.")
|
|
if not_git:
|
|
ctx.console.info(f"Skipped non-git directories: {', '.join(sorted(not_git))}")
|
|
return
|
|
|
|
ctx.console.table(["PROJECT", "STATUS"], rows)
|
|
|
|
if needs_action:
|
|
ctx.console.warn(f"Projects needing action: {', '.join(sorted(needs_action))}")
|
|
else:
|
|
ctx.console.success("All repositories clean and synced.")
|
|
|
|
if not_git:
|
|
ctx.console.info(f"Skipped non-git directories: {', '.join(sorted(not_git))}")
|
|
|
|
|
|
def run_fetch(ctx: FlowContext, args):
|
|
projects_dir = os.path.expanduser(ctx.config.projects_dir)
|
|
if not os.path.isdir(projects_dir):
|
|
ctx.console.error(f"Projects directory not found: {projects_dir}")
|
|
sys.exit(1)
|
|
|
|
had_error = False
|
|
fetched = 0
|
|
for entry in sorted(os.listdir(projects_dir)):
|
|
repo_path = os.path.join(projects_dir, entry)
|
|
if not os.path.isdir(os.path.join(repo_path, ".git")):
|
|
continue
|
|
ctx.console.info(f"Fetching {entry}...")
|
|
result = _git(repo_path, "fetch", "--all", "--quiet")
|
|
fetched += 1
|
|
if result.returncode != 0:
|
|
ctx.console.error(f"Failed to fetch {entry}")
|
|
had_error = True
|
|
|
|
if fetched == 0:
|
|
ctx.console.info("No git repositories found in projects directory.")
|
|
return
|
|
|
|
if had_error:
|
|
sys.exit(1)
|
|
|
|
ctx.console.success("All remotes fetched.")
|
|
|
|
|
|
def run_summary(ctx: FlowContext, args):
|
|
projects_dir = os.path.expanduser(ctx.config.projects_dir)
|
|
if not os.path.isdir(projects_dir):
|
|
ctx.console.error(f"Projects directory not found: {projects_dir}")
|
|
sys.exit(1)
|
|
|
|
headers = ["PROJECT", "STATUS"]
|
|
rows = []
|
|
|
|
for entry in sorted(os.listdir(projects_dir)):
|
|
repo_path = os.path.join(projects_dir, entry)
|
|
if not os.path.isdir(repo_path):
|
|
continue
|
|
|
|
name, issues = _check_repo(repo_path, do_fetch=False)
|
|
if issues is None:
|
|
rows.append([name, "not a git repo"])
|
|
elif issues:
|
|
rows.append([name, "; ".join(issues)])
|
|
else:
|
|
rows.append([name, "clean"])
|
|
|
|
if not rows:
|
|
ctx.console.info("No projects found.")
|
|
return
|
|
|
|
ctx.console.table(headers, rows)
|