diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..286188e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,20 @@ +name: Build + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Build + run: cargo build --verbose diff --git a/.github/workflows/build_images.yml b/.github/workflows/build_images.yml new file mode 100644 index 0000000..57446bf --- /dev/null +++ b/.github/workflows/build_images.yml @@ -0,0 +1,23 @@ +name: Build images + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: extractions/setup-just@v4 + with: + just-version: '1.46.0' + - name: Build images + run: just build-images diff --git a/README.md b/README.md index c00dda4..50da7a0 100644 --- a/README.md +++ b/README.md @@ -32,3 +32,131 @@ git-trainer — это образовательная платформа д Попытки решить конкретное задание можно посмотреть в менеджере попыток. В нём вы можете увидеть дату создания попытки и тесты, запущенные на ней, вместе с их результатами. + +# Как контрибьютить? + +## Задания + +### Миграции + +Чтобы добавить задание, придумайте для него сначала название (`name`), рабочее название (`work_name`), описание (`description`) и развёрнутое описание (`extended_description`). + +- `name` будет использоваться в меню выбора задания и должен быть в человеко-читаемом виде. По названию человек должен суметь понять его суть, приветствуется оригинальность. +- `work_name` используется для названия Docker-контейнера и образов и должен быть переводом `name` на английский. Приветствуется стиль kebab-case в названии. +- `description` будет показываться в меню выбора задания и должен быть очень кратким, но ёмким описанием задания, которое может поместиться в небольшое место на экране. +- `extended_description` используется внутри контейнера, он показывается с помощью `git-trainer task` и должен быть довольно подробным описанием задания, в нём должна быть поставлена проблематика задания и вся информация для его корректного решения. + +Описания заданий хранятся в базе данных, поэтому создайте в папке [migrations](migrations) миграцию с вашим заданием в виде папки, в названии которой находится дата миграции и название задания, можете посмотреть на уже существующие задания. Такое чёткое наименование не обязательно, но приветствуется. + +В этой папке должны находиться два файла, up.sql и down.sql, для принятия и отзыва миграции соответственно. + +up.sql должен выглядеть вот так: + +```sql +INSERT INTO tasks ( + id, + name, + work_name, + description, + extended_description +) +VALUES ( + 1, + 'Привет, мир!', + 'hello-world', + 'В этой задаче Вам предстоит создать новый Git репозиторий и сделать в нём первый коммит.', + 'Давайте начнём с чего-нибудь лёгкого.n + Создайте в папке "hello-world" новый Git репозиторий, в котором напишите код на C, выводящий на экран строчку "Hello, World!".n + После этого сделайте ровно один коммит, добавляющий этот код, с названием "Initial commit".' +); +``` + +ID нового задания должен быть на 1 больше самого большого из существующих, (TODO: сделать с этим что-то...) а `name`, `work_name`, `description` и `extended_description` должны быть указаны в параметрах создания новой записи в базу. + +down.sql выглядит же вот так: + +```sql +DELETE FROM tasks WHERE work_name = 'hello-world'; +``` + +### Docker-образ + +Для начала, вам нужен сам git-репозиторий, с которым пользователь будет работать при запуске задания. Основные требования к репозиторию такие: +- Репозиторий должен имитировать работу над каким-то осмысленным проектом, в идеале это может быть настоящий проект, в котором появилась освещаемая проблема +- Задание не должно требовать от студента владение стеком технологий, кроме того, который изучается по программе на 1 курсе программной инженерии (т.е. базовый C++). Это требование относится именно к тем технологиям, без которых задание не выполнить — то есть если в качестве задания студент будет работать с проектом, написанным на незнакомом ему языке (допустим, Haskell), но оно решается грамотной работой с Git или другими известными технологиями, то такое задание более чем приветствуется. + +Этот git-репозиторий вы можете создать на каком угодно удалённом хранилище репозиториев, но будет очень хорошо, если вы сделаете его в организации [git-trainer-tasks](https://github.com/git-trainer-tasks). Для получения доступа к этой организации, напишите [вот ему](https://t.me/gohy279). + +> [!NOTE] +> Кстати говоря, не обязательно иметь репозиторий для задания, например в уже существующем задании "hello-world" его не предполагается, у вас может быть так же. + +Задания как таковые хранятся в папке [tasks](tasks) по своим названиям. Внутри них должна находиться папка src c Dockerfile в ней и файл justfile: +- В папке src должны находиться все пререквизиты, которые не могут и не должны находиться в репозитории-шаблоне. Например, это могут быть файлы, которые в задании должны будут находиться как незаиндексированные изменения, такие файлы никак не засунешь в репозиторий-шаблон. +- justfile — это скрипт, описывающий как задание преобразуется в Docker-образ. Он может выглядеть вот так: + +```just +default: + -git clone https://github.com/git-trainer-tasks/branching.git tasks/branching/src/repo + docker build -f tasks/branching/src/Dockerfile -t git-trainer:branching . +``` + +- В Dockerfile должна быть описана сама сборка Docker-образа. + +[Пример](tasks/branching/src/Dockerfile): + +```Dockerfile +FROM git-trainer:base-task-image + +ARG USERNAME=student +ARG GIT_USERNAME=student +ARG GIT_EMAIL=student@alivetech.com +ENV DESCRIPTION="В этом репозитории вы с другом пишете алгоритм сортировки подсчётом.\nВаш друг сделал отдельную ветку \"origin/counting_sort\", где имплементировал алгоритм, а вам досталась задача написать функцию вывода вектора на экран в другой ветке.\nНапишите эту функцию в ветке \"print_vector\" и объедините обе ваших ветки с главной веткой main." + +COPY tasks/branching/src/repo counting-sort +RUN sudo chown -R $USERNAME:$USERNAME counting-sort/ + +RUN echo -n $DESCRIPTION > /etc/git-trainer/description +USER $USERNAME +RUN git config --global --add safe.directory /home/$USERNAME/counting-sort +RUN cd counting-sort && git switch main +``` + +> [!IMPORTANT] +> **Самое главное, что при создании образа вы должны написать копию `extended_description` в файл /etc/git-trainer/description.** Также вы должны заменить в нём все переносы строк на "\n" [#2](https://github.com/dsc-sgu/git-trainer/issues/2). + +### Тесты + +Тесты для задания хранятся в папке [tests](tests) по своим названиям. В них должны находиться скрипты .sh с названиями вида "test[n].sh", где "n" — номер теста. + +Каждый отдельный тест должен проверять отдельную степень свободы в сданном решении. По окончании своей работы он должен выдать какой-либо текст, оповещающий либо об успешном прохождении, либо об ошибке, и exit-code: 0 для успешного прохождения и 1 для ошибки. + +[Пример](tests/merge-conflict/test2.sh): + +```sh +#!/bin/bash + +cd "$HOME" + +cd binary-addition && git status &>/dev/null + +if [ "$?" -eq 0 ]; then + echo "2. Git-репозиторий существует." + exit 0 +else + echo "2. Убедитесь, что в директории binary-addition существует Git-репозиторий." + exit 1 +fi +``` + +Этот скрипт проверяет, что в папке "binary-addition" есть Git-репозиторий с помощью сравнения exit-code команды `git status` с 0. + +Скрипты зависят от друг друга: если скрипт с номером n выдал ошибку или не запускался, то скрипт с номером n + 1 не будет запускаться. Так вы можете проверить в первом тесте, что существует, к примеру, Git-репозиторий, а остальные тесты писать будет намного удобнее, исходя из уверенности, что они будут проверять репозиторий тогда и только тогда, когда он действительно существует. + +Заметьте, что с помощью shebang вы можете писать тесты на любом другом языке. У вас есть огромная свобода при написании тестов + +## Сборка и запуск + +Чтобы запустить git-trainer, установите [just](https://github.com/casey/just) и сделайте `just run`. Вы можете отдельно сделать `just build-images` для сборки только образов и `just release` для деплоя приложения. + + + diff --git a/migrations/23042026_fix_the_past/down.sql b/migrations/23042026_fix_the_past/down.sql new file mode 100644 index 0000000..4088c1b --- /dev/null +++ b/migrations/23042026_fix_the_past/down.sql @@ -0,0 +1 @@ +DELETE FROM tasks WHERE work_name = 'fix-the-past'; \ No newline at end of file diff --git a/migrations/23042026_fix_the_past/up.sql b/migrations/23042026_fix_the_past/up.sql new file mode 100644 index 0000000..0ce2125 --- /dev/null +++ b/migrations/23042026_fix_the_past/up.sql @@ -0,0 +1,19 @@ +INSERT INTO tasks ( + id, + name, + work_name, + description, + extended_description +) VALUES ( + 5, + 'Назад в будущее', + 'fix-the-past', + 'Спасите сломанный проект, найдя правильный код в истории и создав новую спасательную ветку.', + 'Вы находитесь в ветке main, где последний коммит (№5) полностью ломает проект. ' || + 'В истории есть коммит №4. Если туда откатиться, то видно, что автор написал правильную функцию,' || + ' но случайно снес половину других файлов. Есть коммит №3 — стабильная версия. ' || + 'Ваша задача: Через git log найдите хэш 4-го коммита, перейдите на него и скопируйте' || + ' функцию triangleArea. Переключитесь на стабильный 3-й коммит. Создайте новую ветку, ' || + 'Вставьте скопированную строчку,' || + ' сохраните файл и сделайте git commit.' + ); \ No newline at end of file diff --git a/migrations/23062026_where-am-i/down.sql b/migrations/23062026_where-am-i/down.sql new file mode 100644 index 0000000..34d7b63 --- /dev/null +++ b/migrations/23062026_where-am-i/down.sql @@ -0,0 +1 @@ +DELETE FROM tasks WHERE work_name = 'where-am-i'; diff --git a/migrations/23062026_where-am-i/up.sql b/migrations/23062026_where-am-i/up.sql new file mode 100644 index 0000000..6417f9e --- /dev/null +++ b/migrations/23062026_where-am-i/up.sql @@ -0,0 +1,10 @@ +INSERT INTO tasks (id, + name, + work_name, + description, + extended_description) +VALUES (6, + 'Где Я?', + 'where-am-i', + 'Программа падает из-за пропавшей константы. Научитесь заглядывать в прошлые коммиты, не ломая текущую ветку.', + 'Студент дописывает программу для физических расчетов. Код не компилируется: удалено значение гравитационной постоянной. Найдите код в истории без checkout и закоммитьте исправление.'); \ No newline at end of file diff --git a/tasks/fix-the-past/justfile b/tasks/fix-the-past/justfile new file mode 100644 index 0000000..11b9ca9 --- /dev/null +++ b/tasks/fix-the-past/justfile @@ -0,0 +1,3 @@ +default: + -git clone https://github.com/git-trainer-tasks/fix-the-past.git tasks/fix-the-past/src/repo + docker build -f tasks/fix-the-past/src/Dockerfile -t git-trainer:fix-the-past . \ No newline at end of file diff --git a/tasks/fix-the-past/src/Dockerfile b/tasks/fix-the-past/src/Dockerfile new file mode 100644 index 0000000..babcb1e --- /dev/null +++ b/tasks/fix-the-past/src/Dockerfile @@ -0,0 +1,17 @@ +FROM git-trainer:base-task-image + +ARG USERNAME=student +ARG GIT_USERNAME=student +ARG GIT_EMAIL=student@alivetech.com +ENV DESCRIPTION="Проект сломался при последнем коммите. Найдите новую функцию в истории (№4) и cout, относящийся к ней, вернитесь в стабильное состояние (№3), создайте ветку, добавьте недостающую функцию и cout, относящийся к ней (аргументы любой функции в этой задаче должны быть 4,5). Сделайте коммит и введите git-trainer submit. Сливать ветку с main пока НЕ нужно!" + +RUN echo -n $DESCRIPTION > /etc/git-trainer/description +COPY ./tasks/fix-the-past/src/repo /home/${USERNAME}/fix-the-past + +WORKDIR /home/${USERNAME}/fix-the-past +RUN sudo chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/fix-the-past +USER $USERNAME +RUN git config --global --add safe.directory /home/${USERNAME}/fix-the-past && git switch main + + + diff --git a/tasks/where-am-i/justfile b/tasks/where-am-i/justfile new file mode 100644 index 0000000..ea554a2 --- /dev/null +++ b/tasks/where-am-i/justfile @@ -0,0 +1,3 @@ +default: + -git clone https://github.com/git-trainer-tasks/where-am-i.git tasks/where-am-i/src/repo + docker build -f tasks/where-am-i/src/Dockerfile -t git-trainer:where-am-i . \ No newline at end of file diff --git a/tasks/where-am-i/src/Dockerfile b/tasks/where-am-i/src/Dockerfile new file mode 100644 index 0000000..31ad41a --- /dev/null +++ b/tasks/where-am-i/src/Dockerfile @@ -0,0 +1,16 @@ +FROM git-trainer:base-task-image + +ARG USERNAME=student +ARG GIT_USERNAME=student +ARG GIT_EMAIL=student@alivetech.com +ENV DESCRIPTION="Произошла авария на расчетном сервере! Наш модуль вычисления орбитальных маневров (src/orbital_calc.cpp) перестал работать и выдает нулевую скорость. Долгие попытки разобраться показали, что во время недавнего масштабного рефакторинга кто-то случайно стер точное значение одной из фундаментальных физических констант, заменив её на ноль. Точно известно, что правильное многозначное число внедрялось в одном из прошлых коммитов, но история проекта забита десятками других правок, добавлением телеметрии и удалением старого кода.\nВаша задача: Найти в истории изменений коммит, в котором добавлялось правильное значение этой константы и восстановить значение в коде и сделать спасительный коммит.\nВАЖНО: в этом задании нельзя использовать git checkout . Вам нужно просто заглянуть в прошлое - найти команду Git, которая позволяет ПОКАЗАТЬ содержимое коммита и изменения в нем прямо в терминале." +RUN echo -n $DESCRIPTION > /etc/git-trainer/description +COPY ./tasks/where-am-i/src/repo /home/${USERNAME}/where-am-i + +# Есть вариант, что это лишнее +RUN sudo apt-get update && sudo apt-get install -y cmake build-essential + +WORKDIR /home/${USERNAME}/where-am-i +RUN sudo chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/where-am-i +USER $USERNAME +RUN git config --global --add safe.directory /home/${USERNAME}/where-am-i && git switch main diff --git a/tests/fix-the-past/test1.sh b/tests/fix-the-past/test1.sh new file mode 100755 index 0000000..d652b4e --- /dev/null +++ b/tests/fix-the-past/test1.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +cd "$HOME/fix-the-past" || exit 1 + +CURRENT_BRANCH=$(git branch --show-current) + +if [ -z "$CURRENT_BRANCH" ]; then + echo "1. Вы находитесь в состоянии Detached HEAD! Создайте ветку: git checkout -b <имя_ветки>." + exit 1 +elif [ "$CURRENT_BRANCH" == "main" ]; then + echo "1. Вы всё ещё в ветке main. Вам нужно найти стабильный коммит и создать ветку от него." + exit 1 +else + echo "1. Отлично! Вы работаете в новой ветке: $CURRENT_BRANCH." + exit 0 +fi diff --git a/tests/fix-the-past/test2.sh b/tests/fix-the-past/test2.sh new file mode 100755 index 0000000..d25eab1 --- /dev/null +++ b/tests/fix-the-past/test2.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +cd "$HOME/fix-the-past" || exit 1 + + +TARGET_COMMIT_MSG="feat(main): implement rectangle area calculation" +TARGET_HASH=$(git log --all --grep="$TARGET_COMMIT_MSG" --format="%H" -n 1) + +if [ -z "$TARGET_HASH" ]; then + echo "Системная ошибка: Целевой стабильный коммит не найден в репозитории." + echo "Пожалуйста, сбросьте задание." + exit 1 +fi + + + +BASE_HASH=$(git merge-base HEAD main) + +if [ -z "$BASE_HASH" ]; then + echo "Системная ошибка: Не удалось вычислить общего предка с main." + exit 1 +fi + +if [ "$BASE_HASH" == "$TARGET_HASH" ]; then + echo "2. Супер! Ваша ветка отпочковалась от правильной стабильной версии." + exit 0 +else + echo "2. Вы взяли за основу не тот коммит! Найдите в логах коммит с rectangleArea и создайте ветку именно от него." + exit 1 +fi diff --git a/tests/fix-the-past/test3.sh b/tests/fix-the-past/test3.sh new file mode 100755 index 0000000..16a6a27 --- /dev/null +++ b/tests/fix-the-past/test3.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +cd "$HOME/fix-the-past" || exit 1 + +if [ ! -f main.cpp ]; then + echo "3. Ошибка: Файл main.cpp вообще не найден! Вы его случайно не удалили?" + exit 1 +fi + +grep -q "[A-Za-z_]* *rectangleArea" main.cpp +HAS_RECTANGLE=$? + +grep -q "[A-Za-z_]* *triangleArea" main.cpp +HAS_TRIANGLE=$? + +if [ "$HAS_RECTANGLE" -eq 0 ] && [ "$HAS_TRIANGLE" -eq 0 ]; then + echo "3. В файле присутствуют обе нужные функции. Отличная работа!" + exit 0 +else + echo "3. В файле main.cpp не хватает кода! Вы точно перенесли triangleArea и сохранили rectangleArea?" + exit 1 +fi diff --git a/tests/fix-the-past/test4.sh b/tests/fix-the-past/test4.sh new file mode 100755 index 0000000..81edefc --- /dev/null +++ b/tests/fix-the-past/test4.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +cd "$HOME/fix-the-past" || exit 1 + +if [ ! -f main.cpp ]; then + echo "4. Ошибка: Файл main.cpp вообще не найден! Вы его случайно не удалили?" + exit 1 +fi + +# delete предыдущего на всякий +rm -f app +g++ main.cpp -o app > /dev/null 2>&1 +COMPILE_COMPLETED=$? + +if [ "$COMPILE_COMPLETED" -ne 0 ]; then + echo "4. Ошибка: Программа не скомпилировалась, проверьте правильно ли вы ее исправили." + exit 1 +fi + +PROGRAM_OUTPUT=$(./app) +STATUS_RUN=$? + +if [ "$STATUS_RUN" -ne 0 ]; then + echo "4. Ошибка во время runtime! Проверьте на правильность вашу программу" + exit 1 +fi + +echo "$PROGRAM_OUTPUT" | grep -q "Площадь прямоугольника (4x5): 20" +RECTANGLE_AREA=$? + +echo "$PROGRAM_OUTPUT" | grep -q "Площадь треугольника (4x5): 10" +TRIANGLE_AREA=$? + +if [ "$RECTANGLE_AREA" -eq 0 ] && [ "$TRIANGLE_AREA" -eq 0 ]; then + echo "4. Отличная работа! Программа скомпилировалась и успешно решила поставленную задачу" + exit 0 +else + echo "4. Ошибка: вывод программы не совпадает с требуемым в условии задачи, Проверьте, есть ли в выводе строки: Площадь прямоугольника (4x5): <возвращаемое значение из соответствующей функции> / Площадь треугольника (4x5): <возвращаемое значение из соответствующей функции>" + exit 1 +fi + diff --git a/tests/fix-the-past/test5.sh b/tests/fix-the-past/test5.sh new file mode 100755 index 0000000..2df37d0 --- /dev/null +++ b/tests/fix-the-past/test5.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +cd "$HOME/fix-the-past" || exit 1 + +rm -f app + +git status --porcelain | grep -q "^" +HAS_CHANGES=$? + +if [ "$HAS_CHANGES" -eq 0 ]; then + echo "5. Почти готово! У вас есть несохраненные изменения, сделайте git commit." + exit 1 +fi + +BASE_HASH=$(git merge-base HEAD main) +CURRENT_HASH=$(git rev-parse HEAD) + +if [ "$BASE_HASH" == "$CURRENT_HASH" ]; then + echo "5. Вы не зафиксировали изменения. Создайте коммит со спасенным кодом!" + exit 1 +else + echo "5. Победа! Вы спасли проект из истории и зафиксировали результат." + exit 0 +fi diff --git a/tests/where-am-i/test1.sh b/tests/where-am-i/test1.sh new file mode 100755 index 0000000..4f0922e --- /dev/null +++ b/tests/where-am-i/test1.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +cd "$HOME/where-am-i" || exit 1 + +rm -rf build +mkdir build && cd build +cmake .. > /dev/null 2>&1 +make > /dev/null 2>&1 + + +if [ $? -ne 0 ]; then + echo "1. Ошибка компиляции! Вы точно вернули константу и правильно её назвали?" + exit 1 +fi + +cd .. +OUTPUT=$(./build/orbital_app) +if echo "$OUTPUT" | grep -q "7672"; then + echo "1. Сборка успешна! Орбитальная скорость рассчитана верно." + exit 0 +else + echo "1. Код компилируется, но расчеты неверны. Точное ли значение нужной константы вы нашли?" + exit 1 +fi \ No newline at end of file diff --git a/tests/where-am-i/test2.sh b/tests/where-am-i/test2.sh new file mode 100755 index 0000000..6db7fde --- /dev/null +++ b/tests/where-am-i/test2.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +cd "$HOME/where-am-i" || exit 1 + +rm -rf build + +git status --porcelain | grep -q "^" +HAS_CHANGES=$? + +if [ "$HAS_CHANGES" -eq 0 ]; then + echo "2. У вас остались незакоммиченные изменения! Сделайте git add и git commit." + exit 1 +fi + +if ! git show HEAD:src/orbital_calc.cpp | grep -q "6.6743015e-11"; then + echo "2. В вашем последнем коммите нет правильного значения константы в нужном файле." + exit 1 +else + echo "2. Изменения успешно сохранены в историю Git!" + exit 0 +fi + diff --git a/tests/where-am-i/test3.sh b/tests/where-am-i/test3.sh new file mode 100755 index 0000000..f3bdb35 --- /dev/null +++ b/tests/where-am-i/test3.sh @@ -0,0 +1,11 @@ +#!/bin/bash +cd "$HOME/where-am-i" || exit 1 + +if git reflog | grep -qiE "checkout: moving from .* to [0-9a-f]{7,}"; then + echo "3. Ага! Кто-то использовал git checkout! Это запрещено в данном задании. Вам нужно было использовать другую команду. Начните задание заново (клавиша 'r')." + exit 1 +else + echo "3. Отличная работа! Вы использовали безопасные методы чтения истории." + exit 0 +fi +