13 KiB
02. Git: коммиты, ветки, diff и исключение больших данных
Git сохраняет историю изменений текста и делает происхождение каждого результата проверяемым.
Мотивация
Архивы project_final_2.zip не отвечают на главные вопросы: что изменилось, зачем,
кто это сделал и к какой версии кода относится модель. Git хранит направленную
историю снимков и позволяет точно назвать состояние проекта идентификатором коммита.
Для воспроизводимого эксперимента нужны как минимум:
- коммит исходного кода;
- версия конфигурации и зависимостей;
- идентификатор входных данных;
- команда запуска и seed;
- сохранённые метрики и окружение.
Git решает первую часть и связывает остальные части ссылками. Он не является универсальным хранилищем больших данных и не гарантирует научную воспроизводимость сам по себе.
Модель данных Git
У файла есть три практически важных состояния:
- рабочая директория — текущие файлы;
- индекс (staging area) — снимок, подготовленный к следующему коммиту;
- репозиторий — уже сохранённые коммиты.
Коммит содержит дерево файлов, метаданные и ссылку на родительский коммит. Его
хеш зависит от содержимого. Ветка — подвижное имя, указывающее на коммит; HEAD
указывает на текущую ветку или непосредственно на коммит.
A---B---C main
\
D---E experiment
Здесь обе ветки разделяют историю до B. Переключение ветки меняет снимок в
рабочей директории, а слияние объединяет линии разработки.
Базовый цикл с осмысленным diff
git status
git diff
git add src/model.py tests/test_model.py
git diff --staged
git commit -m "Validate feature dimensions before training"
git diff показывает незастейдженные изменения, а git diff --staged — точное
содержимое будущего коммита. Проверять второй diff важно: git add . может
случайно включить данные, секрет или отладочный вывод.
Хороший коммит:
- решает одну логическую задачу;
- остаётся запускаемым или явно помечает промежуточное состояние;
- включает связанные тесты;
- объясняет причину изменения;
- не содержит постороннего форматирования.
История — инструмент диагностики. Малые атомарные коммиты упрощают git bisect,
review и отмену конкретного решения.
Ветки и интеграция
Создание отдельной ветки:
git switch -c feature/stable-normalization
Перед слиянием изменения проверяют тестами и чтением полного diff относительно базы. Merge создаёт коммит с двумя родителями и сохраняет форму истории. Rebase переносит коммиты на новую базу и переписывает их идентификаторы.
Переписывать опубликованную общую историю опасно: коллеги уже могли построить на ней свои ветки. Rebase удобен для локальной неопубликованной ветки; merge безопаснее как явная интеграция общей работы.
Конфликт — не ошибка Git, а отсутствие однозначного автоматического решения. Нужно понять намерения обеих сторон, отредактировать файл, запустить проверки, застейджить разрешение и продолжить операцию. Нельзя механически выбирать «ours» или «theirs» без проверки поведения.
Что отслеживать
В Git обычно помещают:
- исходный код, тесты и небольшую документацию;
- декларации зависимостей и конфигурации;
- небольшие неизменяемые примеры данных;
- скрипты получения и преобразования данных.
Не помещают:
.venv/, кэши, байткод и временные файлы;- секреты и локальные
.env; - большие сырые датасеты, чекпойнты, модели;
- легко воспроизводимые результаты сборки.
Пример .gitignore:
.venv/
__pycache__/
.pytest_cache/
.env
data/raw/
artifacts/
*.pyc
gitignore действует только на неотслеживаемые файлы. Если секрет уже закоммичен,
добавление шаблона не удалит его из истории: ключ надо немедленно отозвать, затем
отдельно очищать историю с координацией команды.
Проверка причины игнорирования:
git check-ignore -v data/raw/train.csv
Большие данные и происхождение артефактов
Обычный Git плохо работает с часто меняющимися бинарными файлами: каждый вариант увеличивает историю, а содержательный строковый diff невозможен. Возможные решения:
- объектное хранилище и файл с URI, контрольной суммой и размером;
- Git LFS для версионируемых больших бинарных объектов;
- DVC или аналог для графа данных и пайплайнов;
- скрипт скачивания для публичного неизменяемого источника.
Контрольная сумма подтверждает байтовую идентичность:
from hashlib import sha256
from pathlib import Path
path = Path("data/raw/sample.csv")
digest = sha256(path.read_bytes()).hexdigest()
print(path.stat().st_size, digest)
Она не объясняет смысл данных. Поэтому полезен манифест: источник, лицензия, время получения, схема, число строк, контрольная сумма и шаги обработки.
Воспроизводимый запуск как запись
Пусть обучение запускается на коммите c. Минимальная запись эксперимента:
commit: 4f16c2a...
dirty: false
command: python -m src.train --config configs/base.json
data_sha256: 9e1...
python: 3.12.4
seed: 42
Состояние dirty: true означает, что рабочая директория отличалась от коммита.
Одного хеша тогда недостаточно: незакоммиченные изменения могут быть потеряны.
Для значимого результата лучше коммитить код или сохранять diff как артефакт.
Самостоятельный фрагмент получает метаданные без сторонних библиотек:
import subprocess
def git(*args: str) -> str:
return subprocess.check_output(
["git", *args], text=True, encoding="utf-8"
).strip()
commit = git("rev-parse", "HEAD")
dirty = bool(git("status", "--porcelain"))
print({"commit": commit, "dirty": dirty})
Запуск вне репозитория завершится ошибкой — это полезнее молчаливого значения
unknown, если происхождение результата обязательно.
Поиск причины регрессии
История позволяет перейти от «раньше работало» к проверяемому поиску.
- Найти хороший и плохой коммиты.
- На каждом кандидате запускать один и тот же детерминированный тест.
- Делить диапазон пополам через
git bisect. - После нахождения читать diff и проверять причинность.
Тест должен иметь стабильный код возврата. Если данные, окружение или внешний API изменились, bisect может указать не на причину, а на случайную корреляцию.
Разобранный сценарий
Исследователь меняет нормализацию и получает лучшую метрику.
git statusпоказывает только ожидаемые исходники и тест.git diffподтверждает формулу и новый тест на нулевую дисперсию.- Конфигурация запуска тоже версионируется.
- Большой чекпойнт сохраняется во внешнее хранилище; рядом записываются его SHA-256 и хеш коммита.
git diff --stagedпроверяется до коммита.- Из чистого клона создаётся окружение и повторяется команда.
Результат считается воспроизводимым не потому, что «есть Git», а потому, что каждый вход назван неизменяемым идентификатором и процесс автоматизирован.
Типичные ошибки
- Коммит результата без кода запуска. Артефакт нельзя объяснить или повторить.
- Огромный коммит. Review не отделяет изменение поведения от шума.
- Слепой
git add .. В историю попадают секреты и локальные файлы. - Хеш коммита при dirty-состоянии. Хеш описывает не фактически исполненный код.
- Хранение больших бинарников в Git. Репозиторий растёт навсегда.
- Force-push общей ветки. Идентификаторы переписываются, ссылки ломаются.
- «Одинаковый seed значит воспроизводимость». Версии библиотек, данные, аппаратные ядра и порядок операций также влияют на результат.
Краткое резюме
- Git хранит снимки; индекс отделяет текущую работу от следующего коммита.
- Diff до и после staging — обязательная проверка содержимого.
- Ветки именуют линии истории; rebase переписывает коммиты, merge интегрирует их.
- Большие данные хранят отдельно, но связывают с кодом контрольными суммами.
- Результат воспроизводим только при зафиксированных коде, данных, среде и запуске.
Связанные материалы
- Предыдущая глава:
01_python_environments.md. - Следующая глава:
03_jupyter_seeds_logging.md. - Практика:
../assignments/02_git.md. - Термины:
glossary.md.