сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · CI/CD

Git на практике: rebase, merge, транк

Git на практике команды

Собрал на этой машине настоящий репозиторий и проверил пару вещей руками. Аня влилась в общую ветку за день, тронув одну строку в файле на сорок строк, пока коллега правил другую: слилось само, конфликтов ноль. Аня же, просидевшая в своей ветке две недели (десять её коммитов против семи чужих), получила четыре конфликтующих участка, и каждый пришлось разбирать руками.

Стержень: git спрашивают не про команды, а про последствия - что станет с историей и что увидят коллеги.

// Формулировки: «rebase или merge?», «зачем короткие ветки?», «закоммитили ключ доступа, действия?»

Merge сохраняет историю, rebase её переписывает

Коммит - это снимок состояния файлов плюс ссылка на предыдущий коммит. Имя коммита - хэш, короткая строка, посчитанная по его содержимому и по этой ссылке. Отсюда следует главное: изменилась хоть одна буква в цепочке - у коммита новое имя, и для git это уже другой коммит.

Слияние (merge) берёт две ветки и добавляет коммит, у которого два предка. История становится ветвистой, зато всё, что было, осталось на месте. Перенос (rebase) поступает иначе: он берёт коммиты ветки и накладывает их поверх свежей базы заново. Проверил на стенде: до переноса коммиты ветки назывались одними именами, после - другими, хотя содержимое не поменялось ни на байт. Вот это и есть всё правило целиком.

// Отсюда граница применимости: свою ещё не опубликованную ветку перебазировать можно и приятно, общую - нет. И раздельно от этого стоит выбор про склейку: слить ветку одним коммитом (squash) удобно для чтения истории, но промежуточные шаги пропадают, и потом искать поломку по истории становится грубее - шаг поиска становится крупным.

коммит
снимок файлов плюс ссылка на предыдущий коммит; имя - хэш содержимого
слияние
коммит с двумя предками; обе истории сохраняются
перенос
наложение коммитов ветки поверх новой базы; имена коммитов меняются

Что видят коллеги после переписывания общей ветки

Поставил опыт, который стоит увидеть один раз. Два клона в одной точке, коммит с именем 801e32f. Я поправил у последнего коммита ТОЛЬКО текст сообщения и отправил силой в общий репозиторий - у меня стало d5b2fc5. У коллеги остался 801e32f, и его клон разошёлся с общим: один коммит есть у него и нет в общем, один есть в общем и нет у него.

Дальше он делает обычное обновление, и git послушно сливает две ветки. Итог: в его истории теперь ЧЕТЫРЕ коммита вместо двух - первый, старая версия второго, переписанная версия второго и коммит слияния. Одно и то же изменение лежит в истории дважды. А если он отправит это обратно, старый коммит вернётся всем.

// Поэтому переписывание опубликованной истории - это не вопрос вкуса. Правило простое: общую ветку не переписываем, а на важные ветки ставим запрет на такую отправку прямо в настройках репозитория. Если переписать всё-таки пришлось (например, вычищали секрет), об этом предупреждают команду отдельно и просят обновиться особым образом, а не обычным способом.

отправка силой
замена истории в общем репозитории на свою
расхождение веток
у сторон есть коммиты, которых нет друг у друга

Секрет в истории и цена долгой ветки

Проверил и это. Положил в репозиторий файл с ключом, следующим коммитом удалил его. В рабочем каталоге файла нет: «No such file or directory». А в истории он лежит целиком - я достал его содержимое одной командой по имени того самого коммита. Более того, он находится перебором всех объектов репозитория: в моём крошечном примере их семь, и один из них - тот самый файл.

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

// Вторая цифра из моего стенда - про длину ветки. Однодневная ветка слилась без единого конфликта. Двухнедельная дала четыре конфликтующих участка: за это время общая ветка уехала на семь коммитов, и правки пересеклись по четырём строкам из сорока. Дальше эта зависимость только хуже: расхождение растёт, а разбирать его придётся тому, кто хуже всех помнит чужие правки.

объект репозитория
содержимое файла, каталога или коммита; хранится по своему хэшу
короткие ветки
вливаться в общую ветку за день-два, пока расхождение мало
фича-флаг
переключатель, за которым прячут недоделанное, чтобы не держать ветку

Как отвечать: «В репозиторий закоммитили ключ доступа. Действия?»

Считаю ключ скомпрометированным и первым делом отзываю его и выпускаю новый. Удаление файла следующим коммитом ничего не решает - я это проверял: файла в рабочем каталоге уже нет, а из истории он достаётся одной командой и находится простым перебором объектов репозитория. Плюс он лежит в клонах у коллег, в кэшах сборки и в зеркалах, а публичные репозитории обходят сканеры за минуты. Уже после ротации чищу историю специальными инструментами и предупреждаю команду, что клоны надо обновить особым образом. И ставлю проверку на секреты в двух местах: в скрипт, который git запускает перед каждым коммитом, и в автоматические проверки на стороне сервера, чтобы это не доезжало до общей ветки в следующий раз.

Ответ даёт правильный порядок: сначала ротация, потом уборка, потом профилактика. Порядок тут и есть проверяемое знание - уборка без ротации бесполезна.

На чём валятся

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

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 6, остальные разбираются в тренажёре.

  1. #dvo_git_flow1 / 5
    Чем rebase отличается от merge при обновлении ветки от main?
    A)Rebase переносит коммиты поверх новой базы
    B)Rebase удаляет коммиты ветки, оставляя только последний
    C)Rebase применим лишь к ветке без конфликтов с основной
    D)Rebase сохраняет хэши коммитов, merge их пересчитывает
    показать ответ и разбор
    +A)Rebase переносит коммиты поверх новой базы

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

  2. #dvo_git_flow2 / 5
    Команда релизится несколько раз в день. Почему при этом уходят от долгих фича-веток к транку?
    A)Долгие ветки не проверить в CI до слияния
    B)Короткие ветки дают меньше конфликтов, а незрелое прячут за флагами
    C)Транк экономит место в репозитории и ускоряет клонирование
    D)Долгие ветки мешают ставить теги и вести семантическое версионирование
    показать ответ и разбор
    +B)Короткие ветки дают меньше конфликтов, а незрелое прячут за флагами

    // разбор: Чем дольше ветка живёт отдельно, тем больше расхождение и больнее слияние: конфликты, «а у нас всё работало», поздняя интеграция. Транк-подход требует, чтобы в main постоянно было пригодное к релизу состояние: мелкие вливания по нескольку раз в день, а недоделанное скрыто фича-флагами. Релиз перестаёт быть событием и превращается в рутину.

  3. #dvo_git_flow3 / 5
    Ключ доступа случайно закоммитили и удалили следующим коммитом. Что делать?
    A)Достаточно удаления: в рабочем дереве ключа больше нет
    B)Сделать force-push после удаления файла — история подчистится сама
    C)Считать ключ скомпрометированным и отозвать, потом чистить историю
    D)Закрыть репозиторий от внешних: во внутреннем это не считается утечкой
    показать ответ и разбор
    +C)Считать ключ скомпрометированным и отозвать, потом чистить историю

    // разбор: Коммит с ключом остаётся объектом в истории, в клонах у коллег, в кэшах CI и зеркалах, а публичный репозиторий за минуты обходят сканеры. Порядок такой: сначала отозвать и перевыпустить ключ, потом переписать историю (filter-repo или BFG) и попросить всех перевыпустить клоны. Обратный порядок бесполезен: пока ключ жив, он уже утёк.

  4. #dvo_git_flow4 / 5
    Разработчик сделал git push --force в общую ветку, с которой работает команда. Что случилось?
    A)Ничего страшного: force просто ускоряет обычный пуш
    B)Он переписал общую историю; у коллег ветка разъехалась
    C)Коммиты коллег автоматически смёржатся в новую историю
    D)Ветка станет защищённой от дальнейших изменений
    показать ответ и разбор
    +B)Он переписал общую историю; у коллег ветка разъехалась

    // разбор: git push --force затирает удалённую историю своей локальной. В общей ветке это ломает всех: у коллег их коммиты остаются на старой (теперь висячей) истории, пуши отвергаются, начинается ручное восстановление. Правило: не форсить общие ветки; если очень надо — --force-with-lease (упадёт, если кто-то успел запушить), а общие ветки защищать от force на уровне репозитория.

  5. #dvo_git_flow5 / 5
    Плохой коммит уже запушен в общую ветку. Как отменить его, не ломая историю остальным?
    A)git revert: новый коммит, отменяющий изменения
    B)git reset --hard на прошлый коммит и force-push
    C)Удалить ветку и создать заново от нужного места
    D)git commit --amend поверх плохого коммита
    показать ответ и разбор
    +A)git revert: новый коммит, отменяющий изменения

    // разбор: Для отмены уже опубликованного коммита берут git revert: он создаёт НОВЫЙ коммит, отменяющий изменения, не трогая существующую историю — безопасно для всех, кто уже подтянул ветку. reset --hard + force и amend переписывают историю и годятся только для локальных/непушнутых коммитов. На общих ветках правило простое: отменять revert'ом.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.