В системе контроля версий Git есть два способа объединения одной ветки с другой, выраженные в виде разных команд:
git merge. Коммиты (изменения) из одной ветки переносятся в другую путем создания коммита слияния.
git rebase. Коммиты (изменения) из одной ветки переносятся из одной ветки в другую путем сохранения оригинального порядка изменений.
Говоря проще, при git merge
коммиты одной из веток «схлопываются» в один, а при git rebase
— остаются нетронутыми. При этом ветки объединяются.
Таким образом команда git rebase
позволяет объединить коммиты обеих веток через образование общей истории изменений.
Разница между git merge и git rebase
В этом руководстве будет рассмотрена команда git rebase
, отвечающая за перебазирование коммитов (изменений) из одной ветки в другую.
Все показанные примеры использовали систему контроля версий Git версии 2.34.1, которая запускалась на облачном сервере Timeweb Cloud под управлением операционной системы Ubuntu 22.04.
В блоге Timeweb Cloud есть отдельные публикации, подробно рассказывающие об установке Git на популярные операционные системы:
Понять устройство ребазирования в Git лучше всего на примере абстрактного репозитория, состоящего из нескольких веток. При этом операцию ребазирования необходимо рассматривать поэтапно.
Предположим мы создали репозиторий с единственной веткой master
, в которую сделали только один коммит. Ветка master
приобрела следующий вид:
master
commit_1
После этого на основе master
мы создали новую ветку hypothesis
, внутри который решили протестировать некоторые фичи. Внутри новой ветки мы сделали несколько коммитов, улучшающих кодовую. Ветка приобрела такой вид:
hypothesis
commit_4
commit_3
commit_2
commit_1
А уже потом мы добавили еще один коммит в ветку master
, исправив некоторую уязвимость в срочном порядке. Таким образом, ветка master
стала выглядеть так:
master
commit_5
commit_1
Теперь наш репозиторий имеет структуру из двух веток:
master
commit_5
commit_1
hypothesis
commit_4
commit_3
commit_2
commit_1
Ветка master
является основной, а hypothesis
— второстепенной (производной). Поздние коммиты указываются выше ранних, подобно тому, как их выводит команда git log
.
vds
Предположим, мы хотим продолжить работу над улучшением фичи, под которую ранее выделили отдельную ветку hypothesis
. Однако эта ветка не содержит жизненно важное исправление уязвимости, которое мы сделали в ветке master
.
Поэтому нам хотелось бы «синхронизировать» состояние ветки hypothesis
с веткой master
так, чтобы коммит исправления оказался в ветке с фичей. То есть мы хотим получить примерно такую структуру репозитория:
master
commit_5
commit_1
hypothesis
commit_4
commit_3
commit_2
commit_5
commit_1
Как видно, ветка hypothesis
будет точно повторять историю изменений ветки master
, несмотря на то, что изначально она была создана до коммита commit_5
. Иными словами, ветка hypothesis
будет содержать историю обеих веток — и свою, и master
.
Чтобы получить такой результат, необходимо выполнить ребазирование с помощью команды git rebase
.
Впоследствии изменения, сделанные в hypothesis
, можно будет объединить с веткой master
с помощью классической команды git merge
, создающей коммит слияния.
Тогда структура репозитория станет такой:
master
commit_merge
commit_5
commit_1
hypothesis
commit_4
commit_3
commit_2
commit_5
commit_1
Более того, выполнение git merge
после команды git rebase
может снизить вероятность возникновения конфликтов.
Разобравшись с теоретическим аспектом команды git rebase
, можно перейти к ее тестированию в реальном репозитории некого импровизированного проекта. При этом структура репозитория будет повторять показанный ранее теоретический пример.
Для начала создадим отдельную директорию, в которой будет размещаться репозиторий:
mkdir rebase
После чего перейдем в нее:
cd rebase
Теперь можно инициализировать репозиторий:
git init
В консольном терминале должно вывестись стандартное информационное сообщение:
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
...
А в текущей директории появится скрытый каталог .git
, который можно увидеть с помощью соответствующей команды:
ls -a
Флаг -a
означает all
и позволяет просматривать файловую систему в расширенном режиме. Ее содержимое будет таким:
. .. .git
Перед тем, как начать делать коммиты, необходимо указать базовые сведения о пользователе.
Сперва имя:
git config --global user.name "ИМЯ"
А потом и почту:
git config --global user.email "ИМЯ@ХОСТ.COM"
С помощью простых текстовых файлов мы будем имитировать добавление различных функций в проект. Каждая новая функция будет оформляться в отдельный коммит.
Создадим файл импровизированной функции:
nano function_1
И наполним его содержимом:
Функция 1
Теперь проиндексируем изменения, сделанные в репозитории:
git add .
На всякий случай можно проверить статус индексации:
git status
В консольном терминале должно появится сообщение со списком проиндексированных изменений:
On branch master
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: function_1
Теперь можно выполнить коммит:
git commit -m "commit_1"
После этого в консольном терминале выведется сообщение об успешном внесении изменений в ветку master
:
[master (root-commit) 4eb7cc3] commit_1
1 file changed, 1 insertion(+)
create mode 100644 function_1
Теперь необходимо создать новую ветку hypothesis
:
git checkout -b hypothesis
Флаг -b
необходим, чтобы сразу переключиться на созданную ветку.
В консольном терминале появится сообщение об успешном переключении на новую ветку:
Switched to a new branch 'hypothesis'
Теперь необходимо последовательно выполнить три коммита с тремя файлами по аналогии с веткой master
:
commit_2
с файлом function_2
и содержимым Функция 2
commit_3
с файлом function_3
и содержимым Функция 3
commit_4
с файлом function_4
и содержимым Функция 4
Если после этого просмотреть список всех коммитов:
git log --oneline
То в консольном терминале появится такая последовательность:
d3efb82 (HEAD -> hypothesis) commit_4
c9f57b7 commit_3
c977f16 commit_2
4eb7cc3 (master) commit_1
В этой команде флаг --oneline
необходим для вывода информации о коммитах в сжатом формате одной строки.
Последнее, что нужно сделать, — добавить еще один коммит в основную ветку master
. Переключимся на нее:
git checkout master
В консольном терминале должно появится соответствующее сообщение:
Switched to branch 'master'
После этого создадим еще один файл импровизированной функции:
nano function_5
Содержимое будет следующим:
Функция 5
Теперь можно проиндексировать изменения:
git add .
И выполнить очередной коммит:
git commit -m "commit_5"
Если проверить текущий список коммитов:
git log --oneline
То в ветке master
их будет всего два:
3df7a00 (HEAD -> master) commit_5
4eb7cc3 commit_1
Для выполнения перебазирования сперва необходимо перейти в ветку hypothesis
:
git checkout hypothesis
И выполнить ребазирование:
git rebase master
После этого в консольном терминале появится сообщение об успешном ребазировании:
Successfully rebased and updated refs/heads/hypothesis.
Теперь можно проверить список коммитов:
git log --oneline
В консольном терминале появится список, содержащий коммиты обоих веток в оригинальном порядке:
8ecfd58 (HEAD -> hypothesis) commit_4
f715aba commit_3
ee47470 commit_2
3df7a00 (master) commit_5
4eb7cc3 commit_1
Теперь ветка hypothesis
содержит общую историю всего репозитория.
Как и в случае с git merge
, при использовании команды git rebase
могут возникать конфликты, требующие ручного разрешения.
Давайте модифицируем наш репозиторий таким образом, чтобы искусственно создать конфликт ребазирования.
Создадим в ветке hypothesis
еще один файл:
nano conflict
И запишем в него следующий текст:
Тут должен быть конфликт!
Проиндексируем изменения:
git add .
И выполним очередной коммит:
git commit -m "conflict_1"
Теперь переключимся на ветку master
:
git checkout master
Создадим аналогичный файл:
nano conflict
И наполним его следующим содержимым:
Тут НЕ должен быть конфликт!
Аналогично, выполняем индексацию:
git add .
И делаем коммит:
git commit -m "conflict_2"
Заново откроем созданный файл:
nano conflict
И изменим его содержимое на следующий текст:
Тут точно НЕ должен быть конфликт!
Опять проиндексируется:
git add .
И снова сделаем коммит:
git commit -m "conflict_3"
Теперь можно обратно переключиться на ветку hypothesis
:
git checkout hypothesis
А далее выполнить еще одно ребазирование:
git rebase master
В консольном терминале появится сообщение о конфликте:
Auto-merging conflict
CONFLICT (add/add): Merge conflict in conflict
error: could not apply 6003ed7... conflict_1
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 6003ed7... conflict_1
Git предлагает отредактировать файл conflict
, проиндексировать изменения с помощью команды git add
, после чего продолжить ребазирование, указав флаг --continue
.
Именно так мы и поступим:
nano conflict
Файл будет содержать две конфликтующие версии файла, обрамленные специальными символами:
<<<<<<< HEAD
Тут точно НЕ должен быть конфликт!
=======
Тут должен быть конфликт!
>>>>>>> 6003ed7 (conflict_1)
Наша задача убрать всё лишнее, наполнив файл итоговым вариантом произвольного текста:
Тут однозначно точно единогласно НЕ должен быть никакого конфликта!
Теперь индексируем изменения:
git add .
И продолжаем процесс ребазирования:
git rebase --continue
После этого в консольном терминале откроется текстовый редактор, предлагающий изменение оригинальное название того коммита, в котором возник конфликт:
conflict_1
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# interactive rebase in progress; onto bd7aefc
# Last commands done (4 commands done):
# pick 8ecfd58 commit_4
# pick 6003ed7 conflict_1
# No commands remaining.
# You are currently rebasing branch 'hypothesis' on 'bd7aefc'.
#
# Changes to be committed:
# modified: conflict
#
В консольном терминале появится сообщение об успешном завершении процесса ребазирования:
[detached HEAD 482db49] conflict_1
1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/hypothesis.
Теперь если проверить список коммитов в ветке hypothesis
:
git log --oneline
Можно увидеть оригинальную последовательность всех сделанных изменений:
482db49 (HEAD -> hypothesis) conflict_1
bd5d036 commit_4
407e245 commit_3
948b41c commit_2
bd7aefc (master) conflict_3
d98648d conflict_2
3df7a00 commit_5
4eb7cc3 commit_1
Обратите внимание, что коммиты conflict_2
и conflict_3
, сделанные в ветке master
, располагаются в истории изменений раньше, чем коммит conflict_1
. Впрочем, это касается любых коммитов, сделанных в ветке master
.
Помимо работы с локальными ветками, ребазирование можно выполнить в момент подтягивания изменений из удаленного репозитория. Для этого к стандартной команде pull
необходимо добавить флаг --rebase
:
git pull --rebase remote branch
Здесь:
remote
. Удаленный репозиторий.
branch
. Удаленная ветка.
По сути, такая конфигурация команды pull
является эквивалентом git rebase
за исключением того, что применяемые к текущей ветки изменения (коммиты) берутся из удаленного репозитория.
Команда git rebase
позволяет сформировать достаточно линейную историю целевой ветки, представляющую собой последовательно сделанные коммиты.
Такая последовательность и отсутствие ветвления делает историю проще для восприятия и понимания.
Предварительно выполненная команда git rebase
может существенно снизить вероятность возникновения конфликтов при объединении веток с помощью git merge
.
Конфликты проще разрешать в последовательно идущих коммитах, нежели в коммитах, сливающихся в один единый коммит слияния. Это особенно актуально при отправке веток в удаленные репозитории.
В отличие от слияния, ребазирование частично переписывает историю целевой ветки. При этом лишние элементы истории удаляются.
Свойство существенно перестраивать историю коммитов может приводить к необратимым ошибкам внутри репозитория. А это значит, что некоторые данные могут быть безвозвратно утеряны.
Надежные VDS для ваших проектов
Объединение двух веток методом ребазирования, который реализуется командой git rebase
, существенно отличается от классического слияния, выполняемого командой git merge
.
git merge
превращает коммиты одной ветки ветки в один коммит другой.
git rebase
перемещает коммиты из одной ветки в конец другой, сохраняя оригинальный порядок.
При этом аналогичного поведения ребазирования можно добиться во время использования команды git pull
, используя дополнительный флаг --rebase
.
С одной стороны, команда git rebase
позволяет добиться более чистой и наглядной истории коммитов в основной ветке, что увеличивает простоту поддержки репозитория. С другой стороны, команды git rebase
снижает уровень детализации изменений внутри ветки, упрощая историю и удаляя некоторые ее записи.
По этой причине ребазирование — функция для более опытных пользователей, которые понимают механизм работы Git.
Чаще всего команда git rebase
используется в связке с командой git merge
, позволяя получить наиболее оптимальную структуру репозитория и веток внутри него.