From b4e638ef4f460a1b20fbd0bc8700c6553b5ff54e Mon Sep 17 00:00:00 2001 From: mpabi Date: Sun, 26 Apr 2026 15:54:09 +0200 Subject: [PATCH] Show token repo and store rows per item --- README.md | 4 +- doc/rvctl.md | 46 ++++++----- doc/tokens.md | 27 ++++--- rvctl.py | 211 +++++++++++++++++++++++++++++++++----------------- 4 files changed, 187 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index 5387d0b..d754c85 100644 --- a/README.md +++ b/README.md @@ -156,8 +156,8 @@ Podstawowe komendy tokenow: ./rvctl tokens stats --repo ~/dev/workspace/rv/series/inf/03 ``` -`tokens scan` wypisuje tabele TSV. Remote bez `LOGIN:TOKEN@` bedzie pokazany -jako `url_kind=plain` i `result=no_credentials`. +`tokens scan` wypisuje waskie tabele TSV. Dla jednego endpointu zobaczysz +osobny wiersz `source=repo` oraz osobny wiersz `source=tokens.json`. ## Fetch i switch diff --git a/doc/rvctl.md b/doc/rvctl.md index c7e639c..1a88b11 100644 --- a/doc/rvctl.md +++ b/doc/rvctl.md @@ -301,19 +301,26 @@ Przelaczniki: Typowy wynik: ```text -summary -repo_rootremotesurlsscannedauth_urlsplain_urlsunsupported_urlsfoundaddedserverstoken_path -...111100111... +context +itemvalue +repo_root... +token_path... -remotes -remoteendpointtypeurl_kinduserresult -r1http://77.90.8.171:3001giteaauthu1added +scan +itemvalue +remotes1 +urls1 + +items +itemsourceendpointremotekindusertokenresultstatus +1repohttp://77.90.8.171:3001r1authu1remoteexistingin_sync +1tokens.jsonhttp://77.90.8.171:3001storeu1t1presentin_sync ``` Jesli remote istnieje, ale ma URL bez `LOGIN:TOKEN@`, wynik bedzie mial -`plain_urls=1`, `auth_urls=0`, `found=0`, `added=0`, a w tabeli `remotes` -pojawi sie `url_kind=plain` i `result=no_credentials`. To znaczy, ze `scan` -sprawdzil remote, ale nie znalazl sekretu do zapisania. +w wierszu `repo` wartosci `kind=plain` i `result=no_credentials`. Jezeli +ten sam endpoint istnieje w `tokens.json`, drugi wiersz tego samego `item` +pokaze `source=tokens.json`, `token=t1` i `result=present`. Przyklad: @@ -370,20 +377,19 @@ Przelaczniki: Typowy wynik: ```text +context +itemvalue repo_root... token_path... -repo_endpoints1 -store_endpoints1 -union_endpoints1 -in_sync0 -repo_only0 -store_only0 -repo_ahead0 -store_ahead1 -diverged0 -endpointtyperepo_remotesrepo_urlsrepo_auth_urlsrepo_plain_urlsrepo_usersrepo_tokensstore_usersstore_tokensrepo_only_tokensstore_only_tokensstatus -http://77.90.8.171:3001giteaorigin101001101store_ahead +items +itemsourceendpointremotekindusertokenresultstatus +1repohttp://77.90.8.171:3001r1plainno_credentialsstore_ahead +1tokens.jsonhttp://77.90.8.171:3001storeu1t1presentstore_ahead + +status +itemvalue +store_ahead1 ``` Przyklad: diff --git a/doc/tokens.md b/doc/tokens.md index f2b56f1..cbed284 100644 --- a/doc/tokens.md +++ b/doc/tokens.md @@ -84,16 +84,25 @@ Dzialanie: - skanuje remote URL-e w repo - zapisuje znalezione tokeny do `tokens.json` - zapisuje je per endpoint serwera -- wypisuje wynik jako tabele TSV: `summary` oraz `remotes` +- wypisuje wynik jako waskie tabele TSV: `context`, `scan` oraz `items` -Kolumny w tabeli `remotes`: +Tabela `items` porownuje dwa zrodla dla tego samego endpointu. Ten sam `item` +moze miec dwa wiersze: -- `remote` - nazwa remote, na przyklad `r1` +- `source=repo` - stan remote URL-i w repo +- `source=tokens.json` - stan lokalnego store tokenow + +Kolumny w tabeli `items`: + +- `item` - numer porownywanego endpointu +- `source` - `repo` albo `tokens.json` - `endpoint` - serwer bez sekretu, na przyklad `http://77.90.8.171:3001` -- `type` - typ serwera, na przyklad `gitea` -- `url_kind` - `auth`, `plain` albo `unsupported` -- `user` - login odczytany z URL, tylko dla `auth` -- `result` - `added`, `existing`, `no_credentials` albo `ignored` +- `remote` - nazwa remote, na przyklad `r1`; tylko dla `source=repo` +- `kind` - `auth`, `plain`, `store` albo `missing` +- `user` - login odczytany z URL albo ze store +- `token` - `remote` dla tokenu w URL albo nazwa tokenu ze store, na przyklad `t1` +- `result` - `added`, `existing`, `present`, `no_credentials` albo `missing` +- `status` - relacja repo do `tokens.json`, na przyklad `in_sync` albo `store_ahead` Przyklad: @@ -133,13 +142,13 @@ Pokazuje statystyki per endpoint serwera i porownuje dwa zrodla: - endpointy znalezione w remote URL-ach repo - endpointy zapisane w `tokens.json` -Wynik ma ten sam model co `tokens read`, ale w formie zbiorczej: +Wynik ma ten sam model porownania co `tokens scan`, ale nie zapisuje zmian: - liczbe endpointow w repo - liczbe endpointow w store - laczna unie endpointow - statusy zgodnosci, na przyklad `in_sync`, `store_ahead`, `repo_ahead` -- tabele endpointow z liczbami tokenow po obu stronach +- tabele `items`, w ktorej repo i `tokens.json` sa osobnymi wierszami tego samego itemu Przyklad: diff --git a/rvctl.py b/rvctl.py index ecfd137..699e755 100644 --- a/rvctl.py +++ b/rvctl.py @@ -619,6 +619,122 @@ def compare_server_entries(repo_entry: dict | None, store_entry: dict | None) -> return "diverged", len(repo_only_pairs), len(store_only_pairs) +def token_name_rows(users_data: dict) -> list[tuple[str, str]]: + rows: list[tuple[str, str]] = [] + for user_name in sorted(users_data): + user_entry = users_data[user_name] + if not isinstance(user_entry, dict): + continue + tokens = user_entry.get("tokens", {}) + if not tokens: + rows.append((str(user_name), "")) + continue + for token_name in sorted(tokens): + rows.append((str(user_name), str(token_name))) + return rows + + +def repo_result_for_entry(repo_entry: dict | None) -> tuple[str, str]: + if repo_entry is None: + return "missing", "missing" + if repo_entry.get("auth_urls", 0): + return "auth", "present" + if repo_entry.get("plain_urls", 0): + return "plain", "no_credentials" + return "empty", "no_credentials" + + +def print_token_context( + repo_root: Path | None, + config: WorkspaceConfig, + repo_servers: dict[str, dict], + store_servers: dict[str, dict], + endpoint_count: int, +) -> None: + print("context") + print("item\tvalue") + print(f"repo_root\t{repo_root or ''}") + print(f"token_path\t{config.token_path}") + print(f"repo_endpoints\t{len(repo_servers)}") + print(f"tokens_endpoints\t{len(store_servers)}") + print(f"items\t{endpoint_count}") + + +def print_status_counts(status_counts: dict[str, int]) -> None: + print("status") + print("item\tvalue") + for status_name in ["in_sync", "repo_only", "store_only", "repo_ahead", "store_ahead", "diverged"]: + print(f"{status_name}\t{status_counts[status_name]}") + + +def print_token_item_rows( + endpoint_names: list[str], + repo_servers: dict[str, dict], + store_servers: dict[str, dict], + scan_rows: list[dict[str, str]] | None = None, +) -> dict[str, int]: + status_names = ["in_sync", "repo_only", "store_only", "repo_ahead", "store_ahead", "diverged"] + status_counts = {status_name: 0 for status_name in status_names} + scan_rows_by_endpoint: dict[str, list[dict[str, str]]] = {} + for row in scan_rows or []: + endpoint = row.get("endpoint", "") + if endpoint: + scan_rows_by_endpoint.setdefault(endpoint, []).append(row) + + print("items") + print("item\tsource\tendpoint\tremote\tkind\tuser\ttoken\tresult\tstatus") + for index, endpoint in enumerate(endpoint_names, start=1): + item_id = str(index) + repo_entry = repo_servers.get(endpoint) + store_entry = store_servers.get(endpoint) + status_name, _, _ = compare_server_entries(repo_entry, store_entry) + if status_name in status_counts: + status_counts[status_name] += 1 + + repo_rows = scan_rows_by_endpoint.get(endpoint, []) + if repo_rows: + for row in repo_rows: + token_label = "remote" if row["url_kind"] == "auth" else "" + print( + f"{item_id}\trepo\t{endpoint}\t{row['remote']}\t{row['url_kind']}\t" + f"{row['user']}\t{token_label}\t{row['result']}\t{status_name}" + ) + elif repo_entry is None: + print(f"{item_id}\trepo\t{endpoint}\t\tmissing\t\t\tmissing\t{status_name}") + else: + repo_kind, repo_result = repo_result_for_entry(repo_entry) + remote_names = ",".join(sorted(repo_entry.get("remotes", set()))) + repo_users = token_name_rows(repo_entry.get("users", {})) + if repo_users: + for user_name, _ in repo_users: + print( + f"{item_id}\trepo\t{endpoint}\t{remote_names}\t{repo_kind}\t" + f"{user_name}\tremote\t{repo_result}\t{status_name}" + ) + else: + print( + f"{item_id}\trepo\t{endpoint}\t{remote_names}\t{repo_kind}\t\t\t" + f"{repo_result}\t{status_name}" + ) + + if store_entry is None: + print(f"{item_id}\ttokens.json\t{endpoint}\t\tmissing\t\t\tmissing\t{status_name}") + continue + + store_users = token_name_rows(store_entry.get("users", {})) + if not store_users: + print(f"{item_id}\ttokens.json\t{endpoint}\t\tstore\t\t\tempty\t{status_name}") + continue + + for user_name, token_name in store_users: + print( + f"{item_id}\ttokens.json\t{endpoint}\t\tstore\t{user_name}\t" + f"{token_name}\tpresent\t{status_name}" + ) + + return status_counts + + def resolve_server_from_store(token_data: dict, endpoint: str | None) -> tuple[str, dict]: servers = token_data.get("servers", {}) if endpoint: @@ -707,27 +823,28 @@ def resolve_repo_argument(repo_arg: str | None) -> Path: def run_tokens_scan(config: WorkspaceConfig, args: argparse.Namespace) -> None: - report = sync_tokens_from_repo(config, resolve_repo_argument(args.repo)) - repo_root = report["repo_root"] - print("summary") - print( - "repo_root\tremotes\turls\tscanned\tauth_urls\tplain_urls\t" - "unsupported_urls\tfound\tadded\tservers\ttoken_path" - ) - print( - f"{repo_root or ''}\t{report['remotes']}\t{report['urls']}\t" - f"{report['scanned']}\t{report['auth_urls']}\t{report['plain_urls']}\t" - f"{report['unsupported_urls']}\t{report['found']}\t{report['added']}\t" - f"{report['servers']}\t{config.token_path}" - ) + repo_path = resolve_repo_argument(args.repo) + report = sync_tokens_from_repo(config, repo_path) + repo_root, repo_servers = collect_repo_server_entries(config, repo_path) + token_data = load_token_store(config) + store_servers = token_data.get("servers", {}) + endpoint_names = sorted(set(repo_servers) | set(store_servers)) + + print_token_context(repo_root, config, repo_servers, store_servers, len(endpoint_names)) print() - print("remotes") - print("remote\tendpoint\ttype\turl_kind\tuser\tresult") - for row in report["remote_rows"]: - print( - f"{row['remote']}\t{row['endpoint']}\t{row['type']}\t" - f"{row['url_kind']}\t{row['user']}\t{row['result']}" - ) + print("scan") + print("item\tvalue") + print(f"remotes\t{report['remotes']}") + print(f"urls\t{report['urls']}") + print(f"scanned\t{report['scanned']}") + print(f"auth_urls\t{report['auth_urls']}") + print(f"plain_urls\t{report['plain_urls']}") + print(f"unsupported_urls\t{report['unsupported_urls']}") + print(f"found\t{report['found']}") + print(f"added\t{report['added']}") + print(f"servers\t{report['servers']}") + print() + print_token_item_rows(endpoint_names, repo_servers, store_servers, report["remote_rows"]) def run_tokens_read(config: WorkspaceConfig, args: argparse.Namespace) -> None: @@ -774,57 +891,11 @@ def run_tokens_stats(config: WorkspaceConfig, args: argparse.Namespace) -> None: raise SystemExit(f"Missing server endpoint in repo/store: {args.server}") endpoint_names = [args.server] - status_names = ["in_sync", "repo_only", "store_only", "repo_ahead", "store_ahead", "diverged"] - status_counts = {status_name: 0 for status_name in status_names} - - print(f"repo_root\t{repo_root or ''}") - print(f"token_path\t{config.token_path}") - print(f"repo_endpoints\t{len(repo_servers)}") - print(f"store_endpoints\t{len(store_servers)}") - print(f"union_endpoints\t{len(endpoint_names)}") - - table_rows: list[list[str]] = [] - for endpoint in endpoint_names: - repo_entry = repo_servers.get(endpoint) - store_entry = store_servers.get(endpoint) - status_name, repo_only_count, store_only_count = compare_server_entries(repo_entry, store_entry) - if status_name in status_counts: - status_counts[status_name] += 1 - - sample_entry = repo_entry or store_entry or {} - repo_users = repo_entry.get("users", {}) if repo_entry else {} - store_users = store_entry.get("users", {}) if store_entry else {} - table_rows.append( - [ - endpoint, - str(sample_entry.get("type", "unknown")), - ",".join(sorted(repo_entry.get("remotes", set()))) if repo_entry else "", - str(repo_entry.get("urls", 0) if repo_entry else 0), - str(repo_entry.get("auth_urls", 0) if repo_entry else 0), - str(repo_entry.get("plain_urls", 0) if repo_entry else 0), - str(count_users(repo_users)), - str(count_server_tokens(repo_entry) if repo_entry else 0), - str(count_users(store_users)), - str(count_server_tokens(store_entry) if store_entry else 0), - str(repo_only_count), - str(store_only_count), - status_name, - ] - ) - - for status_name in status_names: - print(f"{status_name}\t{status_counts[status_name]}") - + print_token_context(repo_root, config, repo_servers, store_servers, len(endpoint_names)) print() - print( - "endpoint\ttype\trepo_remotes\trepo_urls\trepo_auth_urls\trepo_plain_urls\t" - "repo_users\trepo_tokens\tstore_users\tstore_tokens\trepo_only_tokens\t" - "store_only_tokens\tstatus" - ) - for row in table_rows: - print( - "\t".join(row) - ) + status_counts = print_token_item_rows(endpoint_names, repo_servers, store_servers) + print() + print_status_counts(status_counts) def run_tokens_write(config: WorkspaceConfig, args: argparse.Namespace) -> None: