Read series from workspace manifest
This commit is contained in:
@@ -26,7 +26,9 @@ python3 rvctl.py "$@"
|
|||||||
./rvctl.py
|
./rvctl.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Ścieżki i domyślne ustawienia są trzymane w `workspace.json`.
|
Lokalne ścieżki i domyślne ustawienia są trzymane w `workspace.json`.
|
||||||
|
Publiczny opis wspólnego workspace, czyli serie, karty i wersje repozytoriów,
|
||||||
|
jest w osobnym repo `edu-workspace/workspace-info`.
|
||||||
|
|
||||||
## Co robi narzędzie
|
## Co robi narzędzie
|
||||||
|
|
||||||
@@ -40,9 +42,10 @@ python3 rvctl.py "$@"
|
|||||||
|
|
||||||
2. Listowanie i pobieranie kart pracy
|
2. Listowanie i pobieranie kart pracy
|
||||||
|
|
||||||
`rvctl` czyta serie, karty i zadania z workspace, pomaga wybrać materiał do
|
`rvctl` czyta serie i karty z `meta/workspace-info`, zadania z pobranych
|
||||||
pracy oraz przygotowuje remotes potrzebne do repo odpowiedzi. Docelowy model
|
kart, pomaga wybrać materiał do pracy oraz przygotowuje remotes potrzebne do
|
||||||
namespace `series`, `cards` i `tasks` jest w `doc/series.md`.
|
repo odpowiedzi. Docelowy model namespace `series`, `cards` i `tasks` jest w
|
||||||
|
`doc/series.md`.
|
||||||
|
|
||||||
3. Uruchamianie środowiska programistycznego w kontenerach
|
3. Uruchamianie środowiska programistycznego w kontenerach
|
||||||
|
|
||||||
@@ -108,6 +111,16 @@ Po tym kroku w workspace jest już repo launchera:
|
|||||||
└── rv-launcher
|
└── rv-launcher
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Repo `workspace-info` trzymamy osobno od narzędzi, w katalogu `meta`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/dev/workspace/rv
|
||||||
|
├── meta
|
||||||
|
│ └── workspace-info
|
||||||
|
└── tools
|
||||||
|
└── rv-launcher
|
||||||
|
```
|
||||||
|
|
||||||
Etap 3: wczytaj token z remota do lokalnego store:
|
Etap 3: wczytaj token z remota do lokalnego store:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -283,6 +296,18 @@ Po konfiguracji tokenów najpierw sprawdź stan store:
|
|||||||
./rvctl tokens compare
|
./rvctl tokens compare
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Następnie pobierz albo zaktualizuj publiczny manifest wspólnego workspace:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./rvctl workspace sync
|
||||||
|
```
|
||||||
|
|
||||||
|
Manifest trafia do:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/dev/workspace/rv/meta/workspace-info
|
||||||
|
```
|
||||||
|
|
||||||
Następnie wylistuj dostępne serie. Jeżeli katalog `series` w workspace jeszcze
|
Następnie wylistuj dostępne serie. Jeżeli katalog `series` w workspace jeszcze
|
||||||
nie istnieje, `rvctl` utworzy go automatycznie:
|
nie istnieje, `rvctl` utworzy go automatycznie:
|
||||||
|
|
||||||
@@ -293,6 +318,7 @@ nie istnieje, `rvctl` utworzy go automatycznie:
|
|||||||
Przykładowy wynik:
|
Przykładowy wynik:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
fiz 3 0
|
||||||
inf 3 0
|
inf 3 0
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -319,6 +345,8 @@ Karty trafiają do `series` w workspace:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
~/dev/workspace/rv
|
~/dev/workspace/rv
|
||||||
|
├── meta
|
||||||
|
│ └── workspace-info
|
||||||
├── tools
|
├── tools
|
||||||
│ └── rv-launcher
|
│ └── rv-launcher
|
||||||
├── tokens
|
├── tokens
|
||||||
@@ -351,10 +379,10 @@ karcie z `workspace.json`, czyli tutaj na `inf bss`:
|
|||||||
Przykładowy wynik:
|
Przykładowy wynik:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
inf bss task1_bss ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks/task1_bss.s
|
inf bss task1_bss ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks/task1_bss.c
|
||||||
inf bss task2_data ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks/task2_data.s
|
inf bss task2_data ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks/task2_data.c
|
||||||
inf bss task3_stack_unused ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks/task3_stack_unused.s
|
inf bss task3_stack_unused ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks/task3_stack_unused.c
|
||||||
inf bss task4_stack_strlen ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks/task4_stack_strlen.s
|
inf bss task4_stack_strlen ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks/task4_stack_strlen.c
|
||||||
```
|
```
|
||||||
|
|
||||||
Przełącz się na wybrane zadanie. `rvctl` bierze login ucznia ze store tokenów,
|
Przełącz się na wybrane zadanie. `rvctl` bierze login ucznia ze store tokenów,
|
||||||
|
|||||||
+35
-9
@@ -22,21 +22,24 @@ Typowy układ:
|
|||||||
~/dev/edu/repos/rv/rv-launcher
|
~/dev/edu/repos/rv/rv-launcher
|
||||||
~/dev/edu/repos/rv/rv32i-hazard3-env
|
~/dev/edu/repos/rv/rv32i-hazard3-env
|
||||||
~/dev/edu/repos/rv/series/<seria>/<karta>
|
~/dev/edu/repos/rv/series/<seria>/<karta>
|
||||||
|
~/dev/workspace/rv/meta/workspace-info
|
||||||
~/dev/workspace/rv/tools/rv-launcher
|
~/dev/workspace/rv/tools/rv-launcher
|
||||||
~/dev/workspace/rv/tools/rv32i-hazard3-env
|
~/dev/workspace/rv/tools/rv32i-hazard3-env
|
||||||
~/dev/workspace/rv/series/<seria>/<karta>
|
~/dev/workspace/rv/series/<seria>/<karta>
|
||||||
```
|
```
|
||||||
|
|
||||||
`rvctl` czyta karty z `series_root` w workspace. Tool repo
|
`rvctl` czyta publiczny opis serii i kart z `workspace-info`, a pobrane karty
|
||||||
`rv32i-hazard3-env` wybiera najpierw z workspace, a potem z fallbacku
|
trzyma w `series_root` w workspace. Tool repo `rv32i-hazard3-env` wybiera
|
||||||
`~/dev/edu/repos/rv`, jeśli taki klon roboczy jeszcze nie istnieje.
|
najpierw z workspace, a potem z fallbacku `~/dev/edu/repos/rv`, jeśli taki klon
|
||||||
|
roboczy jeszcze nie istnieje.
|
||||||
|
|
||||||
Karty pracy też są rozdzielone:
|
Karty pracy są rozdzielone:
|
||||||
|
|
||||||
- `original_series_root` wskazuje repo źródłowe kart, na przykład `~/dev/edu/repos/rv/series`
|
- `workspace_info_path` wskazuje manifest, na przykład `~/dev/workspace/rv/meta/workspace-info/workspace.json`
|
||||||
- `series_root` wskazuje klony testowe w workspace, na przykład `~/dev/workspace/rv/series`
|
- `series_root` wskazuje klony robocze w workspace, na przykład `~/dev/workspace/rv/series`
|
||||||
|
- `original_series_root` zostaje fallbackiem dla lokalnych repo źródłowych
|
||||||
|
|
||||||
Komendy launchera pracują na `series_root`, czyli na klonach testowych.
|
Komendy launchera pracują na `series_root`, czyli na klonach roboczych.
|
||||||
|
|
||||||
## Pobranie repo i przełączenie gałęzi
|
## Pobranie repo i przełączenie gałęzi
|
||||||
|
|
||||||
@@ -203,6 +206,8 @@ Szczegółowy help parsera:
|
|||||||
Komendy:
|
Komendy:
|
||||||
|
|
||||||
- `show-config`
|
- `show-config`
|
||||||
|
- `workspace show`
|
||||||
|
- `workspace sync`
|
||||||
- `list-series`
|
- `list-series`
|
||||||
- `list-cards [series]`
|
- `list-cards [series]`
|
||||||
- `series list`
|
- `series list`
|
||||||
@@ -238,6 +243,9 @@ config_path<TAB>...
|
|||||||
original_root<TAB>...
|
original_root<TAB>...
|
||||||
original_series_root<TAB>...
|
original_series_root<TAB>...
|
||||||
workspace_root<TAB>...
|
workspace_root<TAB>...
|
||||||
|
workspace_info_path<TAB>...
|
||||||
|
workspace_info_url<TAB>...
|
||||||
|
workspace_info_status<TAB>...
|
||||||
series_root<TAB>...
|
series_root<TAB>...
|
||||||
socket_root<TAB>...
|
socket_root<TAB>...
|
||||||
token_path<TAB>...
|
token_path<TAB>...
|
||||||
@@ -259,10 +267,28 @@ Przykład:
|
|||||||
./rvctl show-config
|
./rvctl show-config
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `workspace show`
|
||||||
|
|
||||||
|
Pokazuje lokalizację i stan publicznego manifestu wspólnego workspace.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./rvctl workspace show
|
||||||
|
```
|
||||||
|
|
||||||
|
## `workspace sync`
|
||||||
|
|
||||||
|
Klonuje albo aktualizuje repo `edu-workspace/workspace-info` do katalogu
|
||||||
|
`~/dev/workspace/rv/meta/workspace-info`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./rvctl workspace sync
|
||||||
|
./rvctl workspace sync --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
## `series list`
|
## `series list`
|
||||||
|
|
||||||
Listuje serie dostępne w źródłach i workspace. Komenda tworzy `series_root`,
|
Listuje serie dostępne w `workspace-info` i workspace. Komenda tworzy
|
||||||
jeżeli katalog jeszcze nie istnieje.
|
`series_root`, jeżeli katalog jeszcze nie istnieje.
|
||||||
|
|
||||||
Format wyjścia:
|
Format wyjścia:
|
||||||
|
|
||||||
|
|||||||
+12
-8
@@ -12,13 +12,15 @@ Komendy kart pracy dzielimy na trzy poziomy:
|
|||||||
- `series cards tasks` - operacje na zadaniach w ramach karty
|
- `series cards tasks` - operacje na zadaniach w ramach karty
|
||||||
|
|
||||||
`rvctl` tworzy `series_root`, jeżeli katalog jeszcze nie istnieje. Lista serii
|
`rvctl` tworzy `series_root`, jeżeli katalog jeszcze nie istnieje. Lista serii
|
||||||
i kart może pochodzić z repo źródłowych, a `fetch` pobiera wybraną kartę do
|
i kart pochodzi docelowo z publicznego manifestu
|
||||||
workspace.
|
`~/dev/workspace/rv/meta/workspace-info/workspace.json`. Jeżeli manifestu nie
|
||||||
|
ma, narzędzie używa starego fallbacku przez `original_series_root`.
|
||||||
|
|
||||||
## Szybki przepływ
|
## Szybki przepływ
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./rvctl tokens compare
|
./rvctl tokens compare
|
||||||
|
./rvctl workspace sync
|
||||||
./rvctl series list
|
./rvctl series list
|
||||||
./rvctl series cards list inf
|
./rvctl series cards list inf
|
||||||
./rvctl series cards fetch inf bss
|
./rvctl series cards fetch inf bss
|
||||||
@@ -32,8 +34,9 @@ Dla karty `bss` właściwym repo jest `lab-rv32i-strlen-bss-data-stack`.
|
|||||||
|
|
||||||
### `series list`
|
### `series list`
|
||||||
|
|
||||||
Listuje dostępne serie. Jeżeli `~/dev/workspace/rv/series` nie istnieje,
|
Listuje dostępne serie z manifestu `workspace-info` oraz liczbę kart już
|
||||||
komenda tworzy ten katalog.
|
pobranych do lokalnego workspace. Jeżeli `~/dev/workspace/rv/series` nie
|
||||||
|
istnieje, komenda tworzy ten katalog.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./rvctl series list
|
./rvctl series list
|
||||||
@@ -42,6 +45,7 @@ komenda tworzy ten katalog.
|
|||||||
Typowy wynik:
|
Typowy wynik:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
fiz 3 0
|
||||||
inf 3 0
|
inf 3 0
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -147,10 +151,10 @@ Skrót dla domyślnej karty:
|
|||||||
Przykładowy wynik:
|
Przykładowy wynik:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
inf bss task1_bss ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks/task1_bss.s
|
inf bss task1_bss ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks/task1_bss.c
|
||||||
inf bss task2_data ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks/task2_data.s
|
inf bss task2_data ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks/task2_data.c
|
||||||
inf bss task3_stack_unused ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks/task3_stack_unused.s
|
inf bss task3_stack_unused ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks/task3_stack_unused.c
|
||||||
inf bss task4_stack_strlen ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks/task4_stack_strlen.s
|
inf bss task4_stack_strlen ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks/task4_stack_strlen.c
|
||||||
```
|
```
|
||||||
|
|
||||||
### `series cards tasks show SERIES CARD TASK`
|
### `series cards tasks show SERIES CARD TASK`
|
||||||
|
|||||||
+36
-1
@@ -10,4 +10,39 @@ Publiczny entrypoint launchera:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Implementacja CLI znajduje się w `rvctl.py`. Plik `workspace.json` przechowuje
|
Implementacja CLI znajduje się w `rvctl.py`. Plik `workspace.json` przechowuje
|
||||||
ścieżki workspace, series, socketów, tokenów i fallback do repo źródłowych.
|
lokalne ścieżki workspace, series, socketów, tokenów i adres publicznego
|
||||||
|
manifestu.
|
||||||
|
|
||||||
|
Publiczny manifest wspólnego workspace jest w repo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
edu-workspace/workspace-info
|
||||||
|
```
|
||||||
|
|
||||||
|
Lokalna kopia manifestu znajduje się w:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/dev/workspace/rv/meta/workspace-info
|
||||||
|
```
|
||||||
|
|
||||||
|
Typowy układ:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/dev/workspace/rv
|
||||||
|
├── meta
|
||||||
|
│ └── workspace-info
|
||||||
|
├── tools
|
||||||
|
│ └── rv-launcher
|
||||||
|
├── tokens
|
||||||
|
│ └── tokens.json
|
||||||
|
└── series
|
||||||
|
```
|
||||||
|
|
||||||
|
`workspace-info` opisuje serie, karty, repo źródłowe, repo odpowiedzi i branche.
|
||||||
|
Nie przechowuje tokenów ani lokalnych plików roboczych ucznia.
|
||||||
|
|
||||||
|
Aktualizacja manifestu:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./rvctl workspace sync
|
||||||
|
```
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import re
|
|||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, replace
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib import error as urlerror
|
from urllib import error as urlerror
|
||||||
from urllib import request as urlrequest
|
from urllib import request as urlrequest
|
||||||
@@ -44,6 +44,9 @@ class WorkspaceConfig:
|
|||||||
original_root: Path
|
original_root: Path
|
||||||
original_series_root: Path
|
original_series_root: Path
|
||||||
workspace_root: Path
|
workspace_root: Path
|
||||||
|
workspace_info_path: Path
|
||||||
|
workspace_info_url: str
|
||||||
|
workspace_info: dict | None
|
||||||
series_root: Path
|
series_root: Path
|
||||||
socket_root: Path
|
socket_root: Path
|
||||||
token_path: Path
|
token_path: Path
|
||||||
@@ -59,6 +62,21 @@ class TaskEntry:
|
|||||||
path: Path
|
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 = [
|
SCOPE_FIELDS = [
|
||||||
("a", "activitypub"),
|
("a", "activitypub"),
|
||||||
("A", "admin"),
|
("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)
|
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)
|
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)
|
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")
|
candidate_values = raw.get("tools_root_candidates")
|
||||||
if not candidate_values:
|
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])
|
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_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(
|
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"),
|
source_org=git_raw.get("source_org", "edu-inf"),
|
||||||
answer_org=git_raw.get("answer_org", "c2025-1a-inf"),
|
answer_org=git_raw.get("answer_org", "c2025-1a-inf"),
|
||||||
source_remote=git_raw.get("source_remote", "r1"),
|
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"),
|
origin_remote=git_raw.get("origin_remote", "origin"),
|
||||||
fallback_branch=git_raw.get("fallback_branch", "build"),
|
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(
|
return WorkspaceConfig(
|
||||||
config_path=config_path,
|
config_path=config_path,
|
||||||
original_root=original_root,
|
original_root=original_root,
|
||||||
original_series_root=original_series_root,
|
original_series_root=original_series_root,
|
||||||
workspace_root=workspace_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,
|
series_root=series_root,
|
||||||
socket_root=socket_root,
|
socket_root=socket_root,
|
||||||
token_path=token_path,
|
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]:
|
def series_directories(config: WorkspaceConfig) -> list[Path]:
|
||||||
config.series_root.mkdir(parents=True, exist_ok=True)
|
config.series_root.mkdir(parents=True, exist_ok=True)
|
||||||
return sorted(path for path in config.series_root.iterdir() if path.is_dir())
|
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
|
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:
|
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)
|
cards = source_card_directories(config, series_name)
|
||||||
if not cards:
|
if not cards:
|
||||||
raise SystemExit(f"Missing source series: {config.original_series_root / series_name}")
|
raise SystemExit(f"Missing source series: {config.original_series_root / series_name}")
|
||||||
@@ -220,10 +404,11 @@ def resolve_selector(
|
|||||||
return series_value, card_value
|
return series_value, card_value
|
||||||
|
|
||||||
if series_arg and not card_arg:
|
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 series_exists(config, series_arg):
|
||||||
if not default_card:
|
series_default_card = default_card_for_series(config, series_arg)
|
||||||
|
if not series_default_card:
|
||||||
raise SystemExit("Missing default card in config.")
|
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():
|
if default_series and (config.series_root / default_series / series_arg).is_dir():
|
||||||
return default_series, series_arg
|
return default_series, series_arg
|
||||||
if default_series:
|
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
|
card_path = config.series_root / series_name / card_name
|
||||||
if card_path.is_dir():
|
if card_path.is_dir():
|
||||||
return card_path
|
return card_path
|
||||||
source_card_path = resolve_source_card_path(config, series_name, card_name)
|
card_info = resolve_card_info(config, series_name, card_name)
|
||||||
resolved_path = workspace_card_path(config, series_name, source_card_path)
|
resolved_path = workspace_card_path_for_info(config, card_info)
|
||||||
if resolved_path.is_dir():
|
if resolved_path.is_dir():
|
||||||
return resolved_path
|
return resolved_path
|
||||||
raise SystemExit(f"Missing card path: {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:
|
def print_series(config: WorkspaceConfig) -> None:
|
||||||
config.series_root.mkdir(parents=True, exist_ok=True)
|
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)}
|
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)}
|
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)):
|
for series_name in sorted(set(manifest_by_name) | set(source_by_name) | set(workspace_by_name)):
|
||||||
source_path = source_by_name.get(series_name)
|
manifest = manifest_by_name.get(series_name)
|
||||||
workspace_path = workspace_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
|
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 ''}")
|
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.")
|
raise SystemExit("Missing series name and no default series is configured.")
|
||||||
|
|
||||||
config.series_root.mkdir(parents=True, exist_ok=True)
|
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_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 {}
|
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:
|
if not source_cards and not workspace_cards:
|
||||||
raise SystemExit(f"Missing series: {series_name}")
|
raise SystemExit(f"Missing series: {series_name}")
|
||||||
|
|
||||||
for repo_name in sorted(set(source_cards) | set(workspace_cards)):
|
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)
|
workspace_path = workspace_cards.get(repo_name)
|
||||||
title = read_card_title(workspace_path or source_path) if (workspace_path or source_path) else ""
|
title = read_card_title(workspace_path) if workspace_path else ""
|
||||||
label = card_label_from_repo_name(repo_name)
|
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"
|
status = "workspace" if workspace_path else "source"
|
||||||
if title:
|
if title:
|
||||||
print(f"{label}\t{repo_name}\t{status}\t{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"
|
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(
|
def ensure_answer_token_from_source(
|
||||||
config: WorkspaceConfig,
|
config: WorkspaceConfig,
|
||||||
source_remote: str,
|
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
|
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:
|
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_info = resolve_card_info(config, series_name, card_name)
|
||||||
card_path = workspace_card_path(config, series_name, source_card_path)
|
card_path = workspace_card_path_for_info(config, card_info)
|
||||||
if not card_path.is_dir():
|
if not card_path.is_dir():
|
||||||
raise SystemExit(f"Missing workspace card: {card_path}. Run: ./rvctl series cards fetch {series_name} {card_name}")
|
raise SystemExit(f"Missing workspace card: {card_path}. Run: ./rvctl series cards fetch {series_name} {card_name}")
|
||||||
return card_path
|
return card_path
|
||||||
|
|
||||||
|
|
||||||
def fetch_card_to_workspace(config: WorkspaceConfig, series_name: str, card_name: str, dry_run: bool = False) -> dict[str, str]:
|
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)
|
card_info = resolve_card_info(config, series_name, card_name)
|
||||||
repo_name = source_card_path.name
|
card_config = config_for_card(config, card_info)
|
||||||
target_path = workspace_card_path(config, series_name, source_card_path)
|
repo_name = card_info.repo
|
||||||
source_url = source_url_for_card(config, series_name, source_card_path)
|
target_path = workspace_card_path_for_info(config, card_info)
|
||||||
answer_url = answer_url_for_card(config, repo_name)
|
source_url = source_url_for_card_info(config, card_info)
|
||||||
source_branch = git_current_branch(config, source_card_path)
|
answer_url = answer_url_for_card(card_config, repo_name)
|
||||||
|
source_branch = card_info.branch
|
||||||
|
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
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():
|
if not target_path.exists():
|
||||||
subprocess.run(["git", "clone", source_url, str(target_path)], check=True)
|
subprocess.run(["git", "clone", source_url, str(target_path)], check=True)
|
||||||
cloned = True
|
cloned = True
|
||||||
origin_url = git_capture(target_path, ["remote", "get-url", config.git.origin_remote])
|
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", config.git.source_remote]) is None:
|
if origin_url and git_capture(target_path, ["remote", "get-url", card_config.git.source_remote]) is None:
|
||||||
subprocess.run(
|
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,
|
check=True,
|
||||||
)
|
)
|
||||||
set_git_remote(target_path, config.git.source_remote, source_url)
|
set_git_remote(target_path, card_config.git.source_remote, source_url)
|
||||||
subprocess.run(["git", "-C", str(target_path), "fetch", config.git.source_remote, source_branch], check=True)
|
subprocess.run(["git", "-C", str(target_path), "fetch", card_config.git.source_remote, source_branch], check=True)
|
||||||
current_branch = git_current_branch(config, target_path)
|
current_branch = git_current_branch(card_config, target_path)
|
||||||
if cloned or current_branch == source_branch:
|
if cloned or current_branch == source_branch:
|
||||||
if current_branch != source_branch:
|
if current_branch != source_branch:
|
||||||
local_branch_exists = subprocess.run(
|
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)
|
subprocess.run(["git", "-C", str(target_path), "switch", source_branch], check=True)
|
||||||
else:
|
else:
|
||||||
subprocess.run(
|
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,
|
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(
|
token_status, source_token = ensure_answer_token_from_source(
|
||||||
config,
|
card_config,
|
||||||
config.git.source_remote,
|
card_config.git.source_remote,
|
||||||
config.git.answer_remote,
|
card_config.git.answer_remote,
|
||||||
answer_url,
|
answer_url,
|
||||||
dry_run=dry_run,
|
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("user", "")),
|
||||||
str(source_token.get("value", "")),
|
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 {
|
return {
|
||||||
"series": series_name,
|
"series": series_name,
|
||||||
"card": card_label_from_repo_name(repo_name),
|
"card": card_info.card_id,
|
||||||
"repo": repo_name,
|
"repo": repo_name,
|
||||||
"source": source_url,
|
"source": source_url,
|
||||||
"workspace": str(target_path),
|
"workspace": str(target_path),
|
||||||
"source_remote": config.git.source_remote,
|
"source_remote": card_config.git.source_remote,
|
||||||
"source_branch": source_branch,
|
"source_branch": source_branch,
|
||||||
"answer_remote": config.git.answer_remote,
|
"answer_remote": card_config.git.answer_remote,
|
||||||
"answer_org": config.git.answer_org,
|
"answer_org": card_config.git.answer_org,
|
||||||
"answer_repo": repo_name,
|
"answer_repo": repo_name,
|
||||||
"answer_url": answer_url,
|
"answer_url": answer_url,
|
||||||
"token": config.git.answer_remote,
|
"token": card_config.git.answer_remote,
|
||||||
"token_status": token_status,
|
"token_status": token_status,
|
||||||
"answer_remote_status": answer_remote_status,
|
"answer_remote_status": answer_remote_status,
|
||||||
"status": "dry-run" if dry_run else "ready",
|
"status": "dry-run" if dry_run else "ready",
|
||||||
@@ -2781,14 +2983,16 @@ def switch_card_task(
|
|||||||
branch_override: str | None = None,
|
branch_override: str | None = None,
|
||||||
dry_run: bool = False,
|
dry_run: bool = False,
|
||||||
) -> dict[str, str]:
|
) -> 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)
|
card_path = resolve_workspace_card_path(config, series_name, card_name)
|
||||||
task_name = resolve_submission_task(config, card_path, task_selector)
|
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)
|
source_repo_name = repo_name_from_url(source_url)
|
||||||
answer_url = build_answer_url(config, source_repo_name)
|
answer_url = build_answer_url(card_config, source_repo_name)
|
||||||
student_user = store_user_for_submission(config, source_url, answer_url, required=branch_override is None)
|
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)
|
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(
|
branch_exists = subprocess.run(
|
||||||
["git", "-C", str(card_path), "rev-parse", "--verify", "--quiet", branch_name],
|
["git", "-C", str(card_path), "rev-parse", "--verify", "--quiet", branch_name],
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
@@ -2800,14 +3004,14 @@ def switch_card_task(
|
|||||||
answer_remote_status = "dry-run"
|
answer_remote_status = "dry-run"
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
token_status, source_token = ensure_answer_token_from_source(
|
token_status, source_token = ensure_answer_token_from_source(
|
||||||
config,
|
card_config,
|
||||||
config.git.source_remote,
|
card_config.git.source_remote,
|
||||||
config.git.answer_remote,
|
card_config.git.answer_remote,
|
||||||
answer_url,
|
answer_url,
|
||||||
)
|
)
|
||||||
answer_remote_status = set_git_remote(
|
answer_remote_status = set_git_remote(
|
||||||
card_path,
|
card_path,
|
||||||
config.git.answer_remote,
|
card_config.git.answer_remote,
|
||||||
credentialed_remote_url(
|
credentialed_remote_url(
|
||||||
answer_url,
|
answer_url,
|
||||||
str(source_token.get("user", "")),
|
str(source_token.get("user", "")),
|
||||||
@@ -2825,19 +3029,19 @@ def switch_card_task(
|
|||||||
else:
|
else:
|
||||||
subprocess.run(["git", "-C", str(card_path), "switch", "-c", branch_name], check=True)
|
subprocess.run(["git", "-C", str(card_path), "switch", "-c", branch_name], check=True)
|
||||||
status = "created"
|
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 {
|
return {
|
||||||
"series": series_name,
|
"series": series_name,
|
||||||
"card": card_label_from_repo_name(card_path.name),
|
"card": card_info.card_id,
|
||||||
"task": task_name,
|
"task": task_name,
|
||||||
"student_user": student_user,
|
"student_user": student_user,
|
||||||
"branch": branch_name,
|
"branch": branch_name,
|
||||||
"answer_remote": config.git.answer_remote,
|
"answer_remote": card_config.git.answer_remote,
|
||||||
"answer_url": answer_url,
|
"answer_url": answer_url,
|
||||||
"token_status": token_status,
|
"token_status": token_status,
|
||||||
"answer_remote_status": answer_remote_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,
|
"upstream_status": upstream_status,
|
||||||
"workspace": str(card_path),
|
"workspace": str(card_path),
|
||||||
"status": status,
|
"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)
|
config.series_root.mkdir(parents=True, exist_ok=True)
|
||||||
source_path = config.original_series_root / series_name
|
source_path = config.original_series_root / series_name
|
||||||
workspace_path = config.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():
|
if not cards and not workspace_path.is_dir():
|
||||||
raise SystemExit(f"Missing series: {series_name}")
|
raise SystemExit(f"Missing series: {series_name}")
|
||||||
print(f"series\t{series_name}")
|
print(f"series\t{series_name}")
|
||||||
print(f"source_path\t{source_path if source_path.is_dir() else ''}")
|
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"workspace_path\t{workspace_path}")
|
||||||
print(f"cards_count\t{len(cards)}")
|
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:
|
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)
|
card_info = resolve_card_info(config, series_name, card_name)
|
||||||
target_path = workspace_card_path(config, series_name, source_card_path)
|
card_config = config_for_card(config, card_info)
|
||||||
repo_name = source_card_path.name
|
target_path = workspace_card_path_for_info(config, card_info)
|
||||||
|
repo_name = card_info.repo
|
||||||
print(f"series\t{series_name}")
|
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"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"workspace_path\t{target_path}")
|
||||||
print(f"source_remote\t{config.git.source_remote}")
|
print(f"source_remote\t{card_config.git.source_remote}")
|
||||||
print(f"source_url\t{source_url_for_card(config, series_name, source_card_path)}")
|
print(f"source_url\t{source_url_for_card_info(config, card_info)}")
|
||||||
print(f"answer_remote\t{config.git.answer_remote}")
|
print(f"source_branch\t{card_info.branch}")
|
||||||
print(f"answer_url\t{answer_url_for_card(config, repo_name)}")
|
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():
|
if target_path.is_dir():
|
||||||
print(f"tasks\t{' '.join(task_names(target_path))}")
|
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)
|
print_series_details(config, args.series)
|
||||||
return
|
return
|
||||||
if args.series_command == "fetch":
|
if args.series_command == "fetch":
|
||||||
for source_card_path in source_card_directories(config, args.series):
|
for card_info in source_card_infos(config, args.series):
|
||||||
result = fetch_card_to_workspace(config, args.series, source_card_path.name, dry_run=args.dry_run)
|
result = fetch_card_to_workspace(config, args.series, card_info.card_id, dry_run=args.dry_run)
|
||||||
print_key_values(result)
|
print_key_values(result)
|
||||||
print()
|
print()
|
||||||
return
|
return
|
||||||
@@ -2963,63 +3170,65 @@ def run_tasks(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
|||||||
|
|
||||||
def print_submission_plan(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)
|
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}"
|
selector = f"{series_name}/{card_name}"
|
||||||
card_path = resolve_card_path(config, 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")
|
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)
|
source_repo_name = repo_name_from_url(source_url)
|
||||||
answer_repo_name = build_answer_repo_name(source_repo_name, class_name, args.date)
|
answer_repo_name = build_answer_repo_name(source_repo_name, class_name, args.date)
|
||||||
answer_url = build_answer_url(config, answer_repo_name)
|
answer_url = build_answer_url(card_config, answer_repo_name)
|
||||||
source_branch = git_current_branch(config, card_path)
|
source_branch = git_current_branch(card_config, card_path)
|
||||||
task_name = resolve_submission_task(config, card_path, args.task)
|
task_name = resolve_submission_task(config, card_path, args.task)
|
||||||
student_user = (
|
student_user = (
|
||||||
slug_value(args.nick, "student user")
|
slug_value(args.nick, "student user")
|
||||||
if args.nick
|
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)
|
branch_name = branch_value(args.branch) if args.branch else submission_branch_name(student_user, task_name)
|
||||||
|
|
||||||
print(f"selector\t{selector}")
|
print(f"selector\t{selector}")
|
||||||
print(f"card_path\t{card_path}")
|
print(f"card_path\t{card_path}")
|
||||||
print(f"task\t{task_name}")
|
print(f"task\t{task_name}")
|
||||||
print(f"source_repo\t{config.git.source_org}/{source_repo_name}")
|
print(f"source_repo\t{card_config.git.source_org}/{source_repo_name}")
|
||||||
print(f"source_remote\t{config.git.source_remote}")
|
print(f"source_remote\t{card_config.git.source_remote}")
|
||||||
print(f"source_url\t{source_url}")
|
print(f"source_url\t{source_url}")
|
||||||
print(f"source_branch\t{source_branch}")
|
print(f"source_branch\t{source_branch}")
|
||||||
print(f"answer_remote\t{config.git.answer_remote}")
|
print(f"answer_remote\t{card_config.git.answer_remote}")
|
||||||
print(f"answer_repo\t{config.git.answer_org}/{answer_repo_name}")
|
print(f"answer_repo\t{card_config.git.answer_org}/{answer_repo_name}")
|
||||||
print(f"answer_url\t{answer_url}")
|
print(f"answer_url\t{answer_url}")
|
||||||
print(f"student_user\t{student_user}")
|
print(f"student_user\t{student_user}")
|
||||||
print(f"student_branch\t{branch_name}")
|
print(f"student_branch\t{branch_name}")
|
||||||
|
|
||||||
if args.apply:
|
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(
|
token_status, source_token = ensure_answer_token_from_source(
|
||||||
config,
|
card_config,
|
||||||
config.git.source_remote,
|
card_config.git.source_remote,
|
||||||
config.git.answer_remote,
|
card_config.git.answer_remote,
|
||||||
answer_url,
|
answer_url,
|
||||||
)
|
)
|
||||||
answer_status = set_git_remote(
|
answer_status = set_git_remote(
|
||||||
card_path,
|
card_path,
|
||||||
config.git.answer_remote,
|
card_config.git.answer_remote,
|
||||||
credentialed_remote_url(
|
credentialed_remote_url(
|
||||||
answer_url,
|
answer_url,
|
||||||
str(source_token.get("user", "")),
|
str(source_token.get("user", "")),
|
||||||
str(source_token.get("value", "")),
|
str(source_token.get("value", "")),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
print(f"apply_{config.git.source_remote}\t{source_status}")
|
print(f"apply_{card_config.git.source_remote}\t{source_status}")
|
||||||
print(f"apply_token_{config.git.answer_remote}\t{token_status}")
|
print(f"apply_token_{card_config.git.answer_remote}\t{token_status}")
|
||||||
print(f"apply_{config.git.answer_remote}\t{answer_status}")
|
print(f"apply_{card_config.git.answer_remote}\t{answer_status}")
|
||||||
|
|
||||||
print("commands")
|
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 {card_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))} remote add {card_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))} 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))} 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:
|
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}")
|
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:
|
def print_config(config: WorkspaceConfig) -> None:
|
||||||
print(f"config_path\t{config.config_path}")
|
print(f"config_path\t{config.config_path}")
|
||||||
print(f"original_root\t{config.original_root}")
|
print(f"original_root\t{config.original_root}")
|
||||||
print(f"original_series_root\t{config.original_series_root}")
|
print(f"original_series_root\t{config.original_series_root}")
|
||||||
print(f"workspace_root\t{config.workspace_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"series_root\t{config.series_root}")
|
||||||
print(f"socket_root\t{config.socket_root}")
|
print(f"socket_root\t{config.socket_root}")
|
||||||
print(f"token_path\t{config.token_path}")
|
print(f"token_path\t{config.token_path}")
|
||||||
@@ -3192,6 +3445,7 @@ def print_main_overview(config_path: Path) -> None:
|
|||||||
["command", "arguments", "purpose"],
|
["command", "arguments", "purpose"],
|
||||||
[
|
[
|
||||||
["show-config", "", "show resolved paths and Git defaults"],
|
["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"],
|
["series", "list|cards|fetch|tasks", "work with series, cards and tasks"],
|
||||||
["tasks", "list|show|switch", "shortcut for tasks in the default card"],
|
["tasks", "list|show|switch", "shortcut for tasks in the default card"],
|
||||||
["list-series", "", "compat alias for series list"],
|
["list-series", "", "compat alias for series list"],
|
||||||
@@ -3207,11 +3461,12 @@ def print_main_overview(config_path: Path) -> None:
|
|||||||
["step", "command", "result"],
|
["step", "command", "result"],
|
||||||
[
|
[
|
||||||
["1", "./rvctl tokens compare", "compare repo remotes with tokens.json"],
|
["1", "./rvctl tokens compare", "compare repo remotes with tokens.json"],
|
||||||
["2", "./rvctl series list", "choose a series"],
|
["2", "./rvctl workspace sync", "clone or update workspace-info"],
|
||||||
["3", "./rvctl series cards list inf", "choose a card"],
|
["3", "./rvctl series list", "choose a series"],
|
||||||
["4", "./rvctl series cards fetch inf bss", "fetch card and prepare r1a"],
|
["4", "./rvctl series cards list inf", "choose a card"],
|
||||||
["5", "./rvctl tasks list", "list tasks from the default card"],
|
["5", "./rvctl series cards fetch inf bss", "fetch card and prepare r1a"],
|
||||||
["6", "./rvctl tasks switch 4", "switch to the answer branch for task 4"],
|
["6", "./rvctl tasks list", "list tasks from the default card"],
|
||||||
|
["7", "./rvctl tasks switch 4", "switch to the answer branch for task 4"],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
print()
|
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 = subparsers.add_parser("list-cards", help="List cards in a series.")
|
||||||
cards_parser.add_argument("series", nargs="?", help="Series id, for example 'inf'.")
|
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_parser = subparsers.add_parser("tasks", help="Shortcut for tasks in the default card.")
|
||||||
tasks_subparsers = tasks_parser.add_subparsers(dest="tasks_command", required=True)
|
tasks_subparsers = tasks_parser.add_subparsers(dest="tasks_command", required=True)
|
||||||
|
|
||||||
@@ -3559,6 +3820,9 @@ def main() -> int:
|
|||||||
if args.command == "list-cards":
|
if args.command == "list-cards":
|
||||||
print_cards(config, args.series)
|
print_cards(config, args.series)
|
||||||
return 0
|
return 0
|
||||||
|
if args.command == "workspace":
|
||||||
|
run_workspace(config, args)
|
||||||
|
return 0
|
||||||
if args.command == "tasks":
|
if args.command == "tasks":
|
||||||
run_tasks(config, args)
|
run_tasks(config, args)
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
"original_root": "~/dev/edu/repos/rv",
|
"original_root": "~/dev/edu/repos/rv",
|
||||||
"original_series_root": "~/dev/edu/repos/rv/series",
|
"original_series_root": "~/dev/edu/repos/rv/series",
|
||||||
"workspace_root": "~/dev/workspace/rv",
|
"workspace_root": "~/dev/workspace/rv",
|
||||||
|
"workspace_info_path": "~/dev/workspace/rv/meta/workspace-info/workspace.json",
|
||||||
|
"workspace_info_url": "http://77.90.8.171:3001/edu-workspace/workspace-info.git",
|
||||||
"series_root": "~/dev/workspace/rv/series",
|
"series_root": "~/dev/workspace/rv/series",
|
||||||
"socket_root": "~/dev/workspace/rv/sockets",
|
"socket_root": "~/dev/workspace/rv/sockets",
|
||||||
"token_file": "~/dev/workspace/rv/tokens/tokens.json",
|
"token_file": "~/dev/workspace/rv/tokens/tokens.json",
|
||||||
|
|||||||
Reference in New Issue
Block a user