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.json` trzyma rooty workspace, socketow i toolingu
- `workspace.py` listuje serie i karty z dysku - `workspace.py` listuje serie i karty z dysku
- `workspace.py tmux-container ...` stawia sesje `tmux` z kontenerem w `pane 0` - `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 - `doc/usage.md` opisuje przelaczniki CLI i format listowania
## Przygotowanie katalogu ## Przygotowanie katalogu
@@ -41,14 +42,6 @@ git fetch r1 main
git switch --track -c main 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` ### Droga 2: lokalny `tokens/gitea_tokens.json`
To jest wariant alternatywny, wygodniejszy wtedy, gdy launcher ma sam wykonywac 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 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` ## Wariant alternatywny: `git clone`
Jesli celem nie jest cwiczenie `remote add` i `fetch`, mozna tez uzyc: 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 show-config
python3 workspace.py list-series python3 workspace.py list-series
python3 workspace.py list-cards inf 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 --dry-run
python3 workspace.py tmux-container inf 03 --session rv-inf03 --attach python3 workspace.py tmux-container inf 03 --session rv-inf03 --attach
``` ```
+76 -10
View File
@@ -39,15 +39,7 @@ Przyklad:
```bash ```bash
git remote add r1 http://u1:TOKEN@77.90.8.171:3001/edu-tools/rv-launcher.git git remote add r1 http://u1:TOKEN@77.90.8.171:3001/edu-tools/rv-launcher.git
git fetch r1 main git fetch r1 main
git switch main git switch --track -c main r1/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
``` ```
#### Droga 2: lokalny `tokens/gitea_tokens.json` #### Droga 2: lokalny `tokens/gitea_tokens.json`
@@ -103,7 +95,7 @@ Wariant przez `r1`:
```bash ```bash
git fetch r1 main git fetch r1 main
git switch main git switch --track -c main r1/main
git pull --ff-only r1 main git pull --ff-only r1 main
``` ```
@@ -137,6 +129,7 @@ Komendy:
- `show-config` - `show-config`
- `list-series` - `list-series`
- `list-cards [series]` - `list-cards [series]`
- `submission [series] [card]`
- `tmux-container [series] [card]` - `tmux-container [series] [card]`
## `show-config` ## `show-config`
@@ -151,6 +144,13 @@ workspace_root<TAB>...
series_root<TAB>... series_root<TAB>...
socket_root<TAB>... socket_root<TAB>...
tools_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 tools_root_candidates
... ...
``` ```
@@ -206,6 +206,72 @@ python3 workspace.py list-cards
python3 workspace.py list-cards inf 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]` ## `tmux-container [series] [card]`
Tworzy nowa sesje `tmux` i uruchamia kontener w `pane 0`. 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/tools/rv32i-hazard3-env",
"~/dev/workspace/rv/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": { "defaults": {
"series": "inf", "series": "inf",
"card": "03", "card": "03",
+185
View File
@@ -2,8 +2,10 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import datetime as dt
import json import json
import os import os
import re
import shlex import shlex
import subprocess import subprocess
import sys import sys
@@ -19,6 +21,17 @@ def expand_path(raw_path: str, base_dir: Path) -> Path:
return path.resolve() 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 @dataclass
class WorkspaceConfig: class WorkspaceConfig:
config_path: Path config_path: Path
@@ -28,6 +41,7 @@ class WorkspaceConfig:
tools_root_candidates: list[Path] tools_root_candidates: list[Path]
tools_root: Path tools_root: Path
defaults: dict[str, str] defaults: dict[str, str]
git: GitConfig
def load_config(config_path: Path) -> WorkspaceConfig: 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]) tools_root = next((path for path in tools_root_candidates if path.exists()), tools_root_candidates[0])
defaults = raw.get("defaults", {}) 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( return WorkspaceConfig(
config_path=config_path, config_path=config_path,
@@ -54,6 +78,7 @@ def load_config(config_path: Path) -> WorkspaceConfig:
tools_root_candidates=tools_root_candidates, tools_root_candidates=tools_root_candidates,
tools_root=tools_root, tools_root=tools_root,
defaults=defaults, 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}") 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: def tmux_target_exists(session_name: str) -> bool:
result = subprocess.run( result = subprocess.run(
["tmux", "has-session", "-t", session_name], ["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"series_root\t{config.series_root}")
print(f"socket_root\t{config.socket_root}") print(f"socket_root\t{config.socket_root}")
print(f"tools_root\t{config.tools_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") print("tools_root_candidates")
for path in config.tools_root_candidates: for path in config.tools_root_candidates:
print(f" {path}") 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("--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.") 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 return parser
@@ -283,6 +465,9 @@ def main() -> int:
if args.command == "tmux-container": if args.command == "tmux-container":
run_tmux_container(config, args) run_tmux_container(config, args)
return 0 return 0
if args.command == "submission":
print_submission_plan(config, args)
return 0
parser.error(f"Unsupported command: {args.command}") parser.error(f"Unsupported command: {args.command}")
return 2 return 2