Перенос коммитов
При слиянии двух линий мы сохраняли обе вершины и записывали коммит с двумя родителями. Для маленькой собственной ветки бывает удобен другой способ: представить своё изменение так, будто оно началось от более свежего состояния основной линии. Такой перенос называется rebase.
Разберём его на одной заметке README и независимом изменении CSS. Ветка ещё никому не передавалась, поэтому её историю можно переиграть в отдельной лаборатории. Оглавление курса и архив содержат lesson-07/start. Учебные команды не выполнялись; схема описывает ожидаемый результат, а не фактический лог ProfessorWeb.
Правка поверх общего предка
Начальное состояние — согласованный каталог после прошлого урока, обозначим его N. В Markdown заголовок «Каталог курсов веб-разработки», в CSS зелёный цвет #24814a. Статус должен быть чистым. Создадим локальную ветку:
git switch main
git switch -c feature/readme-note
В конце README.md добавьте отдельный абзац «Изменения каталога обсуждаются до публикации.». Сохраните только эту заметку:
git add README.md
git diff --cached
git commit -m "Описать порядок обсуждения каталога"
git switch main
Назовём полученный feature-коммит P. Теперь, оставаясь на main, в конец public/theme.css добавьте правило:
.catalog {
padding: 1rem;
}
Это условный класс учебного каталога. В данном Git-упражнении мы изучаем историю текста, поэтому не добавляем отдельный HTML и не запускаем страницу. Подготовьте и сохраните CSS:
git add public/theme.css
git commit -m "Задать внутренний отступ каталога"
git log --oneline --graph --all
Коммит основной линии обозначим Q. P и Q имеют общего родителя N. Получилось знакомое расхождение, но сейчас хотим перенести только собственную заметку на свежую основную линию. Снимок feature пока не содержит нового CSS, потому что её коммит создан раньше Q.
Повторное применение изменения
Переключитесь на feature с чистым деревом и выполните перенос на явно указанную main:
git switch feature/readme-note
git status --short
git rebase main
git log --oneline --graph --all
Git определяет коммиты этой ветки, которые требуется переиграть поверх выбранной базы. В нашем сценарии это только P. Его изменение README применяется к состоянию Q, после чего создаётся новый коммит, обозначим P′. Он содержит ту же намеренную правку, но ссылается на другого родителя.
до: P feature/readme-note
/
N — Q main
после: N — Q — P′ feature/readme-note
↑ main
У P′ другой идентификатор. Коммит определяется не только изменённой строкой: его родитель также является частью сохранённого объекта. Поэтому одинаковое сообщение и похожий diff не делают старый и новый коммиты одной и той же вершиной истории. Это главное отличие переноса от соединения прежних вершин.
В рабочем дереве feature после ожидаемого успешного переноса есть и заметка README, и новый CSS основной линии. Человек может сравнить полный результат и намерение своей правки. Rebase не меняет main: её ссылка продолжает указывать на Q, пока вы отдельно не решите включить подготовленный результат.
Документация git-rebase описывает повторное применение коммитов поверх другой базы. Для учебного каталога это способ уточнить расположение локального изменения в графе. Он не является автоматическим доказательством того, что старое изменение подходит новым исходникам.
Включение новой линии
Если итоговое содержимое согласовано, перейдите на основную линию и потребуйте именно простое передвижение:
git switch main
git merge --ff-only feature/readme-note
git status --short
После переноса Q является предком P′, поэтому ожидается fast-forward. Параметр --ff-only откажет, если граф уже изменился и такое передвижение невозможно. Он выражает ваше намерение включить подготовленную линейную последовательность, а не неожиданно создать другой вид объединения.
В отличие от прошлого урока, отдельного коммита с двумя родителями здесь нет. Основная линия последовательно содержит Q, затем P′. Содержимое сохраняет обе задачи, а граф описывает их в новом порядке. Какой вариант истории удобнее команде, зависит от её процесса; понимать оба механизма полезнее, чем объявлять один универсально правильным.
Не продолжайте команды автоматически, если после rebase остались конфликты. Для нашей независимой пары файлов они не ожидаются. Если вы поменяли другие строки, Git может остановиться на текущем переигрываемом коммите. Это ещё не готовая вершина feature и не обычное состояние законченной операции.
Остановка и общая история
При конфликте переноса прочитайте статус, сопоставьте изменения с новой базой, исправьте файл и добавьте решение. Продолжение начатой операции выглядит так:
git add README.md
git rebase --continue
Путь в этом фрагменте подходит только случаю, где конфликтует README. Выбирайте действительно перечисленные конфликтующие файлы. Во время rebase не выполняйте обычный merge из прошлого урока: Git сейчас повторно применяет отдельное изменение. Смысл сторон в редакторе конфликтов может отличаться от привычного merge, поэтому читайте контекст операции.
Если не хотите продолжать, git rebase --abort возвращает ветку к исходному состоянию начатого переноса. Команда --skip имеет другой смысл: она пропускает текущий коммит. Использовать её как способ убрать неприятный конфликт нельзя без понимания, какую задачу вы исключаете из результата.
В этом уроке feature является только локальной и ещё никому не переданной. Если другие разработчики уже строят работу на P, появление вместо него P′ потребует согласования истории. Даже одинаковый текст не устраняет разницу в идентификаторах. Поэтому перенос опубликованной общей ветки не является следующим автоматическим шагом курса.
Отдельный сохранённый коммит полезен и после переноса: он позволяет увидеть намерение заметки независимо от CSS. Если в одной feature смешать десятки несвязанных задач, линейный граф сам по себе не сделает историю понятной. Связность коммитов, которую разобрали раньше, остаётся важной при любом способе объединения.
Полезно сопоставить этот результат с альтернативным merge тех же двух задач. Содержимое рабочего дерева могло бы быть одинаковым: новый CSS и заметка README. Но граф был бы другим — исходный P сохранился бы, а отдельная вершина объединила бы его с Q. При rebase мы получили новый P′ и последовательную линию. Следовательно, совпадение файлов ещё не означает совпадения истории. Для передачи работы важны и содержимое, и договор о том, какие идентификаторы уже доступны другим людям.
Также перенос следует рассматривать относительно точно выбранной базы. Мы передали имя main, которое в момент команды указывает на локальный Q. Оно не означает автоматически самую свежую версию на сервере. В лаборатории вообще нет удалённого репозитория. Позже, при изучении удалённых веток, придётся отдельно получить их состояние и понять, какую вершину вы выбираете. Пока явная локальная база делает пример однозначным и не скрывает сетевых действий.
Ожидаемый итог — чистая main с CSS и заметкой README, записанными последовательными коммитами. Далее рассмотрим возврат опубликованного изменения: создадим обратную правку, сохранив общую историю и последующие полезные изменения.