From a0930de5e11fb216f4c871055318296d1e9a321a Mon Sep 17 00:00:00 2001 From: mpabi Date: Sun, 26 Apr 2026 10:13:29 +0200 Subject: [PATCH] Sync tokens.json from remote URLs --- README.md | 14 ++++-- doc/usage.md | 15 ++++-- workspace.json | 1 + workspace.py | 131 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d88259a..a1bddbd 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,8 @@ Masz dwie drogi. ### Droga 1: remote `r1` z tokenem w URL To jest wariant dydaktyczny, jesli uczen ma cwiczyc reczne dodawanie remota z -tokenem do konkretnego zdalnego endpointu. +tokenem do konkretnego zdalnego endpointu. Jesli launcher zobaczy URL w formacie +`http://LOGIN:TOKEN@...`, zapisze ten token lokalnie do `tokens/tokens.json`. Przyklad: @@ -40,7 +41,7 @@ git fetch r1 main git switch --track -c main r1/main ``` -### Droga 2: lokalny `tokens/gitea_tokens.json` +### Droga 2: lokalny `tokens/tokens.json` To jest wariant alternatywny, wygodniejszy wtedy, gdy launcher ma sam wykonywac `clone`, `fetch` i `push`, albo gdy uzytkownik po zajeciach chce pracowac juz na @@ -49,7 +50,7 @@ wlasnych repo bez wpisywania tokena do kazdego remota. Przed operacjami wymagajacymi autoryzacji dodaj lokalny token do: ```bash -~/dev/workspace/rv/tokens/gitea_tokens.json +~/dev/workspace/rv/tokens/tokens.json ``` Minimalny format: @@ -73,16 +74,19 @@ Przyklad: ```bash mkdir -p ~/dev/workspace/rv/tokens chmod 700 ~/dev/workspace/rv/tokens -chmod 600 ~/dev/workspace/rv/tokens/gitea_tokens.json +chmod 600 ~/dev/workspace/rv/tokens/tokens.json ``` -Jesli token jest trzymany tylko w `tokens/gitea_tokens.json`, remote `r1` moze +Jesli token jest trzymany tylko w `tokens/tokens.json`, remote `r1` moze byc zapisany bez sekretu: ```bash git remote add r1 http://77.90.8.171:3001/edu-tools/rv-launcher.git ``` +Launcher umie tez przemigrowac stary plik `tokens/gitea_tokens.json` do nowego +`tokens/tokens.json`. + ## Fetch i switch Domyslna galaz launchera to `main`. diff --git a/doc/usage.md b/doc/usage.md index 71927d1..322ac36 100644 --- a/doc/usage.md +++ b/doc/usage.md @@ -32,7 +32,8 @@ Masz dwie drogi. #### Droga 1: remote `r1` z tokenem w URL To jest wariant dydaktyczny, jesli uczen ma cwiczyc reczne dodawanie remota z -tokenem do zdalnego endpointu. +tokenem do zdalnego endpointu. Jesli launcher zobaczy URL w formacie +`http://LOGIN:TOKEN@...`, zapisze ten token lokalnie do `tokens/tokens.json`. Przyklad: @@ -42,12 +43,12 @@ git fetch r1 main git switch --track -c main r1/main ``` -#### Droga 2: lokalny `tokens/gitea_tokens.json` +#### Droga 2: lokalny `tokens/tokens.json` Przed operacjami wymagajacymi autoryzacji dodaj lokalny token do: ```bash -~/dev/workspace/rv/tokens/gitea_tokens.json +~/dev/workspace/rv/tokens/tokens.json ``` Minimalny format pliku: @@ -71,14 +72,17 @@ Przyklad: ```bash mkdir -p ~/dev/workspace/rv/tokens chmod 700 ~/dev/workspace/rv/tokens -chmod 600 ~/dev/workspace/rv/tokens/gitea_tokens.json +chmod 600 ~/dev/workspace/rv/tokens/tokens.json ``` -Wariant z `tokens/gitea_tokens.json` jest wygodniejszy wtedy, gdy launcher ma +Wariant z `tokens/tokens.json` jest wygodniejszy wtedy, gdy launcher ma sam wykonywac `clone`, `fetch` i `push`, albo gdy uzytkownik po zajeciach chce pracowac z wieloma repo na swoim koncie bez wpisywania tokena do kazdego remota. +Launcher umie tez przemigrowac stary plik `tokens/gitea_tokens.json` do nowego +`tokens/tokens.json`. + ### Fetch i switch Domyslna galaz launchera to `main`. @@ -143,6 +147,7 @@ config_path... workspace_root... series_root... socket_root... +token_path... tools_root... git_base_url... git_source_org... diff --git a/workspace.json b/workspace.json index 91f3c68..6143e86 100644 --- a/workspace.json +++ b/workspace.json @@ -3,6 +3,7 @@ "workspace_root": "~/dev/workspace/rv", "series_root": "~/dev/workspace/rv/series", "socket_root": "~/dev/workspace/rv/sockets", + "token_file": "~/dev/workspace/rv/tokens/tokens.json", "tools_root_candidates": [ "~/dev/workspace/tools/rv32i-hazard3-env", "~/dev/workspace/rv/tools/rv32i-hazard3-env" diff --git a/workspace.py b/workspace.py index 30cee30..045fa4c 100755 --- a/workspace.py +++ b/workspace.py @@ -11,6 +11,7 @@ import subprocess import sys from dataclasses import dataclass from pathlib import Path +from urllib.parse import unquote, urlsplit def expand_path(raw_path: str, base_dir: Path) -> Path: @@ -38,6 +39,8 @@ class WorkspaceConfig: workspace_root: Path series_root: Path socket_root: Path + token_path: Path + legacy_token_path: Path tools_root_candidates: list[Path] tools_root: Path defaults: dict[str, str] @@ -51,6 +54,11 @@ def load_config(config_path: Path) -> WorkspaceConfig: workspace_root = expand_path(raw.get("workspace_root", "~/dev/workspace/rv"), base_dir) series_root = expand_path(raw.get("series_root", str(workspace_root / "series")), base_dir) socket_root = expand_path(raw.get("socket_root", str(workspace_root / "sockets")), base_dir) + token_path = expand_path(raw.get("token_file", str(workspace_root / "tokens" / "tokens.json")), base_dir) + legacy_token_path = expand_path( + raw.get("legacy_token_file", str(workspace_root / "tokens" / "gitea_tokens.json")), + base_dir, + ) candidate_values = raw.get("tools_root_candidates") if not candidate_values: @@ -75,6 +83,8 @@ def load_config(config_path: Path) -> WorkspaceConfig: workspace_root=workspace_root, series_root=series_root, socket_root=socket_root, + token_path=token_path, + legacy_token_path=legacy_token_path, tools_root_candidates=tools_root_candidates, tools_root=tools_root, defaults=defaults, @@ -183,6 +193,124 @@ def git_capture(repo_path: Path, git_args: list[str]) -> str | None: return result.stdout.strip() +def git_repo_root(repo_path: Path) -> Path | None: + repo_root = git_capture(repo_path, ["rev-parse", "--show-toplevel"]) + if not repo_root: + return None + return Path(repo_root) + + +def migrate_legacy_token_store(config: WorkspaceConfig) -> None: + if config.token_path.exists() or not config.legacy_token_path.exists(): + return + + config.token_path.parent.mkdir(parents=True, exist_ok=True) + os.chmod(config.token_path.parent, 0o700) + config.legacy_token_path.replace(config.token_path) + os.chmod(config.token_path, 0o600) + + +def load_token_store(config: WorkspaceConfig) -> dict: + migrate_legacy_token_store(config) + + if not config.token_path.exists(): + return {"users": {}} + + raw_data = json.loads(config.token_path.read_text(encoding="utf-8")) + if not isinstance(raw_data, dict): + raise SystemExit(f"Invalid token store format: {config.token_path}") + raw_data.setdefault("users", {}) + return raw_data + + +def write_token_store(config: WorkspaceConfig, token_data: dict) -> None: + config.token_path.parent.mkdir(parents=True, exist_ok=True) + os.chmod(config.token_path.parent, 0o700) + + temp_path = config.token_path.with_suffix(config.token_path.suffix + ".tmp") + temp_path.write_text(json.dumps(token_data, indent=2, sort_keys=True) + "\n", encoding="utf-8") + os.chmod(temp_path, 0o600) + temp_path.replace(config.token_path) + os.chmod(config.token_path, 0o600) + + +def next_token_name(tokens: dict[str, str]) -> str: + index = 1 + while f"t{index}" in tokens: + index += 1 + return f"t{index}" + + +def register_token(token_data: dict, user_name: str, token_value: str) -> bool: + users = token_data.setdefault("users", {}) + user_entry = users.setdefault(user_name, {}) + tokens = user_entry.setdefault("tokens", {}) + + for existing_token in tokens.values(): + if existing_token == token_value: + return False + + tokens[next_token_name(tokens)] = token_value + return True + + +def remote_urls(repo_path: Path) -> list[str]: + remote_output = git_capture(repo_path, ["remote"]) + if not remote_output: + return [] + + urls: list[str] = [] + seen: set[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: + continue + seen.add(remote_url) + urls.append(remote_url) + return urls + + +def remote_credentials(remote_url: str) -> tuple[str, str] | None: + split_url = urlsplit(remote_url) + if split_url.scheme not in {"http", "https"}: + return None + if split_url.username is None or split_url.password is None: + return None + + return unquote(split_url.username), unquote(split_url.password) + + +def sync_tokens_from_repo(config: WorkspaceConfig, repo_path: Path) -> bool: + repo_root = git_repo_root(repo_path) + if repo_root is None: + migrate_legacy_token_store(config) + return False + + found_credentials: list[tuple[str, str]] = [] + for remote_url in remote_urls(repo_root): + credentials = remote_credentials(remote_url) + if credentials is not None: + found_credentials.append(credentials) + + if not found_credentials: + migrate_legacy_token_store(config) + return False + + token_data = load_token_store(config) + changed = False + for user_name, token_value in found_credentials: + if register_token(token_data, user_name, token_value): + changed = True + + if changed or not config.token_path.exists(): + write_token_store(config, token_data) + + return changed + + def git_current_branch(config: WorkspaceConfig, repo_path: Path) -> str: branch_name = git_capture(repo_path, ["branch", "--show-current"]) if branch_name: @@ -253,6 +381,7 @@ def print_submission_plan(config: WorkspaceConfig, args: argparse.Namespace) -> series_name, card_name = resolve_selector(config, args.series, args.card) selector = f"{series_name}/{card_name}" card_path = resolve_card_path(config, series_name, card_name) + sync_tokens_from_repo(config, card_path) class_name = slug_value(args.class_name, "class") branch_name = slug_value(args.branch or args.nick, "branch") source_url = args.source_url or discover_source_url(config, card_path, series_name, card_name) @@ -376,6 +505,7 @@ def print_config(config: WorkspaceConfig) -> None: print(f"workspace_root\t{config.workspace_root}") print(f"series_root\t{config.series_root}") print(f"socket_root\t{config.socket_root}") + print(f"token_path\t{config.token_path}") print(f"tools_root\t{config.tools_root}") print(f"git_base_url\t{config.git.base_url}") print(f"git_source_org\t{config.git.source_org}") @@ -452,6 +582,7 @@ def main() -> int: parser = build_parser() args = parser.parse_args() config = load_config(args.config) + sync_tokens_from_repo(config, Path.cwd()) if args.command == "show-config": print_config(config)