Git и командная работа
Три области Git
Git хранит снимки проекта. Изменение проходит три места:
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 сохраняет историю как есть и добавляет коммит слияния 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 и код-ревью
Типичный процесс в команде:
- Задача в трекере → ветка
feature/LH-123-csv-export. - Небольшие логичные коммиты, тесты.
- Push и pull request (merge request в GitLab): описание «что и зачем», как проверить, скриншоты.
- CI запускает линтеры, типы и тесты.
- Ревью: коллеги оставляют замечания, вы исправляете или аргументированно отвечаете.
- Слияние (часто 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
Проверка сообщений коммитов. В команде приняты 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")
import re
PATTERN = re.compile(r"(feat|fix|docs|refactor|test|chore)(\([a-z0-9-]+\))?!?: \S.*(?<!\.)")
def is_valid_commit(message):
first_line = message.splitlines()[0] if message else ""
return PATTERN.fullmatch(first_line) is not None
Сопоставление с .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"])
from fnmatch import fnmatch
def _matches(parts, pattern):
if pattern.endswith("/"):
name = pattern[:-1]
return any(fnmatch(part, name) for part in parts[:-1])
return any(fnmatch(part, pattern) for part in parts)
def is_ignored(path, patterns):
parts = path.split("/")
ignored = False
for pattern in patterns:
negate = pattern.startswith("!")
if negate:
pattern = pattern[1:]
if _matches(parts, pattern):
ignored = not negate
return ignored
Какая команда подходит для ситуации?
| git revert | git reset --hard | git restore | git stash | |
|---|---|---|---|---|
| Отменить уже запушенный в общую ветку коммит, не переписывая историю | ||||
| Выбросить незакоммиченные правки в одном файле | ||||
| Временно убрать незаконченную работу, чтобы срочно переключиться на другую ветку | ||||
| Полностью откатить свою локальную ветку на два коммита назад, правки не нужны |
Чем git rebase main отличается от git merge main в вашей фичевой ветке?
Какая команда Git бинарным поиском находит коммит, в котором появилась ошибка? (одно слово после git)
Войдите, чтобы отмечать пройденные уроки.