Add submission repo planning for card repos
This commit is contained in:
+185
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user