Read series from workspace manifest
This commit is contained in:
@@ -12,7 +12,7 @@ import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
from pathlib import Path
|
||||
from urllib import error as urlerror
|
||||
from urllib import request as urlrequest
|
||||
@@ -44,6 +44,9 @@ class WorkspaceConfig:
|
||||
original_root: Path
|
||||
original_series_root: Path
|
||||
workspace_root: Path
|
||||
workspace_info_path: Path
|
||||
workspace_info_url: str
|
||||
workspace_info: dict | None
|
||||
series_root: Path
|
||||
socket_root: Path
|
||||
token_path: Path
|
||||
@@ -59,6 +62,21 @@ class TaskEntry:
|
||||
path: Path
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CardInfo:
|
||||
series_id: str
|
||||
card_id: str
|
||||
repo: str
|
||||
branch: str
|
||||
title: str
|
||||
source_org: str
|
||||
answer_org: str
|
||||
source_remote: str
|
||||
answer_remote: str
|
||||
fallback_branch: str
|
||||
source_path: Path | None = None
|
||||
|
||||
|
||||
SCOPE_FIELDS = [
|
||||
("a", "activitypub"),
|
||||
("A", "admin"),
|
||||
@@ -102,6 +120,10 @@ def load_config(config_path: Path) -> WorkspaceConfig:
|
||||
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)
|
||||
workspace_info_path = expand_path(
|
||||
raw.get("workspace_info_path", str(workspace_root / "meta" / "workspace-info" / "workspace.json")),
|
||||
base_dir,
|
||||
)
|
||||
|
||||
candidate_values = raw.get("tools_root_candidates")
|
||||
if not candidate_values:
|
||||
@@ -114,8 +136,10 @@ def load_config(config_path: Path) -> WorkspaceConfig:
|
||||
tools_root = next((path for path in tools_root_candidates if path.exists()), tools_root_candidates[0])
|
||||
defaults = raw.get("defaults", {})
|
||||
git_raw = raw.get("git", {})
|
||||
base_url = git_raw.get("base_url", "http://77.90.8.171:3001").rstrip("/")
|
||||
workspace_info_url = raw.get("workspace_info_url", f"{base_url}/edu-workspace/workspace-info.git")
|
||||
git = GitConfig(
|
||||
base_url=git_raw.get("base_url", "http://77.90.8.171:3001").rstrip("/"),
|
||||
base_url=base_url,
|
||||
source_org=git_raw.get("source_org", "edu-inf"),
|
||||
answer_org=git_raw.get("answer_org", "c2025-1a-inf"),
|
||||
source_remote=git_raw.get("source_remote", "r1"),
|
||||
@@ -123,12 +147,16 @@ def load_config(config_path: Path) -> WorkspaceConfig:
|
||||
origin_remote=git_raw.get("origin_remote", "origin"),
|
||||
fallback_branch=git_raw.get("fallback_branch", "build"),
|
||||
)
|
||||
workspace_info = json.loads(workspace_info_path.read_text(encoding="utf-8")) if workspace_info_path.is_file() else None
|
||||
|
||||
return WorkspaceConfig(
|
||||
config_path=config_path,
|
||||
original_root=original_root,
|
||||
original_series_root=original_series_root,
|
||||
workspace_root=workspace_root,
|
||||
workspace_info_path=workspace_info_path,
|
||||
workspace_info_url=workspace_info_url,
|
||||
workspace_info=workspace_info,
|
||||
series_root=series_root,
|
||||
socket_root=socket_root,
|
||||
token_path=token_path,
|
||||
@@ -139,6 +167,131 @@ def load_config(config_path: Path) -> WorkspaceConfig:
|
||||
)
|
||||
|
||||
|
||||
def workspace_info_root(config: WorkspaceConfig) -> Path:
|
||||
return config.workspace_info_path.parent
|
||||
|
||||
|
||||
def read_workspace_info_file(config: WorkspaceConfig, raw_path: str) -> dict:
|
||||
info_path = expand_path(raw_path, workspace_info_root(config))
|
||||
return json.loads(info_path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def workspace_series_manifests(config: WorkspaceConfig) -> dict[str, dict]:
|
||||
if not config.workspace_info:
|
||||
return {}
|
||||
|
||||
manifests: dict[str, dict] = {}
|
||||
for entry in config.workspace_info.get("series", []):
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
series_id = str(entry.get("id", "")).strip()
|
||||
if not series_id:
|
||||
continue
|
||||
manifest = dict(entry)
|
||||
file_name = entry.get("file")
|
||||
if isinstance(file_name, str) and file_name:
|
||||
manifest = read_workspace_info_file(config, file_name)
|
||||
manifest.setdefault("id", series_id)
|
||||
manifests[series_id] = manifest
|
||||
return manifests
|
||||
|
||||
|
||||
def workspace_series_manifest(config: WorkspaceConfig, series_name: str) -> dict | None:
|
||||
return workspace_series_manifests(config).get(series_name)
|
||||
|
||||
|
||||
def series_exists(config: WorkspaceConfig, series_name: str) -> bool:
|
||||
if workspace_series_manifest(config, series_name) is not None:
|
||||
return True
|
||||
return (config.series_root / series_name).is_dir() or (config.original_series_root / series_name).is_dir()
|
||||
|
||||
|
||||
def default_card_for_series(config: WorkspaceConfig, series_name: str) -> str | None:
|
||||
manifest = workspace_series_manifest(config, series_name)
|
||||
if manifest and isinstance(manifest.get("default_card"), str):
|
||||
return manifest["default_card"]
|
||||
return config.defaults.get("card")
|
||||
|
||||
|
||||
def config_for_card(config: WorkspaceConfig, card_info: CardInfo) -> WorkspaceConfig:
|
||||
return replace(
|
||||
config,
|
||||
git=replace(
|
||||
config.git,
|
||||
source_org=card_info.source_org,
|
||||
answer_org=card_info.answer_org,
|
||||
source_remote=card_info.source_remote,
|
||||
answer_remote=card_info.answer_remote,
|
||||
fallback_branch=card_info.fallback_branch,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def card_infos_from_manifest(config: WorkspaceConfig, series_name: str) -> list[CardInfo]:
|
||||
manifest = workspace_series_manifest(config, series_name)
|
||||
if not manifest:
|
||||
return []
|
||||
|
||||
source_org = str(manifest.get("source_org", config.git.source_org))
|
||||
answer_org = str(manifest.get("answer_org", config.git.answer_org))
|
||||
source_remote = str(manifest.get("source_remote", config.git.source_remote))
|
||||
answer_remote = str(manifest.get("answer_remote", config.git.answer_remote))
|
||||
fallback_branch = str(manifest.get("fallback_branch", config.git.fallback_branch))
|
||||
cards = []
|
||||
for raw_card in manifest.get("cards", []):
|
||||
if not isinstance(raw_card, dict):
|
||||
continue
|
||||
card_id = str(raw_card.get("id", "")).strip()
|
||||
if not card_id:
|
||||
continue
|
||||
repo_name = str(raw_card.get("repo", card_id)).strip()
|
||||
branch_name = str(raw_card.get("branch", fallback_branch)).strip() or fallback_branch
|
||||
title = str(raw_card.get("title", "")).strip()
|
||||
source_path = config.original_series_root / series_name / repo_name
|
||||
cards.append(
|
||||
CardInfo(
|
||||
series_id=series_name,
|
||||
card_id=card_id,
|
||||
repo=repo_name,
|
||||
branch=branch_name,
|
||||
title=title,
|
||||
source_org=source_org,
|
||||
answer_org=answer_org,
|
||||
source_remote=source_remote,
|
||||
answer_remote=answer_remote,
|
||||
fallback_branch=fallback_branch,
|
||||
source_path=source_path if source_path.is_dir() else None,
|
||||
)
|
||||
)
|
||||
return cards
|
||||
|
||||
|
||||
def card_infos_from_source_dirs(config: WorkspaceConfig, series_name: str) -> list[CardInfo]:
|
||||
return [
|
||||
CardInfo(
|
||||
series_id=series_name,
|
||||
card_id=card_label_from_repo_name(path.name),
|
||||
repo=path.name,
|
||||
branch=git_current_branch(config, path),
|
||||
title=read_card_title(path),
|
||||
source_org=config.git.source_org,
|
||||
answer_org=config.git.answer_org,
|
||||
source_remote=config.git.source_remote,
|
||||
answer_remote=config.git.answer_remote,
|
||||
fallback_branch=config.git.fallback_branch,
|
||||
source_path=path,
|
||||
)
|
||||
for path in source_card_directories(config, series_name)
|
||||
]
|
||||
|
||||
|
||||
def source_card_infos(config: WorkspaceConfig, series_name: str) -> list[CardInfo]:
|
||||
manifest_cards = card_infos_from_manifest(config, series_name)
|
||||
if manifest_cards:
|
||||
return manifest_cards
|
||||
return card_infos_from_source_dirs(config, series_name)
|
||||
|
||||
|
||||
def series_directories(config: WorkspaceConfig) -> list[Path]:
|
||||
config.series_root.mkdir(parents=True, exist_ok=True)
|
||||
return sorted(path for path in config.series_root.iterdir() if path.is_dir())
|
||||
@@ -170,7 +323,38 @@ def card_label_from_repo_name(repo_name: str) -> str:
|
||||
return repo_name
|
||||
|
||||
|
||||
def card_info_matches(card_info: CardInfo, normalized: str) -> bool:
|
||||
candidates = {card_info.card_id, card_info.repo, card_label_from_repo_name(card_info.repo)}
|
||||
return normalized in candidates or any(normalized in candidate for candidate in candidates)
|
||||
|
||||
|
||||
def resolve_card_info(config: WorkspaceConfig, series_name: str, card_name: str) -> CardInfo:
|
||||
cards = source_card_infos(config, series_name)
|
||||
if not cards:
|
||||
source_hint = config.original_series_root / series_name
|
||||
if config.workspace_info_path.is_file():
|
||||
source_hint = config.workspace_info_path
|
||||
raise SystemExit(f"Missing source series: {source_hint}")
|
||||
|
||||
normalized = slug_value(card_name, "card")
|
||||
matches = [card_info for card_info in cards if card_info_matches(card_info, normalized)]
|
||||
if not matches:
|
||||
available = ", ".join(card.card_id for card in cards)
|
||||
raise SystemExit(f"Missing card '{card_name}' in series '{series_name}'. Available: {available}")
|
||||
if len(matches) > 1:
|
||||
available = ", ".join(card.repo for card in matches)
|
||||
raise SystemExit(f"Ambiguous card '{card_name}' in series '{series_name}'. Matches: {available}")
|
||||
return matches[0]
|
||||
|
||||
|
||||
def resolve_source_card_path(config: WorkspaceConfig, series_name: str, card_name: str) -> Path:
|
||||
manifest_card = resolve_card_info(config, series_name, card_name)
|
||||
if manifest_card.source_path:
|
||||
return manifest_card.source_path
|
||||
return config.original_series_root / series_name / manifest_card.repo
|
||||
|
||||
|
||||
def resolve_source_card_path_from_dirs(config: WorkspaceConfig, series_name: str, card_name: str) -> Path:
|
||||
cards = source_card_directories(config, series_name)
|
||||
if not cards:
|
||||
raise SystemExit(f"Missing source series: {config.original_series_root / series_name}")
|
||||
@@ -220,10 +404,11 @@ def resolve_selector(
|
||||
return series_value, card_value
|
||||
|
||||
if series_arg and not card_arg:
|
||||
if (config.series_root / series_arg).is_dir() or (config.original_series_root / series_arg).is_dir():
|
||||
if not default_card:
|
||||
if series_exists(config, series_arg):
|
||||
series_default_card = default_card_for_series(config, series_arg)
|
||||
if not series_default_card:
|
||||
raise SystemExit("Missing default card in config.")
|
||||
return series_arg, default_card
|
||||
return series_arg, series_default_card
|
||||
if default_series and (config.series_root / default_series / series_arg).is_dir():
|
||||
return default_series, series_arg
|
||||
if default_series:
|
||||
@@ -245,8 +430,8 @@ def resolve_card_path(config: WorkspaceConfig, series_name: str, card_name: str)
|
||||
card_path = config.series_root / series_name / card_name
|
||||
if card_path.is_dir():
|
||||
return card_path
|
||||
source_card_path = resolve_source_card_path(config, series_name, card_name)
|
||||
resolved_path = workspace_card_path(config, series_name, source_card_path)
|
||||
card_info = resolve_card_info(config, series_name, card_name)
|
||||
resolved_path = workspace_card_path_for_info(config, card_info)
|
||||
if resolved_path.is_dir():
|
||||
return resolved_path
|
||||
raise SystemExit(f"Missing card path: {resolved_path}")
|
||||
@@ -254,12 +439,13 @@ def resolve_card_path(config: WorkspaceConfig, series_name: str, card_name: str)
|
||||
|
||||
def print_series(config: WorkspaceConfig) -> None:
|
||||
config.series_root.mkdir(parents=True, exist_ok=True)
|
||||
manifest_by_name = workspace_series_manifests(config)
|
||||
source_by_name = {path.name: path for path in source_series_directories(config)}
|
||||
workspace_by_name = {path.name: path for path in series_directories(config)}
|
||||
for series_name in sorted(set(source_by_name) | set(workspace_by_name)):
|
||||
source_path = source_by_name.get(series_name)
|
||||
for series_name in sorted(set(manifest_by_name) | set(source_by_name) | set(workspace_by_name)):
|
||||
manifest = manifest_by_name.get(series_name)
|
||||
workspace_path = workspace_by_name.get(series_name)
|
||||
source_count = len(source_card_directories(config, series_name)) if source_path else 0
|
||||
source_count = len(source_card_infos(config, series_name)) if (manifest or series_name in source_by_name) else 0
|
||||
workspace_count = len(card_directories(workspace_path)) if workspace_path else 0
|
||||
print(f"{series_name}\t{source_count}\t{workspace_count}\t{workspace_path or ''}")
|
||||
|
||||
@@ -270,17 +456,21 @@ def print_cards(config: WorkspaceConfig, series_arg: str | None) -> None:
|
||||
raise SystemExit("Missing series name and no default series is configured.")
|
||||
|
||||
config.series_root.mkdir(parents=True, exist_ok=True)
|
||||
source_cards = {path.name: path for path in source_card_directories(config, series_name)}
|
||||
source_cards = {card.repo: card for card in source_card_infos(config, series_name)}
|
||||
workspace_series_path = config.series_root / series_name
|
||||
workspace_cards = {path.name: path for path in card_directories(workspace_series_path)} if workspace_series_path.is_dir() else {}
|
||||
if not source_cards and not workspace_cards:
|
||||
raise SystemExit(f"Missing series: {series_name}")
|
||||
|
||||
for repo_name in sorted(set(source_cards) | set(workspace_cards)):
|
||||
source_path = source_cards.get(repo_name)
|
||||
source_card = source_cards.get(repo_name)
|
||||
workspace_path = workspace_cards.get(repo_name)
|
||||
title = read_card_title(workspace_path or source_path) if (workspace_path or source_path) else ""
|
||||
label = card_label_from_repo_name(repo_name)
|
||||
title = read_card_title(workspace_path) if workspace_path else ""
|
||||
if not title and source_card:
|
||||
title = source_card.title
|
||||
if not title and source_card and source_card.source_path:
|
||||
title = read_card_title(source_card.source_path)
|
||||
label = source_card.card_id if source_card else card_label_from_repo_name(repo_name)
|
||||
status = "workspace" if workspace_path else "source"
|
||||
if title:
|
||||
print(f"{label}\t{repo_name}\t{status}\t{title}")
|
||||
@@ -2508,6 +2698,13 @@ def source_url_for_card(config: WorkspaceConfig, series_name: str, source_card_p
|
||||
return f"{config.git.base_url}/{config.git.source_org}/{source_card_path.name}.git"
|
||||
|
||||
|
||||
def source_url_for_card_info(config: WorkspaceConfig, card_info: CardInfo) -> str:
|
||||
card_config = config_for_card(config, card_info)
|
||||
if card_info.source_path:
|
||||
return source_url_for_card(card_config, card_info.series_id, card_info.source_path)
|
||||
return f"{card_config.git.base_url}/{card_info.source_org}/{card_info.repo}.git"
|
||||
|
||||
|
||||
def ensure_answer_token_from_source(
|
||||
config: WorkspaceConfig,
|
||||
source_remote: str,
|
||||
@@ -2539,21 +2736,26 @@ def workspace_card_path(config: WorkspaceConfig, series_name: str, source_card_p
|
||||
return config.series_root / series_name / source_card_path.name
|
||||
|
||||
|
||||
def workspace_card_path_for_info(config: WorkspaceConfig, card_info: CardInfo) -> Path:
|
||||
return config.series_root / card_info.series_id / card_info.repo
|
||||
|
||||
|
||||
def resolve_workspace_card_path(config: WorkspaceConfig, series_name: str, card_name: str) -> Path:
|
||||
source_card_path = resolve_source_card_path(config, series_name, card_name)
|
||||
card_path = workspace_card_path(config, series_name, source_card_path)
|
||||
card_info = resolve_card_info(config, series_name, card_name)
|
||||
card_path = workspace_card_path_for_info(config, card_info)
|
||||
if not card_path.is_dir():
|
||||
raise SystemExit(f"Missing workspace card: {card_path}. Run: ./rvctl series cards fetch {series_name} {card_name}")
|
||||
return card_path
|
||||
|
||||
|
||||
def fetch_card_to_workspace(config: WorkspaceConfig, series_name: str, card_name: str, dry_run: bool = False) -> dict[str, str]:
|
||||
source_card_path = resolve_source_card_path(config, series_name, card_name)
|
||||
repo_name = source_card_path.name
|
||||
target_path = workspace_card_path(config, series_name, source_card_path)
|
||||
source_url = source_url_for_card(config, series_name, source_card_path)
|
||||
answer_url = answer_url_for_card(config, repo_name)
|
||||
source_branch = git_current_branch(config, source_card_path)
|
||||
card_info = resolve_card_info(config, series_name, card_name)
|
||||
card_config = config_for_card(config, card_info)
|
||||
repo_name = card_info.repo
|
||||
target_path = workspace_card_path_for_info(config, card_info)
|
||||
source_url = source_url_for_card_info(config, card_info)
|
||||
answer_url = answer_url_for_card(card_config, repo_name)
|
||||
source_branch = card_info.branch
|
||||
|
||||
if not dry_run:
|
||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
@@ -2563,15 +2765,15 @@ def fetch_card_to_workspace(config: WorkspaceConfig, series_name: str, card_name
|
||||
if not target_path.exists():
|
||||
subprocess.run(["git", "clone", source_url, str(target_path)], check=True)
|
||||
cloned = True
|
||||
origin_url = git_capture(target_path, ["remote", "get-url", config.git.origin_remote])
|
||||
if origin_url and git_capture(target_path, ["remote", "get-url", config.git.source_remote]) is None:
|
||||
origin_url = git_capture(target_path, ["remote", "get-url", card_config.git.origin_remote])
|
||||
if origin_url and git_capture(target_path, ["remote", "get-url", card_config.git.source_remote]) is None:
|
||||
subprocess.run(
|
||||
["git", "-C", str(target_path), "remote", "rename", config.git.origin_remote, config.git.source_remote],
|
||||
["git", "-C", str(target_path), "remote", "rename", card_config.git.origin_remote, card_config.git.source_remote],
|
||||
check=True,
|
||||
)
|
||||
set_git_remote(target_path, config.git.source_remote, source_url)
|
||||
subprocess.run(["git", "-C", str(target_path), "fetch", config.git.source_remote, source_branch], check=True)
|
||||
current_branch = git_current_branch(config, target_path)
|
||||
set_git_remote(target_path, card_config.git.source_remote, source_url)
|
||||
subprocess.run(["git", "-C", str(target_path), "fetch", card_config.git.source_remote, source_branch], check=True)
|
||||
current_branch = git_current_branch(card_config, target_path)
|
||||
if cloned or current_branch == source_branch:
|
||||
if current_branch != source_branch:
|
||||
local_branch_exists = subprocess.run(
|
||||
@@ -2584,15 +2786,15 @@ def fetch_card_to_workspace(config: WorkspaceConfig, series_name: str, card_name
|
||||
subprocess.run(["git", "-C", str(target_path), "switch", source_branch], check=True)
|
||||
else:
|
||||
subprocess.run(
|
||||
["git", "-C", str(target_path), "switch", "--track", "-c", source_branch, f"{config.git.source_remote}/{source_branch}"],
|
||||
["git", "-C", str(target_path), "switch", "--track", "-c", source_branch, f"{card_config.git.source_remote}/{source_branch}"],
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(["git", "-C", str(target_path), "pull", "--ff-only", config.git.source_remote, source_branch], check=True)
|
||||
subprocess.run(["git", "-C", str(target_path), "pull", "--ff-only", card_config.git.source_remote, source_branch], check=True)
|
||||
|
||||
token_status, source_token = ensure_answer_token_from_source(
|
||||
config,
|
||||
config.git.source_remote,
|
||||
config.git.answer_remote,
|
||||
card_config,
|
||||
card_config.git.source_remote,
|
||||
card_config.git.answer_remote,
|
||||
answer_url,
|
||||
dry_run=dry_run,
|
||||
)
|
||||
@@ -2603,21 +2805,21 @@ def fetch_card_to_workspace(config: WorkspaceConfig, series_name: str, card_name
|
||||
str(source_token.get("user", "")),
|
||||
str(source_token.get("value", "")),
|
||||
)
|
||||
answer_remote_status = set_git_remote(target_path, config.git.answer_remote, answer_remote_url)
|
||||
answer_remote_status = set_git_remote(target_path, card_config.git.answer_remote, answer_remote_url)
|
||||
|
||||
return {
|
||||
"series": series_name,
|
||||
"card": card_label_from_repo_name(repo_name),
|
||||
"card": card_info.card_id,
|
||||
"repo": repo_name,
|
||||
"source": source_url,
|
||||
"workspace": str(target_path),
|
||||
"source_remote": config.git.source_remote,
|
||||
"source_remote": card_config.git.source_remote,
|
||||
"source_branch": source_branch,
|
||||
"answer_remote": config.git.answer_remote,
|
||||
"answer_org": config.git.answer_org,
|
||||
"answer_remote": card_config.git.answer_remote,
|
||||
"answer_org": card_config.git.answer_org,
|
||||
"answer_repo": repo_name,
|
||||
"answer_url": answer_url,
|
||||
"token": config.git.answer_remote,
|
||||
"token": card_config.git.answer_remote,
|
||||
"token_status": token_status,
|
||||
"answer_remote_status": answer_remote_status,
|
||||
"status": "dry-run" if dry_run else "ready",
|
||||
@@ -2781,14 +2983,16 @@ def switch_card_task(
|
||||
branch_override: str | None = None,
|
||||
dry_run: bool = False,
|
||||
) -> dict[str, str]:
|
||||
card_info = resolve_card_info(config, series_name, card_name)
|
||||
card_config = config_for_card(config, card_info)
|
||||
card_path = resolve_workspace_card_path(config, series_name, card_name)
|
||||
task_name = resolve_submission_task(config, card_path, task_selector)
|
||||
source_url = discover_source_url(config, card_path, series_name, card_name)
|
||||
source_url = discover_source_url(card_config, card_path, series_name, card_name)
|
||||
source_repo_name = repo_name_from_url(source_url)
|
||||
answer_url = build_answer_url(config, source_repo_name)
|
||||
student_user = store_user_for_submission(config, source_url, answer_url, required=branch_override is None)
|
||||
answer_url = build_answer_url(card_config, source_repo_name)
|
||||
student_user = store_user_for_submission(card_config, source_url, answer_url, required=branch_override is None)
|
||||
branch_name = branch_value(branch_override) if branch_override else submission_branch_name(student_user, task_name)
|
||||
current_branch = git_current_branch(config, card_path)
|
||||
current_branch = git_current_branch(card_config, card_path)
|
||||
branch_exists = subprocess.run(
|
||||
["git", "-C", str(card_path), "rev-parse", "--verify", "--quiet", branch_name],
|
||||
stdout=subprocess.DEVNULL,
|
||||
@@ -2800,14 +3004,14 @@ def switch_card_task(
|
||||
answer_remote_status = "dry-run"
|
||||
if not dry_run:
|
||||
token_status, source_token = ensure_answer_token_from_source(
|
||||
config,
|
||||
config.git.source_remote,
|
||||
config.git.answer_remote,
|
||||
card_config,
|
||||
card_config.git.source_remote,
|
||||
card_config.git.answer_remote,
|
||||
answer_url,
|
||||
)
|
||||
answer_remote_status = set_git_remote(
|
||||
card_path,
|
||||
config.git.answer_remote,
|
||||
card_config.git.answer_remote,
|
||||
credentialed_remote_url(
|
||||
answer_url,
|
||||
str(source_token.get("user", "")),
|
||||
@@ -2825,19 +3029,19 @@ def switch_card_task(
|
||||
else:
|
||||
subprocess.run(["git", "-C", str(card_path), "switch", "-c", branch_name], check=True)
|
||||
status = "created"
|
||||
upstream_status = set_branch_upstream(card_path, branch_name, config.git.answer_remote, dry_run=dry_run)
|
||||
upstream_status = set_branch_upstream(card_path, branch_name, card_config.git.answer_remote, dry_run=dry_run)
|
||||
|
||||
return {
|
||||
"series": series_name,
|
||||
"card": card_label_from_repo_name(card_path.name),
|
||||
"card": card_info.card_id,
|
||||
"task": task_name,
|
||||
"student_user": student_user,
|
||||
"branch": branch_name,
|
||||
"answer_remote": config.git.answer_remote,
|
||||
"answer_remote": card_config.git.answer_remote,
|
||||
"answer_url": answer_url,
|
||||
"token_status": token_status,
|
||||
"answer_remote_status": answer_remote_status,
|
||||
"upstream": f"{config.git.answer_remote}/{branch_name}",
|
||||
"upstream": f"{card_config.git.answer_remote}/{branch_name}",
|
||||
"upstream_status": upstream_status,
|
||||
"workspace": str(card_path),
|
||||
"status": status,
|
||||
@@ -2848,29 +3052,32 @@ def print_series_details(config: WorkspaceConfig, series_name: str) -> None:
|
||||
config.series_root.mkdir(parents=True, exist_ok=True)
|
||||
source_path = config.original_series_root / series_name
|
||||
workspace_path = config.series_root / series_name
|
||||
cards = source_card_directories(config, series_name)
|
||||
cards = source_card_infos(config, series_name)
|
||||
if not cards and not workspace_path.is_dir():
|
||||
raise SystemExit(f"Missing series: {series_name}")
|
||||
print(f"series\t{series_name}")
|
||||
print(f"source_path\t{source_path if source_path.is_dir() else ''}")
|
||||
print(f"manifest\t{config.workspace_info_path if workspace_series_manifest(config, series_name) else ''}")
|
||||
print(f"workspace_path\t{workspace_path}")
|
||||
print(f"cards_count\t{len(cards)}")
|
||||
print(f"cards\t{' '.join(card_label_from_repo_name(path.name) for path in cards)}")
|
||||
print(f"cards\t{' '.join(card.card_id for card in cards)}")
|
||||
|
||||
|
||||
def print_card_details(config: WorkspaceConfig, series_name: str, card_name: str) -> None:
|
||||
source_card_path = resolve_source_card_path(config, series_name, card_name)
|
||||
target_path = workspace_card_path(config, series_name, source_card_path)
|
||||
repo_name = source_card_path.name
|
||||
card_info = resolve_card_info(config, series_name, card_name)
|
||||
card_config = config_for_card(config, card_info)
|
||||
target_path = workspace_card_path_for_info(config, card_info)
|
||||
repo_name = card_info.repo
|
||||
print(f"series\t{series_name}")
|
||||
print(f"card\t{card_label_from_repo_name(repo_name)}")
|
||||
print(f"card\t{card_info.card_id}")
|
||||
print(f"repo\t{repo_name}")
|
||||
print(f"source_path\t{source_card_path}")
|
||||
print(f"source_path\t{card_info.source_path or ''}")
|
||||
print(f"workspace_path\t{target_path}")
|
||||
print(f"source_remote\t{config.git.source_remote}")
|
||||
print(f"source_url\t{source_url_for_card(config, series_name, source_card_path)}")
|
||||
print(f"answer_remote\t{config.git.answer_remote}")
|
||||
print(f"answer_url\t{answer_url_for_card(config, repo_name)}")
|
||||
print(f"source_remote\t{card_config.git.source_remote}")
|
||||
print(f"source_url\t{source_url_for_card_info(config, card_info)}")
|
||||
print(f"source_branch\t{card_info.branch}")
|
||||
print(f"answer_remote\t{card_config.git.answer_remote}")
|
||||
print(f"answer_url\t{answer_url_for_card(card_config, repo_name)}")
|
||||
if target_path.is_dir():
|
||||
print(f"tasks\t{' '.join(task_names(target_path))}")
|
||||
|
||||
@@ -2883,8 +3090,8 @@ def run_series(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
||||
print_series_details(config, args.series)
|
||||
return
|
||||
if args.series_command == "fetch":
|
||||
for source_card_path in source_card_directories(config, args.series):
|
||||
result = fetch_card_to_workspace(config, args.series, source_card_path.name, dry_run=args.dry_run)
|
||||
for card_info in source_card_infos(config, args.series):
|
||||
result = fetch_card_to_workspace(config, args.series, card_info.card_id, dry_run=args.dry_run)
|
||||
print_key_values(result)
|
||||
print()
|
||||
return
|
||||
@@ -2963,63 +3170,65 @@ def run_tasks(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
||||
|
||||
def print_submission_plan(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
||||
series_name, card_name = resolve_selector(config, args.series, args.card)
|
||||
card_info = resolve_card_info(config, series_name, card_name)
|
||||
card_config = config_for_card(config, card_info)
|
||||
selector = f"{series_name}/{card_name}"
|
||||
card_path = resolve_card_path(config, series_name, card_name)
|
||||
sync_tokens_from_repo(config, card_path)
|
||||
sync_tokens_from_repo(card_config, card_path)
|
||||
class_name = slug_value(args.class_name, "class")
|
||||
source_url = args.source_url or discover_source_url(config, card_path, series_name, card_name)
|
||||
source_url = args.source_url or discover_source_url(card_config, card_path, series_name, card_name)
|
||||
source_repo_name = repo_name_from_url(source_url)
|
||||
answer_repo_name = build_answer_repo_name(source_repo_name, class_name, args.date)
|
||||
answer_url = build_answer_url(config, answer_repo_name)
|
||||
source_branch = git_current_branch(config, card_path)
|
||||
answer_url = build_answer_url(card_config, answer_repo_name)
|
||||
source_branch = git_current_branch(card_config, card_path)
|
||||
task_name = resolve_submission_task(config, card_path, args.task)
|
||||
student_user = (
|
||||
slug_value(args.nick, "student user")
|
||||
if args.nick
|
||||
else store_user_for_submission(config, source_url, answer_url, required=args.branch is None)
|
||||
else store_user_for_submission(card_config, source_url, answer_url, required=args.branch is None)
|
||||
)
|
||||
branch_name = branch_value(args.branch) if args.branch else submission_branch_name(student_user, task_name)
|
||||
|
||||
print(f"selector\t{selector}")
|
||||
print(f"card_path\t{card_path}")
|
||||
print(f"task\t{task_name}")
|
||||
print(f"source_repo\t{config.git.source_org}/{source_repo_name}")
|
||||
print(f"source_remote\t{config.git.source_remote}")
|
||||
print(f"source_repo\t{card_config.git.source_org}/{source_repo_name}")
|
||||
print(f"source_remote\t{card_config.git.source_remote}")
|
||||
print(f"source_url\t{source_url}")
|
||||
print(f"source_branch\t{source_branch}")
|
||||
print(f"answer_remote\t{config.git.answer_remote}")
|
||||
print(f"answer_repo\t{config.git.answer_org}/{answer_repo_name}")
|
||||
print(f"answer_remote\t{card_config.git.answer_remote}")
|
||||
print(f"answer_repo\t{card_config.git.answer_org}/{answer_repo_name}")
|
||||
print(f"answer_url\t{answer_url}")
|
||||
print(f"student_user\t{student_user}")
|
||||
print(f"student_branch\t{branch_name}")
|
||||
|
||||
if args.apply:
|
||||
source_status = set_git_remote(card_path, config.git.source_remote, source_url)
|
||||
source_status = set_git_remote(card_path, card_config.git.source_remote, source_url)
|
||||
token_status, source_token = ensure_answer_token_from_source(
|
||||
config,
|
||||
config.git.source_remote,
|
||||
config.git.answer_remote,
|
||||
card_config,
|
||||
card_config.git.source_remote,
|
||||
card_config.git.answer_remote,
|
||||
answer_url,
|
||||
)
|
||||
answer_status = set_git_remote(
|
||||
card_path,
|
||||
config.git.answer_remote,
|
||||
card_config.git.answer_remote,
|
||||
credentialed_remote_url(
|
||||
answer_url,
|
||||
str(source_token.get("user", "")),
|
||||
str(source_token.get("value", "")),
|
||||
),
|
||||
)
|
||||
print(f"apply_{config.git.source_remote}\t{source_status}")
|
||||
print(f"apply_token_{config.git.answer_remote}\t{token_status}")
|
||||
print(f"apply_{config.git.answer_remote}\t{answer_status}")
|
||||
print(f"apply_{card_config.git.source_remote}\t{source_status}")
|
||||
print(f"apply_token_{card_config.git.answer_remote}\t{token_status}")
|
||||
print(f"apply_{card_config.git.answer_remote}\t{answer_status}")
|
||||
|
||||
print("commands")
|
||||
print(f" git -C {shlex.quote(str(card_path))} remote add {config.git.source_remote} {shlex.quote(source_url)}")
|
||||
print(f" git -C {shlex.quote(str(card_path))} remote add {config.git.answer_remote} {shlex.quote(answer_url)}")
|
||||
print(f" git -C {shlex.quote(str(card_path))} fetch {config.git.source_remote} {shlex.quote(source_branch)}")
|
||||
print(f" git -C {shlex.quote(str(card_path))} remote add {card_config.git.source_remote} {shlex.quote(source_url)}")
|
||||
print(f" git -C {shlex.quote(str(card_path))} remote add {card_config.git.answer_remote} {shlex.quote(answer_url)}")
|
||||
print(f" git -C {shlex.quote(str(card_path))} fetch {card_config.git.source_remote} {shlex.quote(source_branch)}")
|
||||
print(f" git -C {shlex.quote(str(card_path))} switch -c {shlex.quote(branch_name)}")
|
||||
print(f" git -C {shlex.quote(str(card_path))} push -u {config.git.answer_remote} {shlex.quote(branch_name)}")
|
||||
print(f" git -C {shlex.quote(str(card_path))} push -u {card_config.git.answer_remote} {shlex.quote(branch_name)}")
|
||||
|
||||
|
||||
def tmux_target_exists(session_name: str) -> bool:
|
||||
@@ -3107,11 +3316,55 @@ def run_tmux_container(config: WorkspaceConfig, args: argparse.Namespace) -> Non
|
||||
print(f"Attach with: tmux attach -t {session_name}")
|
||||
|
||||
|
||||
def workspace_info_status(config: WorkspaceConfig) -> str:
|
||||
info_root = workspace_info_root(config)
|
||||
if (info_root / ".git").is_dir():
|
||||
return "git"
|
||||
if config.workspace_info_path.is_file():
|
||||
return "file"
|
||||
return "missing"
|
||||
|
||||
|
||||
def run_workspace(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
||||
if args.workspace_command == "show":
|
||||
print(f"workspace_info_path\t{config.workspace_info_path}")
|
||||
print(f"workspace_info_url\t{config.workspace_info_url}")
|
||||
print(f"status\t{workspace_info_status(config)}")
|
||||
if config.workspace_info:
|
||||
print(f"workspace\t{config.workspace_info.get('workspace', '')}")
|
||||
print(f"series\t{len(config.workspace_info.get('series', []))}")
|
||||
return
|
||||
|
||||
if args.workspace_command == "sync":
|
||||
info_root = workspace_info_root(config)
|
||||
if args.dry_run:
|
||||
print(f"workspace_info_path\t{config.workspace_info_path}")
|
||||
print(f"workspace_info_url\t{config.workspace_info_url}")
|
||||
print(f"status\t{workspace_info_status(config)}")
|
||||
print(f"command\t{'pull' if (info_root / '.git').is_dir() else 'clone'}")
|
||||
return
|
||||
info_root.parent.mkdir(parents=True, exist_ok=True)
|
||||
if (info_root / ".git").is_dir():
|
||||
subprocess.run(["git", "-C", str(info_root), "pull", "--ff-only"], check=True)
|
||||
elif info_root.exists():
|
||||
raise SystemExit(f"Workspace info path exists but is not a git repo: {info_root}")
|
||||
else:
|
||||
subprocess.run(["git", "clone", config.workspace_info_url, str(info_root)], check=True)
|
||||
print(f"workspace_info_path\t{config.workspace_info_path}")
|
||||
print(f"status\tready")
|
||||
return
|
||||
|
||||
raise SystemExit(f"Unsupported workspace command: {args.workspace_command}")
|
||||
|
||||
|
||||
def print_config(config: WorkspaceConfig) -> None:
|
||||
print(f"config_path\t{config.config_path}")
|
||||
print(f"original_root\t{config.original_root}")
|
||||
print(f"original_series_root\t{config.original_series_root}")
|
||||
print(f"workspace_root\t{config.workspace_root}")
|
||||
print(f"workspace_info_path\t{config.workspace_info_path}")
|
||||
print(f"workspace_info_url\t{config.workspace_info_url}")
|
||||
print(f"workspace_info_status\t{workspace_info_status(config)}")
|
||||
print(f"series_root\t{config.series_root}")
|
||||
print(f"socket_root\t{config.socket_root}")
|
||||
print(f"token_path\t{config.token_path}")
|
||||
@@ -3192,6 +3445,7 @@ def print_main_overview(config_path: Path) -> None:
|
||||
["command", "arguments", "purpose"],
|
||||
[
|
||||
["show-config", "", "show resolved paths and Git defaults"],
|
||||
["workspace", "show|sync", "show or update meta/workspace-info"],
|
||||
["series", "list|cards|fetch|tasks", "work with series, cards and tasks"],
|
||||
["tasks", "list|show|switch", "shortcut for tasks in the default card"],
|
||||
["list-series", "", "compat alias for series list"],
|
||||
@@ -3207,11 +3461,12 @@ def print_main_overview(config_path: Path) -> None:
|
||||
["step", "command", "result"],
|
||||
[
|
||||
["1", "./rvctl tokens compare", "compare repo remotes with tokens.json"],
|
||||
["2", "./rvctl series list", "choose a series"],
|
||||
["3", "./rvctl series cards list inf", "choose a card"],
|
||||
["4", "./rvctl series cards fetch inf bss", "fetch card and prepare r1a"],
|
||||
["5", "./rvctl tasks list", "list tasks from the default card"],
|
||||
["6", "./rvctl tasks switch 4", "switch to the answer branch for task 4"],
|
||||
["2", "./rvctl workspace sync", "clone or update workspace-info"],
|
||||
["3", "./rvctl series list", "choose a series"],
|
||||
["4", "./rvctl series cards list inf", "choose a card"],
|
||||
["5", "./rvctl series cards fetch inf bss", "fetch card and prepare r1a"],
|
||||
["6", "./rvctl tasks list", "list tasks from the default card"],
|
||||
["7", "./rvctl tasks switch 4", "switch to the answer branch for task 4"],
|
||||
],
|
||||
)
|
||||
print()
|
||||
@@ -3311,6 +3566,12 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
cards_parser = subparsers.add_parser("list-cards", help="List cards in a series.")
|
||||
cards_parser.add_argument("series", nargs="?", help="Series id, for example 'inf'.")
|
||||
|
||||
workspace_parser = subparsers.add_parser("workspace", help="Manage workspace metadata manifest.")
|
||||
workspace_subparsers = workspace_parser.add_subparsers(dest="workspace_command", required=True)
|
||||
workspace_subparsers.add_parser("show", help="Show workspace-info configuration.")
|
||||
workspace_sync_parser = workspace_subparsers.add_parser("sync", help="Clone or update workspace-info into meta/.")
|
||||
workspace_sync_parser.add_argument("--dry-run", action="store_true", help="Print plan without cloning or pulling.")
|
||||
|
||||
tasks_parser = subparsers.add_parser("tasks", help="Shortcut for tasks in the default card.")
|
||||
tasks_subparsers = tasks_parser.add_subparsers(dest="tasks_command", required=True)
|
||||
|
||||
@@ -3559,6 +3820,9 @@ def main() -> int:
|
||||
if args.command == "list-cards":
|
||||
print_cards(config, args.series)
|
||||
return 0
|
||||
if args.command == "workspace":
|
||||
run_workspace(config, args)
|
||||
return 0
|
||||
if args.command == "tasks":
|
||||
run_tasks(config, args)
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user