diff options
| author | Danilo M. <danix@danix.xyz> | 2026-06-24 09:41:43 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-06-24 09:41:43 +0200 |
| commit | eeaba25642392532901cd4c95c23054e9a7ba2b3 (patch) | |
| tree | fef19a8b397ccdbaf1c828c3a1c268730e36a698 /gitctl-helper | |
| parent | 23bf41452709a632a7f1afe2d78551da767d8d56 (diff) | |
| download | gitctl-eeaba25642392532901cd4c95c23054e9a7ba2b3.tar.gz gitctl-eeaba25642392532901cd4c95c23054e9a7ba2b3.zip | |
feat: repo list - list all repos, public and private
New gitctl repo list / helper list-repos verb. Source of truth is the
bare repos on disk (REPO_BASE/*.git), not cgitrc, so private repos
(absent from cgitrc since cgit auto-discovery is off) show too. Each
repo is tagged PUB/PRIV by cgitrc membership, with its section and a
60-char description from the repo's description file. Client renders an
aligned table, green public / dim private on a TTY.
Adds repo_sections_from() primitive; find_repo_section becomes a thin
lookup over it. Self-tests cover the cgitrc parse and a temp-FS scan.
Updates README, SKILL.md, bash completion (repo list action), TODO.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'gitctl-helper')
| -rwxr-xr-x | gitctl-helper | 95 |
1 files changed, 90 insertions, 5 deletions
diff --git a/gitctl-helper b/gitctl-helper index 8cdf460..6c6dbc3 100755 --- a/gitctl-helper +++ b/gitctl-helper @@ -65,8 +65,12 @@ def list_sections_from(lines): return out -def find_repo_section(lines, url): - """Return the section a repo.url= belongs to, or None if absent.""" +def repo_sections_from(lines): + """Map every repo.url= in cgitrc to the section= it falls under (file order). + + cgitrc lists only the repos exposed to the web (cgit auto-discovery is off), + so membership here is exactly what makes a repo public.""" + out = {} current = None for line in lines: s = SECTION_RE.match(line.rstrip("\n")) @@ -74,9 +78,46 @@ def find_repo_section(lines, url): current = s.group(1) continue u = URL_LINE_RE.match(line.rstrip("\n")) - if u and u.group(1) == url: - return current - return None + if u: + out[u.group(1)] = current + return out + + +def find_repo_section(lines, url): + """Return the section a repo.url= belongs to, or None if absent.""" + return repo_sections_from(lines).get(url) + + +DESC_MAX = 60 + + +def read_description(url): + """First line of the bare repo's description file, truncated. '' if missing + or the gitolite placeholder.""" + try: + with open(os.path.join(repo_path_for(url), "description")) as f: + d = f.readline().strip() + except OSError: + return "" + if d.startswith("Unnamed repository"): + return "" + return d[:DESC_MAX - 1] + "…" if len(d) > DESC_MAX else d + + +def list_repos(repo_base, cgitrc_lines): + """Every bare repo under repo_base (the full set, public and private), each + tagged public/private and with its section + description. Returns a list of + (name, public_bool, section, desc) sorted by name.""" + public = repo_sections_from(cgitrc_lines) + out = [] + for entry in sorted(os.listdir(repo_base)): + if not entry.endswith(".git"): + continue + if not os.path.isdir(os.path.join(repo_base, entry)): + continue + name = entry[:-4] + out.append((name, name in public, public.get(name) or "", read_description(name))) + return out def repo_path_for(url): @@ -212,6 +253,37 @@ def _self_test(): assert find_repo_section(fixture, "cad-projects") == "Generic Projects" assert find_repo_section(fixture, "my-slackbuilds") == "SlackBuilds" assert find_repo_section(fixture, "nope") is None + assert repo_sections_from(fixture) == { + "cad-projects": "Generic Projects", + "my-slackbuilds": "SlackBuilds", + } + + # list_repos: scans a fake REPO_BASE, tags public/private by cgitrc and + # reads each description file. cad-projects is in the fixture (public), + # secret-stuff is not (private). + import tempfile + rb = tempfile.mkdtemp() + for n, d in [("cad-projects", "CAD work"), ("secret-stuff", "")]: + os.mkdir(os.path.join(rb, n + ".git")) + if d: + open(os.path.join(rb, n + ".git", "description"), "w").write(d + "\n") + open(os.path.join(rb, "not-a-repo"), "w").write("ignore me\n") # non-.git skipped + # repo_path_for/read_description read from the real REPO_BASE; point it here. + global REPO_BASE + saved_base, REPO_BASE = REPO_BASE, rb + rows = list_repos(rb, fixture) + REPO_BASE = saved_base + assert rows == [ + ("cad-projects", True, "Generic Projects", "CAD work"), + ("secret-stuff", False, "", ""), + ], rows + # truncation: a long description is cut to DESC_MAX chars (ellipsis included). + os.mkdir(os.path.join(rb, "wordy.git")) + open(os.path.join(rb, "wordy.git", "description"), "w").write("x" * 100 + "\n") + REPO_BASE = rb + long = read_description("wordy") + REPO_BASE = saved_base + assert len(long) == DESC_MAX and long.endswith("…"), long block = build_repo_block("publisher", "by Your Name") assert block == [ @@ -390,6 +462,13 @@ def main(): "push until gitolite has built the bare repo.") re_.add_argument("--url", required=True, help="repo name to test for (as <url>.git)") + sub.add_parser("list-repos", + help="list every bare repo, tagged public/private", + description="Scan the repository base for all bare repos (the full " + "set, not just the cgit-exposed ones), tag each public or " + "private by cgitrc membership, and print one " + "name<TAB>PUB|PRIV<TAB>section<TAB>description line each.") + a = p.parse_args(argv) if a.verb == "list-sections": @@ -413,6 +492,12 @@ def main(): if a.verb == "repo-exists": validate_url(a.url) return 0 if os.path.isdir(repo_path_for(a.url)) else 1 + if a.verb == "list-repos": + with open(CGITRC) as f: + cgit = f.readlines() + for name, pub, section, desc in list_repos(REPO_BASE, cgit): + print(f"{name}\t{'PUB' if pub else 'PRIV'}\t{section}\t{desc}") + return 0 return 1 |
