From 918b192212601ff1e9a5191aaaaaac04e92dc70e Mon Sep 17 00:00:00 2001 From: mpabi Date: Sun, 26 Apr 2026 10:36:25 +0200 Subject: [PATCH] Compare repo and store endpoints in token stats --- README.md | 2 +- doc/tokens.md | 19 +++--- doc/workspace.md | 23 +++++-- workspace.py | 172 ++++++++++++++++++++++++++++++++++++++++------- 4 files changed, 179 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 2cb4b83..e4d452b 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ Podstawowe komendy tokenow: ```bash python3 workspace.py tokens scan python3 workspace.py tokens read -python3 workspace.py tokens stats +python3 workspace.py tokens stats --repo ~/dev/workspace/rv/series/inf/03 ``` ## Fetch i switch diff --git a/doc/tokens.md b/doc/tokens.md index e7dd9fc..670ee55 100644 --- a/doc/tokens.md +++ b/doc/tokens.md @@ -118,22 +118,23 @@ python3 workspace.py tokens read --show-secrets ### `tokens stats` -Pokazuje statystyki per endpoint serwera. +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: -- endpoint -- type -- scheme -- host -- port -- users -- tokens +- 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 Przyklad: ```bash -python3 workspace.py tokens stats +python3 workspace.py tokens stats --repo ~/dev/workspace/rv/series/inf/03 ``` ### `tokens write` diff --git a/doc/workspace.md b/doc/workspace.md index 6ec346c..d0dd85c 100644 --- a/doc/workspace.md +++ b/doc/workspace.md @@ -283,24 +283,39 @@ python3 workspace.py tokens read --server http://77.90.8.171:3001 ## `tokens stats` -Pokazuje statystyki tokenow per endpoint serwera. +Pokazuje statystyki endpointow z repo i `tokens.json`, a takze ich zgodnosc +wzgledem siebie. Przelaczniki: +- `--repo PATH` + Sciezka wewnatrz repo, z ktorego maja byc odczytane remote URL-e. - `--server ENDPOINT` Ogranicza wynik do jednego endpointu. Typowy wynik: ```text -endpointtypeschemehostportuserstokens -http://77.90.8.171:3001giteahttp77.90.8.171300111 +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 ``` Przyklad: ```bash -python3 workspace.py tokens stats +python3 workspace.py tokens stats --repo ~/dev/workspace/rv/series/inf/03 ``` ## `tokens write` diff --git a/workspace.py b/workspace.py index c9de00c..a6d5400 100755 --- a/workspace.py +++ b/workspace.py @@ -376,21 +376,26 @@ def register_token(token_data: dict, server_info: dict, user_name: str, token_va def remote_urls(repo_path: Path) -> list[str]: + return [remote_url for _, remote_url in remote_name_urls(repo_path)] + + +def remote_name_urls(repo_path: Path) -> list[tuple[str, str]]: remote_output = git_capture(repo_path, ["remote"]) if not remote_output: return [] - urls: list[str] = [] - seen: set[str] = set() + urls: list[tuple[str, str]] = [] + seen: set[tuple[str, str]] = set() for remote_name in remote_output.splitlines(): url_output = git_capture(repo_path, ["remote", "get-url", "--all", remote_name]) if not url_output: continue for remote_url in url_output.splitlines(): - if remote_url in seen: + key = (remote_name, remote_url) + if key in seen: continue - seen.add(remote_url) - urls.append(remote_url) + seen.add(key) + urls.append(key) return urls @@ -467,6 +472,89 @@ def token_store_servers(token_data: dict) -> list[tuple[str, dict]]: return sorted(servers.items()) +def token_pairs_from_users(users_data: dict) -> set[tuple[str, str]]: + token_pairs: set[tuple[str, str]] = set() + for user_name, user_entry in users_data.items(): + if not isinstance(user_entry, dict): + continue + for token_value in user_entry.get("tokens", {}).values(): + if isinstance(token_value, str): + token_pairs.add((str(user_name), token_value)) + return token_pairs + + +def count_users(users_data: dict) -> int: + return len(users_data) + + +def collect_repo_server_entries(config: WorkspaceConfig, repo_path: Path) -> tuple[Path | None, dict[str, dict]]: + repo_root = git_repo_root(repo_path) + if repo_root is None: + return None, {} + + repo_servers: dict[str, dict] = {} + for remote_name, remote_url in remote_name_urls(repo_root): + server_info = server_info_from_url(config, remote_url) + if server_info is None: + continue + + endpoint = server_info["endpoint"] + entry = repo_servers.setdefault( + endpoint, + { + "type": server_info["type"], + "scheme": server_info["scheme"], + "host": server_info["host"], + "port": server_info["port"], + "remotes": set(), + "urls": 0, + "auth_urls": 0, + "plain_urls": 0, + "users": {}, + }, + ) + entry["remotes"].add(remote_name) + entry["urls"] += 1 + for field_name in ["type", "scheme", "host", "port"]: + if field_name not in entry or entry[field_name] in {"", None, "unknown"}: + entry[field_name] = server_info[field_name] + + credentials = remote_credentials(remote_url) + if credentials is None: + entry["plain_urls"] += 1 + continue + + entry["auth_urls"] += 1 + user_name, token_value = credentials + user_entry = entry["users"].setdefault(user_name, {"tokens": {}}) + tokens = user_entry.setdefault("tokens", {}) + if token_value not in tokens.values(): + tokens[next_token_name(tokens)] = token_value + + return repo_root, repo_servers + + +def compare_server_entries(repo_entry: dict | None, store_entry: dict | None) -> tuple[str, int, int]: + if repo_entry is None and store_entry is None: + return "missing", 0, 0 + if repo_entry is not None and store_entry is None: + return "repo_only", count_server_tokens(repo_entry), 0 + if repo_entry is None and store_entry is not None: + return "store_only", 0, count_server_tokens(store_entry) + + repo_pairs = token_pairs_from_users(repo_entry.get("users", {})) + store_pairs = token_pairs_from_users(store_entry.get("users", {})) + repo_only_pairs = repo_pairs - store_pairs + store_only_pairs = store_pairs - repo_pairs + if not repo_only_pairs and not store_only_pairs: + return "in_sync", 0, 0 + if repo_only_pairs and not store_only_pairs: + return "repo_ahead", len(repo_only_pairs), 0 + if store_only_pairs and not repo_only_pairs: + return "store_ahead", 0, len(store_only_pairs) + return "diverged", len(repo_only_pairs), len(store_only_pairs) + + def resolve_server_from_store(token_data: dict, endpoint: str | None) -> tuple[str, dict]: servers = token_data.get("servers", {}) if endpoint: @@ -599,28 +687,65 @@ def run_tokens_read(config: WorkspaceConfig, args: argparse.Namespace) -> None: def run_tokens_stats(config: WorkspaceConfig, args: argparse.Namespace) -> None: + repo_root, repo_servers = collect_repo_server_entries(config, resolve_repo_argument(args.repo)) token_data = load_token_store(config) - servers = token_store_servers(token_data) + store_servers = token_data.get("servers", {}) + endpoint_names = sorted(set(repo_servers) | set(store_servers)) if args.server: - server_entry = token_data.get("servers", {}).get(args.server) - if server_entry is None: - raise SystemExit(f"Missing server endpoint in token store: {args.server}") - servers = [(args.server, server_entry)] + if args.server not in set(endpoint_names): + raise SystemExit(f"Missing server endpoint in repo/store: {args.server}") + endpoint_names = [args.server] - print("endpoint\ttype\tscheme\thost\tport\tusers\ttokens") - for endpoint, server_entry in servers: + 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() + 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( - [ - endpoint, - str(server_entry.get("type", "unknown")), - str(server_entry.get("scheme", "")), - str(server_entry.get("host", "")), - str(server_entry.get("port", "")), - str(len(server_entry.get("users", {}))), - str(count_server_tokens(server_entry)), - ] - ) + "\t".join(row) ) @@ -979,6 +1104,7 @@ def build_parser() -> argparse.ArgumentParser: "stats", help="Print per-server token statistics from tokens.json.", ) + add_repo_option(tokens_stats_parser) tokens_stats_parser.add_argument("--server", help="Filter output to one server endpoint.") tokens_write_parser = tokens_subparsers.add_parser(