Git для автоматизатора
Я сделал два коммита в своей ветке - d23305e и 308f35e. Потом выполнил rebase на свежий main. Изменения те же, содержимое то же, а коммиты теперь 8943f7f и adcedfc. Старых больше нет: rebase не двигает коммиты, он создаёт новые. Из этого одного наблюдения следует всё правило безопасности.
Стержень: rebase применяют только к своим неопубликованным веткам, а атомарные коммиты с внятными сообщениями - предпосылка и ревью, и поиска виновника.
// Формулировки: «чем merge отличается от rebase?», «как найти сломавший коммит?», «что кладут в .gitignore?».
Merge и rebase: замер на хэшах
Сначала словарь, без него дальше не разобраться. Коммит - сохранённое состояние проекта с описанием, у каждого есть неизменный номер-отпечаток (хэш) вроде d23305e. Ветка - просто указатель на коммит, от которого растёт твоя работа. main - основная ветка, куда всё вливается.
Теперь замер. У меня два коммита в ветке, а коллега тем временем поправил конфиг в main. Вариант merge: появляется отдельный коммит слияния, история ветвится и видно, что было две линии работы, мои коммиты остаются с прежними номерами. Вариант rebase: мои изменения пересаживаются поверх свежего main, история становится одной прямой линией, но номера коммитов сменились - это уже другие коммиты с тем же содержимым.
// Отсюда правило, и теперь оно очевидно. Пока ветка только моя и никуда не отправлена, переписывать её безопасно. Как только на неё смотрят коллеги, rebase создаст у них расхождение: у них старые коммиты, у меня новые с другими номерами, а git считает их разными. Итог - болезненные конфликты и потерянная работа. Поэтому опубликованную общую ветку не переписывают.
- коммит и его хэш
- сохранённое состояние с неизменным номером-отпечатком
- merge / rebase
- слить с отдельным коммитом слияния / пересадить свои коммиты поверх, создав новые
Конфликт вживую
Конфликт возникает, когда двое поменяли одно и то же место. Вот настоящий вывод: я поднял таймаут с 5 до 30, а коллега в это же время переключил браузер на firefox и добавил работу без окна. Git не стал угадывать и оставил в файле обе версии, разделённые маркерами: сверху между <<<<<<< HEAD и ======= - то, что уже в ветке, снизу до >>>>>>> - то, что приносишь ты.
Разрешить конфликт - значит собрать из двух версий одну правильную руками. Здесь правильный ответ - взять всё: и таймаут 30, и firefox, и работу без окна, потому что правки не противоречат друг другу, просто попали в один участок файла.
// А вот популярная привычка «выберу свою сторону, там точно мои изменения» стоит дорого: в этом примере из файла молча исчезли бы и firefox, и режим без окна - чужая работа за день. Причём тесты после такого пройдут, и заметят пропажу нескоро. После разрешения конфликта прогон тестов обязателен до отправки.
<<<<<<< HEAD
timeout = 5
browser = "firefox"
headless = True
=======
timeout = 30
browser = "chrome"
>>>>>>> mine- маркеры конфликта
- две версии участка в файле, выбрать и собрать нужно вручную
- цена «своей стороны»
- чужие правки исчезают молча, а тесты при этом зелёные
Атомарные коммиты и поиск виновника
Есть команда, ради которой стоит вести историю аккуратно. Я собрал репозиторий из 41 коммита и сломал функцию расчёта на двадцать седьмом. Дальше запустил git bisect - двоичный поиск по истории: он берёт середину, спрашивает «здесь уже сломано?», отбрасывает половину и повторяет. Виновника он нашёл за 4 шага сужения. Перебор по одному потребовал бы до 41 прогона.
Математика простая: каждый шаг делит оставшийся отрезок пополам, поэтому нужно порядка логарифма по основанию два от числа коммитов - для 41 это около пяти прогонов. Причём проверку можно отдать скрипту, и тогда поиск идёт сам.
// Но работает это только на нормальной истории. Если коммиты называются «wip» и «fix2» и каждый содержит по пять разнородных правок, то найденный виновник ничего не объясняет: внутри всё равно надо разбираться руками. Атомарный коммит - одно логическое изменение с человеческим описанием («починил ожидание в тесте оплаты») - это вложение, которое возвращается в первый же красный ночной прогон.
- git bisect
- двоичный поиск сломавшего коммита: около пяти прогонов вместо сорока
- атомарный коммит
- одно логическое изменение и внятное описание
Что не должно попасть в репозиторий
Проверил на живом примере. Положил в файл токен, закоммитил. Следующим коммитом убрал его и подставил чтение из переменной окружения - настройки, которую задают снаружи программы, не храня в коде. В рабочем файле чисто. А потом одной командой достал этот токен из старого коммита: он никуда не делся и лежит в истории у каждого, кто склонировал репозиторий, то есть скачал себе проект вместе со всей его историей.
Отсюда правило: секрет, однажды попавший в историю, считается скомпрометированным навсегда. Его не прячут, а отзывают и выпускают новый. Переписывание истории тут помогает слабо: копии уже разошлись по машинам и серверам сборки.
// Поэтому файл .gitignore - список того, что git игнорирует - заводят до первого коммита. Туда идут результаты прогонов, отчёты, скриншоты, локальные настройки и всё, что содержит пароли. Заодно это лечит распухший репозиторий: видео упавших тестов, случайно попавшие в историю, останутся в ней навсегда, даже если удалить файлы.
- .gitignore
- список того, что не попадает в репозиторий: артефакты, локальные настройки, секреты
- утёкший секрет
- достаётся из старого коммита одной командой; лечится только заменой
Как отвечать: «Чем merge отличается от rebase и какое правило?»
Оба приносят изменения из main в мою ветку, но по-разному. Merge создаёт отдельный коммит слияния и сохраняет фактическую историю: видно, что была ветка и когда её влили, мои коммиты остаются прежними. Rebase пересаживает мои изменения поверх свежего main, история становится прямой линией - но это уже другие коммиты. Я проверял специально: до rebase мои коммиты были с одними номерами, после - с другими, при том же содержимом. Rebase не двигает коммиты, он создаёт новые взамен старых. Отсюда правило: rebase делаю только со своей веткой, пока она не опубликована. Если переписать ветку, с которой уже работают коллеги, у них останутся старые коммиты, у меня новые, git будет считать их разными - и получатся тяжёлые конфликты и потерянная работа. На практике я держу свою ветку свежей через rebase на main, пока она моя, а вливаю через пул-реквест - запрос на вливание, который перед слиянием читают коллеги. И конфликты разбираю руками, понимая обе стороны: привычка «оставлю свою сторону» однажды молча снесла бы чужую работу за день, причём тесты после этого были бы зелёными.
Почему это сильный ответ: разница объяснена через наблюдаемый факт (номера коммитов меняются), из него выведено правило с механикой поломки, и добавлена практика работы с конфликтами.
На чём валят
- −Работать прямо в main: твоя недописанная правка красит прогон всей команде.
- −Сделать rebase уже опубликованной общей ветки - у команды разъезжается история.
- −Разрешать конфликт «оставлю свою сторону» не глядя - чужая работа исчезает, а тесты зелёные.
- −Коммиты «wip» и «fix2»: двоичный поиск найдёт коммит, который ничего не объясняет.
- −Закоммитить токен: он достаётся из старого коммита одной командой, менять надо сам токен.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 13, остальные разбираются в тренажёре.
- Зачем в Git нужны ветки (branches)?A)Вести изменения изолированно от основной ветки и влить их позжеB)Хранить резервную копию репозитория на случай потери основной веткиC)Ускорять клонирование репозитория за счёт разбиения его историиD)Разделять доступ к коду между разработчиками по их правам
показать ответ и разбор
+A)Вести изменения изолированно от основной ветки и влить их позже// разбор: Ветка — независимая линия разработки: можно писать фичу или чинить баг, не трогая стабильную основную ветку (main). Пока ветка отдельна, её изменения не влияют на других; готовое вливают обратно через merge или pull request. Это основа командной работы и code review. Автотесты обычно пишут в feature-ветке, гоняют в CI и вливают только зелёными. Ветка — не бэкап и не механизм прав доступа.
- Чем git commit отличается от git push?A)Commit отправляет изменения на сервер, а push фиксирует их локальноB)Это синонимы: оба сохраняют изменения в удалённом репозиторииC)commit фиксирует изменения в локальной истории, push отправляет их на серверD)Commit сохраняет только один файл, а push отправляет сразу все изменённые файлы проекта
показать ответ и разбор
+C)commit фиксирует изменения в локальной истории, push отправляет их на сервер// разбор: git commit фиксирует подготовленные изменения снимком в локальной истории репозитория — на сервере их ещё нет. git push отправляет накопленные локальные коммиты в удалённый репозиторий (origin), делая их видимыми команде. Можно сделать несколько коммитов и один push. Частая ошибка новичка: закоммитил и думает, что коллеги уже видят правки; пока не сделан push, изменения остаются только у него.
- Чем merge отличается от rebase при вливании изменений?A)Merge стирает историю исходной ветки, а rebase сохраняет её полностью без измененийB)Merge работает только локально, а rebase — только на удалённом сервереC)Это одна команда с разными флагами, итоговый результат истории идентиченD)merge склеивает ветки коммитом слияния, rebase переносит коммиты поверх целевой
показать ответ и разбор
+D)merge склеивает ветки коммитом слияния, rebase переносит коммиты поверх целевой// разбор: merge объединяет две ветки, создавая коммит слияния, — история ветвления остаётся видна как есть. rebase переносит коммиты ветки поверх вершины целевой, пересоздавая их и выстраивая историю в линию, будто работа шла последовательно. merge безопаснее и сохраняет контекст ветвления; rebase даёт чистую линейную историю, но переписывает коммиты. Что выбрать — вопрос принятого в команде workflow, а не правильности.
- Когда при слиянии веток возникает конфликт (merge conflict)?A)Когда в одной из веток остались незакоммиченные изменения в рабочей копииB)Когда обе ветки изменили одни и те же строки файла по-разномуC)Когда ветки создали разные разработчики с разными правами доступаD)Когда в целевой ветке оказалось больше коммитов, чем в исходной
показать ответ и разбор
+B)Когда обе ветки изменили одни и те же строки файла по-разному// разбор: Git сливает ветки автоматически, пока их изменения не пересекаются. Конфликт возникает, когда обе ветки правили одни и те же строки одного файла (или один удалил файл, а другой его изменил) — Git не может решить, чью версию брать, и просит разрешить вручную. Автоматизатор ловит это, когда его feature-ветка отстала от main и правки задели те же места. Разрешение — выбрать или объединить строки и закоммитить результат.
- Зачем изменения вливают через pull request, а не пушат прямо в main?A)Чтобы код прошёл ревью и проверки CI до попадания в основную веткуB)Чтобы Git автоматически исправил конфликты слияния перед вливанием ветокC)Потому что прямой push в main технически не выходит в репозиторииD)Чтобы ускорить слияние, намеренно пропустив запуск автотестов на изменениях
показать ответ и разбор
+A)Чтобы код прошёл ревью и проверки CI до попадания в основную ветку// разбор: Pull request — точка контроля перед вливанием в main: коллеги делают code review, а CI автоматически прогоняет линтеры и автотесты на изменениях. Ветку вливают, только когда ревью пройдено и проверки зелёные. Это ловит баги и регрессии до того, как они попадут в основную ветку и к пользователям. Прямой push в main обходит и ревью, и защитные проверки, поэтому его обычно запрещают правилами ветки (branch protection).
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.