company_banner

Поддержание аккуратной истории в Git с помощью интерактивного rebase

Автор оригинала: Tobias Günther
  • Перевод

Прим. перев.: эта статья была написана автором Git-клиента Tower, Tobias Günther, и опубликована в блоге GitLab. В ней просто и наглядно рассказывается об основных возможностях интерактивного rebase'а, что может стать отличным введением для тех, кто только начинает им пользоваться.

Interactive rebase — один из самых универсальных инструментов Git'а. В этой статье мы поговорим о том, как с его помощью корректировать сообщения при коммитах, исправлять ошибки, и о многом другом.

Интерактивный rebase иногда называют «швейцарским армейским ножом» Git’а, поскольку он объединяет в себе так много различных инструментов для совершенно разных сценариев применения. При этом главным вариантом использования, без сомнения, является очистка локальной истории коммитов.

Обратите внимание на слово «локальной»: rebase следует использовать только для очистки локальной истории коммитов (например, перед включением одной из ваших локальных feature-веток в общую ветку команды). И наоборот, этот мощный инструмент НЕ следует использовать для исправления коммитов в ветке, которая уже загружена и открыта для совместной работы в удаленном репозитории. Интерактивный rebase — инструмент для «переписывания» истории Git, и его не следует использовать для редактирования коммитов, которые уже открыты для других.

Теперь, когда все необходимые предупреждения сделаны, давайте перейдем к практике.

Примечание: для визуализации сценариев и последовательностей операций для некоторых скриншотов я использовал GUI к Git под названием Tower.

Редактирование сообщения в старом коммите

Иногда вы замечаете опечатку в старом коммите или вспоминаете, что забыли упомянуть нечто важное в его описании. Если бы речь шла о самом последнем коммите, можно было бы воспользоваться опцией --amend команды git commit. Но в случае более старых коммитов придется воспользоваться интерактивным rebase’ом.

Вот пример описания к коммиту, которое мы будем исправлять:

«Плохое» сообщение к коммиту, которое мы будем исправлять
«Плохое» сообщение к коммиту, которое мы будем исправлять

Первый шаг при использовании interactive rebase — определить, какой частью истории мы собираемся манипулировать. В примере выше для того, чтобы изменить «плохое» коммит-сообщение, мы должны начать с его родительского коммита.

Начинаем с родительского коммита
Начинаем с родительского коммита

Теперь нужно скормить хэш родительского коммита команде:

$ git rebase -i 0023cddd

Откроется окно редактора со списком коммитов для изменения. Не удивляйтесь тому, что они приведены в обратном порядке: в рамках интерактивного rebase’а Git будет повторно применять прошлые коммиты один за другим. Другими словами, с точки зрения Git коммиты выстроены в правильном порядке.

Окно редактора со списком выбранных коммитов
Окно редактора со списком выбранных коммитов

Обратите внимание: в этом окне менять ничего не нужно! Или, говоря о нашем конкретном примере: не пытайтесь здесь поменять сообщение к коммиту ca9aacb! В этом окне вы только помечаете нужный коммит ключевым словом для соответствующего действия. Поскольку нужно перефразировать описание коммита, мы вводим reword. Сохраните изменения и закройте окно редактора. После этого появится новое окно, содержащее сообщение к старому коммиту. Теперь можно вносить изменения:

Редактирование описания старого коммита
Редактирование описания старого коммита

Сохраните изменения и закройте окно. Поздравляю — сессия интерактивного rebase’а завершена, сообщение к коммиту успешно отредактировано!

Объединение нескольких коммитов в один

Rebase также можно использовать для объединения нескольких старых коммитов в один. При этом, конечно, актуальным остается золотое правило систем управления версиями: в большинстве случаев лучше создавать множество мелких коммитов, нежели несколько крупных. Однако, как и во всем остальном, мы можем внезапно обнаружить, что несколько перестарались со следованием этому правилу, и решить, что было бы хорошо объединить несколько старых коммитов в один.

Давайте предположим, что нужно объединить следующие выбранные коммиты в один:

Объединяем несколько коммитов в один
Объединяем несколько коммитов в один

Как и в первом случае, процесс начинается с запуска сессии интерактивного rebase’а на коммите-предшественнике тех, что мы хотим изменить.

$ git rebase -i 2b504bee

Снова откроется окно редактора с историей коммитов, которые мы хотим объединить:

Помечаем нужные строки кодовым словом «squash»
Помечаем нужные строки кодовым словом «squash»

Действию, которое мы собираемся произвести над коммитами, соответствует кодовое слово squash. В данном случае следует помнить лишь об одной тонкости: строка, помеченная «squash», будет объединена со строкой, которая находится выше нее. Именно поэтому на скриншоте выше я пометил словом squash строку №2 (она будет объединена со строкой №1).

Сохраните изменения и закройте окно редактора. Как и в первом случае, появится новое окно с просьбой ввести сообщение для нового, объединенного коммита:

Вводим сообщение для нового коммита
Вводим сообщение для нового коммита

Сохраните сообщение и закройте окно. Будет создан новый коммит, содержащий изменения обоих старых коммитов. Вуаля!

Исправление ошибок

Interactive rebase отлично подходит для исправления ошибок в предыдущих коммитах. При этом не имеет значения, какая именно это ошибка: забыли ли вы внести определенное изменение, должны ли были удалить файл, или просто опечатались…

Обычное решение в подобной ситуации — просто сделать новый коммит, исправляющий ошибку. Но с другой стороны, это внесет дополнительную путаницу в историю: сначала у нас оригинальный коммит, затем мы добавили еще один, исправляющий ошибки… в общем, не слишком «чистый» рабочий подход. Очень скоро в истории коммитов станет нелегко разобраться, поскольку она будет забита всеми этими исправлениями/заплатками.

Именно для таких случаев и предназначен fixup. Этот инструмент берет коммит с быстрым исправлением, применяет его изменения к оригинальном коммиту (тем самым исправляя его), и удаляет корректирующий коммит.

Как работает fixup
Как работает fixup

После этого все будет выглядеть так, словно с оригинальным коммитом не было никаких проблем! Итак, давайте изучим весь процесс на практическом примере.

Прежде всего необходимо исправить проблему: добавить новый файл, внести изменения в существующий, удалить устаревшие файлы. Другими словами, внести изменения, исправляющие ошибку.

Следующий шаг — сделать коммит этих изменений в репозиторий, но с небольшой добавкой: делать коммит надо с флагом --fixup, попутно указав хэш «плохого» коммита:

$ git add corrections.txt

$ git commit --fixup 2b504bee

Если теперь посмотреть на историю, вы увидите, что был создан ничем не примечательный коммит (разве вы ожидали чего-то иного?). Но при более внимательном взгляде становятся заметны некоторые особенности: к новому коммиту были автоматически добавлены пометка «fixup!» и описание старого «плохого» коммита:

Оригинальный коммит и корректирующий коммит (fixup)
Оригинальный коммит и корректирующий коммит (fixup)

Теперь пора запускать interactive rebase. Опять же, в качестве отправной точки выбираем коммит, предшествующий «плохому»:

$ git rebase -i 0023cddd --autosquash

А вторым ингредиентом нашего секретного соуса выступает флаг --autosquash. Он позволяет не вносить дополнительных правок в открывшемся окне редактора. Внимательно посмотрите на скриншот:

Корректирующий коммит помечен как «fixup» и размещен в правильном порядке
Корректирующий коммит помечен как «fixup» и размещен в правильном порядке

Git автоматически сделал две вещи:

  1. Он пометил новый коммит как fixup.

  2. И переупорядочил строки так, чтобы fixup-коммит оказался непосредственно под «плохим» коммитом. Дело в том, что fixup работает в точности как squash: он объединяет выделенный коммит с коммитом выше.

Таким образом, нам ничего делать не надо. Сохраните изменения и закройте окно редактора.

Давайте еще раз взглянем на историю коммитов:

Счастливый финал!
Счастливый финал!

Мало того, что оригинальный коммит теперь содержит правки из вспомогательного, но и некрасивый вспомогательный коммит (с исправлениями) исчез из истории. Все красиво, словно никогда и не было никаких проблем!

Откройте для себя возможности interactive rebase

Существует множество различных вариантов использования интерактивного rebase’а: большинство из них связаны с исправлением ошибок. Подробнее узнать о других способах можно в бесплатном (англоязычном) курсе «First Aid Kit for Git» — коллекции коротких видео (по 2-3 минуты на эпизод).

Примечание оригинального редактора: забавно, но мне пришлось воспользоваться interactive rebase при редактировании этой статьи! Один из коммитов включал изображение, размер которого превышал 1 Мб (что противоречит правилам сайта GitLab). Пришлось вернуться к этому коммиту и включить в него изображение подходящего размера. Спасибо за урок, Вселенная! ?

P.S. от переводчика

Читайте также в нашем блоге:

Флант
DevOps-as-a-Service, Kubernetes, обслуживание 24×7

Похожие публикации

Комментарии 44

    +1

    Нужно только добавить замечание, что все это годится только, если история локальная или в приватном репо. То, что мы опубликовали наружу — все, мы не имеем права изменять (потому что кто-то на том же гитхаб, например, мог сделать форк и при переписывании истории все ломается).

      +4
      Да, этому посвящен целый абзац в самом начале.
        0
        Ну этот вопрос в туториале стоило бы разжевать подробнее, важный момент же.

        Стоило бы разжевать, почему этого делать не стоит (реально не стоит?) и что получится, как избежать проблем.

        В дополнение статье:

        Просто так залить свою исправленную историю коммитов не получится. Сервер не примет. Решается использованием git push -f (force push). Тогда часть истории на сервере будет переписана. Но очень сильно и неприятно удивятся прочие разработчики при попытке сделать git pull, т.к. их локальная история очень сильно расходится с серверной. Тоже решаемо локально, но это потеря времени.

        Таким образом, переписывать историю на сервере можно, если осторожно. Т.е., например, если разработчик один и никто больше репозиторий локально не имеет. Или в своей личной ветке, которую никто не использует. Или если сделать это очень быстро, пока другие разработчики, использующие ту же ветку, не обновились (т.е. не имеют локально оргинальных версии исправляемых коммитов). Или таки делать это после общения с коллегами, когда все пришли к выводу, что так лучше всего, и расхождение истории после git pull не станет для них неожиданным сюрпризом.
          +2
          Это же перевод статьи, которая написана простым языком в основном для пользователей начального уровня. Поэтому в ней сразу сделана оговорка, что в первую очередь это следует применять на локальной истории. Ваше дополнение правильное и полезное, но выходит за рамки этой статьи.
            0

            Про "сделать очень быстро, пока другие не заметили" — надо понимать, что этими самыми "другими" может быть, например, хук-скрипт, который зеркалит ветку репозитория или его весь целиком на другой ресурс. Например, команда живёт на гитлабе, а зеркало — на гитхабе, куда все коммиты из мастера зеркалит CI-скрипт. В этом случае "пока другие не заметили" уже не прокатит. Мы однажды с таким столкнулись и обнаружили, что на гитхабе уже не клоны коммитов, а мерж-коммиты (история разделилась, FF-слияние перестало работать)

              +1
              Я бы сказал, что это просто недоработка зеркалирующего скрипта. Чтобы учесть возможность изменения истории, нужно пушить как-то так:
              git push -f mirror origin/branchname:branchname
                +1

                ну, да, наверное, но при этом мы еще мину замедленного действия под себя закладываем.

                  0
                  Какую мину, например?
                    0

                    Если случайно ветки разъедутся и некто перепишет локальную историю — она без вопросов уедет на Удаленный сервер. Что в случае публичного репо ломает всю модель работы любых коллабораторов

                      0
                      Ломает или нет — зависит, очевидно, от модели работы. Коллабораторы вполне могут работать и с переписыванием истории, если они к ней готовы.
                        0

                        Как правило — это не так. По крайней мере для публичных или полупубличных проектов.

                  0

                  Не-не, тогда результат push -f окажется в паблике. Так в паблике оказалась лишь небольшая цепочка мерж-коммитов, это не то, чтобы хорошо, но гораздо лучше переписывания истории (предположим, у кого-то тоже скрипт, который сделит за пабликом, пуллит оттуда коммиты себе в локальную копию и собирает/релизит. А тут вдруг мы сделали push -f… ) Мы просто сделали у себя обратный мерж — и всё починилось.

                    0
                    Так в паблике оказалась лишь небольшая цепочка мерж-коммитов, это не то, чтобы хорошо, но гораздо лучше переписывания истории
                    Выскажу непопулярное (наверное) мнение, что нет ничего страшного в переписывании истории в определенных ветках, если все заинтересованные пользователи о таких ветках осведомлены и умеют с ним работать.
                    кого-то тоже скрипт, который сделит за пабликом, пуллит оттуда коммиты себе в локальную копию
                    Если вы о команде git pull, от использования ее нужно отучать неопытных пользователей гита гораздо раньше, чем от переписывания истории.
                      0
                      Выскажу непопулярное (наверное) мнение, что нет ничего страшного в переписывании истории в определенных ветках, если все заинтересованные пользователи о таких ветках осведомлены и умеют с ним работать.

                      Ну вот в данном конкретном случае это не так. Пользователей — неопределённый "широкий круг", которым незачем эти странные "осведомления". А "определённая ветка" — это, внезапно, мастер.


                      Чуть детальнее — проект на гитлабе, где мы вольны делать с ветками что угодно (и push -f вполне нормальный рабочий инструмент; вся ветка живёт обычно ради автоматизированного тестирования). Это внутренняя кухня, которая никого снаружи не касается. А вот ветка master — публичная. Из закрытого проекта на гитлабе она зеркалится в открытый проект на гитхабе, где её все видят как одну из немногих веток проекта. Поэтому на мастер-ветку у нас действуют особые правила — там никогда ничего не переписываем, никаких push -f, никаких commit --amend. Синхронизация делается скриптом CI который как раз делает git pull/git push, и ещё дополнительно проверяет ветку по имени, чтобы не опубликовать случайно какую-то другую. Придумывать что-то сложнее надобности нет, покуда за несколько лет работы был лишь единственный инцидент с джуном, который сделал --amend.


                      И да, есть отдельная процедура для зеркалирования "в обратную сторону" — на случай приёма pr с гитхаба. Но там проще, покуда целевым репозиторием гитлаба пользуются только "осведомлённые пользователи".

            +3

            Скорее все что попало в master изменять не следует. Фиче ветки вполне можно. Хранить ветку исключительно на локальный машине чревато потерей всей работы при сбое накопителя. Лично терял код, над которым работал 2 недели. Больше не хочу.


            Но даже частичный ребейз мастера можно разрулить без слёз с использованием fork-point.

              0

              fork-point? не слышал ) Но это и неудивительно — git слишком мощный инструмент


              Хранить ветку исключительно на локальный машине чревато потерей всей работы при сбое накопителя

              несомненно. Но никто не мешает отгружать код в свой личный форк. А потом из него уже лить в основной репозиторий. Это на самом деле решает очень много проблем. Если подытожить есть принципиально несколько моделей разработки:


              1. вся разработка в одном репе. Есть выделенные ветки вроде master/dev, куда льются изменения и которые общие. Остальные ветки — фиче-ветки, над которыми работают обычно индивидуальные разрабы. Касательно того как запретить создавать из фиче-ветки новую фиче-ветку — вообще без идей.
              2. есть основная репа с каким-то набором долгоживущих веток (master/dev, может по версиям ПО). При необходимости доработок — разработчик форкает репо и разрабатывает в нем. Как только работа завершена — вливает все изменения в исходный репо через MR/PR. Заодно такой подход избавляет от ада с CI/CD — у каждого он получается свой ) и сломать его нереально физически. Ну, и посмотреть "секретные" переменные не получится разрабу в основном репо. На самом деле гитлаб очень подталкивает к такой модели, потому что в нем только репозитории являются 1st class сущностями… На каждый чих — по новому репо.
                +3
                Касательно того как запретить создавать из фиче-ветки новую фиче-ветку — вообще без идей.

                А это и не нужно — часто бывает ситуация, когда фича в процессе ревью, а нужно начать работать над новой фичей с использованием измненений из прошлой. При необходимости новую ветку можно потом зарибейзить на main/dev.

                +2
                Лично терял код, над которым работал 2 недели.

                Стоит добавить, что покуда не сработал git gc, потерянные коммиты можно найти в git reflog

                  –1

                  reflog не спасет от ssd trim к сожалению

                    +1

                    Эм, ну, я даже не знаю, что сказать. Это же gc совершенно разного уровня. И трим не ломает же поведение рефлога…

                      0

                      Это я про свою ситуацию. trim swap файла на расположенного на f2fs порушил всю файловую систему.

              +2
              А есть вариант в процессе rebase разделить один commit на несколько? Частая задача, перед merge в мастер — прибираюсь в свой ветке и вижу слишком большой commit, который можно было бы разделить на несколько, но в момент когда он создавался я почему-то это не сделал.
                0
                А есть вариант в процессе rebase разделить один commit на несколько?

                в целом — да.

                  0
                  Можно.
                  1. Всё тот же git rebase -i
                  2. В редакторе у нужного коммита ставите e (edit)
                  3. Ребейз остановится на этом коммите.
                  4. Делаете ваши чёрные дела и продолжаете rebase (git rebase --continue).
                    +1
                    Делаете ваши чёрные дела

                    А именно: делаете reset, создаете из получившихся изменений сколь угодно много коммитов и продолжаете ребейз.

                      0
                      Спасибо, именно этой инфы мне и не доставало)
                        0

                        более того, после reset можно выполнить git add --patch и построчно выбирать, что включить в следующий коммит

                      +1

                      Два варианта.


                      1. Интерактивный ребейс, ставим буковку e (edit) на тот коммит, который хотим поделить. Продолжаем ребейс, он останавливается на этом коммите. Там делаем reset HEAD~ — и оказываемся на родительском коммите, но со всеми изменениями в дереве. Дальше можно интерактивным коммитом (или пачкой таких коммитов) коммитать изменения одно за другим. По окончании процесса rebase --continue — и оставшиеся изменения (если такие остались) будут зафиксированы в исходном коммите, который делили.

                      Недостаток — эти частичные коммиты сложно тестировать. В дереве есть ВСЕ изменения, поэтому если вы, например, закоммитали отдельно работу с новой переменной и забыли туда же закоммитать её объявление/определение, это обнаружится не сразу.


                      1. Интерактивный ребейс, ставим буковку e (edit) на родительский коммит к тому, который хотим поделить. Продолжаем ребейс. Он останавливается на запрошенном коммите. Дальше сравниваем его со следующим и берём выбранные изменения. В терминах сред IDE Intelij — это "compare with local" — где открывается список различающихся файлов, каждый из которых можно ткнуть и получить двухсторонний diff, а потом копировать изменения из сравниваемого файла (изменения из которого надо поделить) с текущим. Здесь всё легче, покуда рабочее состояние файлов — это родительский коммит + некоторые (выбранные) изменения из следующего. Можно попробовать собрать, прогнать тесты — и если всё ок, коммитать, потом вносить следующую пачку изменений и т.д. Так же можно сравнивать не только со следующим, но с любым другим (например, последние (условно) 42 коммита были на тему "попробовал… не получилось… откатил… снова попробовал… вот она, окончательная версия". Можно начать ребейс от старта этого челленджа, и творить аккуратную историю, сравнивая с конечным коммитом, где уже "всё хорошо". Так промежуточные пробные эксперименты вообще никак не будут видимы, можно будет составить последовательность коммитов, которые прямо и целенаправленно реализуют конечную фичу.

                      В этом случае можно в процессе добавления промежуточных коммитов пушить результат на сервер, где гоняется CI, и быть уверенным, что все тесты благополучно проходятся.


                      Если довести такой ребейс до конца, так что ничего важного вообще не осталось (кроме, может быть, пары-тройки неудачных пробелов/табуляций) — после окончательного rebase --continue можно оставшуюся цепочку слить любым ненапряжным способом (accept-ours, accept-others и т.д.) — и потом просто выкинуть. Но тут лучше вообще делать не интерактивный rebase, а изначально ставить тэг на "окончательный" конец ветки, после чего делать reset hard на родительский коммит к первым изменениям и дальше вот так через compare with local сравнивать конечную цель с текущим состоянием кода. Тогда потом без всяких ребейсов удаляем тэг и всё.

                        0
                        закоммитали отдельно работу с новой переменной и забыли туда же закоммитать её объявление/определение, это обнаружится не сразу.
                        В этом случае можно в процессе добавления промежуточных коммитов пушить результат на сервер, где гоняется CI, и быть уверенным, что все тесты благополучно проходятся.

                        А можно локально прогнать каждый коммит потом через rebase --exec

                      –3

                      Огромный мануал для операции, которая совершенно не нужна

                        +3

                        Аргументируйте, пожалуйста — почему эта операция не нужна? Или нужна, но не через git rebase?

                            0

                            ну, и что это доказывает, что при работе с исходным кодом надо включать голову? Указанные примеры не доказывают, что нельзя (вообще, в принципе) использовать тот же rebase, например, для влития изолированного функционала. Очень даже можно.
                            Ну, и не рассмотрен пример — как ловить эти проблемы при помощи пайплайнов тестов...

                              0

                              Конечно можно, но не нужно. Лучше силы потратить на более полезные вещи.

                          0

                          Ваш тезис непонятен: вы считаете, что поддерживать чистоту коммитов — это не важно или имеете знание, как проделать подобные операции по расчистке коммитов без rebase? Если второе, то поделитесь инфой, хотя бы общим намёком, куда копать.

                              0

                              Ребейз переписывает историю. Для достижения результата (рабочий код который зарабатывает деньги) переписывание истории не нужная операция.

                                0
                                Для достижения результата (рабочий код который зарабатывает деньги) переписывание истории не нужная операция.

                                это можно переформулировать и так — "для достижения результата — git вообще не нужен" или "вообще без разницы — чем там пользуются разработчики, лишь бы приносили деньги, но пускай они будут довольны"

                                  0

                                  Нет, гит как раз таки нужен для совместной работы. А вот ребейз нужен тем кому очень важен процесс но не важен результат.

                                    0

                                    Ну, не гит как таковой, а средства коллаборации. Те же системы аналоги — svn, mercurial. Пока ещё не сдохли. Где-то встречаю.

                            0

                            Несмотря на регалии автора статья явно для новичкового уровня (я вообще был бы крайне признателен авторам/переводчикам, чтобы они это указывали в начале статьи, я бы лучше планировал время на прочтение) и разве кое-что интересное всплыло в комментариях про зеркалирование и форк-репозитории.


                            Возможно, у автора статьи найдутся более интересные и сложные темы, которые стоит перевести?

                              +1
                              А вы не читали примечание от переводчика в самом начале этой статьи?

                              В ней просто и наглядно рассказывается об основных возможностях интерактивного rebase'а, что может стать отличным введением для тех, кто только начинает им пользоваться.
                                0

                                Ради справедливости, но не флейма — эта приписка ничего не означает. Я видел материалы, которые просто объясняют сложные вещи, но при этом не были нацелены на новичков ) и наоборот )

                                  +1

                                  Ох, блин… вот что значит "издержки скорочтения"!.. Да, вы правы: я пропустил эту ремарку, спасибо что подсказали. Предыдущий мой комментарий уже не удалить, поэтому прошу считать неактуальным в части "было бы хорошо предупреждать". В части "переводить более сложные статьи" по-прежнему предлагаю переводчикам переводить не только популярные темы (увы, количество плюсиков на сложных темах меньше).

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                              Самое читаемое