lesson ── git-labs ── ~30 мин ── 9 шагов
Цель - руками настроить branch protection на main, увидеть как protection блокирует прямой push, опробовать CODEOWNERS-эквивалент через required reviewers, и увидеть отказ force-push.
Forge здесь - Gitea (ADR-015), локальный клон GitHub. REST API большой частью совпадает с GitHub'овским. В sandbox два контейнера: workstation (терминал) и forge (Gitea на forge:3000). Сеть air-gapped, github.com недоступен.
Демо-репо student/demo уже создан в Gitea на первом старте.
интерактивный sandbox
Поднимется контейнер gitlab/git-base с git, bash, pre-commit. В браузере откроется терминал, можно сразу git init. Каждый шаг проверяется автоматически. Сеть air-gapped, github.com недоступен.
stack ── git · bash · 256 MB RAM · air-gapped · самоуничтожается через 30 мин простоя
Контейнер forge (Gitea) стартует около 5-10 секунд - сначала запускается web-сервер, потом entrypoint создаёт админа и demo-репо. Проверь, что API отвечает:
# -f = fail на 4xx/5xx, -s = silent, -S = показать ошибку при -s
# >/dev/null 2>&1 = подавить и stdout, и stderr - нам важен только exit-code
until curl -fsS http://forge:3000/api/v1/version >/dev/null 2>&1; do
echo "waiting for forge..."
sleep 1
done
curl -s http://forge:3000/api/v1/version # -s без -f = просто без прогресс-бара
Должен вывести что-то вроде {"version":"1.22.x"}. Если зависло
на минуту - forge не поднялся (это редко, но проверь логи через
Reset sandbox).
Если ничего не пишется - sandbox ещё запускается, дай 30 секунд.
✓ Forge готов отвечать API-запросам.
Студенческий аккаунт уже создан: student / student-pass-only-for-sandbox.
Для удобства - сохрани креды в переменную:
export USER=student
export PASS=student-pass-only-for-sandbox
export REPO=demo
Проверь, что креды работают:
# -u user:pass = HTTP Basic Auth для Gitea API
curl -s -u $USER:$PASS http://forge:3000/api/v1/user | head -50
Должен вывести JSON с твоим юзером.
✓ Логин работает. Дальше клонируем demo-репо.
cd /home/student/work
# креды прямо в URL (user:pass@host) - удобно для урока, в проде так не делать
git clone http://student:student-pass-only-for-sandbox@forge:3000/student/demo.git
cd demo
git log --oneline
Репо клонировался через топологию (DNS-имя forge). В нём один
коммит от auto_init - это README.md. Дальше ты будешь его
защищать.
✓ Demo-репо клонирован.
Через REST API: PUT на /api/v1/repos/{owner}/{repo}/branch_protections.
# -X POST = метод запроса; -H = заголовок; -d = тело (JSON)
curl -fsS -u $USER:$PASS \
-X POST http://forge:3000/api/v1/repos/student/demo/branch_protections \
-H 'Content-Type: application/json' \
-d '{"branch_name": "main",
"enable_push": false,
"enable_merge_whitelist": false,
"required_approvals": 1,
"block_on_rejected_reviews": true,
"dismiss_stale_approvals": true,
"enable_status_check": false
}'
Видишь JSON-ответ с настроенным правилом. main теперь защищён: прямой push заблокирован, merge только через PR с approve.
✓ Protection включён на main.
cd /home/student/work/demo
echo "direct edit" >> README.md
git add README.md && git commit -m "direct edit"
git push origin main # отказ: "Protected branch hook declined"
Должна выйти ошибка от сервера: remote: Protected branch hook declined. Push отклонён - это и есть защита в действии.
Откатим локальный коммит, чтобы не было хвоста:
git reset --hard origin/main # снять локальный коммит, синхронизироваться с remote
Если push прошёл - protection не включился. Проверь шаг enable-protection ещё раз.
✓ Прямой push отклонён. Дальше - правильный путь через PR.
cd /home/student/work/demo
git switch -c feat/typo
echo "improved readme" > README.md
git add . && git commit -m "fix: improve README"
git push -u origin feat/typo # на feature-ветку защита не распространяется
Push в feature-ветку прошёл - на неё защита не распространяется. Дальше - открой PR через API:
# head = ветка с изменениями, base = куда мержить (как на GitHub: head -> base)
curl -fsS -u $USER:$PASS \
-X POST http://forge:3000/api/v1/repos/student/demo/pulls \
-H 'Content-Type: application/json' \
-d '{"title":"fix: improve README","body":"better wording","head":"feat/typo","base":"main"}'Ответом - JSON с "number": 1. PR открыт.
✓ PR открыт. Дальше попробуем смержить без approve.
# -i = включить response-заголовки в вывод (нужны для увидеть HTTP 405)
curl -i -u $USER:$PASS \
-X POST http://forge:3000/api/v1/repos/student/demo/pulls/1/merge \
-H 'Content-Type: application/json' \
-d '{"Do":"merge"}'Должен прийти 405 Method Not Allowed с сообщением вроде
pull request does not meet required approvals. Это и есть
required_approvals: 1 в действии.
В GitHub UI это «Merge button disabled» с подсказкой «1 approval required». В Gitea - HTTP 405 от API. Поведение то же.
✓ Merge без approve заблокирован.
В реальной команде approve ставит другой человек. Здесь у нас один student - в режиме урока можно approve самому (Gitea это разрешает через API; в GitHub strict-mode настройки этого не позволяют, но это деталь конфига).
# POST review: event=APPROVED ставит approve, body = текст комментария
curl -fsS -u $USER:$PASS \
-X POST http://forge:3000/api/v1/repos/student/demo/pulls/1/reviews \
-H 'Content-Type: application/json' \
-d '{"event":"APPROVED","body":"lgtm"}'# после approve тот же merge-запрос проходит
curl -fsS -u $USER:$PASS \
-X POST http://forge:3000/api/v1/repos/student/demo/pulls/1/merge \
-H 'Content-Type: application/json' \
-d '{"Do":"merge"}'Теперь merge прошёл - approve есть, protection доволен. Проверь:
git fetch && git log --oneline origin/main # видишь init + merge-коммит PR
Видишь два коммита в main: init + merge-коммит твоего PR.
✓ PR смержен с approve. Полный цикл пройден.
Branch protection также блокирует переписывание истории на main.
cd /home/student/work/demo
git switch main
git pull
git commit --allow-empty -m "rebase-source" # --allow-empty = коммит без изменений
git reset --hard HEAD~2 # снять 2 коммита - переписать историю
git push --force-with-lease origin main # попытка force-push: будет отклонена
Должна выйти ошибка protected branch hook declined. force-push
отклонён.
Откатим состояние:
git fetch && git reset --hard origin/main # вернуться к remote-состоянию main
Если push прошёл - проверь, что enable_force_push в protection НЕ включён.
✓ Force-push в main отклонён. Урок пройден - защита работает.
Branch protection - правила на стороне сервера, которые блокируют операции, не вписывающиеся в политику. Required approvals не дают мержить без review. Force-push отдельно запрещается. CODEOWNERS в Gitea выражается через protected_files + ручное назначение reviewers.
команды
curl -u user:pass -X PUT .../branch_protectionsвключить protection на веткеcurl -X POST .../pullsоткрыть PR через APIgit push origin mainесли main protected - получишь отказgit push --force-with-lease origin mainтоже отказ на protectedконцепции