← назад к разделу

Разработчик пишет в задаче: «починил, правка в ветке fix-cart-total, проверь». Вы открываете стенд — поведение прежнее. Полдня уходит на переписку, и выясняется, что ветка ещё не влита: на стенде собрана другая версия кода, и правки там физически нет.

Второй повод разобраться — автотесты: они лежат в том же репозитории, что и код, вместе с тестовыми данными и чек-листами. Даже замена одного селектора — это ветка, коммит и запрос на слияние.

одна задача, две ветки — и два способа их свести mainкоммит коллегиваша веткаправка автотеста обе правки — одни строки<<<<<<< HEAD … ваша версияGit не угадывает — решаете вы mergeкоммит слиянияобе линии остались rebaseb1′b2′прямая линиякоммиты новые

Сверху: ваша ветка ушла от 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 — только в своей ветке.
  • Конфликт разбирают по смыслу: маркеры → addcommit, а --abort отменяет слияние целиком.
  • log по диапазону сборок и blame по строке превращают «раньше работало» в конкретный коммит.

Что почитать дальше