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
+51 -8
View File
@@ -5,6 +5,7 @@ To repo zawiera launcher do workspace `rv`:
- `workspace.json` trzyma rooty workspace, socketow i toolingu
- `workspace.py` listuje serie i karty z dysku
- `workspace.py tmux-container ...` stawia sesje `tmux` z kontenerem w `pane 0`
- `workspace.py submission ...` wylicza repo odpowiedzi w `zsl-inf` i branch ucznia
- `doc/usage.md` opisuje przelaczniki CLI i format listowania
## Przygotowanie katalogu
@@ -41,14 +42,6 @@ git fetch r1 main
git switch --track -c main r1/main
```
Jesli po zajeciach uzytkownik bedzie chcial wyslac projekt na swoje konto
Gitea, lepiej dodac osobny remote `r2`, zamiast przepinac `r1`:
```bash
git remote add r2 http://LOGIN:TOKEN@77.90.8.171:3001/LOGIN/rv-launcher.git
git push -u r2 main
```
### Droga 2: lokalny `tokens/gitea_tokens.json`
To jest wariant alternatywny, wygodniejszy wtedy, gdy launcher ma sam wykonywac
@@ -127,6 +120,55 @@ git fetch origin feat/x
git switch --track -c feat/x origin/feat/x
```
## Repo odpowiedzi kart pracy
Model pracy kart jest taki:
- `r1` wskazuje repo z materialem z `edu-inf`
- `a1` wskazuje wspolne repo odpowiedzi w `zsl-inf`
- kazdy uczen wysyla swoja prace na branch o nazwie swojego nicku
Repo odpowiedzi nie zawiera nicku w nazwie. Launcher buduje je w formacie:
```text
zsl-inf/<repo_z_edu-inf>-<klasa>-<data>
```
Przyklad:
```text
zsl-inf/lab-rv32i-strlen-bss-data-stack-4i-2026-04-26
```
Przyklad planu dla ucznia `u1`:
```bash
python3 workspace.py submission inf 03 --class 4i --nick u1
```
Przyklad konfiguracji remote'ow w repo karty:
```bash
python3 workspace.py submission inf 03 --class 4i --nick u1 --apply
```
Launcher wtedy:
- czyta URL zrodlowego repo z `origin` aktualnej karty i ustawia go jako `r1`
- bierze basename tego repo z `edu-inf` i z niego buduje nazwe repo odpowiedzi
- wylicza repo odpowiedzi w `zsl-inf` i ustawia je jako `a1`
- proponuje branch ucznia, na przyklad `u1`
Typowy wynik to:
```text
source_repo edu-inf/lab-rv32i-strlen-bss-data-stack
source_remote r1
answer_remote a1
answer_repo zsl-inf/lab-rv32i-strlen-bss-data-stack-4i-2026-04-26
student_branch u1
```
## Wariant alternatywny: `git clone`
Jesli celem nie jest cwiczenie `remote add` i `fetch`, mozna tez uzyc:
@@ -146,6 +188,7 @@ Przyklady:
python3 workspace.py show-config
python3 workspace.py list-series
python3 workspace.py list-cards inf
python3 workspace.py submission inf 03 --class 4i --nick u1
python3 workspace.py tmux-container inf 03 --dry-run
python3 workspace.py tmux-container inf 03 --session rv-inf03 --attach
```
+76 -10
View File
@@ -39,15 +39,7 @@ Przyklad:
```bash
git remote add r1 http://u1:TOKEN@77.90.8.171:3001/edu-tools/rv-launcher.git
git fetch r1 main
git switch main
git pull --ff-only r1 main
```
Jesli po zajeciach uzytkownik bedzie chcial wysylac projekty na swoje konto
Gitea, moze przepiac remote `r1` na wlasne repo:
```bash
git remote set-url r1 http://LOGIN:TOKEN@77.90.8.171:3001/LOGIN/rv-launcher.git
git switch --track -c main r1/main
```
#### Droga 2: lokalny `tokens/gitea_tokens.json`
@@ -103,7 +95,7 @@ Wariant przez `r1`:
```bash
git fetch r1 main
git switch main
git switch --track -c main r1/main
git pull --ff-only r1 main
```
@@ -137,6 +129,7 @@ Komendy:
- `show-config`
- `list-series`
- `list-cards [series]`
- `submission [series] [card]`
- `tmux-container [series] [card]`
## `show-config`
@@ -151,6 +144,13 @@ workspace_root<TAB>...
series_root<TAB>...
socket_root<TAB>...
tools_root<TAB>...
git_base_url<TAB>...
git_source_org<TAB>...
git_answer_org<TAB>...
git_source_remote<TAB>...
git_answer_remote<TAB>...
git_origin_remote<TAB>...
git_fallback_branch<TAB>...
tools_root_candidates
...
```
@@ -206,6 +206,72 @@ python3 workspace.py list-cards
python3 workspace.py list-cards inf
```
## `submission [series] [card]`
Wylicza flow oddawania rozwiazan:
- `r1` jako repo z materialem z `edu-inf`
- `a1` jako repo odpowiedzi w `zsl-inf`
- branch ucznia na podstawie jego nicku
Nazwa repo odpowiedzi jest budowana z nazwy repo zrodlowego z `edu-inf`, klasy
i daty:
```text
<repo_z_edu-inf>-<klasa>-<data>
```
Przyklad:
```text
lab-rv32i-strlen-bss-data-stack-4i-2026-04-26
```
Argumenty pozycyjne:
- `series`
Id serii albo pelny selector, na przyklad `inf` albo `inf/03`.
- `card`
Numer karty, na przyklad `03`.
Przelaczniki:
- `--class NAME`
Id klasy, na przyklad `4i`.
- `--nick NAME`
Nick ucznia. Domyslnie z niego powstaje nazwa brancha.
- `--branch NAME`
Nadpisuje domyslna nazwe brancha.
- `--date YYYY-MM-DD`
Data zajec uzywana w nazwie repo odpowiedzi. Domyslnie dzisiejsza.
- `--source-url URL`
Nadpisuje URL repo zrodlowego. Bez tego launcher czyta `origin` z repo karty.
- `--apply`
Dodaje albo aktualizuje remote `r1` i `a1` w repo karty.
Typowy format wyjscia:
```text
selector<TAB>inf/03
card_path<TAB>...
source_repo<TAB>edu-inf/lab-rv32i-strlen-bss-data-stack
source_remote<TAB>r1
source_url<TAB>http://77.90.8.171:3001/edu-inf/lab-rv32i-strlen-bss-data-stack.git
source_branch<TAB>deploy
answer_remote<TAB>a1
answer_repo<TAB>zsl-inf/lab-rv32i-strlen-bss-data-stack-4i-2026-04-26
answer_url<TAB>http://77.90.8.171:3001/zsl-inf/lab-rv32i-strlen-bss-data-stack-4i-2026-04-26.git
student_branch<TAB>u1
```
Przyklady:
```bash
python3 workspace.py submission inf 03 --class 4i --nick u1
python3 workspace.py submission inf 03 --class 4i --nick u1 --apply
python3 workspace.py submission inf/03 --class 4i --nick u2 --date 2026-04-26
```
## `tmux-container [series] [card]`
Tworzy nowa sesje `tmux` i uruchamia kontener w `pane 0`.
+9
View File
@@ -7,6 +7,15 @@
"~/dev/workspace/tools/rv32i-hazard3-env",
"~/dev/workspace/rv/tools/rv32i-hazard3-env"
],
"git": {
"base_url": "http://77.90.8.171:3001",
"source_org": "edu-inf",
"answer_org": "zsl-inf",
"source_remote": "r1",
"answer_remote": "a1",
"origin_remote": "origin",
"fallback_branch": "main"
},
"defaults": {
"series": "inf",
"card": "03",
+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