From 62048453045e09512f309bc501728a2da3b9aaed Mon Sep 17 00:00:00 2001 From: mpabi Date: Wed, 29 Apr 2026 02:46:19 +0200 Subject: [PATCH] Add series task branch flow --- README.md | 80 +++++- doc/containers.md | 48 ++-- doc/rvctl.md | 336 ++++++++++++++++++++----- doc/series.md | 167 ++++++------- doc/tokens.md | 8 +- rvctl.py | 607 +++++++++++++++++++++++++++++++++++++++++++--- workspace.json | 6 +- 7 files changed, 1029 insertions(+), 223 deletions(-) diff --git a/README.md b/README.md index 674d28a..ac3f8ae 100644 --- a/README.md +++ b/README.md @@ -275,7 +275,7 @@ item server proto host org repo user remote tok 1 gitea http 77.90.8.171:3001 edu-tools rv-launcher u1 r1 r1 S e59cc...13be forever ``` -## Szybki start +## Kolejny krok: serie i karty pracy Najpierw zobacz konfigurację i dostępne komendy: @@ -290,7 +290,37 @@ Po konfiguracji tokenów najpierw sprawdź stan store: ./rvctl tokens compare ``` -### Po pobraniu kart pracy +Następnie wylistuj dostępne serie. Jeżeli katalog `series` w workspace jeszcze +nie istnieje, `rvctl` utworzy go automatycznie: + +```bash +./rvctl series list +``` + +Przykładowy wynik: + +```text +inf 3 0 +``` + +Po wybraniu serii `inf` wylistuj karty: + +```bash +./rvctl series cards list inf +``` + +Przykładowy wynik zawiera krótką nazwę karty, pełną nazwę repo, status i tytuł: + +```text +bss lab-rv32i-strlen-bss-data-stack source rv32i-c / bss-data-stack +``` + +Przykładowa karta `bss` odpowiada repo `lab-rv32i-strlen-bss-data-stack`. +Pobierz ją do workspace: + +```bash +./rvctl series cards fetch inf bss +``` Karty trafiają do `series` w workspace: @@ -302,18 +332,48 @@ Karty trafiają do `series` w workspace: │ └── tokens.json └── series └── inf - └── 03 + └── lab-rv32i-strlen-bss-data-stack ``` `rvctl` pracuje na kartach z `series_root`, czyli na katalogu `~/dev/workspace/rv/series`. -Od tego momentu możesz listować serie i karty: +Po pobraniu karty `rvctl` przygotowuje też remote odpowiedzi `r1a`. Remote +powstaje z tokena `r1` i wskazuje na repo pracy: + +```text +answer_remote r1a +answer_org c2025-1a-inf +answer_repo lab-rv32i-strlen-bss-data-stack +answer_url http://77.90.8.171:3001/c2025-1a-inf/lab-rv32i-strlen-bss-data-stack.git +``` + +Następnie wylistuj zadania w karcie: ```bash -./rvctl list-series -./rvctl list-cards inf -./rvctl submission inf 03 --class 4i --nick u1 +./rvctl series cards tasks list inf bss +``` + +Przykładowy wynik: + +```text +inf bss task1_bss ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks +inf bss task2_data ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks +inf bss task3_stack_unused ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks +inf bss task4_stack_strlen ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks +``` + +Przełącz się na wybrane zadanie. `rvctl` bierze login ucznia ze store tokenów i +tworzy branch pracy w formacie `Ta`: + +```bash +./rvctl series cards tasks switch inf bss 4 +``` + +Dla użytkownika `u1` i zadania `task4_stack_strlen` branch będzie miał nazwę: + +```text +u1T4a ``` ### Po uruchomieniu środowiska kontenerowego @@ -330,7 +390,7 @@ kontenerowego. Wtedy pojawia się pod `tools`: │ └── tokens.json └── series └── inf - └── 03 + └── lab-rv32i-strlen-bss-data-stack ``` Repozytoria źródłowe poza workspace, na przykład `~/dev/edu/repos/rv`, są @@ -340,8 +400,8 @@ do zwykłej pracy w workspace. Przykładowe komendy dla środowiska kontenerowego: ```bash -./rvctl tmux-container inf 03 --dry-run -./rvctl tmux-container inf 03 --session rv-inf03 --attach +./rvctl tmux-container inf bss --dry-run +./rvctl tmux-container inf bss --session rv-inf-bss --attach ``` `--dry-run` jest przydatny przy sprawdzaniu planu uruchomienia lub konfiguracji diff --git a/doc/containers.md b/doc/containers.md index 1ab8184..e3d5ce1 100644 --- a/doc/containers.md +++ b/doc/containers.md @@ -18,19 +18,19 @@ Typowy przepływ pracy: ```bash ./rvctl tokens compare -./rvctl series cards show inf 03 -./rvctl series cards tasks list inf 03 -./rvctl containers show inf 03 -./rvctl containers start inf 03 --session rv-inf03 --attach +./rvctl series cards show inf bss +./rvctl series cards tasks list inf bss +./rvctl containers show inf bss +./rvctl containers start inf bss --session rv-inf-bss --attach ./rvctl containers list -./rvctl containers stop rv-inf03 +./rvctl containers stop rv-inf-bss ``` ## Model Docelowo komendy `containers` pracują na trzech warstwach: -- karta pracy, np. `inf 03` +- karta pracy, np. `inf bss` - opcjonalne zadanie, np. `--task task1` - instancja środowiska, np. `--instance shell` @@ -52,19 +52,19 @@ defaults.tmux_window Pokazuje plan uruchomienia środowiska bez tworzenia sesji `tmux`. ```bash -./rvctl containers show inf 03 -./rvctl containers show inf/03 --task task1 +./rvctl containers show inf bss +./rvctl containers show inf/bss --task task1 ``` Typowe pola: ```text -selector inf/03 +selector inf/bss task task1 -card_path ~/dev/workspace/rv/series/inf/03 -task_path ~/dev/workspace/rv/series/inf/03/task1 +card_path ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack +task_path ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/task1 tools_root ~/dev/workspace/rv/tools/rv32i-hazard3-env -session rv-inf03 +session rv-inf-bss window rv instance shell ``` @@ -72,7 +72,7 @@ instance shell Ta komenda jest odpowiednikiem obecnego trybu: ```bash -./rvctl tmux-container inf 03 --dry-run +./rvctl tmux-container inf bss --dry-run ``` ### `containers start [SERIES] [CARD]` @@ -80,9 +80,9 @@ Ta komenda jest odpowiednikiem obecnego trybu: Uruchamia sesję `tmux` i kontener dla wybranej karty. ```bash -./rvctl containers start inf 03 -./rvctl containers start inf/03 --session rv-inf03 -./rvctl containers start inf 03 --task task1 --instance shell --attach +./rvctl containers start inf bss +./rvctl containers start inf/bss --session rv-inf-bss +./rvctl containers start inf bss --task task1 --instance shell --attach ``` Przełączniki: @@ -112,7 +112,7 @@ Przykładowy wynik: ```text session selector task instance window -------- -------- ----- -------- ------ -rv-inf03 inf/03 task1 shell rv +rv-inf-bss inf/bss task1 shell rv ``` ### `containers attach SESSION` @@ -120,13 +120,13 @@ rv-inf03 inf/03 task1 shell rv Podłącza terminal do istniejącej sesji. ```bash -./rvctl containers attach rv-inf03 +./rvctl containers attach rv-inf-bss ``` Technicznie odpowiada to: ```bash -tmux attach -t rv-inf03 +tmux attach -t rv-inf-bss ``` ### `containers stop SESSION` @@ -134,13 +134,13 @@ tmux attach -t rv-inf03 Zamyka sesję `tmux`, a razem z nią uruchomiony w niej proces kontenera. ```bash -./rvctl containers stop rv-inf03 +./rvctl containers stop rv-inf-bss ``` Technicznie odpowiada to: ```bash -tmux kill-session -t rv-inf03 +tmux kill-session -t rv-inf-bss ``` ## Aliasowanie Starej Komendy @@ -148,9 +148,9 @@ tmux kill-session -t rv-inf03 Stara komenda może zostać jako alias kompatybilności: ```text -tmux-container inf 03 --dry-run -> containers show inf 03 -tmux-container inf 03 --session NAME -> containers start inf 03 --session NAME -tmux-container inf/03 --attach -> containers start inf/03 --attach +tmux-container inf bss --dry-run -> containers show inf bss +tmux-container inf bss --session NAME -> containers start inf bss --session NAME +tmux-container inf/bss --attach -> containers start inf/bss --attach ``` Dokumentacja i nowe przykłady powinny promować namespace `containers`. diff --git a/doc/rvctl.md b/doc/rvctl.md index 2c1aa38..d305f54 100644 --- a/doc/rvctl.md +++ b/doc/rvctl.md @@ -205,6 +205,16 @@ Komendy: - `show-config` - `list-series` - `list-cards [series]` +- `series list` +- `series show SERIES` +- `series fetch SERIES [--dry-run]` +- `series cards list SERIES` +- `series cards show SERIES CARD` +- `series cards fetch SERIES CARD` +- `series cards submission SERIES CARD --class NAME` +- `series cards tasks list SERIES CARD` +- `series cards tasks show SERIES CARD TASK` +- `series cards tasks switch SERIES CARD TASK` - `tokens scan` - `tokens compare` - `tokens read` @@ -246,25 +256,247 @@ Przykład: ./rvctl show-config ``` -## `list-series` +## `series list` -Listuje katalogi serii znalezione w `series_root`. +Listuje serie dostępne w źródłach i workspace. Komenda tworzy `series_root`, +jeżeli katalog jeszcze nie istnieje. -Każda linia ma format: +Format wyjścia: ```text - + ``` Przykład: +```bash +./rvctl series list +``` + +## `series show SERIES` + +Pokazuje szczegóły jednej serii. + +Argumenty: + +- `SERIES` + Id serii, na przykład `inf`. + +Format wyjścia: + +```text +series... +source_path... +workspace_path... +cards_count... +cards... +``` + +## `series fetch SERIES` + +Pobiera albo aktualizuje wszystkie karty z serii. + +Argumenty: + +- `SERIES` + Id serii, na przykład `inf`. + +Przełączniki: + +- `--dry-run` + Pokazuje plan bez klonowania repo i bez zmian w token store. + +Przykład: + +```bash +./rvctl series fetch inf --dry-run +``` + +## `series cards list SERIES` + +Listuje karty w wybranej serii. + +Argumenty: + +- `SERIES` + Id serii, na przykład `inf`. + +Format wyjścia: + +```text + +``` + +Przykład: + +```bash +./rvctl series cards list inf +``` + +## `series cards show SERIES CARD` + +Pokazuje szczegóły jednej karty. + +Argumenty: + +- `SERIES` + Id serii, na przykład `inf`. +- `CARD` + Pełna nazwa repo albo unikalny fragment, na przykład `bss`. + +Format wyjścia: + +```text +series... +card... +repo... +source_path... +workspace_path... +source_remote... +source_url... +answer_remote... +answer_url... +tasks... +``` + +## `series cards fetch SERIES CARD` + +Pobiera kartę z repo źródłowego do `series_root`. `CARD` może być pełną nazwą +repo albo unikalnym fragmentem, na przykład `bss`. + +Argumenty: + +- `SERIES` + Id serii, na przykład `inf`. +- `CARD` + Pełna nazwa repo albo unikalny fragment, na przykład `bss`. + +Przełączniki: + +- `--dry-run` + Pokazuje plan bez klonowania repo, bez zapisu tokena `r1a` i bez ustawiania + remota odpowiedzi. + +```bash +./rvctl series cards fetch inf bss +``` + +Po pobraniu komenda ustawia: + +- `r1` na repo źródłowe w `edu-inf` +- `r1a` na repo pracy w `c2025-1a-inf` + +`r1a` jest tworzony z tokena `r1` i wskazuje na repo o tej samej nazwie co +karta. + +## `series cards submission SERIES CARD` + +Alias funkcjonalny dla `submission`, ale z jawnym namespace kart. + +Argumenty: + +- `SERIES` + Id serii, na przykład `inf`. +- `CARD` + Pełna nazwa repo albo unikalny fragment, na przykład `bss`. + +Przełączniki: + +- `--class NAME` + Id klasy albo grupy. +- `--nick NAME` + Nick ucznia. Jeżeli go nie podasz, `rvctl` bierze użytkownika ze store + tokenów. +- `--task TASK` + Zadanie używane do zbudowania domyślnej nazwy brancha. Domyślnie + `defaults.task` z `workspace.json`. +- `--branch NAME` + Nadpisuje nazwę brancha odpowiedzi. Bez tego branch ma format + `Ta`, na przykład `u1T1a`. +- `--date YYYY-MM-DD` + Data zajęć widoczna w planie. +- `--source-url URL` + Nadpisuje URL repo źródłowego. +- `--apply` + Zapisuje remotes `r1` i `r1a` w repo karty. + +Przykład: + +```bash +./rvctl series cards submission inf bss --class c2025-1a-inf --apply +``` + +## `series cards tasks list SERIES CARD` + +Listuje zadania z `src/tasks` pobranej karty. + +Argumenty: + +- `SERIES` + Id serii, na przykład `inf`. +- `CARD` + Pełna nazwa repo albo unikalny fragment, na przykład `bss`. + +```bash +./rvctl series cards tasks list inf bss +``` + +## `series cards tasks show SERIES CARD TASK` + +Pokazuje szczegóły jednego zadania. + +Argumenty: + +- `SERIES` + Id serii, na przykład `inf`. +- `CARD` + Pełna nazwa repo albo unikalny fragment, na przykład `bss`. +- `TASK` + Pełna nazwa zadania albo unikalny fragment, na przykład `task1`. + +```bash +./rvctl series cards tasks show inf bss task1 +``` + +## `series cards tasks switch SERIES CARD TASK` + +Tworzy albo przełącza lokalny branch pracy dla zadania. + +Argumenty: + +- `SERIES` + Id serii, na przykład `inf`. +- `CARD` + Pełna nazwa repo albo unikalny fragment, na przykład `bss`. +- `TASK` + Pełna nazwa zadania albo unikalny fragment, na przykład `task4` albo `4`. + +Przełączniki: + +- `--branch NAME` + Nadpisuje automatyczną nazwę brancha. +- `--dry-run` + Pokazuje plan bez przełączania brancha. + +Domyślna nazwa brancha powstaje z użytkownika w `tokens.json`, numeru zadania i +końcówki `a`. Dla użytkownika `u1` i zadania `task4_stack_strlen` będzie to +`u1T4a`. + +```bash +./rvctl series cards tasks switch inf bss 4 +``` + +## `list-series` + +Alias kompatybilności dla `series list`. + ```bash ./rvctl list-series ``` ## `list-cards [series]` -Listuje karty z wybranej serii. +Alias kompatybilności dla `series cards list SERIES`. Argumenty: @@ -272,18 +504,6 @@ Argumenty: Opcjonalne id serii, na przykład `inf`. Jeśli go brak, brana jest domyślna seria z `workspace.json`. -Format wyjścia: - -```text - -``` - -Jeśli `README.md` nie ma nagłówka `#`, skrypt wypisuje: - -```text - -``` - Przykłady: ```bash @@ -317,7 +537,7 @@ Przykład: ```bash ./rvctl tokens scan -./rvctl tokens scan --repo ~/dev/workspace/rv/series/inf/03 +./rvctl tokens scan --repo ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack ``` ## `tokens compare` @@ -358,7 +578,7 @@ Przykład: ```bash ./rvctl tokens compare -./rvctl tokens compare --repo ~/dev/workspace/rv/series/inf/03 +./rvctl tokens compare --repo ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack ``` ## `tokens list store|remote|both` @@ -466,7 +686,7 @@ Przełączniki: Przykład: ```bash -./rvctl tokens sync store r1 --repo ~/dev/workspace/rv/series/inf/03 +./rvctl tokens sync store r1 --repo ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack ``` ## `tokens remove store|remote|both REMOTE_ID` @@ -491,7 +711,7 @@ Przykłady: ./rvctl tokens remove store r1 ./rvctl tokens remove remote r1 ./rvctl tokens remove both r1 -./rvctl tokens remove remote r1 --repo ~/dev/workspace/rv/series/inf/03 +./rvctl tokens remove remote r1 --repo ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack ``` ## `tokens read` @@ -559,7 +779,7 @@ in_sync1 Przykład: ```bash -./rvctl tokens stats --repo ~/dev/workspace/rv/series/inf/03 +./rvctl tokens stats --repo ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack ``` ## `tokens write` @@ -588,7 +808,7 @@ Przełączniki: Przykład: ```bash -./rvctl tokens write --repo ~/dev/workspace/rv/series/inf/03 --remote r1 --server http://77.90.8.171:3001 +./rvctl tokens write --repo ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack --remote r1 --server http://77.90.8.171:3001 ``` ## `tokens update REMOTE_ID` @@ -639,8 +859,8 @@ Przełączniki: Przykłady: ```bash -./rvctl tokens update --from remotes --repo ~/dev/workspace/rv/series/inf/03 -./rvctl tokens update --from store --repo ~/dev/workspace/rv/series/inf/03 --remote r1 --server http://77.90.8.171:3001 +./rvctl tokens update --from remotes --repo ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack +./rvctl tokens update --from store --repo ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack --remote r1 --server http://77.90.8.171:3001 ``` ## `submission [series] [card]` @@ -648,65 +868,71 @@ Przykłady: Wylicza przepływ oddawania rozwiązań: - `r1` jako repo z materiałem z `edu-inf` -- `a1` jako repo odpowiedzi w `zsl-inf` -- branch ucznia na podstawie jego nicku +- `r1a` jako repo odpowiedzi w `c2025-1a-inf` +- branch ucznia liczony ze store tokenów i zadania -Nazwa repo odpowiedzi jest budowana z nazwy repo źródłowego z `edu-inf`, klasy -i daty: +Nazwa repo odpowiedzi jest taka sama jak nazwa repo źródłowego karty: ```text --- + ``` Przykład: ```text -lab-rv32i-strlen-bss-data-stack-4i-2026-04-26 +lab-rv32i-strlen-bss-data-stack ``` Argumenty pozycyjne: - `series` - Id serii albo pełny selector, na przykład `inf` albo `inf/03`. + Id serii albo pełny selector, na przykład `inf` albo `inf/bss`. - `card` - Numer karty, na przykład `03`. + Karta albo unikalny fragment nazwy repo, na przykład `bss`. Przełączniki: - `--class NAME` - Id klasy, na przykład `4i`. + Id klasy albo grupy, na przykład `c2025-1a-inf`. - `--nick NAME` - Nick ucznia. Domyślnie z niego powstaje nazwa brancha. + Nick ucznia. Jeżeli go nie podasz, `rvctl` bierze użytkownika ze store + tokenów. +- `--task TASK` + Zadanie używane do zbudowania domyślnej nazwy brancha. Domyślnie + `defaults.task` z `workspace.json`. - `--branch NAME` - Nadpisuje domyślną nazwę brancha. + Nadpisuje domyślną nazwę brancha. Bez tego branch ma format + `Ta`, na przykład `u1T1a`. - `--date YYYY-MM-DD` - Data zajęć używana w nazwie repo odpowiedzi. Domyślnie dzisiejsza. + Data zajęć. Zostaje w raporcie, ale nie zmienia nazwy repo odpowiedzi. - `--source-url URL` - Nadpisuje URL repo źródłowego. Bez tego launcher czyta `origin` z repo karty. + Nadpisuje URL repo źródłowego. Bez tego launcher czyta `r1` albo `origin` z + repo karty. - `--apply` - Dodaje albo aktualizuje remote `r1` i `a1` w repo karty. + Dodaje albo aktualizuje remote `r1` i `r1a` w repo karty. Typowy format wyjścia: ```text -selectorinf/03 +selectorinf/bss card_path... source_repoedu-inf/lab-rv32i-strlen-bss-data-stack source_remoter1 source_urlhttp://77.90.8.171:3001/edu-inf/lab-rv32i-strlen-bss-data-stack.git source_branchdeploy -answer_remotea1 -answer_repozsl-inf/lab-rv32i-strlen-bss-data-stack-4i-2026-04-26 -answer_urlhttp://77.90.8.171:3001/zsl-inf/lab-rv32i-strlen-bss-data-stack-4i-2026-04-26.git -student_branchu1 +answer_remoter1a +answer_repoc2025-1a-inf/lab-rv32i-strlen-bss-data-stack +answer_urlhttp://77.90.8.171:3001/c2025-1a-inf/lab-rv32i-strlen-bss-data-stack.git +student_useru1 +student_branchu1T1a ``` Przykłady: ```bash -./rvctl submission inf 03 --class 4i --nick u1 -./rvctl submission inf 03 --class 4i --nick u1 --apply -./rvctl submission inf/03 --class 4i --nick u2 --date 2026-04-26 +./rvctl submission inf bss --class c2025-1a-inf +./rvctl submission inf bss --class c2025-1a-inf --task 4 --apply +./rvctl series cards submission inf bss --class c2025-1a-inf --branch u1T4a ``` ## `tmux-container [series] [card]` @@ -716,9 +942,9 @@ Tworzy nową sesję `tmux` i uruchamia kontener w `pane 0`. Argumenty pozycyjne: - `series` - Id serii albo pełny selector, na przykład `inf` albo `inf/03`. + Id serii albo pełny selector, na przykład `inf` albo `inf/bss`. - `card` - Numer karty, na przykład `03`. + Karta albo unikalny fragment nazwy repo, na przykład `bss`. Przełączniki: @@ -735,16 +961,16 @@ Przełączniki: Reguły wyboru karty: -- `tmux-container inf 03` -> seria `inf`, karta `03` -- `tmux-container inf/03` -> pełny selector -- `tmux-container 03` -> domyślna seria + karta `03` +- `tmux-container inf bss` -> seria `inf`, karta `bss` +- `tmux-container inf/bss` -> pełny selector +- `tmux-container bss` -> domyślna seria + karta `bss` - `tmux-container inf` -> seria `inf` + domyślna karta - bez argumentów -> domyślna seria i domyślna karta Przykłady: ```bash -./rvctl tmux-container 03 --dry-run -./rvctl tmux-container inf 03 --session rv-inf03 -./rvctl tmux-container inf/03 --attach +./rvctl tmux-container bss --dry-run +./rvctl tmux-container inf bss --session rv-inf-bss +./rvctl tmux-container inf/bss --attach ``` diff --git a/doc/series.md b/doc/series.md index 432beb9..0ef3991 100644 --- a/doc/series.md +++ b/doc/series.md @@ -1,7 +1,7 @@ # Series -Ten dokument opisuje docelowy model komend `rvctl` do pracy z seriami, kartami -pracy i zadaniami w kartach. +Ten dokument opisuje komendy `rvctl` do pracy z seriami, kartami pracy i +zadaniami w kartach. ## Zasada @@ -11,73 +11,51 @@ Komendy kart pracy dzielimy na trzy poziomy: - `series cards` - operacje na kartach w ramach serii - `series cards tasks` - operacje na zadaniach w ramach karty -Używamy pluralnych namespace'ów `cards` i `tasks`, bo opisują kolekcje zasobów -i są spójne z pluralnym `tokens`. +`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 +workspace. ## Szybki przepływ -Typowy przepływ pracy: - ```bash ./rvctl tokens compare ./rvctl series list -./rvctl series show inf -./rvctl series fetch inf ./rvctl series cards list inf -./rvctl series cards show inf 03 -./rvctl series cards tasks list inf 03 -./rvctl series cards tasks show inf 03 task1 -./rvctl series cards fetch inf 03 -./rvctl series cards submission inf 03 --class 4i --nick u1 +./rvctl series cards fetch inf bss +./rvctl series cards tasks list inf bss +./rvctl series cards tasks switch inf bss 1 ``` +Dla karty `bss` właściwym repo jest `lab-rv32i-strlen-bss-data-stack`. + ## Serie ### `series list` -Listuje dostępne serie w workspace. +Listuje dostępne serie. Jeżeli `~/dev/workspace/rv/series` nie istnieje, +komenda tworzy ten katalog. ```bash ./rvctl series list ``` -Przykładowy wynik: +Typowy wynik: ```text -series cards workspace ------- ----- ------------------------------- -inf 12 ~/dev/workspace/rv/series/inf +inf 3 0 ``` +Kolumny oznaczają: seria, liczba kart w źródłach, liczba kart pobranych do +workspace. + ### `series show SERIES` -Pokazuje informacje o serii jako całości. +Pokazuje informacje o serii. ```bash ./rvctl series show inf ``` -Typowe pola: - -```text -series inf -workspace_path ~/dev/workspace/rv/series/inf -source_path ~/dev/edu/repos/rv/series/inf -cards_count 12 -cards 01 02 03 ... -``` - -### `series fetch SERIES` - -Pobiera albo aktualizuje całą serię w workspace. - -```bash -./rvctl series fetch inf -``` - -Ta komenda pracuje na wszystkich kartach w serii. Do pobrania jednej karty -służy `series cards fetch`. - ## Karty ### `series cards list SERIES` @@ -91,38 +69,45 @@ Listuje karty w wybranej serii. Przykładowy wynik: ```text -series card repo ------- ---- ------------------------------------ -inf 03 lab-rv32i-strlen-bss-data-stack +bss lab-rv32i-strlen-bss-data-stack source rv32i-c / bss-data-stack +``` + +Pierwsza kolumna jest krótką nazwą karty. Pełna nazwa repo zostaje w drugiej +kolumnie. + +### `series cards fetch SERIES CARD` + +Pobiera albo aktualizuje jedną kartę w workspace. `CARD` może być krótką nazwą +albo unikalnym fragmentem pełnej nazwy repo. + +```bash +./rvctl series cards fetch inf bss +``` + +Karta trafia do: + +```text +~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack +``` + +Po pobraniu `rvctl` przygotowuje remotes: + +- `r1` - repo źródłowe karty w `edu-inf` +- `r1a` - repo pracy w `c2025-1a-inf` + +Remote `r1a` jest tworzony z tokena `r1` i wskazuje na repo o tej samej nazwie +co karta: + +```text +http://77.90.8.171:3001/c2025-1a-inf/lab-rv32i-strlen-bss-data-stack.git ``` ### `series cards show SERIES CARD` -Pokazuje informacje o konkretnej karcie. +Pokazuje informacje o karcie. ```bash -./rvctl series cards show inf 03 -``` - -Typowe pola: - -```text -series inf -card 03 -workspace_path ~/dev/workspace/rv/series/inf/03 -source_path ~/dev/edu/repos/rv/series/inf/03 -source_repo edu-inf/lab-rv32i-strlen-bss-data-stack -source_remote r1 -answer_remote a1 -tasks task1 task2 ... -``` - -### `series cards fetch SERIES CARD` - -Pobiera albo aktualizuje jedną kartę w workspace. - -```bash -./rvctl series cards fetch inf 03 +./rvctl series cards show inf bss ``` ### `series cards submission SERIES CARD` @@ -130,14 +115,15 @@ Pobiera albo aktualizuje jedną kartę w workspace. Pokazuje albo stosuje konfigurację repo odpowiedzi dla karty. ```bash -./rvctl series cards submission inf 03 --class 4i --nick u1 -./rvctl series cards submission inf 03 --class 4i --nick u1 --apply +./rvctl series cards submission inf bss --class c2025-1a-inf +./rvctl series cards submission inf bss --class c2025-1a-inf --task 4 --apply ``` -Bez `--apply` komenda tylko pokazuje plan. Z `--apply` ustawia remotes potrzebne -do pracy z repo odpowiedzi. +Bez `--apply` komenda pokazuje plan. Z `--apply` ustawia remotes potrzebne do +pracy z repo odpowiedzi. Jeżeli nie podasz `--branch`, branch jest liczony ze +store tokenów i zadania, na przykład `u1T1a`. -## Zadania W Karcie +## Zadania w karcie Zadania są częścią karty, dlatego trzymamy je pod `series cards tasks`. @@ -146,16 +132,16 @@ Zadania są częścią karty, dlatego trzymamy je pod `series cards tasks`. Listuje zadania dostępne w konkretnej karcie. ```bash -./rvctl series cards tasks list inf 03 +./rvctl series cards tasks list inf bss ``` Przykładowy wynik: ```text -series card task path ------- ---- ----- -------------------------------------- -inf 03 task1 ~/dev/workspace/rv/series/inf/03/task1 -inf 03 task2 ~/dev/workspace/rv/series/inf/03/task2 +inf bss task1_bss ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks +inf bss task2_data ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks +inf bss task3_stack_unused ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks +inf bss task4_stack_strlen ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks ``` ### `series cards tasks show SERIES CARD TASK` @@ -163,33 +149,38 @@ inf 03 task2 ~/dev/workspace/rv/series/inf/03/task2 Pokazuje informacje o konkretnym zadaniu w karcie. ```bash -./rvctl series cards tasks show inf 03 task1 +./rvctl series cards tasks show inf bss task1 ``` -Typowe pola: +### `series cards tasks switch SERIES CARD TASK` + +Tworzy albo przełącza lokalny branch pracy dla wybranego zadania. + +```bash +./rvctl series cards tasks switch inf bss 4 +``` + +Nazwa brancha jest liczona automatycznie z użytkownika w `tokens.json` i numeru +zadania. Dla użytkownika `u1` i zadania `task4_stack_strlen` powstanie: ```text -series inf -card 03 -task task1 -workspace_path ~/dev/workspace/rv/series/inf/03/task1 -card_path ~/dev/workspace/rv/series/inf/03 +u1T4a ``` +Jeżeli branch już istnieje, `rvctl` przełącza na niego repo. Jeżeli go nie ma, +tworzy go od aktualnego commita karty. + Na tym poziomie nie wprowadzamy osobnego `fetch`: zadania są pobierane razem z kartą przez `series cards fetch` albo razem z całą serią przez `series fetch`. ## Aliasowanie Starych Komend -Stare komendy można zostawić jako aliasy kompatybilności: +Stare komendy zostają jako aliasy kompatybilności: ```text list-series -> series list list-cards inf -> series cards list inf -submission inf 03 --class K --nick N -> series cards submission inf 03 --class K --nick N +submission inf bss --class K -> series cards submission inf bss --class K ``` -Komendy `series cards tasks ...` nie mają starego odpowiednika i powinny być -wprowadzane tylko w nowym namespace. - Dokumentacja i nowe przykłady powinny promować namespace `series`. diff --git a/doc/tokens.md b/doc/tokens.md index 62b57da..6df9116 100644 --- a/doc/tokens.md +++ b/doc/tokens.md @@ -98,7 +98,7 @@ Znaczenie: Dla innego repo podaj `--repo`: ```bash -./rvctl tokens list remote --repo ~/dev/workspace/rv/series/inf/03 +./rvctl tokens list remote --repo ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack ``` ## Synchronizacja @@ -233,9 +233,9 @@ Domyślnie komendy tokenów pracują na repo zawierającym `rvctl`. Dla kart pra albo innych repo podaj `--repo`. ```bash -./rvctl tokens list remote --repo ~/dev/workspace/rv/series/inf/03 -./rvctl tokens compare --repo ~/dev/workspace/rv/series/inf/03 -./rvctl tokens sync store r1 --repo ~/dev/workspace/rv/series/inf/03 +./rvctl tokens list remote --repo ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack +./rvctl tokens compare --repo ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack +./rvctl tokens sync store r1 --repo ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack ``` Store tokenów nadal pozostaje jeden: diff --git a/rvctl.py b/rvctl.py index 75c910e..dc391b1 100755 --- a/rvctl.py +++ b/rvctl.py @@ -111,9 +111,9 @@ def load_config(config_path: Path) -> WorkspaceConfig: 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"), + answer_org=git_raw.get("answer_org", "c2025-1a-inf"), source_remote=git_raw.get("source_remote", "r1"), - answer_remote=git_raw.get("answer_remote", "a1"), + answer_remote=git_raw.get("answer_remote", "r1a"), origin_remote=git_raw.get("origin_remote", "origin"), fallback_branch=git_raw.get("fallback_branch", "build"), ) @@ -134,8 +134,7 @@ def load_config(config_path: Path) -> WorkspaceConfig: def series_directories(config: WorkspaceConfig) -> list[Path]: - if not config.series_root.is_dir(): - raise SystemExit(f"Missing series root: {config.series_root}") + config.series_root.mkdir(parents=True, exist_ok=True) return sorted(path for path in config.series_root.iterdir() if path.is_dir()) @@ -143,6 +142,51 @@ def card_directories(series_path: Path) -> list[Path]: return sorted(path for path in series_path.iterdir() if path.is_dir()) +def source_series_directories(config: WorkspaceConfig) -> list[Path]: + if not config.original_series_root.is_dir(): + return [] + return sorted(path for path in config.original_series_root.iterdir() if path.is_dir()) + + +def source_card_directories(config: WorkspaceConfig, series_name: str) -> list[Path]: + source_series_path = config.original_series_root / series_name + if not source_series_path.is_dir(): + return [] + return sorted(path for path in source_series_path.iterdir() if path.is_dir()) + + +def card_label_from_repo_name(repo_name: str) -> str: + if repo_name.startswith("lab-"): + repo_name = repo_name[4:] + marker = "-strlen-" + if marker in repo_name: + return repo_name.split(marker, 1)[1].split("-", 1)[0] + return repo_name + + +def resolve_source_card_path(config: WorkspaceConfig, series_name: str, card_name: str) -> Path: + cards = source_card_directories(config, series_name) + if not cards: + raise SystemExit(f"Missing source series: {config.original_series_root / series_name}") + + normalized = slug_value(card_name, "card") + matches = [] + for card_path in cards: + repo_name = card_path.name + label = card_label_from_repo_name(repo_name) + candidates = {repo_name, label} + if normalized in candidates or any(normalized in candidate for candidate in candidates): + matches.append(card_path) + + if not matches: + available = ", ".join(card_label_from_repo_name(path.name) for path in cards) + raise SystemExit(f"Missing card '{card_name}' in series '{series_name}'. Available: {available}") + if len(matches) > 1: + available = ", ".join(path.name for path in matches) + raise SystemExit(f"Ambiguous card '{card_name}' in series '{series_name}'. Matches: {available}") + return matches[0] + + def read_card_title(card_path: Path) -> str: readme_path = card_path / "README.md" if not readme_path.is_file(): @@ -170,12 +214,19 @@ def resolve_selector( return series_value, card_value if series_arg and not card_arg: - if (config.series_root / series_arg).is_dir(): + if (config.series_root / series_arg).is_dir() or (config.original_series_root / series_arg).is_dir(): if not default_card: raise SystemExit("Missing default card in config.") return series_arg, default_card if default_series and (config.series_root / default_series / series_arg).is_dir(): return default_series, series_arg + if default_series: + try: + source_card_path = resolve_source_card_path(config, default_series, series_arg) + if workspace_card_path(config, default_series, source_card_path).is_dir(): + return default_series, series_arg + except SystemExit: + pass series_value = series_arg or default_series card_value = card_arg or default_card @@ -186,15 +237,25 @@ def resolve_selector( def resolve_card_path(config: WorkspaceConfig, series_name: str, card_name: str) -> Path: card_path = config.series_root / series_name / card_name - if not card_path.is_dir(): - raise SystemExit(f"Missing card path: {card_path}") - return card_path + if card_path.is_dir(): + return card_path + source_card_path = resolve_source_card_path(config, series_name, card_name) + resolved_path = workspace_card_path(config, series_name, source_card_path) + if resolved_path.is_dir(): + return resolved_path + raise SystemExit(f"Missing card path: {resolved_path}") def print_series(config: WorkspaceConfig) -> None: - for series_path in series_directories(config): - card_count = len(card_directories(series_path)) - print(f"{series_path.name}\t{card_count}\t{series_path}") + config.series_root.mkdir(parents=True, exist_ok=True) + 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)} + for series_name in sorted(set(source_by_name) | set(workspace_by_name)): + source_path = source_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 + 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 ''}") def print_cards(config: WorkspaceConfig, series_arg: str | None) -> None: @@ -202,16 +263,23 @@ def print_cards(config: WorkspaceConfig, series_arg: str | None) -> None: if not series_name: raise SystemExit("Missing series name and no default series is configured.") - series_path = config.series_root / series_name - if not series_path.is_dir(): - raise SystemExit(f"Missing series path: {series_path}") + config.series_root.mkdir(parents=True, exist_ok=True) + source_cards = {path.name: path for path in source_card_directories(config, 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 {} + if not source_cards and not workspace_cards: + raise SystemExit(f"Missing series: {series_name}") - for card_path in card_directories(series_path): - title = read_card_title(card_path) + for repo_name in sorted(set(source_cards) | set(workspace_cards)): + source_path = source_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 "" + label = card_label_from_repo_name(repo_name) + status = "workspace" if workspace_path else "source" if title: - print(f"{card_path.name}\t{title}\t{card_path}") + print(f"{label}\t{repo_name}\t{status}\t{title}") else: - print(f"{card_path.name}\t{card_path}") + print(f"{label}\t{repo_name}\t{status}") def slug_value(raw_value: str, field_name: str) -> str: @@ -221,6 +289,13 @@ def slug_value(raw_value: str, field_name: str) -> str: return slug +def branch_value(raw_value: str, field_name: str = "branch") -> str: + branch_name = re.sub(r"[^A-Za-z0-9._-]+", "-", raw_value.strip()).strip("-") + if not branch_name: + raise SystemExit(f"Empty {field_name} after branch conversion: {raw_value!r}") + return branch_name + + def git_capture(repo_path: Path, git_args: list[str]) -> str | None: result = subprocess.run( ["git", "-C", str(repo_path)] + git_args, @@ -2388,13 +2463,17 @@ def repo_name_from_url(remote_url: str) -> str: 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]) + return source_repo_name 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 answer_url_for_card(config: WorkspaceConfig, repo_name: str) -> str: + return f"{config.git.base_url}/{config.git.answer_org}/{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: @@ -2413,21 +2492,393 @@ def set_git_remote(repo_path: Path, remote_name: str, remote_url: str) -> str: return "added" +def source_url_for_card(config: WorkspaceConfig, series_name: str, source_card_path: Path) -> str: + source_url = git_capture(source_card_path, ["remote", "get-url", config.git.source_remote]) + if source_url: + return source_url + source_url = git_capture(source_card_path, ["remote", "get-url", config.git.origin_remote]) + if source_url: + return source_url + return f"{config.git.base_url}/{config.git.source_org}/{source_card_path.name}.git" + + +def ensure_answer_token_from_source( + config: WorkspaceConfig, + source_remote: str, + answer_remote: str, + answer_url: str, + dry_run: bool = False, +) -> tuple[str, dict]: + token_data = load_token_store(config, write_normalized=not dry_run) + answer_server_info = server_info_from_url(config, answer_url) + if answer_server_info is None: + raise SystemExit(f"Only http/https remote URLs are supported: {answer_url}") + source_token = resolve_token_from_store(token_data, answer_server_info["endpoint"], source_remote, None) + if dry_run: + return "dry-run", source_token + status = register_token( + token_data, + answer_server_info, + answer_remote, + str(source_token.get("user", "")), + str(source_token.get("value", "")), + hydrate=False, + reset_api_metadata=True, + ) + write_token_store(config, token_data) + return status, source_token + + +def workspace_card_path(config: WorkspaceConfig, series_name: str, source_card_path: Path) -> Path: + return config.series_root / series_name / source_card_path.name + + +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_path = workspace_card_path(config, series_name, source_card_path) + if not card_path.is_dir(): + raise SystemExit(f"Missing workspace card: {card_path}. Run: ./rvctl series cards fetch {series_name} {card_name}") + return card_path + + +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) + repo_name = source_card_path.name + target_path = workspace_card_path(config, series_name, source_card_path) + source_url = source_url_for_card(config, series_name, source_card_path) + answer_url = answer_url_for_card(config, repo_name) + source_branch = git_current_branch(config, source_card_path) + + if not dry_run: + target_path.parent.mkdir(parents=True, exist_ok=True) + if target_path.exists() and git_repo_root(target_path) is None: + raise SystemExit(f"Workspace card path exists but is not a git repo: {target_path}") + cloned = False + if not target_path.exists(): + subprocess.run(["git", "clone", source_url, str(target_path)], check=True) + cloned = True + origin_url = git_capture(target_path, ["remote", "get-url", config.git.origin_remote]) + if origin_url and git_capture(target_path, ["remote", "get-url", config.git.source_remote]) is None: + subprocess.run( + ["git", "-C", str(target_path), "remote", "rename", config.git.origin_remote, config.git.source_remote], + check=True, + ) + set_git_remote(target_path, config.git.source_remote, source_url) + subprocess.run(["git", "-C", str(target_path), "fetch", config.git.source_remote, source_branch], check=True) + current_branch = git_current_branch(config, target_path) + if cloned or current_branch == source_branch: + if current_branch != source_branch: + local_branch_exists = subprocess.run( + ["git", "-C", str(target_path), "rev-parse", "--verify", "--quiet", source_branch], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ).returncode == 0 + if local_branch_exists: + subprocess.run(["git", "-C", str(target_path), "switch", source_branch], check=True) + else: + subprocess.run( + ["git", "-C", str(target_path), "switch", "--track", "-c", source_branch, f"{config.git.source_remote}/{source_branch}"], + check=True, + ) + subprocess.run(["git", "-C", str(target_path), "pull", "--ff-only", config.git.source_remote, source_branch], check=True) + + token_status, source_token = ensure_answer_token_from_source( + config, + config.git.source_remote, + config.git.answer_remote, + answer_url, + dry_run=dry_run, + ) + answer_remote_status = "dry-run" + if not dry_run: + answer_remote_url = credentialed_remote_url( + answer_url, + str(source_token.get("user", "")), + str(source_token.get("value", "")), + ) + answer_remote_status = set_git_remote(target_path, config.git.answer_remote, answer_remote_url) + + return { + "series": series_name, + "card": card_label_from_repo_name(repo_name), + "repo": repo_name, + "source": source_url, + "workspace": str(target_path), + "source_remote": config.git.source_remote, + "source_branch": source_branch, + "answer_remote": config.git.answer_remote, + "answer_org": config.git.answer_org, + "answer_repo": repo_name, + "answer_url": answer_url, + "token": config.git.answer_remote, + "token_status": token_status, + "answer_remote_status": answer_remote_status, + "status": "dry-run" if dry_run else "ready", + } + + +def print_key_values(values: dict[str, str]) -> None: + for key, value in values.items(): + print(f"{key}\t{value}") + + +def task_names(card_path: Path) -> list[str]: + task_root = card_path / "src" / "tasks" + if not task_root.is_dir(): + return [] + names = { + path.stem + for path in task_root.iterdir() + if path.is_file() and path.stem.startswith("task") + } + return sorted(names) + + +def task_branch_part(task_name: str) -> str: + match = re.search(r"(?i)task[-_]?(\d+)", task_name) + if match is None: + match = re.fullmatch(r"(?i)t(\d+)", task_name.strip()) + if match is None: + match = re.search(r"(\d+)", task_name) + if match is None: + raise SystemExit(f"Task name does not contain a task number: {task_name}") + return f"T{int(match.group(1))}" + + +def resolve_submission_task(config: WorkspaceConfig, card_path: Path, task_arg: str | None) -> str: + task_selector = task_arg or config.defaults.get("task") + if not task_selector: + raise SystemExit("Missing task selector. Pass --task or set defaults.task in workspace.json.") + + names = task_names(card_path) + if not names: + return task_selector + + matches = [name for name in names if name == task_selector] + if not matches: + try: + selector_part = task_branch_part(task_selector) + except SystemExit: + selector_part = "" + number_matches = [] + for name in names: + try: + if selector_part and task_branch_part(name) == selector_part: + number_matches.append(name) + except SystemExit: + pass + matches = number_matches + if not matches: + matches = [name for name in names if task_selector in name] + + if not matches: + raise SystemExit(f"Missing task '{task_selector}' in card: {card_path}") + if len(matches) > 1: + raise SystemExit(f"Ambiguous task '{task_selector}'. Matches: {', '.join(matches)}") + return matches[0] + + +def store_user_for_submission( + config: WorkspaceConfig, + source_url: str, + answer_url: str, + required: bool = True, +) -> str: + token_data = load_token_store(config) + endpoints = [] + for remote_url in [source_url, answer_url, config.git.base_url]: + server_info = server_info_from_url(config, remote_url) + if server_info is None: + continue + endpoint = server_info["endpoint"] + if endpoint not in endpoints: + endpoints.append(endpoint) + + for endpoint in endpoints: + for remote_id in [config.git.source_remote, config.git.answer_remote]: + token_record = find_token_record(token_data, endpoint, remote_id) + if token_record and token_record.get("user"): + return slug_value(str(token_record["user"]), "token user") + + users = { + str(token_record.get("user", "")) + for token_record in token_data.get("tokens", []) + if token_server_endpoint(token_record) in endpoints and token_record.get("user") + } + if len(users) == 1: + return slug_value(next(iter(users)), "token user") + + if required: + raise SystemExit("Missing --nick and no unique user in tokens.json for the answer branch.") + return "" + + +def submission_branch_name(user_name: str, task_name: str) -> str: + return branch_value(f"{slug_value(user_name, 'student user')}{task_branch_part(task_name)}a") + + +def print_card_tasks(config: WorkspaceConfig, series_name: str, card_name: str) -> None: + card_path = resolve_workspace_card_path(config, series_name, card_name) + for task_name in task_names(card_path): + print(f"{series_name}\t{card_label_from_repo_name(card_path.name)}\t{task_name}\t{card_path / 'src' / 'tasks'}") + + +def print_card_task(config: WorkspaceConfig, series_name: str, card_name: str, task_name: str) -> None: + card_path = resolve_workspace_card_path(config, series_name, card_name) + matches = [name for name in task_names(card_path) if name == task_name or task_name in name] + if not matches: + raise SystemExit(f"Missing task '{task_name}' in card: {card_path}") + if len(matches) > 1: + raise SystemExit(f"Ambiguous task '{task_name}'. Matches: {', '.join(matches)}") + print(f"series\t{series_name}") + print(f"card\t{card_label_from_repo_name(card_path.name)}") + print(f"task\t{matches[0]}") + print(f"card_path\t{card_path}") + print(f"task_root\t{card_path / 'src' / 'tasks'}") + + +def switch_card_task( + config: WorkspaceConfig, + series_name: str, + card_name: str, + task_selector: str, + branch_override: str | None = None, + dry_run: bool = False, +) -> dict[str, str]: + card_path = resolve_workspace_card_path(config, series_name, card_name) + task_name = resolve_submission_task(config, card_path, task_selector) + source_url = discover_source_url(config, card_path, series_name, card_name) + source_repo_name = repo_name_from_url(source_url) + answer_url = build_answer_url(config, source_repo_name) + student_user = store_user_for_submission(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) + current_branch = git_current_branch(config, card_path) + branch_exists = subprocess.run( + ["git", "-C", str(card_path), "rev-parse", "--verify", "--quiet", branch_name], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ).returncode == 0 + + if dry_run: + status = "dry-run" + elif current_branch == branch_name: + status = "unchanged" + elif branch_exists: + subprocess.run(["git", "-C", str(card_path), "switch", branch_name], check=True) + status = "switched" + else: + subprocess.run(["git", "-C", str(card_path), "switch", "-c", branch_name], check=True) + status = "created" + + return { + "series": series_name, + "card": card_label_from_repo_name(card_path.name), + "task": task_name, + "student_user": student_user, + "branch": branch_name, + "workspace": str(card_path), + "status": status, + } + + +def print_series_details(config: WorkspaceConfig, series_name: str) -> None: + config.series_root.mkdir(parents=True, exist_ok=True) + source_path = config.original_series_root / series_name + workspace_path = config.series_root / series_name + cards = source_card_directories(config, series_name) + if not cards and not workspace_path.is_dir(): + raise SystemExit(f"Missing series: {series_name}") + print(f"series\t{series_name}") + print(f"source_path\t{source_path if source_path.is_dir() else ''}") + print(f"workspace_path\t{workspace_path}") + print(f"cards_count\t{len(cards)}") + print(f"cards\t{' '.join(card_label_from_repo_name(path.name) for path in cards)}") + + +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) + target_path = workspace_card_path(config, series_name, source_card_path) + repo_name = source_card_path.name + print(f"series\t{series_name}") + print(f"card\t{card_label_from_repo_name(repo_name)}") + print(f"repo\t{repo_name}") + print(f"source_path\t{source_card_path}") + print(f"workspace_path\t{target_path}") + print(f"source_remote\t{config.git.source_remote}") + print(f"source_url\t{source_url_for_card(config, series_name, source_card_path)}") + print(f"answer_remote\t{config.git.answer_remote}") + print(f"answer_url\t{answer_url_for_card(config, repo_name)}") + if target_path.is_dir(): + print(f"tasks\t{' '.join(task_names(target_path))}") + + +def run_series(config: WorkspaceConfig, args: argparse.Namespace) -> None: + if args.series_command == "list": + print_series(config) + return + if args.series_command == "show": + print_series_details(config, args.series) + return + if args.series_command == "fetch": + for source_card_path in source_card_directories(config, args.series): + result = fetch_card_to_workspace(config, args.series, source_card_path.name, dry_run=args.dry_run) + print_key_values(result) + print() + return + if args.series_command != "cards": + raise SystemExit(f"Unsupported series command: {args.series_command}") + + if args.cards_command == "list": + print_cards(config, args.series) + return + if args.cards_command == "show": + print_card_details(config, args.series, args.card) + return + if args.cards_command == "fetch": + print_key_values(fetch_card_to_workspace(config, args.series, args.card, dry_run=args.dry_run)) + return + if args.cards_command == "submission": + print_submission_plan(config, args) + return + if args.cards_command != "tasks": + raise SystemExit(f"Unsupported series cards command: {args.cards_command}") + + if args.tasks_command == "list": + print_card_tasks(config, args.series, args.card) + return + if args.tasks_command == "show": + print_card_task(config, args.series, args.card, args.task) + return + if args.tasks_command == "switch": + print_key_values(switch_card_task(config, args.series, args.card, args.task, args.branch, args.dry_run)) + return + raise SystemExit(f"Unsupported series cards tasks command: {args.tasks_command}") + + 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) sync_tokens_from_repo(config, card_path) 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) + task_name = resolve_submission_task(config, card_path, args.task) + student_user = ( + slug_value(args.nick, "student user") + if args.nick + else store_user_for_submission(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) print(f"selector\t{selector}") print(f"card_path\t{card_path}") + print(f"task\t{task_name}") 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}") @@ -2435,12 +2886,28 @@ def print_submission_plan(config: WorkspaceConfig, args: argparse.Namespace) -> 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_user\t{student_user}") 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) + token_status, source_token = ensure_answer_token_from_source( + config, + config.git.source_remote, + config.git.answer_remote, + answer_url, + ) + answer_status = set_git_remote( + card_path, + config.git.answer_remote, + credentialed_remote_url( + answer_url, + str(source_token.get("user", "")), + str(source_token.get("value", "")), + ), + ) print(f"apply_{config.git.source_remote}\t{source_status}") + print(f"apply_token_{config.git.answer_remote}\t{token_status}") print(f"apply_{config.git.answer_remote}\t{answer_status}") print("commands") @@ -2621,10 +3088,11 @@ def print_main_overview(config_path: Path) -> None: ["command", "arguments", "purpose"], [ ["show-config", "", "show resolved paths and Git defaults"], - ["list-series", "", "list series from workspace"], - ["list-cards", "[series]", "list cards in a selected series"], + ["series", "list|cards|fetch|tasks", "work with series, cards and tasks"], + ["list-series", "", "compat alias for series list"], + ["list-cards", "[series]", "compat alias for series cards list"], ["tmux-container", "[series] [card]", "start tmux with container in pane 0"], - ["submission", "[series] [card] --class K --nick N", "prepare answer repo and student branch"], + ["submission", "[series] [card] --class K [--task T]", "prepare answer repo and student branch"], ["tokens", "list|compare|cmp|sync|update|remove|rm|add|read|stats|write", "manage tokens.json and Git remote credentials"], ], ) @@ -2634,10 +3102,10 @@ def print_main_overview(config_path: Path) -> None: ["step", "command", "result"], [ ["1", "./rvctl tokens compare", "compare repo remotes with tokens.json"], - ["2", "./rvctl list-series", "choose a series"], - ["3", "./rvctl list-cards inf", "choose a card"], - ["4", "./rvctl tmux-container inf ", "start the working container"], - ["5", "./rvctl submission inf --class 4i --nick u1", "prepare answer remote and branch"], + ["2", "./rvctl series list", "choose a series"], + ["3", "./rvctl series cards list inf", "choose a card"], + ["4", "./rvctl series cards fetch inf bss", "fetch card and prepare r1a"], + ["5", "./rvctl series cards tasks list inf bss", "list card tasks"], ], ) print() @@ -2686,8 +3154,8 @@ def print_tokens_overview() -> None: ["write store r1 to remote", "./rvctl tokens sync store r1 --repo PATH"], ["remove r1 from store", "./rvctl tokens rm store r1"], ["remove Git remote r1", "./rvctl tokens rm remote r1"], - ["compare selected card", "./rvctl tokens cmp --repo ~/dev/workspace/rv/series/inf/03"], - ["stats selected card", "./rvctl tokens stats --repo ~/dev/workspace/rv/series/inf/03"], + ["compare selected card", "./rvctl tokens cmp --repo ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack"], + ["stats selected card", "./rvctl tokens stats --repo ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack"], ["write auth to r1", "./rvctl tokens write --repo PATH --remote r1 --server URL"], ], ) @@ -2741,8 +3209,8 @@ def build_parser() -> argparse.ArgumentParser: "tmux-container", help="Create a tmux session with pane 0 running the container shell for a selected card.", ) - tmux_parser.add_argument("series", nargs="?", help="Series id or full selector like 'inf/03'.") - tmux_parser.add_argument("card", nargs="?", help="Card id, for example '03'.") + tmux_parser.add_argument("series", nargs="?", help="Series id or full selector like 'inf/bss'.") + tmux_parser.add_argument("card", nargs="?", help="Card id or unique fragment, for example 'bss'.") tmux_parser.add_argument("--session", help="tmux session name.") tmux_parser.add_argument("--window", help="tmux window name.") tmux_parser.add_argument("--instance", help="RV_INSTANCE passed to the tool launcher.") @@ -2753,22 +3221,23 @@ def build_parser() -> argparse.ArgumentParser: "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("series", nargs="?", help="Series id or full selector like 'inf/bss'.") + submission_parser.add_argument("card", nargs="?", help="Card id or unique fragment, for example 'bss'.") + submission_parser.add_argument("--class", dest="class_name", required=True, help="Class id, for example 'c2025-1a-inf'.") + submission_parser.add_argument("--nick", help="Student nick. Defaults to the user from tokens.json.") + submission_parser.add_argument("--task", help="Task name or unique fragment. Defaults to defaults.task.") submission_parser.add_argument( "--branch", - help="Override the submission branch name. Defaults to the value of --nick.", + help="Override the submission branch name. Defaults to Ta.", ) 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.", + help="Lesson date reported in the submission plan. 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.", + help="Override the source repo URL. By default launcher reads r1 or origin from the card repo.", ) submission_parser.add_argument( "--apply", @@ -2776,6 +3245,63 @@ def build_parser() -> argparse.ArgumentParser: help="Write remotes into the selected card repo instead of only printing the plan.", ) + series_parser = subparsers.add_parser("series", help="Work with series, cards and tasks.") + series_subparsers = series_parser.add_subparsers(dest="series_command", required=True) + + series_subparsers.add_parser("list", help="List available series.") + + series_show_parser = series_subparsers.add_parser("show", help="Show one series.") + series_show_parser.add_argument("series", help="Series id, for example 'inf'.") + + series_fetch_parser = series_subparsers.add_parser("fetch", help="Fetch all cards in a series.") + series_fetch_parser.add_argument("series", help="Series id, for example 'inf'.") + series_fetch_parser.add_argument("--dry-run", action="store_true", help="Print plan without cloning.") + + cards_group_parser = series_subparsers.add_parser("cards", help="Work with cards in a series.") + cards_subparsers = cards_group_parser.add_subparsers(dest="cards_command", required=True) + + cards_list_parser = cards_subparsers.add_parser("list", help="List cards in a series.") + cards_list_parser.add_argument("series", help="Series id, for example 'inf'.") + + cards_show_parser = cards_subparsers.add_parser("show", help="Show one card.") + cards_show_parser.add_argument("series", help="Series id, for example 'inf'.") + cards_show_parser.add_argument("card", help="Card id or unique fragment, for example 'bss'.") + + cards_fetch_parser = cards_subparsers.add_parser("fetch", help="Fetch one card into workspace.") + cards_fetch_parser.add_argument("series", help="Series id, for example 'inf'.") + cards_fetch_parser.add_argument("card", help="Card id or unique fragment, for example 'bss'.") + cards_fetch_parser.add_argument("--dry-run", action="store_true", help="Print plan without cloning.") + + cards_submission_parser = cards_subparsers.add_parser("submission", help="Print/apply answer remotes for a card.") + cards_submission_parser.add_argument("series", help="Series id, for example 'inf'.") + cards_submission_parser.add_argument("card", help="Card id or unique fragment, for example 'bss'.") + cards_submission_parser.add_argument("--class", dest="class_name", required=True, help="Class id.") + cards_submission_parser.add_argument("--nick", help="Student nick. Defaults to the user from tokens.json.") + cards_submission_parser.add_argument("--task", help="Task name or unique fragment. Defaults to defaults.task.") + cards_submission_parser.add_argument("--branch", help="Override the submission branch name.") + cards_submission_parser.add_argument("--date", default=dt.date.today().isoformat(), help="Lesson date in YYYY-MM-DD.") + cards_submission_parser.add_argument("--source-url", help="Override source repo URL.") + cards_submission_parser.add_argument("--apply", action="store_true", help="Write remotes into the selected card repo.") + + tasks_group_parser = cards_subparsers.add_parser("tasks", help="Work with tasks in a card.") + tasks_subparsers = tasks_group_parser.add_subparsers(dest="tasks_command", required=True) + + tasks_list_parser = tasks_subparsers.add_parser("list", help="List tasks in a card.") + tasks_list_parser.add_argument("series", help="Series id, for example 'inf'.") + tasks_list_parser.add_argument("card", help="Card id or unique fragment, for example 'bss'.") + + tasks_show_parser = tasks_subparsers.add_parser("show", help="Show one task.") + tasks_show_parser.add_argument("series", help="Series id, for example 'inf'.") + tasks_show_parser.add_argument("card", help="Card id or unique fragment, for example 'bss'.") + tasks_show_parser.add_argument("task", help="Task name or unique fragment.") + + tasks_switch_parser = tasks_subparsers.add_parser("switch", help="Create or switch to the answer branch for a task.") + tasks_switch_parser.add_argument("series", help="Series id, for example 'inf'.") + tasks_switch_parser.add_argument("card", help="Card id or unique fragment, for example 'bss'.") + tasks_switch_parser.add_argument("task", help="Task name or unique fragment, for example 'task4' or '4'.") + tasks_switch_parser.add_argument("--branch", help="Override the branch name.") + tasks_switch_parser.add_argument("--dry-run", action="store_true", help="Print plan without switching branches.") + tokens_parser = subparsers.add_parser("tokens", help="Manage local token store and repo remotes.") tokens_subparsers = tokens_parser.add_subparsers(dest="tokens_command", required=True) @@ -2919,6 +3445,9 @@ def main() -> int: if args.command == "submission": print_submission_plan(config, args) return 0 + if args.command == "series": + run_series(config, args) + return 0 if args.command == "tokens": if args.tokens_command in {"compare", "cmp"}: run_tokens_compare(config, args) diff --git a/workspace.json b/workspace.json index 5a44a4c..406b47b 100644 --- a/workspace.json +++ b/workspace.json @@ -13,15 +13,15 @@ "git": { "base_url": "http://77.90.8.171:3001", "source_org": "edu-inf", - "answer_org": "zsl-inf", + "answer_org": "c2025-1a-inf", "source_remote": "r1", - "answer_remote": "a1", + "answer_remote": "r1a", "origin_remote": "origin", "fallback_branch": "build" }, "defaults": { "series": "inf", - "card": "03", + "card": "bss", "task": "task1", "editor": "nvim", "instance": "shell",