Git на практике: rebase, merge, транк
Собрал на этой машине настоящий репозиторий и проверил пару вещей руками. Аня влилась в общую ветку за день, тронув одну строку в файле на сорок строк, пока коллега правил другую: слилось само, конфликтов ноль. Аня же, просидевшая в своей ветке две недели (десять её коммитов против семи чужих), получила четыре конфликтующих участка, и каждый пришлось разбирать руками.
Стержень: git спрашивают не про команды, а про последствия - что станет с историей и что увидят коллеги.
// Формулировки: «rebase или merge?», «зачем короткие ветки?», «закоммитили ключ доступа, действия?»
Merge сохраняет историю, rebase её переписывает
Коммит - это снимок состояния файлов плюс ссылка на предыдущий коммит. Имя коммита - хэш, короткая строка, посчитанная по его содержимому и по этой ссылке. Отсюда следует главное: изменилась хоть одна буква в цепочке - у коммита новое имя, и для git это уже другой коммит.
Слияние (merge) берёт две ветки и добавляет коммит, у которого два предка. История становится ветвистой, зато всё, что было, осталось на месте. Перенос (rebase) поступает иначе: он берёт коммиты ветки и накладывает их поверх свежей базы заново. Проверил на стенде: до переноса коммиты ветки назывались одними именами, после - другими, хотя содержимое не поменялось ни на байт. Вот это и есть всё правило целиком.
// Отсюда граница применимости: свою ещё не опубликованную ветку перебазировать можно и приятно, общую - нет. И раздельно от этого стоит выбор про склейку: слить ветку одним коммитом (squash) удобно для чтения истории, но промежуточные шаги пропадают, и потом искать поломку по истории становится грубее - шаг поиска становится крупным.
- коммит
- снимок файлов плюс ссылка на предыдущий коммит; имя - хэш содержимого
- слияние
- коммит с двумя предками; обе истории сохраняются
- перенос
- наложение коммитов ветки поверх новой базы; имена коммитов меняются
Что видят коллеги после переписывания общей ветки
Поставил опыт, который стоит увидеть один раз. Два клона в одной точке, коммит с именем 801e32f. Я поправил у последнего коммита ТОЛЬКО текст сообщения и отправил силой в общий репозиторий - у меня стало d5b2fc5. У коллеги остался 801e32f, и его клон разошёлся с общим: один коммит есть у него и нет в общем, один есть в общем и нет у него.
Дальше он делает обычное обновление, и git послушно сливает две ветки. Итог: в его истории теперь ЧЕТЫРЕ коммита вместо двух - первый, старая версия второго, переписанная версия второго и коммит слияния. Одно и то же изменение лежит в истории дважды. А если он отправит это обратно, старый коммит вернётся всем.
// Поэтому переписывание опубликованной истории - это не вопрос вкуса. Правило простое: общую ветку не переписываем, а на важные ветки ставим запрет на такую отправку прямо в настройках репозитория. Если переписать всё-таки пришлось (например, вычищали секрет), об этом предупреждают команду отдельно и просят обновиться особым образом, а не обычным способом.
- отправка силой
- замена истории в общем репозитории на свою
- расхождение веток
- у сторон есть коммиты, которых нет друг у друга
Секрет в истории и цена долгой ветки
Проверил и это. Положил в репозиторий файл с ключом, следующим коммитом удалил его. В рабочем каталоге файла нет: «No such file or directory». А в истории он лежит целиком - я достал его содержимое одной командой по имени того самого коммита. Более того, он находится перебором всех объектов репозитория: в моём крошечном примере их семь, и один из них - тот самый файл.
Значит, удаление новым коммитом убирает файл из последнего среза, а не из истории. Он остаётся в клонах у коллег (клон - полная копия репозитория вместе со всей историей), в кэшах сборки, в зеркалах. Публичные репозитории обходят сканеры за минуты. Отсюда жёсткий порядок действий: сначала отозвать и перевыпустить ключ, и только потом заниматься уборкой истории.
// Вторая цифра из моего стенда - про длину ветки. Однодневная ветка слилась без единого конфликта. Двухнедельная дала четыре конфликтующих участка: за это время общая ветка уехала на семь коммитов, и правки пересеклись по четырём строкам из сорока. Дальше эта зависимость только хуже: расхождение растёт, а разбирать его придётся тому, кто хуже всех помнит чужие правки.
- объект репозитория
- содержимое файла, каталога или коммита; хранится по своему хэшу
- короткие ветки
- вливаться в общую ветку за день-два, пока расхождение мало
- фича-флаг
- переключатель, за которым прячут недоделанное, чтобы не держать ветку
Как отвечать: «В репозиторий закоммитили ключ доступа. Действия?»
Считаю ключ скомпрометированным и первым делом отзываю его и выпускаю новый. Удаление файла следующим коммитом ничего не решает - я это проверял: файла в рабочем каталоге уже нет, а из истории он достаётся одной командой и находится простым перебором объектов репозитория. Плюс он лежит в клонах у коллег, в кэшах сборки и в зеркалах, а публичные репозитории обходят сканеры за минуты. Уже после ротации чищу историю специальными инструментами и предупреждаю команду, что клоны надо обновить особым образом. И ставлю проверку на секреты в двух местах: в скрипт, который git запускает перед каждым коммитом, и в автоматические проверки на стороне сервера, чтобы это не доезжало до общей ветки в следующий раз.
Ответ даёт правильный порядок: сначала ротация, потом уборка, потом профилактика. Порядок тут и есть проверяемое знание - уборка без ротации бесполезна.
На чём валятся
- −Считают, что удаление файла новым коммитом убирает секрет из репозитория.
- −Чистят историю, но забывают отозвать сам ключ.
- −Переписывают общую ветку и отправляют силой: у коллег появляется задвоенная история.
- −Держат ветки неделями и получают конфликты там, где их могло не быть вовсе.
- −Заводят переключатели для недоделанного и никогда их не убирают.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 6, остальные разбираются в тренажёре.
- Чем rebase отличается от merge при обновлении ветки от main?A)Rebase переносит коммиты поверх новой базыB)Rebase удаляет коммиты ветки, оставляя только последнийC)Rebase применим лишь к ветке без конфликтов с основнойD)Rebase сохраняет хэши коммитов, merge их пересчитывает
показать ответ и разбор
+A)Rebase переносит коммиты поверх новой базы// разбор: Merge оставляет обе истории и добавляет коммит слияния — история точная, но ветвистая. Rebase переписывает коммиты ветки поверх свежего main: содержимое то же, хэши новые, история линейная. Отсюда правило: перебазировать можно свою ветку до публикации, а общую переписывать больно — у коллег останется старая версия тех же коммитов.
- Команда релизится несколько раз в день. Почему при этом уходят от долгих фича-веток к транку?A)Долгие ветки не проверить в CI до слиянияB)Короткие ветки дают меньше конфликтов, а незрелое прячут за флагамиC)Транк экономит место в репозитории и ускоряет клонированиеD)Долгие ветки мешают ставить теги и вести семантическое версионирование
показать ответ и разбор
+B)Короткие ветки дают меньше конфликтов, а незрелое прячут за флагами// разбор: Чем дольше ветка живёт отдельно, тем больше расхождение и больнее слияние: конфликты, «а у нас всё работало», поздняя интеграция. Транк-подход требует, чтобы в main постоянно было пригодное к релизу состояние: мелкие вливания по нескольку раз в день, а недоделанное скрыто фича-флагами. Релиз перестаёт быть событием и превращается в рутину.
- Ключ доступа случайно закоммитили и удалили следующим коммитом. Что делать?A)Достаточно удаления: в рабочем дереве ключа больше нетB)Сделать force-push после удаления файла — история подчистится самаC)Считать ключ скомпрометированным и отозвать, потом чистить историюD)Закрыть репозиторий от внешних: во внутреннем это не считается утечкой
показать ответ и разбор
+C)Считать ключ скомпрометированным и отозвать, потом чистить историю// разбор: Коммит с ключом остаётся объектом в истории, в клонах у коллег, в кэшах CI и зеркалах, а публичный репозиторий за минуты обходят сканеры. Порядок такой: сначала отозвать и перевыпустить ключ, потом переписать историю (filter-repo или BFG) и попросить всех перевыпустить клоны. Обратный порядок бесполезен: пока ключ жив, он уже утёк.
- Разработчик сделал git push --force в общую ветку, с которой работает команда. Что случилось?A)Ничего страшного: force просто ускоряет обычный пушB)Он переписал общую историю; у коллег ветка разъехаласьC)Коммиты коллег автоматически смёржатся в новую историюD)Ветка станет защищённой от дальнейших изменений
показать ответ и разбор
+B)Он переписал общую историю; у коллег ветка разъехалась// разбор: git push --force затирает удалённую историю своей локальной. В общей ветке это ломает всех: у коллег их коммиты остаются на старой (теперь висячей) истории, пуши отвергаются, начинается ручное восстановление. Правило: не форсить общие ветки; если очень надо — --force-with-lease (упадёт, если кто-то успел запушить), а общие ветки защищать от force на уровне репозитория.
- Плохой коммит уже запушен в общую ветку. Как отменить его, не ломая историю остальным?A)git revert: новый коммит, отменяющий измененияB)git reset --hard на прошлый коммит и force-pushC)Удалить ветку и создать заново от нужного местаD)git commit --amend поверх плохого коммита
показать ответ и разбор
+A)git revert: новый коммит, отменяющий изменения// разбор: Для отмены уже опубликованного коммита берут git revert: он создаёт НОВЫЙ коммит, отменяющий изменения, не трогая существующую историю — безопасно для всех, кто уже подтянул ветку. reset --hard + force и amend переписывают историю и годятся только для локальных/непушнутых коммитов. На общих ветках правило простое: отменять revert'ом.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.