# 02. Git: коммиты, ветки, diff и исключение больших данных > Git сохраняет историю изменений текста и делает происхождение каждого результата проверяемым. ## Мотивация Архивы `project_final_2.zip` не отвечают на главные вопросы: что изменилось, зачем, кто это сделал и к какой версии кода относится модель. Git хранит направленную историю снимков и позволяет точно назвать состояние проекта идентификатором коммита. Для воспроизводимого эксперимента нужны как минимум: - коммит исходного кода; - версия конфигурации и зависимостей; - идентификатор входных данных; - команда запуска и seed; - сохранённые метрики и окружение. Git решает первую часть и связывает остальные части ссылками. Он не является универсальным хранилищем больших данных и не гарантирует научную воспроизводимость сам по себе. ## Модель данных Git У файла есть три практически важных состояния: 1. **рабочая директория** — текущие файлы; 2. **индекс** (staging area) — снимок, подготовленный к следующему коммиту; 3. **репозиторий** — уже сохранённые коммиты. Коммит содержит дерево файлов, метаданные и ссылку на родительский коммит. Его хеш зависит от содержимого. Ветка — подвижное имя, указывающее на коммит; `HEAD` указывает на текущую ветку или непосредственно на коммит. ```text A---B---C main \ D---E experiment ``` Здесь обе ветки разделяют историю до `B`. Переключение ветки меняет снимок в рабочей директории, а слияние объединяет линии разработки. ## Базовый цикл с осмысленным diff ```text 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 и отмену конкретного решения. ## Ветки и интеграция Создание отдельной ветки: ```text git switch -c feature/stable-normalization ``` Перед слиянием изменения проверяют тестами и чтением полного diff относительно базы. **Merge** создаёт коммит с двумя родителями и сохраняет форму истории. **Rebase** переносит коммиты на новую базу и переписывает их идентификаторы. Переписывать опубликованную общую историю опасно: коллеги уже могли построить на ней свои ветки. Rebase удобен для локальной неопубликованной ветки; merge безопаснее как явная интеграция общей работы. Конфликт — не ошибка Git, а отсутствие однозначного автоматического решения. Нужно понять намерения обеих сторон, отредактировать файл, запустить проверки, застейджить разрешение и продолжить операцию. Нельзя механически выбирать «ours» или «theirs» без проверки поведения. ## Что отслеживать В Git обычно помещают: - исходный код, тесты и небольшую документацию; - декларации зависимостей и конфигурации; - небольшие неизменяемые примеры данных; - скрипты получения и преобразования данных. Не помещают: - `.venv/`, кэши, байткод и временные файлы; - секреты и локальные `.env`; - большие сырые датасеты, чекпойнты, модели; - легко воспроизводимые результаты сборки. Пример `.gitignore`: ```text .venv/ __pycache__/ .pytest_cache/ .env data/raw/ artifacts/ *.pyc ``` `gitignore` действует только на неотслеживаемые файлы. Если секрет уже закоммичен, добавление шаблона не удалит его из истории: ключ надо немедленно отозвать, затем отдельно очищать историю с координацией команды. Проверка причины игнорирования: ```text git check-ignore -v data/raw/train.csv ``` ## Большие данные и происхождение артефактов Обычный Git плохо работает с часто меняющимися бинарными файлами: каждый вариант увеличивает историю, а содержательный строковый diff невозможен. Возможные решения: - объектное хранилище и файл с URI, контрольной суммой и размером; - Git LFS для версионируемых больших бинарных объектов; - DVC или аналог для графа данных и пайплайнов; - скрипт скачивания для публичного неизменяемого источника. Контрольная сумма подтверждает байтовую идентичность: ```python 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`. Минимальная запись эксперимента: ```text 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 как артефакт. Самостоятельный фрагмент получает метаданные без сторонних библиотек: ```python 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`, если происхождение результата обязательно. ## Поиск причины регрессии История позволяет перейти от «раньше работало» к проверяемому поиску. 1. Найти хороший и плохой коммиты. 2. На каждом кандидате запускать один и тот же детерминированный тест. 3. Делить диапазон пополам через `git bisect`. 4. После нахождения читать diff и проверять причинность. Тест должен иметь стабильный код возврата. Если данные, окружение или внешний API изменились, bisect может указать не на причину, а на случайную корреляцию. ## Разобранный сценарий Исследователь меняет нормализацию и получает лучшую метрику. 1. `git status` показывает только ожидаемые исходники и тест. 2. `git diff` подтверждает формулу и новый тест на нулевую дисперсию. 3. Конфигурация запуска тоже версионируется. 4. Большой чекпойнт сохраняется во внешнее хранилище; рядом записываются его SHA-256 и хеш коммита. 5. `git diff --staged` проверяется до коммита. 6. Из чистого клона создаётся окружение и повторяется команда. Результат считается воспроизводимым не потому, что «есть Git», а потому, что каждый вход назван неизменяемым идентификатором и процесс автоматизирован. ## Типичные ошибки - **Коммит результата без кода запуска.** Артефакт нельзя объяснить или повторить. - **Огромный коммит.** Review не отделяет изменение поведения от шума. - **Слепой `git add .`.** В историю попадают секреты и локальные файлы. - **Хеш коммита при dirty-состоянии.** Хеш описывает не фактически исполненный код. - **Хранение больших бинарников в Git.** Репозиторий растёт навсегда. - **Force-push общей ветки.** Идентификаторы переписываются, ссылки ломаются. - **«Одинаковый seed значит воспроизводимость».** Версии библиотек, данные, аппаратные ядра и порядок операций также влияют на результат. ## Краткое резюме - Git хранит снимки; индекс отделяет текущую работу от следующего коммита. - Diff до и после staging — обязательная проверка содержимого. - Ветки именуют линии истории; rebase переписывает коммиты, merge интегрирует их. - Большие данные хранят отдельно, но связывают с кодом контрольными суммами. - Результат воспроизводим только при зафиксированных коде, данных, среде и запуске. ## Связанные материалы - Предыдущая глава: [`01_python_environments.md`](01_python_environments.md). - Следующая глава: [`03_jupyter_seeds_logging.md`](03_jupyter_seeds_logging.md). - Практика: [`../assignments/02_git.md`](../assignments/02_git.md). - Термины: [`glossary.md`](glossary.md).