Разработчик пишет в задаче: «починил, правка в ветке fix-cart-total, проверь». Вы открываете стенд — поведение прежнее. Полдня уходит на переписку, и выясняется, что ветка ещё не влита: на стенде собрана другая версия кода, и правки там физически нет.
Второй повод разобраться — автотесты: они лежат в том же репозитории, что и код, вместе с тестовыми данными и чек-листами. Даже замена одного селектора — это ветка, коммит и запрос на слияние.
Сверху: ваша ветка ушла от main, а main тем временем уехал вперёд чужим коммитом. Обе правки задели одни и те же строки — Git останавливается и спрашивает, чья версия верна. Дальше два исхода: merge добавляет коммит слияния с двумя родителями и оставляет обе линии видимыми, rebase перекладывает те же правки поверх свежего main — история выпрямляется, но коммиты получаются новые, с другими идентификаторами.
Репозиторий, коммит, ветка
Папки «тесты_финал», «тесты_финал_2», «тесты_финал_правка_Пети» — попытка вести историю руками; через месяц никто не скажет, какая версия рабочая. Git решает это иначе: репозиторий хранит не копии папок, а цепочку снимков проекта.
Один снимок — это коммит: состояние всех файлов плюс автор, время, сообщение и идентификатор (длинную шестнадцатеричную строку обычно пишут первыми семью символами — a1b2c3d). Именно на коммит ссылаются, когда говорят «правка вошла в сборку».
Ветка — передвижная закладка на этой цепочке. Новый коммит сдвигает закладку вперёд. Общая ветка обычно называется main: из неё собирают то, что попадает на стенды. Ваша задача живёт в отдельной ветке, а переключение между ветками меняет файлы прямо в рабочей папке.
День в ветке: взять, поправить, отправить
Чтобы понимать, где сейчас лежит чужая правка, хватает одного потока — у разработчика и у автоматизатора он одинаковый и отличается только содержимым коммита.
git switch main && git pull # свежая общая ветка
git switch -c fix-login-test # своя ветка под задачу
git add tests/login_test.py # отобрали, что войдёт в снимок
git commit -m "test: ждём кнопку, а не фиксированную паузу"
git push -u origin fix-login-test # отправили на сервер
Дальше на сервере открывают запрос на слияние — pull request в GitHub, merge request в GitLab: описание, обсуждение, автопроверки и кнопка слияния. Пока её не нажали, ваша правка есть только в ветке.
Отсюда два следствия для проверок. Первое: проверять надо ту сборку, где правка действительно есть, — и номер сборки идёт в баг-репорт обязательно. Второе: ветка живёт недолго — влили и удалили; долгожитель копит расхождения с main и превращает слияние в отдельную работу.
merge и rebase: два способа свести ветки
«Сделай rebase на main, потом сольём» — фраза, на которой обычно и спотыкаются. Ситуация за ней простая: пока вы правили тест, в main уехали чужие коммиты, и ветки разошлись.
merge сводит их как есть: Git создаёт коммит слияния с двумя родителями, и в истории видно ромб — разошлись, сошлись. Ничего не переписывается.
rebase переносит ваши коммиты поверх свежей верхушки main: история становится прямой, будто вы начали работу сегодня утром. Плата за красоту — это новые коммиты: содержимое то же, идентификаторы другие, старые заменены.
По умолчанию берут merge: он ничего не переписывает, а значит, ошибиться нечем. Rebase уместен в своей ветке, которую больше никто не забирал, — в том числе ежедневный git pull --rebase. Общее правило звучит так: историю, уже отправленную в общую ветку, не переписывают. У коллег, успевших забрать старые коммиты, история разойдётся с вашей; по той же причине push --force в main запрещён почти везде.
Конфликт слияния — вопрос, а не авария
Конфликт — не поломка Git, а честное «я не знаю»: обе ветки поменяли одни и те же строки по-разному. Файлы со спором показывает git status, а внутри файла стоят маркеры:
<<<<<<< HEAD
wait_for(button, timeout=5)
=======
wait_for(button, timeout=15)
>>>>>>> fix-login-test
Сверху — версия ветки, на которой вы стоите (её и обозначает HEAD), снизу — той, которую вливаете. Порядок такой: открыть файл, решить по смыслу задачи, убрать маркеры, git add файл, потом git commit. Передумали на середине — git merge --abort возвращает всё как было.
Главная грабля — решать механически. «Оставить свою» в общем файле с данными или чек-листом затирает чужие проверки, и пропажа всплывает через неделю. Профилактика дешевле разбора: короткие ветки и регулярное подтягивание main делают конфликты редкими и маленькими.
Срочно переключиться: stash
Половина правки в файле, ничего не закоммичено, и тут просят срочно проверить исправление на другой ветке. Переключиться Git даст не всегда: если правка задела файл, который в той ветке выглядит иначе, он остановится, чтобы не затереть работу. А когда даст — незавершённое переедет вместе с вами и рискует попасть в чужой коммит.
git stash # спрятать незавершённое в карман
git switch hotfix-payments # ушли проверять срочное
git switch - # вернулись в свою ветку
git stash pop # достали работу обратно
Stash — карман на полчаса, а не архив: вернулись к задаче — сразу pop. Десяток забытых stash — верный способ потом не вспомнить, что в них лежало.
revert против reset: в общей ветке только revert
Влитая правка сломала стенд, и нужно откатить. Инструмента два, и выбор между ними — это вопрос «видел ли это кто-то ещё».
reset сдвигает закладку ветки назад, то есть переписывает историю: подходит, пока коммиты лежат только на вашей машине (--hard вдобавок выбрасывает изменения — единственное место в Git, где работа теряется по-настоящему). revert ничего не переписывает: он создаёт новый коммит с обратными изменениями. История не теряет ни строчки, поэтому влитое в main откатывают только так.
Для проверок у revert есть последствие: дефект после отката не закрывают. Код вернулся к прежнему состоянию, причина никуда не делась — задачу возвращают в работу, а перепроверять надо сборку, в которую revert уже вошёл.
В какой сборке появилась правка: log и blame
«Раньше работало» в баг-репорте не помогает никому. «На 4.18 работает, на 4.19 нет» сокращает поиск до списка коммитов между двумя сборками — и этот список смотрят прямо из репозитория.
git log --oneline v4.18..v4.19 # что вошло между сборками
git log --oneline -- tests/login_test.py # история одного файла
git blame tests/login_test.py # кто менял каждую строку
blame показывает по каждой строке коммит, автора и дату. Название обманывает: это археология, а не поиск виноватых. Нашли странную строку — идёте в её коммит, сообщение объясняет «зачем», а если не объясняет, вы хотя бы знаете, кого спросить.
Коротко
- Коммит — снимок проекта с автором, временем и идентификатором; ветка — передвижная закладка на цепочке коммитов.
- Пока запрос на слияние не принят, правка есть только в ветке: проверять надо сборку, где она действительно собрана.
- merge оставляет обе линии и добавляет коммит с двумя родителями; rebase перекладывает ваши коммиты поверх
main, создавая новые. - Отправленное в общую ветку не переписывают: там merge и revert, а reset, rebase и force-push — только в своей ветке.
- Конфликт разбирают по смыслу: маркеры →
add→commit, а--abortотменяет слияние целиком. logпо диапазону сборок иblameпо строке превращают «раньше работало» в конкретный коммит.
Что почитать дальше
- Как устроена автоматизация — код тестов, который живёт в этом репозитории.
- Как писать баг-репорт — куда идут номер сборки и диапазон коммитов.
- Команды Linux и терминал — та же командная строка, соседний навык.
- Тест-план и тест-сьюты — где фиксируют стенд и сборку до начала проверок.