lesson ── git-labs ── ~20 мин ── 7 шагов
Цель - увидеть руками разницу между git merge и git rebase. Сначала
поставишь две ветки в типичную ситуацию «разошлись», потом склеишь их
одним способом, потом ещё раз тем же сценарием другим способом, и
сравнишь графы истории.
Никакого реального GitHub не нужно. Всё локально, в эфемерном контейнере с git внутри. После - контейнер исчезает, репозитории удаляются.
интерактивный sandbox
Поднимется контейнер gitlab/git-base с git, bash, pre-commit. В браузере откроется терминал, можно сразу git init. Каждый шаг проверяется автоматически. Сеть air-gapped, github.com недоступен.
stack ── git · bash · 256 MB RAM · air-gapped · самоуничтожается через 30 мин простоя
Сделаем минимальный репозиторий с одним общим коммитом, потом разойдёмся в две ветки и сделаем по коммиту в каждую. Это классическая ситуация «main ушёл вперёд, я работал в feature».
cd /home/student/work
mkdir merge-vs-rebase && cd merge-vs-rebase
git init
echo "v1" > file.txt
git add file.txt
git commit -m "init"
git switch -c feature # -c = создать ветку и переключиться
echo "feature line" >> file.txt # >> append, не перезатирает v1
git commit -am "feature: add line" # -a stage отслеживаемых, -m сообщение
git switch main
echo "main line" >> file.txt
git commit -am "main: add line"
Теперь main и feature разошлись от общего коммита init.
Каждая команда отдельной строкой - если что-то не так, читай ошибку.
✓ Два расходящихся коммита от общего предка. Готов для эксперимента.
Граф показывает форму истории. Запусти:
git log --oneline --graph --all # --graph = ASCII-граф, --all = показать все refs
Должен показать:
* <sha> (main) main: add line
| * <sha> (feature) feature: add line
|/
* <sha> init
Это V-образное расхождение от init. Запомни форму - сравним её после merge и после rebase.
Если nothing видно - убедись что ты в /home/student/work/merge-vs-rebase.
✓ V-форма видна. Дальше merge.
Сейчас ты на main. Слей feature:
git merge feature # обе ветки ушли - получится three-way merge с конфликтом
Откроется редактор с дефолтным сообщением merge-коммита. Сохрани
без правок (Ctrl-O, Enter, Ctrl-X в nano). Будет конфликт - в
file.txt: один и тот же файл правили в двух местах. Открой
file.txt, удали маркеры <<<<<<<, =======, >>>>>>>,
оставь обе строки в каком-то порядке. Сохрани.
git add file.txt # помечаем конфликт разрешённым
git commit # без -m: откроет редактор с merge-сообщением
Снова сохрани сообщение merge-коммита.
Конфликт - это нормально. nano открывается через `nano file.txt`.
✓ Merge-коммит создан. Посмотри новый граф.
git log --oneline --graph --all
Теперь видишь:
* <sha> (HEAD -> main) Merge branch 'feature'
|\
| * <sha> (feature) feature: add line
* | <sha> main: add line
|/
* <sha> init
Сравни с тем, что было до merge. Прежние коммиты на месте, добавился merge-коммит сверху, история «затянулась» в одну точку. Это и есть merge: история сохранена ровно как было, плюс шов.
✓ Граф с merge-коммитом виден. Дальше - откатим и сделаем то же самое через rebase.
Чтобы попробовать rebase на той же ситуации, верни main до его состояния «один коммит после init». Тебе нужны два коммита: init и main: add line. Сейчас сверху лежит merge-коммит, его надо убрать.
git reset --hard HEAD~1 # снять последний коммит (merge), стереть и working tree
git log --oneline
Должно остаться: main: add line и init. Feature не трогаем,
она в стороне.
`git reset --hard HEAD~1` снимает один последний коммит (merge) с потерей.
✓ main снова на месте до merge. Готов для rebase.
Теперь переключись на feature и сделай rebase на main:
git switch feature
git rebase main # переписать коммиты feature поверх main
Снова конфликт в file.txt (та же причина, что и на шаге merge). Открой файл:
nano file.txt
Внутри увидишь блок с маркерами конфликта - семь угловых скобок
подряд (<<<<<<<), знаки равенства (=======), снова угловые
(>>>>>>>). Сейчас файл выглядит примерно так:
v1
<<<<<<< HEAD
main line
=======
feature line
>>>>>>> 699c3b1 (feature: add line)
Удали все три строки с маркерами (<<<<<<<, =======, >>>>>>>),
строки main line и feature line оставь. Должно получиться ровно
так - три строки, никаких угловых скобок и знаков равенства:
v1
main line
feature line
Сохрани (Ctrl-O, Enter, Ctrl-X). Теперь помечаем конфликт разрешённым и продолжаем rebase:
git add file.txt # помечаем конфликт разрешённым
git rebase --continue # после конфликта - не commit, а --continue
Откроется редактор с сообщением переписанного коммита. Сохрани без правок. rebase завершится.
Если verify красный - три самые частые причины, проверь их по порядку: 1. В file.txt остались маркеры `<<<<<<<`, `=======`, `>>>>>>>` - открой `nano file.txt` и убери их руками, оставь три строки без скобок. Потом `git add file.txt` и `git rebase --continue`. 2. Ты сделал `git commit` вместо `git rebase --continue` - это не доводит rebase до конца. Запусти `git status`, если видишь «interactive rebase in progress» - доделай через `git rebase --continue`. 3. Rebase ещё не закончен (есть незакоммиченные правки) - `git status` подскажет что именно осталось.
✓ Rebase прошёл. Граф изменится сильно.
git log --oneline --graph --all
Видишь:
* <sha> (HEAD -> feature) feature: add line
* <sha> (main) main: add line
* <sha> init
Полностью линейная история. Никакого merge-коммита, никакой V-формы. Коммит «feature: add line» теперь стоит поверх main: add line, с другим SHA - потому что его базовый коммит изменился.
Это и есть rebase: твоя ветка переписана так, будто она с самого начала ответвлялась от свежего main.
✓ Линейная история. Ты увидел разницу руками. Подробнее - в главе 8.
Merge сохраняет историю как было, ценой merge-коммита. Rebase
переписывает поверх, ценой новых SHA. Граф git log --oneline --graph --all показывает разницу мгновенно.
команды
git log --oneline --graph --allувидеть ветки и их формуgit merge featureслить feature в текущую (с merge-коммитом)git rebase mainпереписать текущую ветку поверх maingit reflogесли запутался - откатить через reflogконцепции