Add submission repo planning for card repos

This commit is contained in:
mpabi
2026-04-26 09:55:27 +02:00
parent 7a281fb57b
commit e286dd48b9
4 changed files with 321 additions and 18 deletions
+185
View File
@@ -2,8 +2,10 @@
from __future__ import annotations
import argparse
import datetime as dt
import json
import os
import re
import shlex
import subprocess
import sys
@@ -19,6 +21,17 @@ def expand_path(raw_path: str, base_dir: Path) -> Path:
return path.resolve()
@dataclass
class GitConfig:
base_url: str
source_org: str
answer_org: str
source_remote: str
answer_remote: str
origin_remote: str
fallback_branch: str
@dataclass
class WorkspaceConfig:
config_path: Path
@@ -28,6 +41,7 @@ class WorkspaceConfig:
tools_root_candidates: list[Path]
tools_root: Path
defaults: dict[str, str]
git: GitConfig
def load_config(config_path: Path) -> WorkspaceConfig:
@@ -45,6 +59,16 @@ 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", {})
git = GitConfig(
base_url=git_raw.get("base_url", "http://77.90.8.171:3001").rstrip("/"),
source_org=git_raw.get("source_org", "edu-inf"),
answer_org=git_raw.get("answer_org", "zsl-inf"),
source_remote=git_raw.get("source_remote", "r1"),
answer_remote=git_raw.get("answer_remote", "a1"),
origin_remote=git_raw.get("origin_remote", "origin"),
fallback_branch=git_raw.get("fallback_branch", "main"),
)
return WorkspaceConfig(
config_path=config_path,
@@ -54,6 +78,7 @@ def load_config(config_path: Path) -> WorkspaceConfig:
tools_root_candidates=tools_root_candidates,
tools_root=tools_root,
defaults=defaults,
git=git,
)
@@ -138,6 +163,129 @@ def print_cards(config: WorkspaceConfig, series_arg: str | None) -> None:
print(f"{card_path.name}\t{card_path}")
def slug_value(raw_value: str, field_name: str) -> str:
slug = re.sub(r"[^a-z0-9._-]+", "-", raw_value.strip().lower()).strip("-")
if not slug:
raise SystemExit(f"Empty {field_name} after slug conversion: {raw_value!r}")
return slug
def git_capture(repo_path: Path, git_args: list[str]) -> str | None:
result = subprocess.run(
["git", "-C", str(repo_path)] + git_args,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
check=False,
)
if result.returncode != 0:
return None
return result.stdout.strip()
def git_current_branch(config: WorkspaceConfig, repo_path: Path) -> str:
branch_name = git_capture(repo_path, ["branch", "--show-current"])
if branch_name:
return branch_name
head_ref = git_capture(repo_path, ["symbolic-ref", "--short", f"refs/remotes/{config.git.origin_remote}/HEAD"])
if head_ref and "/" in head_ref:
return head_ref.split("/", 1)[1]
return config.git.fallback_branch
def fallback_source_repo_name(series_name: str, card_name: str) -> str:
return "-".join([slug_value(series_name, "series"), slug_value(card_name, "card")])
def fallback_source_url(config: WorkspaceConfig, series_name: str, card_name: str) -> str:
repo_name = fallback_source_repo_name(series_name, card_name)
return f"{config.git.base_url}/{config.git.source_org}/{repo_name}.git"
def discover_source_url(config: WorkspaceConfig, repo_path: Path, series_name: str, card_name: str) -> str:
source_url = git_capture(repo_path, ["remote", "get-url", config.git.origin_remote])
if source_url:
return source_url
source_url = git_capture(repo_path, ["remote", "get-url", config.git.source_remote])
if source_url:
return source_url
return fallback_source_url(config, series_name, card_name)
def repo_name_from_url(remote_url: str) -> str:
repo_name = remote_url.rstrip("/").rsplit("/", 1)[-1]
if repo_name.endswith(".git"):
repo_name = repo_name[:-4]
return slug_value(repo_name, "source repo")
def build_answer_repo_name(source_repo_name: str, class_name: str, date_value: str) -> str:
return "-".join([source_repo_name, slug_value(class_name, "class"), date_value])
def build_answer_url(config: WorkspaceConfig, answer_repo_name: str) -> str:
return f"{config.git.base_url}/{config.git.answer_org}/{answer_repo_name}.git"
def set_git_remote(repo_path: Path, remote_name: str, remote_url: str) -> str:
existing_url = git_capture(repo_path, ["remote", "get-url", remote_name])
if existing_url == remote_url:
return "unchanged"
if existing_url:
subprocess.run(
["git", "-C", str(repo_path), "remote", "set-url", remote_name, remote_url],
check=True,
)
return "updated"
subprocess.run(
["git", "-C", str(repo_path), "remote", "add", remote_name, remote_url],
check=True,
)
return "added"
def print_submission_plan(config: WorkspaceConfig, args: argparse.Namespace) -> None:
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)
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)
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)
print(f"selector\t{selector}")
print(f"card_path\t{card_path}")
print(f"source_repo\t{config.git.source_org}/{source_repo_name}")
print(f"source_remote\t{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_url\t{answer_url}")
print(f"student_branch\t{branch_name}")
if args.apply:
source_status = set_git_remote(card_path, config.git.source_remote, source_url)
answer_status = set_git_remote(card_path, config.git.answer_remote, answer_url)
print(f"apply_{config.git.source_remote}\t{source_status}")
print(f"apply_{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))} 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)}")
def tmux_target_exists(session_name: str) -> bool:
result = subprocess.run(
["tmux", "has-session", "-t", session_name],
@@ -229,6 +377,13 @@ def print_config(config: WorkspaceConfig) -> None:
print(f"series_root\t{config.series_root}")
print(f"socket_root\t{config.socket_root}")
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}")
print(f"git_answer_org\t{config.git.answer_org}")
print(f"git_source_remote\t{config.git.source_remote}")
print(f"git_answer_remote\t{config.git.answer_remote}")
print(f"git_origin_remote\t{config.git.origin_remote}")
print(f"git_fallback_branch\t{config.git.fallback_branch}")
print("tools_root_candidates")
for path in config.tools_root_candidates:
print(f" {path}")
@@ -263,6 +418,33 @@ def build_parser() -> argparse.ArgumentParser:
tmux_parser.add_argument("--attach", action="store_true", help="Attach to the session after creation.")
tmux_parser.add_argument("--dry-run", action="store_true", help="Print commands instead of running tmux.")
submission_parser = subparsers.add_parser(
"submission",
help="Print or apply source/answer git remotes for a selected card.",
)
submission_parser.add_argument("series", nargs="?", help="Series id or full selector like 'inf/03'.")
submission_parser.add_argument("card", nargs="?", help="Card id, for example '03'.")
submission_parser.add_argument("--class", dest="class_name", required=True, help="Class id, for example '4i'.")
submission_parser.add_argument("--nick", required=True, help="Student nick used as the branch name.")
submission_parser.add_argument(
"--branch",
help="Override the submission branch name. Defaults to the value of --nick.",
)
submission_parser.add_argument(
"--date",
default=dt.date.today().isoformat(),
help="Lesson date used in the answer repo name. Default: today in YYYY-MM-DD.",
)
submission_parser.add_argument(
"--source-url",
help="Override the source repo URL. By default launcher reads the current origin from the card repo.",
)
submission_parser.add_argument(
"--apply",
action="store_true",
help="Write remotes into the selected card repo instead of only printing the plan.",
)
return parser
@@ -283,6 +465,9 @@ def main() -> int:
if args.command == "tmux-container":
run_tmux_container(config, args)
return 0
if args.command == "submission":
print_submission_plan(config, args)
return 0
parser.error(f"Unsupported command: {args.command}")
return 2