Git и командная работа

⏱ 40 мин · 🧩 заданий: 5

Три области Git

Git хранит снимки проекта. Изменение проходит три места:

рабочая директория ──git add──▶ индекс (staging) ──git commit──▶ репозиторий (история) ▲ │ └──────────────────── git restore / git checkout ◀─────────────────┘
git init                         # новый репозиторий
git status                       # что изменено, что в индексе
git add app.py tests/            # положить изменения в индекс
git add -p                       # по кусочкам: удобно разбить правки на логичные коммиты
git diff                         # рабочая директория vs индекс
git diff --staged                # индекс vs последний коммит
git commit -m "feat: экспорт отчёта в CSV"
git log --oneline --graph --all  # история веток картинкой

Коммит — это снимок всех файлов плюс ссылка на родителя, автор и сообщение. Его идентификатор — SHA-хеш содержимого, поэтому изменить коммит «на месте» нельзя: любое редактирование создаёт новый коммит.

Ветки

Ветка — просто подвижный указатель на коммит. HEAD указывает на текущую ветку.

git switch -c feature/csv-export      # создать ветку и перейти (раньше: git checkout -b)
# ... коммиты ...
git switch main
git merge feature/csv-export          # слить ветку в main
git branch -d feature/csv-export      # удалить слитую ветку
merge: rebase: A──B──C──────M main A──B──C main \ / \ D──E──┘ feature D'──E' feature (новые коммиты!)

Merge сохраняет историю как есть и добавляет коммит слияния M. Rebase переписывает ваши коммиты поверх свежего main — история получается линейной. Правило: перебазируйте только свои ветки, которые ещё никто не забрал.

git switch feature/csv-export
git fetch origin
git rebase origin/main                # переиграть свои коммиты поверх свежего main
git rebase -i HEAD~3                  # интерактивно: склеить (squash), переименовать, переставить
git push --force-with-lease           # после rebase; безопаснее, чем --force

Конфликты

Конфликт возникает, когда две ветки изменили одни и те же строки. Git помечает их в файле:

<<<<<<< HEAD
TIMEOUT = 30
=======
TIMEOUT = 60
>>>>>>> feature/slow-api

Порядок действий: открыть файл, оставить правильный вариант (часто — комбинацию), удалить маркеры, прогнать тесты, затем git add файл и git commit (или git rebase --continue). Передумали — git merge --abort / git rebase --abort.

Меньше конфликтов

Маленькие ветки, живущие день-два; частые git fetch и rebase на main; автоформатер (black или ruff format) у всей команды — тогда нет конфликтов из-за пробелов и кавычек.

Удалённые репозитории

git clone git@github.com:team/service.git
git remote -v                         # origin → адрес
git fetch origin                      # скачать новое, ничего не меняя локально
git pull --rebase                     # fetch + rebase своей ветки
git push -u origin feature/csv-export # отправить и запомнить upstream

Pull request и код-ревью

Типичный процесс в команде:

  1. Задача в трекере → ветка feature/LH-123-csv-export.
  2. Небольшие логичные коммиты, тесты.
  3. Push и pull request (merge request в GitLab): описание «что и зачем», как проверить, скриншоты.
  4. CI запускает линтеры, типы и тесты.
  5. Ревью: коллеги оставляют замечания, вы исправляете или аргументированно отвечаете.
  6. Слияние (часто squash merge — вся ветка одним коммитом), ветка удаляется, деплой.

Хороший pull request — до ~400 строк изменений: большой никто не прочитает внимательно. На ревью смотрят на корректность, понятность, тесты, обработку ошибок, безопасность, а не на стиль — его проверяет линтер.

Сообщения коммитов

fix(export): экранировать запятые в CSV

Названия товаров с запятой ломали колонки в выгрузке.
Перешёл с ручной склейки строк на csv.writer.

Closes LH-123

Первая строка до ~72 символов в повелительном наклонении («добавить», «исправить»), пустая строка, затем тело: почему сделано изменение. «Что» видно из диффа. Многие команды используют Conventional Commits (feat:, fix:, docs:…) — по ним автоматически генерируют changelog и версии.

Проверку сообщений и кода до коммита делают хуки. Регулярное выражение для проверки формата — хорошая разминка:

import re

PATTERN = re.compile(r"(feat|fix|docs|refactor|test|chore)(\([a-z0-9-]+\))?!?: \S.*")

for msg in ["feat(auth): вход по коду", "Fixed stuff", "fix!: сменить формат ответа", "chore: "]:
    print(f"{msg!r:32} → {'ok' if PATTERN.fullmatch(msg) else 'отклонено'}")

.gitignore для Python

__pycache__/
*.py[cod]
.venv/
venv/
*.egg-info/
dist/
build/
.pytest_cache/
.mypy_cache/
.ruff_cache/
.coverage
htmlcov/
.env            # секреты никогда не коммитим!
*.sqlite3
.idea/
.vscode/

Секрет попал в историю

Удаление файла новым коммитом не помогает: он остаётся в истории. Первым делом отзовите ключ (смените пароль, токен), и только потом чистите историю (git filter-repo). Считайте любой запушенный секрет скомпрометированным.

pre-commit

pre-commit запускает проверки перед каждым коммитом. Файл .pre-commit-config.yaml:

repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.9
    hooks:
      - id: ruff          # линтер с автоисправлением
        args: [--fix]
      - id: ruff-format   # форматер
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: check-added-large-files
      - id: detect-private-key
      - id: end-of-file-fixer
pip install pre-commit
pre-commit install        # повесить хук на git commit
pre-commit run --all-files

Откат изменений

Ситуация Команда
Выбросить правки в файле git restore app.py
Убрать файл из индекса git restore --staged app.py
Поправить последний коммит (ещё не запушен) git commit --amend
Отменить запушенный коммит git revert <sha> — новый коммит с обратными изменениями
Откатить локальную ветку git reset --soft HEAD~1 (правки в индексе), --hard (правки исчезнут)
Отложить незаконченное git stash, потом git stash pop
«Я всё сломал» git reflog — журнал всех положений HEAD, оттуда можно вернуть потерянный коммит

Поиск коммита с ошибкой

git bisect start
git bisect bad                 # текущая версия сломана
git bisect good v1.4.0         # а эта работала
git bisect run pytest tests/test_export.py   # Git сам найдёт первый плохой коммит
git bisect reset
git blame -L 40,60 app/export.py             # кто и в каком коммите менял строки

Модели ветвления

  • Git flow: main, develop, feature/*, release/*, hotfix/*. Подходит для продуктов с редкими релизами по версиям.
  • GitHub flow / trunk-based: короткие ветки от main, быстрый PR, слияние и автоматический деплой. Незаконченное прячется за feature flags. Стандарт для веб-сервисов.

Вопросы на собеседовании

  • Чем merge отличается от rebase? Когда rebase опасен?
  • Чем reset отличается от revert?
  • Что такое HEAD, ветка, тег?
  • Как разрешить конфликт? Как найти, в каком коммите появился баг?
  • Что делать, если закоммитили пароль?
  • Как у вас в команде устроены ветки и ревью?

Итог

  • Рабочая директория → индекс → коммит. Ветка — указатель на коммит.
  • Merge сохраняет историю, rebase делает её линейной; общие ветки не переписываем.
  • Небольшие PR, понятные сообщения коммитов, CI и pre-commit.
  • revert для общей истории, reset — только для локальной; reflog спасает потерянное.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Проверка сообщений коммитов. В команде приняты Conventional Commits: тип(область): описание или тип: описание, где тип — один из feat, fix, docs, refactor, test, chore, область — строчные латинские буквы, цифры и дефис, а описание непустое и не заканчивается точкой. После типа или области может стоять ! (ломающее изменение). Напишите is_valid_commit(message), проверяющую первую строку сообщения. Используйте регулярное выражение.

Какие тесты проверяют код (4)
assert is_valid_commit("feat(auth): вход по одноразовому коду") and is_valid_commit("fix: падение на пустом файле")
assert is_valid_commit("refactor(api-v2)!: убрать устаревший эндпоинт") and is_valid_commit("docs: README\n\nПодробности в теле")
assert not is_valid_commit("Fixed bug") and not is_valid_commit("feat: добавил кнопку.") and not is_valid_commit("feature: x")
assert not is_valid_commit("fix(Auth): x") and not is_valid_commit("fix: ") and not is_valid_commit("") and not is_valid_commit("fix:x")
Задание 2 Программа на Python

Сопоставление с .gitignore. Напишите is_ignored(path, patterns), упрощённо повторяющую логику .gitignore. Путь — строка вида src/app/__pycache__/x.pyc. Правила:

  • шаблон без / (например *.pyc, __pycache__) сравнивается с каждой частью пути через fnmatch;
  • шаблон с / в конце (build/) означает папку: совпадает, если любая часть пути, кроме последней, подходит под имя;
  • шаблон, начинающийся с !, отменяет игнорирование; правила применяются по порядку, побеждает последнее совпавшее.
Какие тесты проверяют код (2)
rules = ["__pycache__", "*.pyc", ".venv/", "build/", "*.log", "!keep.log"]
assert is_ignored("src/__pycache__/mod.cpython-312.pyc", rules)
assert is_ignored("app/models.pyc", rules) and is_ignored(".venv/lib/site.py", rules)
assert not is_ignored("src/app.py", rules) and not is_ignored("build", rules)

rules = ["*.log", "!keep.log"]
assert is_ignored("logs/debug.log", rules) and not is_ignored("logs/keep.log", rules)
assert is_ignored("keep.log", ["!keep.log", "*.log"])

Задание 3 Заполнить таблицу

Какая команда подходит для ситуации?

git revertgit reset --hardgit restoregit stash
Отменить уже запушенный в общую ветку коммит, не переписывая историю
Выбросить незакоммиченные правки в одном файле
Временно убрать незаконченную работу, чтобы срочно переключиться на другую ветку
Полностью откатить свою локальную ветку на два коммита назад, правки не нужны
Отметьте ячейку — «+», пустая — «−».
Задание 4 Один вариант ответа

Чем git rebase main отличается от git merge main в вашей фичевой ветке?

Задание 5 Ввод ответа

Какая команда Git бинарным поиском находит коммит, в котором появилась ошибка? (одно слово после git)

Войдите, чтобы отмечать пройденные уроки.