Compare repo and store endpoints in token stats

This commit is contained in:
mpabi
2026-04-26 10:36:25 +02:00
parent 663242c855
commit 918b192212
4 changed files with 179 additions and 37 deletions
+1 -1
View File
@@ -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
+10 -9
View File
@@ -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`
+19 -4
View File
@@ -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
endpoint<TAB>type<TAB>scheme<TAB>host<TAB>port<TAB>users<TAB>tokens
http://77.90.8.171:3001<TAB>gitea<TAB>http<TAB>77.90.8.171<TAB>3001<TAB>1<TAB>1
repo_root<TAB>...
token_path<TAB>...
repo_endpoints<TAB>1
store_endpoints<TAB>1
union_endpoints<TAB>1
in_sync<TAB>0
repo_only<TAB>0
store_only<TAB>0
repo_ahead<TAB>0
store_ahead<TAB>1
diverged<TAB>0
endpoint<TAB>type<TAB>repo_remotes<TAB>repo_urls<TAB>repo_auth_urls<TAB>repo_plain_urls<TAB>repo_users<TAB>repo_tokens<TAB>store_users<TAB>store_tokens<TAB>repo_only_tokens<TAB>store_only_tokens<TAB>status
http://77.90.8.171:3001<TAB>gitea<TAB>origin<TAB>1<TAB>0<TAB>1<TAB>0<TAB>0<TAB>1<TAB>1<TAB>0<TAB>1<TAB>store_ahead
```
Przyklad:
```bash
python3 workspace.py tokens stats
python3 workspace.py tokens stats --repo ~/dev/workspace/rv/series/inf/03
```
## `tokens write`
+146 -20
View File
@@ -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:
print(
"\t".join(
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(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)),
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(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(