сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Автоматизация тестирования

Git для автоматизатора

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, остальные разбираются в тренажёре.

  1. #vcs_git1 / 5
    Зачем в Git нужны ветки (branches)?
    A)Вести изменения изолированно от основной ветки и влить их позже
    B)Хранить резервную копию репозитория на случай потери основной ветки
    C)Ускорять клонирование репозитория за счёт разбиения его истории
    D)Разделять доступ к коду между разработчиками по их правам
    показать ответ и разбор
    +A)Вести изменения изолированно от основной ветки и влить их позже

    // разбор: Ветка — независимая линия разработки: можно писать фичу или чинить баг, не трогая стабильную основную ветку (main). Пока ветка отдельна, её изменения не влияют на других; готовое вливают обратно через merge или pull request. Это основа командной работы и code review. Автотесты обычно пишут в feature-ветке, гоняют в CI и вливают только зелёными. Ветка — не бэкап и не механизм прав доступа.

  2. #vcs_git2 / 5
    Чем git commit отличается от git push?
    A)Commit отправляет изменения на сервер, а push фиксирует их локально
    B)Это синонимы: оба сохраняют изменения в удалённом репозитории
    C)commit фиксирует изменения в локальной истории, push отправляет их на сервер
    D)Commit сохраняет только один файл, а push отправляет сразу все изменённые файлы проекта
    показать ответ и разбор
    +C)commit фиксирует изменения в локальной истории, push отправляет их на сервер

    // разбор: git commit фиксирует подготовленные изменения снимком в локальной истории репозитория — на сервере их ещё нет. git push отправляет накопленные локальные коммиты в удалённый репозиторий (origin), делая их видимыми команде. Можно сделать несколько коммитов и один push. Частая ошибка новичка: закоммитил и думает, что коллеги уже видят правки; пока не сделан push, изменения остаются только у него.

  3. #vcs_git3 / 5
    Чем merge отличается от rebase при вливании изменений?
    A)Merge стирает историю исходной ветки, а rebase сохраняет её полностью без изменений
    B)Merge работает только локально, а rebase — только на удалённом сервере
    C)Это одна команда с разными флагами, итоговый результат истории идентичен
    D)merge склеивает ветки коммитом слияния, rebase переносит коммиты поверх целевой
    показать ответ и разбор
    +D)merge склеивает ветки коммитом слияния, rebase переносит коммиты поверх целевой

    // разбор: merge объединяет две ветки, создавая коммит слияния, — история ветвления остаётся видна как есть. rebase переносит коммиты ветки поверх вершины целевой, пересоздавая их и выстраивая историю в линию, будто работа шла последовательно. merge безопаснее и сохраняет контекст ветвления; rebase даёт чистую линейную историю, но переписывает коммиты. Что выбрать — вопрос принятого в команде workflow, а не правильности.

  4. #vcs_git4 / 5
    Когда при слиянии веток возникает конфликт (merge conflict)?
    A)Когда в одной из веток остались незакоммиченные изменения в рабочей копии
    B)Когда обе ветки изменили одни и те же строки файла по-разному
    C)Когда ветки создали разные разработчики с разными правами доступа
    D)Когда в целевой ветке оказалось больше коммитов, чем в исходной
    показать ответ и разбор
    +B)Когда обе ветки изменили одни и те же строки файла по-разному

    // разбор: Git сливает ветки автоматически, пока их изменения не пересекаются. Конфликт возникает, когда обе ветки правили одни и те же строки одного файла (или один удалил файл, а другой его изменил) — Git не может решить, чью версию брать, и просит разрешить вручную. Автоматизатор ловит это, когда его feature-ветка отстала от main и правки задели те же места. Разрешение — выбрать или объединить строки и закоммитить результат.

  5. #vcs_git5 / 5
    Зачем изменения вливают через pull request, а не пушат прямо в main?
    A)Чтобы код прошёл ревью и проверки CI до попадания в основную ветку
    B)Чтобы Git автоматически исправил конфликты слияния перед вливанием веток
    C)Потому что прямой push в main технически не выходит в репозитории
    D)Чтобы ускорить слияние, намеренно пропустив запуск автотестов на изменениях
    показать ответ и разбор
    +A)Чтобы код прошёл ревью и проверки CI до попадания в основную ветку

    // разбор: Pull request — точка контроля перед вливанием в main: коллеги делают code review, а CI автоматически прогоняет линтеры и автотесты на изменениях. Ветку вливают, только когда ревью пройдено и проверки зелёные. Это ловит баги и регрессии до того, как они попадут в основную ветку и к пользователям. Прямой push в main обходит и ревью, и защитные проверки, поэтому его обычно запрещают правилами ветки (branch protection).

дальше

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

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