Что вы научитесь делать

После лекции вы сможете осознанно выбирать способ изменения истории, восстанавливать потерянную ссылку на коммит, переносить отдельное исправление, искать источник регрессии и объяснять назначение Git LFS, submodules, hooks и CI.

1. Что означает «переписать историю»

Git хранит неизменяемые объекты и изменяемые ссылки. Когда мы исправляем сообщение коммита или переносим его на другого родителя, появляется новый объект. Старый некоторое время может оставаться в базе; имя ветки уже ведёт к новой истории.

Поэтому перед действием полезно задать три вопроса: какое состояние я хочу получить, какую ссылку или какие файлы изменит команда и пользуется ли кто-то ещё текущими коммитами?

Учебный репозиторий

Демонстрация 1 выполняется последовательно. Начните в каталоге для учебных работ, где ещё нет папки git-advanced. Следующие демонстрации явно указывают свои стартовые условия. Блоки «Справочно» — отдельные примеры, а не общий скрипт.

mkdir git-advanced
cd git-advanced
git init -b main
git config user.name "Student"
git config user.email "[email protected]"
git config core.editor nano
printf '# Advanced Git lab\n' > README.md
git add README.md
git commit -m "Create the advanced lab"
git tag base
git switch -c feature-app
printf 'print("Hello")\n' > app.py
git add app.py
git commit -m "Add the application"
printf 'Run python3 app.py\n' > usage.txt
git add usage.txt
git commit -m "Document application usage"
git switch main
printf '\nProject conventions.\n' >> README.md
git add README.md
git commit -m "Document project conventions"

У истории общий предок A, вершина основной ветки B и два коммита задачи C, D. Стрелки показывают ссылки от коммитов к родителям:

flowchart TD
    B["B: main"] --> A["A: base"]
    D["D: feature-app"] --> C["C"]
    C --> A

2. Merge, rebase и аккуратная история

Сравнение операций

Операция Что происходит в нашем примере Когда полезно
merge Появляется M с родителями B и D; C и D сохраняют идентификаторы Нужно объединить линии развития, сохранив их связь
rebase Изменения C и D воспроизводятся поверх B как C′ и D′ Нужно обновить основание ветки задачи и получить последовательную историю
merge --squash и затем commit На основной ветке создаётся один обычный коммит с итогом задачи В основной истории важна задача целиком, без промежуточных шагов

У squash-коммита нет второго родителя, поэтому Git не записывает сам факт родства через merge. У rebase переписываются перенесённые коммиты. Универсально лучшего варианта нет: команда выбирает политику истории и применяет её последовательно.

Демонстрация 1. Один набор изменений, две истории

Продолжаем на main. Сначала создаём отдельную ветку для варианта со слиянием:

git switch -c demo-merge main
git merge --no-ff feature-app -m "Merge the application feature"
git log --oneline --decorate --graph --all
git switch feature-app
git branch feature-before-rebase
git rebase main
git log --oneline --decorate --graph --all
git diff demo-merge feature-app
git cat-file -p HEAD

В этом специально подобранном примере финальные файлы совпадают, поэтому последний diff пустой. Но графы и идентификаторы различаются. Ветка feature-before-rebase сохраняет ссылку на прежнюю линию C, D, а feature-app указывает на D′.

flowchart TD
    D2["D′: feature-app"] --> C2["C′"]
    C2 --> B["B: main"]
    B --> A["A: base"]
    D["D: feature-before-rebase"] --> C["C"]
    C --> A

Rebase может остановиться на конфликте при воспроизведении отдельного коммита. После исправления файлов выполните git add и git rebase --continue. Чтобы отказаться от незавершённой операции, используйте git rebase --abort. --skip пропускает текущий переносимый коммит: это допустимо только когда его изменение действительно не нужно. При rebase смысл сторон ours и theirs отличается от привычного взгляда «моя ветка / чужая ветка»; не выбирайте сторону по одному названию. Описание rebase.