Введение

«У вас конфликт при merge. Что будете делать?» — вопрос, который добивает кандидатов, уверенно прошедших вопросы про lifecycle. На словах все знают, что такое ветки, но объяснить, почему конфликт вообще возникает и что происходит «под капотом», может один из десяти.

И это странно, потому что конфликт — не катастрофа и не «баг Git». Это штатная ситуация, у которой есть понятная механика. Если её понимать, конфликты перестают быть страшными: вы просто читаете маркеры и решаете, какой вариант оставить.

На собеседованиях я спрашиваю про ветки почти всегда: это тема, где видно, работал ли человек в команде или писал пет-проекты в одиночку. Одиночка ветки почти не использует — а значит, не знает ни merge, ни конфликтов, ни разницы с rebase. Разберём всё по порядку: как устроены ветки, что происходит при merge, как выглядят и решаются конфликты и когда вместо merge используют rebase.

Как устроены ветки

Первый факт, который нужно понимать: ветка в Git — это просто подвижный указатель на коммит. Никакой «папки с копией проекта» не существует. Когда вы делаете git branch feature, Git создаёт новый указатель, который ссылается на тот же коммит, где вы сейчас находитесь.

Отдельно существует указатель HEAD — он показывает, на какой ветке вы находитесь. Дальше всё просто: вы делаете коммиты, и указатель ветки движется вперёд. Создать ветку и переключиться на неё — одна команда:

branch-workflow.sh
git checkout -b feature        # создать ветку и переключиться
# эквивалентно двум командам:
git branch feature
git checkout feature

Когда вы в ветке делаете коммит, feature движется вперёд, а main остаётся на месте. История «разошлась» — это нормальное состояние разработки, и именно для работы с расхождением существуют merge и rebase.

Ключевая мысль

Ветка — это не копия кода, а лёгкий указатель на коммит. HEAD показывает текущую ветку, а каждый новый коммит двигает указатель вперёд. Поэтому создание ветки в Git — мгновенная и дешёвая операция.

Ещё один момент, который стоит проговорить на собеседовании: почему ветки вообще нужны. Ответ — изоляция работы. Пока фича в отдельной ветке, она не ломает основную линию, и в main в любой момент можно выпустить релиз. После того как фича готова, ветку вливают обратно через merge — или удаляют, если работа оказалась ненужной.

Merge: fast-forward и настоящий merge

Теперь главное. Когда вы делаете git merge, Git смотрит на историю и выбирает один из двух сценариев.

Сценарий первый — fast-forward. Он возможен, когда ветка, которую вы вливаете, «сидит» прямо на вершине текущей: между ними нет расходящихся коммитов. Тогда Git просто двигает указатель текущей ветки вперёд — никакого нового коммита не создаётся. Как пишется в официальной документации Git, при fast-forward merge Git только обновляет указатель ветки, чтобы он совпал с вливаемой веткой, и не создаёт merge-коммит.

Сценарий второй — настоящий (true) merge. Он нужен, когда истории разошлись: у двух веток разные новые коммиты. Git делает трёхстороннее слияние (three-way merge), используя три снапшота: вершину текущей ветки, вершину вливаемой и их общего предка. Результат — новый коммит, который объединяет обе линии истории, и такой коммит особенный: у него два родителя.

merge-types.sh
git checkout main
git merge feature
# если истории разошлись — создаётся merge-коммит с двумя родителями
# если feature прямо поверх main — произойдёт fast-forward
git merge --no-ff feature
# --no-ff принудительно создаёт merge-коммит, даже когда возможен fast-forward

Зачем нужен --no-ff? Чтобы зафиксировать в истории сам факт слияния фичи. Часто это требование командного процесса: каждый merge — это отдельный «узел» в истории, который можно откатить целиком.

Типичная ошибка кандидата: сказать «merge всегда создаёт новый коммит». Нет — при fast-forward merge коммита не создаётся, указатель просто перемещается. А при настоящем merge создаётся коммит с двумя родителями — это и есть его отличие от обычного коммита.

Конфликты: как выглядят и как решать

Конфликт возникает в одном случае: две ветки изменили одну и ту же часть одного и того же файла по-разному. Git не может сам решить, чей вариант правильный, — и останавливает merge. Это не ошибка: merge «завис», и вы должны решить конфликт вручную.

Git вставляет в файл маркеры конфликта:

ConflictExample.kt
<<<<<<< HEAD
fun buildGreeting(name: String) = "Привет, $name!"
=======
fun buildGreeting(name: String) = "Hello, $name!"
>>>>>>> feature

Всё выше разделителя ======= — версия из HEAD (ветка, в которую вы вливаете), ниже — версия из вливаемой ветки. Ваша задача — оставить один вариант, соединить оба или написать свой, а затем удалить все три строки маркеров.

После того как все конфликты разрешены, файлы нужно пометить как решённые командой git add — стейджинг файла и означает «конфликт решён» — и завершить merge коммитом. Если вы поняли, что вливать не нужно вообще, — git merge --abort отменит merge и вернёт рабочую директорию к состоянию до него.

resolve-conflict.sh
git status                  # показывает файлы с конфликтами (unmerged)
# редактируем файлы: оставляем нужный код, убираем маркеры
git add <file>              # пометить файл как решённый
git commit                  # завершить merge
git merge --abort           # если решили отменить merge целиком
  • Причина конфликта: обе ветки изменили одну и ту же часть файла — Git не может выбрать сам.
  • Маркеры: <<<<<<< HEAD — ваша версия, ======= — разделитель, >>>>>>> ветка — чужая версия.
  • Решение: отредактировать файл, убрать маркеры, git add — и завершить merge коммитом.
  • Отмена: git merge --abort, если merge нужно отменить.

Merge или rebase

Второй способ объединить разошедшиеся ветки — git rebase. Он работает иначе: вместо создания merge-коммита rebase переигрывает ваши коммиты поверх другой ветки. Git берёт каждый коммит вашей ветки, вычисляет его изменения и применяет их заново на вершину чужой ветки.

Главный эффект — линейная история: после rebase выглядит так, будто вся работа шла последовательно, без ветвления. Потом можно сделать fast-forward и получить чистую прямую линию. Именно за это rebase любят: история читается как оглавление.

rebase-example.sh
git checkout feature
git rebase main
# коммиты feature переиграны поверх main — история стала линейной
git checkout main
git merge feature
# теперь это fast-forward, merge-коммита не будет

Но у rebase есть жёсткое правило, и его стоит произнести на собеседовании дословно: не нужно делать rebase коммитов, которые уже существуют вне вашего репозитория — например, уже запушены в общую ветку. Rebase переписывает коммиты (создаёт новые с другими хешами), и если кто-то другой уже строил работу на старых, история у него разъедется. В официальном руководстве Git это правило сформулировано прямо: не ребазьте коммиты, которые находятся вне вашего репозитория и на которые люди могли опереться.

Что выбрать

Merge сохраняет факт слияния отдельным коммитом и не трогает существующую историю. Rebase даёт линейную историю, но переписывает коммиты — поэтому применять его стоит к локальной работе, а не к опубликованным коммитам.

Ошибки кандидатов на собеседовании

Соберём типичные промахи, которые я слышу на интервью:

  • «Конфликт — это когда Git сломался». Нет, конфликт — штатная ситуация с понятной механикой и маркерами.
  • «Merge всегда создаёт коммит». При fast-forward — не создаёт.
  • «Чтобы отменить merge, откатываю всё reset'ом». Есть штатный git merge --abort.
  • «Rebase — это то же самое, что merge». Механика разная: merge объединяет истории, rebase переигрывает коммиты и меняет хеши.
  • «Я решаю конфликт, выбирая свою версию всегда». Не всегда это правильно: иногда нужно соединить оба изменения, а иногда и спросить коллегу.

Как отвечать, чтобы отличиться: начните с модели — «ветка это указатель на коммит», затем объясните два сценария merge, затем конфликт через маркеры и git add как отметку решения. И добавьте позицию: «в командной работе я предпочитаю merge с --no-ff для релизных веток, а rebase использую, чтобы подтянуть свежие изменения в свою фиче-ветку перед merge request». Позиция важнее перечисления команд.

И пара слов о подготовке. Если вы знаете теорию, но ни разу не решали конфликт руками — это легко починить за один вечер: создайте тестовый репозиторий, сделайте в двух ветках разные правки одного файла и проведите merge сами. Если же вы чувствуете, что таких «белых пятен» много — я провожу диагностику уровня: за 30–40 минут разберём ваши пробелы в Git, Kotlin, Coroutines и архитектуре и составим план до middle. Без обязательств — если менторство вам не подойдёт, честно скажу.

Нужна помощь в переходе на middle?

Ветки и merge — одна из тем, где видно разницу между junior и middle. На мок-собеседованиях мы тренируем такие вопросы до уверенного ответа.

Итоги

Ветки в Git — это лёгкие указатели на коммиты, а merge — это выбор из двух сценариев: fast-forward (просто переместить указатель) или настоящий merge с созданием коммита с двумя родителями. Конфликт — это штатная ситуация, когда две ветки изменили одну и ту же часть файла: Git помечает её маркерами, вы решаете вручную и отмечаете решение через git add.

Три тезиса, которые стоит унести с собой:

  • Ветка — указатель, а не копия. Это объясняет, почему ветки в Git дешёвые и почему история «разъезжается» естественным образом.
  • Merge и rebase — разные механики. Merge объединяет истории, rebase переигрывает коммиты поверх другой ветки ради линейной истории. Публичные коммиты не ребазят.
  • Конфликт решается по маркерам. <<<<<<< HEAD — ваша версия, >>>>>>> ветка — чужая; после правки — git add и завершающий коммит.

Если вы узнали себя в половине ошибок из списка — это не повод для паники, Git закрывается практикой за неделю-две. Но если вместе с ним «плавают» и другие темы из middle-списка, зубрить дальше не нужно. Напишите в Telegram — разберём вашу ситуацию и составим план, что закрывать первым.