Files
cleveragents-core/tools/forgejo-sync.py
T
drew 0bc734c020 style: ruff format the controller-state-machine branch (288 files)
Applies `ruff format` to the accumulated formatting debt on this branch.
Formatting-only — no behavioral changes. Required for CI/lint's format
gate (`nox -s format -- --check`), which the branch was failing on 288
tracked files that drifted from ruff's canonical style.

In-progress WIP files are intentionally excluded so this commit stays a
clean formatting-only diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 00:09:17 -04:00

126 lines
3.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""Sync the local Forgejo cache at ``tools/.cache/forgejo.sqlite``.
This tool is the human-facing wrapper around ``_pipeline_cache.PipelineCache``.
On a cold cache it pulls the full master commit history and every PR the
repo has ever had. On a warm cache it pulls only deltas (new commits since
last sync, PRs whose ``updated_at`` has advanced).
Usage
-----
Typical incremental refresh::
python3 tools/forgejo-sync.py
Seed a fresh cache from scratch (destructive)::
python3 tools/forgejo-sync.py --full
After any sync, back-fill ``merged_by`` details for every merged PR so
downstream reports don't have to fetch per-PR detail on read::
python3 tools/forgejo-sync.py --backfill
Show cache statistics::
python3 tools/forgejo-sync.py --stats
The cache file lives under ``tools/.cache/`` and is gitignored.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
# Ensure we can import the sibling cache module regardless of cwd.
sys.path.insert(0, str(Path(__file__).resolve().parent))
from _pipeline_cache import ( # noqa: E402
PipelineCache,
DEFAULT_CACHE_PATH,
)
def _load_token() -> str:
token = os.environ.get("GITEA_TOKEN")
if token:
return token.strip().strip('"').strip("'")
repo_root = Path(__file__).resolve().parent.parent
for envfile in (repo_root / ".devcontainer" / ".env", repo_root / ".env"):
if not envfile.is_file():
continue
for line in envfile.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, _, v = line.partition("=")
if k.strip() == "GITEA_TOKEN":
return v.strip().strip('"').strip("'")
sys.exit(
"ERROR: GITEA_TOKEN not found. Set it in the environment "
"or in .devcontainer/.env (or .env)."
)
def _parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(
description="Sync the local Forgejo cache (SQLite at tools/.cache/forgejo.sqlite).",
)
p.add_argument(
"--full",
action="store_true",
help="Truncate commits & pulls tables and resync from scratch. "
"Use after a force-push to master or to rebuild from clean state.",
)
p.add_argument(
"--backfill",
action="store_true",
help="After syncing, fetch PR detail for every closed+merged PR missing "
"merged_by (one-shot per PR, permanent). Adds a few minutes on a "
"cold cache but makes subsequent reports offline-instant.",
)
p.add_argument(
"--stats",
action="store_true",
help="Print cache statistics and exit; do not sync.",
)
p.add_argument(
"--cache",
type=Path,
default=DEFAULT_CACHE_PATH,
help=f"Cache file path (default: {DEFAULT_CACHE_PATH}).",
)
p.add_argument(
"--quiet",
action="store_true",
help="Suppress progress logs; summary JSON is still printed.",
)
return p.parse_args()
def main() -> None:
args = _parse_args()
cache = PipelineCache.open(args.cache)
if args.stats:
print(json.dumps(cache.stats(), indent=2))
return
token = _load_token()
summary = cache.sync(
token,
full=args.full,
backfill_details=args.backfill,
progress=not args.quiet,
)
summary["cache"] = cache.stats()
print(json.dumps(summary, indent=2))
if __name__ == "__main__":
main()