После лекции вы сможете осознанно выбирать способ изменения истории, восстанавливать потерянную ссылку на коммит, переносить отдельное исправление, искать источник регрессии и объяснять назначение Git LFS, submodules, hooks и CI.
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
| Операция | Что происходит в нашем примере | Когда полезно |
|---|---|---|
merge |
Появляется M с родителями B и D; C и D сохраняют идентификаторы | Нужно объединить линии развития, сохранив их связь |
rebase |
Изменения C и D воспроизводятся поверх B как C′ и D′ | Нужно обновить основание ветки задачи и получить последовательную историю |
merge --squash и затем commit |
На основной ветке создаётся один обычный коммит с итогом задачи | В основной истории важна задача целиком, без промежуточных шагов |
У squash-коммита нет второго родителя, поэтому Git не записывает сам факт родства через merge. У rebase переписываются перенесённые коммиты. Универсально лучшего варианта нет: команда выбирает политику истории и применяет её последовательно.
Продолжаем на 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.