что такое rebase как он работает
Обзор Git Rebase
Удаление и перемещение коммитов в git опасны потерей данных или истории изменений. Git Rebase выполняет эти и другие операции. Владение этой командой повышает уверенность при работе с репозиторием.
Введение
Системы контроля версиями полезны, когда они могут рассказать историю всех изменений. Эта история может быть как реальный рассказ о жизни и развитии программной системы. Но может быть и кучкой коммитов, разобраться в которых нельзя и которые не несут пользы.
Вести аккуратную историю помогает команда git rebase. Основная ее цель “перетаскивать” коммиты из одного места в другое. Также с помощью нее можно избавляться от ненужных коммитов, склеивать одни и менять порядок других.
Умение пользоваться командой git rebase придает уверенности при работе с репозиторием.
Предостережение
Ребейзинг может “удалить” коммиты. Ребейзинг может испортить историю коммитов.
Однако в действительности удалить коммиты может только сборщик мусора. Это знание не сильно поможет, если потерять все ссылки на коммит, но если добавить лишнюю ветку или просто запомнить sha-1 хеш коммита, то всегда можно восстановить ребейзнутые коммиты.
О структуре коммитов
Ребейзинг переприменяет коммиты, то есть создает коммиты с таким же содержанием, но в другом месте. Новые коммиты получат новые хеши.
Указатели на коммиты
Некоторые названия настолько неудачно подобраны, что, услышав термин, человек сразу понимает о чем идет речь, понимает приблизительно правильно и у него нет причин разбираться глубже.
Проще говоря, ветка указывает на коммит. Поэтому если ответвить 5 веток от master, то все они будут указывать на один и тот же коммит. Однако новые коммиты будут свои в каждой из веток.
Создание коммита
Каждый коммит (кроме начального) имеет родительский коммит, который для него аналог “предыдущего”. Из одного коммита может выходить несколько дочерних коммитов. Это делается с помощью ветвления. А так как ветки могут соединяться в одну, то у коммита может быть и несколько родительских коммитов. Обычно их два, но есть возможность сделать merge трех и более коммитов одновременно.
Иногда коммит бывает создан не в том месте и не в то время. Не в том месте означает, что он был создан не в той ветке, а не в то время означает, что коммит был создан, но потом ветка разработки обогатилась новым кодом и хочется пересоздать коммит, начиная с более свежего кода.
Пересоздание коммитов
Что же делать? Подтягивать изменения. Можно слить ветку develop в ветку с задачей. Безопасно, но история будет подпорчена. Делается ребейзинг
Предполагается, что develop уже содержит новые коммиты, то есть, что уже были сделаны fetch/pull.
Как работает команда git rebase? В простом случае, это работает так:
Ниже приведены три дерева коммита
Ветка с задачей исходит из актуального коммита
Код разработки обновился, ветка с задачей исходит из старого коммита
Выполнен ребейзинг, ветка с задачей исходит из актуального коммита
Стоит обратить внимание на хеш коммита до ребейзинга (8f26890) и после (3b16148). Разумеется, по старому хешу можно найти старый коммит.
Создадим ветку, указывающую на старый коммит
Нетрудно видеть, что старый коммит остался в целости и сохранности, однако, если на него никто не указывает, то он не отображается в истории коммитов. Такие висячие коммиты удаляются сборщиком мусора в последствии (например, через месяц).
Потеря коммитов
Чтобы избавиться от риска потерять коммиты, перед ребейзингом следует создать дополнительную ветку
Простой Rebase
Формат команды такой
Но в Merge все наоборот!
Поэтому при ребейзинге придется дважды переключаться с ветки на ветку
Однако если использоваться слияние для подтягивания изменений, то сценарий похожий
Разница есть только со сценарием завершения задачи
Таким образом, для того, чтобы определить “что и куда”, надо задать вопрос “какую ветку надо изменить?”. Находиться следует в той ветке, которую надо изменить.
Интерактивный Rebase
Ребейзинг позволяет также переписывать историю своих коммитов.
Такая команда делает то же самое, что и раньше, но перед этим открывает текстовый редактор, в котором можно изменить поведение по умолчанию.
Rebase и коммиты удаленного репозитория
Конфликты
Конфликты случаются везде, где накладываются изменения из разных мест. Это происходит при слиянии (git merge), при отмене коммитов (git revert) и даже при переключении (git checkout, git stash). Во всех командах конфликты разрешаются по общим правилам. Поэтому в ребейзинге разрешение конфликтов такое же как и при слиянии.
При слиянии создается специальный мерж-коммит, в котором хранятся все изменения, сделанные в отдельной ветке. Поэтому все конфликты разрешаются в этом же коммите. В истории хранятся все коммиты из ветки с задачей и мерж-коммит. Последний можно рассматривать как завершающий штрих.
В помощниках вроде TortoiseGit процесс разрешения конфликтов почти ничем не отличается от слияния. В консоли используются следующие команды.
Детальный Rebase
Существует еще одна форма команды ребейзинга
FROM, TO, START – хеши коммитов или названия веток/тегов. Команда копирует коммиты из диапазона FROM..TO в START.
Стандартная команда, выполненная, находясь внутри ветки feature/A/super_code
Может быть заменена на
Примеры
Необходимо скопировать последний коммит из ветки с задачей в develop
(в git можно указать на родительский коммит с помощью значка шапочки)
Коммит сделан в ветке feature/A, а должен был быть в ветке bug/B.
Чистая история коммитов
Рефакторинг привыкли относить только к коду, но хорошая и наглядная история коммитов в репозитории может дать больше информации разработчику. Правило бойскаута из книги “Чистый код” Роберта Мартина «оставь место стоянки чище, чем оно было до твоего прихода» применимо и к истории коммитов. Поэтому использование Git Rebase также полезно в работе с репозиторием, как инструменты рефакторинга в IDE для работы с кодом.
Руководство по Git. Часть №2: золотое правило и другие основы rebase
Посмотрим, что происходит, когда вы выполняете git rebase и почему нужно быть внимательным.
Суть rebase
Как именно происходит rebase:
Можно сказать, что rebase — это открепить ветку (branch), которую вы хотите переместить, и подключить ее к другой ветке. Такое определение соответствует действительности, но попробуем заглянуть чуть глубже. Если вы посмотрите документацию, вот что там написано относительно rebase: «Применить коммиты к другой ветке (Reapply commits on top of another base tip)».
Главное слово здесь — применить, потому что rebase — это не просто копипаст ветки в другую ветку. Rebase последовательно берет все коммиты из выбранной ветки и заново применяет их к новой ветке.
Такое поведение приводит к двум моментам:
Вот более правильная интерпретация того, что происходит при rebase:
Как видите, ветка feature содержит абсолютно новые коммиты. Как было сказано ранее, тот же самый набор изменений, но абсолютно новые объекты с точки зрения Git.
Теперь давайте обсудим «Золотое правило».
Золотое правило rebase
Золотое правило rebase звучит так — «НИКОГДА не выполняйте rebase расшаренной ветки!». Под расшаренной веткой понимается ветка, которая существует в сетевом репозитории и с которой могут работать другие люди, кроме вас.
Часто это правило применяют без должного понимания, поэтому разберем, почему оно появилось, тем более что это поможет лучше понять работу Git.
Давайте рассмотрим ситуацию, когда разработчик нарушает золотое правило, и что происходит в этом случае.
Предположим, Боб и Анна вместе работают над проектом. Ниже представлено, как выглядят репозитории Боба и Анны и исходный репозиторий на GitHub:
У всех пользователей репозитории синхронизируются с GitHub.
Теперь Боб, нарушая золотое правило, выполняет rebase, и в это же время Анна, работая в ветке feature, создает новый коммит:
Вы видите, что произойдет?
Боб пытается выполнить пуш коммита, ему приходит отказ примерно такого содержания:
Выполнение Git не было успешным, потому что Git не знает, как объединить feature ветку Боба с feature веткой GitHub.
Единственным решением, позволяющим Бобу выполнить push, станет использование ключа force, который говорит GitHub-репозиторию удалить у себя ветку feature и принять за эту ветку ту, которая пушится Бобом. После этого мы получим следующую ситуацию:
Теперь Анна хочет запушить свои изменения, и вот что будет:
Руководство по Git. Часть №2: золотое правило и другие основы rebase
Посмотрим, что происходит, когда вы выполняете git rebase и почему нужно быть внимательным.
Это вторая и третья части гайда по Git из блога Pierre de Wulf в переводе команды Mail.ru Cloud Solutions. Первую часть можно почитать тут.
Суть rebase
Как именно происходит rebase:
Можно сказать, что rebase — это открепить ветку (branch), которую вы хотите переместить, и подключить ее к другой ветке. Такое определение соответствует действительности, но попробуем заглянуть чуть глубже. Если вы посмотрите документацию, вот что там написано относительно rebase: «Применить коммиты к другой ветке (Reapply commits on top of another base tip)».
Главное слово здесь — применить, потому что rebase — это не просто копипаст ветки в другую ветку. Rebase последовательно берет все коммиты из выбранной ветки и заново применяет их к новой ветке.
Такое поведение приводит к двум моментам:
Как видите, ветка feature содержит абсолютно новые коммиты. Как было сказано ранее, тот же самый набор изменений, но абсолютно новые объекты с точки зрения Git.
Это также означает, что старые коммиты не уничтожаются. Они становятся просто недоступными напрямую. Если вы помните, ветка — всего лишь ссылка на коммит. Таким образом, если ни ветка, ни тег не ссылаются на коммит, к нему невозможно получить доступ средствами Git, хотя на диске он продолжает присутствовать.
Теперь давайте обсудим «Золотое правило».
Золотое правило rebase
Золотое правило rebase звучит так — «НИКОГДА не выполняйте rebase расшаренной ветки!». Под расшаренной веткой понимается ветка, которая существует в сетевом репозитории и с которой могут работать другие люди, кроме вас.
Часто это правило применяют без должного понимания, поэтому разберем, почему оно появилось, тем более что это поможет лучше понять работу Git.
Давайте рассмотрим ситуацию, когда разработчик нарушает золотое правило, и что происходит в этом случае.
Предположим, Боб и Анна вместе работают над проектом. Ниже представлено, как выглядят репозитории Боба и Анны и исходный репозиторий на GitHub:
У всех пользователей репозитории синхронизируются с GitHub.
Теперь Боб, нарушая золотое правило, выполняет rebase, и в это же время Анна, работая в ветке feature, создает новый коммит:
Вы видите, что произойдет?
Боб пытается выполнить пуш коммита, ему приходит отказ примерно такого содержания:
Выполнение Git не было успешным, потому что Git не знает, как объединить feature ветку Боба с feature веткой GitHub.
Единственным решением, позволяющим Бобу выполнить push, станет использование ключа force, который говорит GitHub-репозиторию удалить у себя ветку feature и принять за эту ветку ту, которая пушится Бобом. После этого мы получим следующую ситуацию:
Теперь Анна хочет запушить свои изменения, и вот что будет:
Это нормально, Git сказал Анне, что у нее нет синхронизированной версии ветки feature, то есть ее версия ветки и версия ветки в GitHub — разные. Анна должна выполнить pull. Точно таким же образом, как Git сливает локальную ветку с веткой в репозитории, когда вы выполняете push, Git пытается слить ветку в репозитории с локальной веткой, когда вы выполняете pull.
Перед выполнением pull коммиты в локальной и GitHub-ветках выглядят так:
Когда вы выполняете pull, Git выполняет слияние для устранения разности репозиториев. И вот, к чему это приводит:
Коммит M — это коммит слияния (merge commit). Наконец, ветки feature Анны и GitHub полностью объединены. Анна вздохнула с облегчением, все конфликты устранены, она может выполнить push.
Боб выполняет pull, теперь все синхронизированы:
Глядя на получившийся беспорядок, вы должны были убедиться в важности золотого правила. Также учтите, что подобный беспорядок был создан всего одним разработчиком и на ветке, которая расшарена всего между двумя людьми. Представьте, что будет в команде из десяти человек.
Одним из многочисленных достоинств Git является то, что можно без проблем откатиться на любое время назад. Но чем больше допущено ошибок, подобных описанной, тем сложнее это сделать.
Также учтите, что появляются дубликаты коммитов в сетевом репозитории. В нашем случае — D и D’, содержащие одни и те же данные. По сути, количество дублированных коммитов может быть таким же большим, как и количество коммитов в вашей rebased ветке.
Если вы все еще не убеждены, давайте представим Эмму — третью разработчицу. Она работает в ветке feature перед тем, как Боб совершает свою ошибку, и в настоящий момент хочет выполнить push. Предположим, что к моменту ее push наш маленький предыдущий сценарий уже завершился. Вот что выйдет:
Этот текст мог заставить вас подумать, что rebase используют только для перемещения одной ветки на верхушку другой ветки. Это необязательно — вы можете выполнять rebase и на одной ветке.
Красота pull rebase
Как вы видели выше, проблем Анны можно было избежать, если бы она использовала pull rebase. Рассмотрим этот вопрос подробнее.
Допустим, Боб работает в ветке, отходящей от мастера, тогда его история может выглядеть вот так:
Боб решает, что настало время выполнить pull, что, как вы уже поняли, приведет к некоторым неясностям. Поскольку репозиторий Боба отходил от GitHub, Git спросит делать ли объединение, и результат будет таким:
Это решение подходит и работает нормально, однако, вам может быть полезно знать, что есть другие варианты решения проблемы. Одним из них является pull-rebase.
Когда вы делаете pull-rebase, Git пытается выяснить, какие коммиты есть только в вашей ветке, а какие — в сетевом репозитории. Затем Git объединяет коммиты из сетевого репозитория с самым свежим коммитом, присутствующим и в локальном, и в сетевом репозитории. После чего выполняет rebase ваших локальных коммитов в конец ветки.
Звучит сложно, поэтому проиллюстрируем:
Как вы помните, при rebase Git применяет коммиты один за одним, то есть в данном случаем применяет в конец ветки master коммит E, потом F. Получился rebase сам в себя. Выглядит неплохо, но возникает вопрос — зачем так делать?
По моему мнению, самая большая проблема с объединением веток в том, что загрязняется история коммитов. Поэтому pull-rebase — более элегантное решение. Я бы даже пошел дальше и сказал, что когда нужно скачать последние изменения в вашу ветку, вы всегда должны использовать pull-rebase. Но нужно помнить: поскольку rebase применяет все коммиты по очереди, то когда вы делаете rebase 20 коммитов, вам, возможно, придется решать один за другим 20 конфликтов.
Как правило, можно использовать следующий подход: одно большое изменение, сделанное давно — merge, два маленьких изменения, сделанных недавно — pull-rebase.
Сила rebase onto
Предположим, история ваших коммитов выглядит так:
Итак, вы хотите выполнить rebase ветки feature 2 в ветку master. Если вы выполните обычный rebase в ветку master, получите это:
Нелогично выглядит то, что коммит D существует в обоих ветках: в feature 1 и feature 2. Если вы переместите ветку feature 1 в конец ветки мастер, получится, что коммит D будет применен два раза.
Предположим, что вам нужно получить другой результат:
Для реализации подобного сценария как раз и предназначен git rebase onto.
Сначала прочтем документацию:
Нас интересует вот это:
С помощью этой опции указывается, в какой точке создавать новые коммиты.
Если эта опция не указана, то стартовой точкой станет upstream.
Для понимания приведу еще один рисунок:
То есть ветка master — это newbase, а ветка feature 1 — upstream.
Поддержание аккуратной истории в Git с помощью интерактивного rebase
Прим. перев.: эта статья была написана автором Git-клиента Tower, Tobias Günther, и опубликована в блоге GitLab. В ней просто и наглядно рассказывается об основных возможностях интерактивного rebase’а, что может стать отличным введением для тех, кто только начинает им пользоваться.
Interactive rebase — один из самых универсальных инструментов Git’а. В этой статье мы поговорим о том, как с его помощью корректировать сообщения при коммитах, исправлять ошибки, и о многом другом.
Интерактивный rebase иногда называют «швейцарским армейским ножом» Git’а, поскольку он объединяет в себе так много различных инструментов для совершенно разных сценариев применения. При этом главным вариантом использования, без сомнения, является очистка локальной истории коммитов.
Обратите внимание на слово «локальной»: rebase следует использовать только для очистки локальной истории коммитов (например, перед включением одной из ваших локальных feature-веток в общую ветку команды). И наоборот, этот мощный инструмент НЕ следует использовать для исправления коммитов в ветке, которая уже загружена и открыта для совместной работы в удаленном репозитории. Интерактивный rebase — инструмент для «переписывания» истории Git, и его не следует использовать для редактирования коммитов, которые уже открыты для других.
Теперь, когда все необходимые предупреждения сделаны, давайте перейдем к практике.
Примечание: для визуализации сценариев и последовательностей операций для некоторых скриншотов я использовал GUI к Git под названием Tower.
Редактирование сообщения в старом коммите
Вот пример описания к коммиту, которое мы будем исправлять:
«Плохое» сообщение к коммиту, которое мы будем исправлять
Первый шаг при использовании interactive rebase — определить, какой частью истории мы собираемся манипулировать. В примере выше для того, чтобы изменить «плохое» коммит-сообщение, мы должны начать с его родительского коммита.
Начинаем с родительского коммита
Теперь нужно скормить хэш родительского коммита команде:
Откроется окно редактора со списком коммитов для изменения. Не удивляйтесь тому, что они приведены в обратном порядке: в рамках интерактивного rebase’а Git будет повторно применять прошлые коммиты один за другим. Другими словами, с точки зрения Git коммиты выстроены в правильном порядке.
Окно редактора со списком выбранных коммитов
Редактирование описания старого коммита
Сохраните изменения и закройте окно. Поздравляю — сессия интерактивного rebase’а завершена, сообщение к коммиту успешно отредактировано!
Объединение нескольких коммитов в один
Rebase также можно использовать для объединения нескольких старых коммитов в один. При этом, конечно, актуальным остается золотое правило систем управления версиями: в большинстве случаев лучше создавать множество мелких коммитов, нежели несколько крупных. Однако, как и во всем остальном, мы можем внезапно обнаружить, что несколько перестарались со следованием этому правилу, и решить, что было бы хорошо объединить несколько старых коммитов в один.
Давайте предположим, что нужно объединить следующие выбранные коммиты в один:
Объединяем несколько коммитов в один
Как и в первом случае, процесс начинается с запуска сессии интерактивного rebase’а на коммите-предшественнике тех, что мы хотим изменить.
Снова откроется окно редактора с историей коммитов, которые мы хотим объединить:
Помечаем нужные строки кодовым словом «squash»
Сохраните изменения и закройте окно редактора. Как и в первом случае, появится новое окно с просьбой ввести сообщение для нового, объединенного коммита:
Вводим сообщение для нового коммита
Сохраните сообщение и закройте окно. Будет создан новый коммит, содержащий изменения обоих старых коммитов. Вуаля!
Исправление ошибок
Interactive rebase отлично подходит для исправления ошибок в предыдущих коммитах. При этом не имеет значения, какая именно это ошибка: забыли ли вы внести определенное изменение, должны ли были удалить файл, или просто опечатались…
Обычное решение в подобной ситуации — просто сделать новый коммит, исправляющий ошибку. Но с другой стороны, это внесет дополнительную путаницу в историю: сначала у нас оригинальный коммит, затем мы добавили еще один, исправляющий ошибки… в общем, не слишком «чистый» рабочий подход. Очень скоро в истории коммитов станет нелегко разобраться, поскольку она будет забита всеми этими исправлениями/заплатками.
Как работает fixup
После этого все будет выглядеть так, словно с оригинальным коммитом не было никаких проблем! Итак, давайте изучим весь процесс на практическом примере.
Прежде всего необходимо исправить проблему: добавить новый файл, внести изменения в существующий, удалить устаревшие файлы. Другими словами, внести изменения, исправляющие ошибку.
$ git add corrections.txt
Если теперь посмотреть на историю, вы увидите, что был создан ничем не примечательный коммит (разве вы ожидали чего-то иного?). Но при более внимательном взгляде становятся заметны некоторые особенности: к новому коммиту были автоматически добавлены пометка «fixup!» и описание старого «плохого» коммита:
Оригинальный коммит и корректирующий коммит (fixup)
Теперь пора запускать interactive rebase. Опять же, в качестве отправной точки выбираем коммит, предшествующий «плохому»:
Корректирующий коммит помечен как «fixup» и размещен в правильном порядке
Git автоматически сделал две вещи:
И переупорядочил строки так, чтобы fixup-коммит оказался непосредственно под «плохим» коммитом. Дело в том, что fixup работает в точности как squash : он объединяет выделенный коммит с коммитом выше.
Таким образом, нам ничего делать не надо. Сохраните изменения и закройте окно редактора.
Давайте еще раз взглянем на историю коммитов:
Мало того, что оригинальный коммит теперь содержит правки из вспомогательного, но и некрасивый вспомогательный коммит (с исправлениями) исчез из истории. Все красиво, словно никогда и не было никаких проблем!
Откройте для себя возможности interactive rebase
Существует множество различных вариантов использования интерактивного rebase’а: большинство из них связаны с исправлением ошибок. Подробнее узнать о других способах можно в бесплатном (англоязычном) курсе «First Aid Kit for Git» — коллекции коротких видео (по 2-3 минуты на эпизод).