Введение

Для начала я отвечу на вопрос «зачем этот пост?»

Ну, во-первых, я уже давно обещал Виталию из Evrone написать хоть что-то в блог. Обещаного, как говорится, лучше вообще не ждать, а то расстроишься.

А во-вторых вот уже полтора года как я провожу собеседования на позицию Señor NodeJS developer, при этом на момент первого собеседования я сам писал на NodeJS едва ли с полгода. И в качестве единственного (ну ладно, не единственного, но единственного технического) вопроса по git задаю именно этот.

Почему именно его? Потому что ответ на этот вопрос сразу поведает мне о том, насколько глубоко кандидат копался в git. И ответов тут может быть несколько, от «я не знаю» до «а diff-то в git динамический!».

После собеседования неплохо бы оставлять кандидатам обратную связь, но вот незадача — я не смог через полчаса гугления смог! найти удовлетворяющую меня статью по этому вопросу. Надо бы это исправить дополнить.

Давайте попробуем разобраться в возможный ответах и дойдём до самого ультимативного

Неинтересные варианты ответов

«Я не знаю»

Пожалуй, тут особо и нечего комментировать. Если рассказать по вопросу “в чём разница между git merge и git rebase” нечего, то нужно идти учить матчасть. Например прочитать вот эту статью.

«Merge соединяет код, а rebase как бы его поверх делает»

Если совсем вольно трактовать поведение git — да, можно считать это верным ответом, ведь, действительно, merge пытается “соединить” две ветки в одну за счёт создания merge-коммита, а rebase применяет коммиты одной ветки поверх другой.

«Rebase изменяет историю»

Так-то да, но если вы противник изменения истории — используйте Mercurial. Да и без последующих комментариев это на вопрос не отвечает.

«Merge объединяет код сразу, а rebase по одному коммиту»

Это уже интереснее, хотя суть проблемы никак не выявляет. Действительно, при разрешении конфликтов git для merge вычисляет конфликты между последними состояними (коммитами, если хотите) веток, а rebase делает это для каждого коммита (из ветки, которую мы rebase’им).

«Diff строится по-разному для merge и rebase: merge разрешает все конфликты в merge-коммите, а rebase разрешает их для каждого коммита»

А вот это уже гораздо ближе к истине. Правда, есть нюанс (ahem, вы меня поняли), но про “нюанс” я уточняю вопросом “а к чему это приводит”?

«А к чему это приводит?»

«К различиям в git blame»

Итак, первый ответ на вопрос “А к чему это приводит”.

Объяснение тут довольно простое (но не для всех очевидное): при наличии конфликтов — даже при их автоматическом разрешении — само изменение, разрешающее конфликт будет относиться к merge-коммиту в случае merge, в то время как при rebase изменение, относящееся к разрешению конфликта будет относиться к тому коммиту, для которого разрешался конфликт.

Для демонстрации сего на двухчасовом мероприятии я даже собрал небольшой репозиторий с примером. Если есть время, рекомендую ознакомиться.

А так как я являюсь яростным сторонником git-blame-driven подхода, то мне бы очень не хотелось по какому-то спорному изменению видеть в git blame имя коммита Merge branch 'feature/fix-some-bug' into 'master', а хотелось бы видеть непосредственно тот коммит, к которому логически это изменение относится.

Кстати, это делает меня сторонником крайне спорного подхода: прежде, чем мержить свою ветку в master, неплохо бы её отребейзить на тот же master, чтобы и получить логическое преимущество наличия merge-коммита и при этом не потерять корректный git blame.

«К различиям в коде»

А вот тут самое интересное. Потому как остальные различия не относятся к результату в коде, а только к истории.

Можно что-то и поломать. Давайте посмотрим.
Я приведу полный листинг bash-команд без комментариев, а дальше вы уж как-нибудь сами разберётесь.

# Создаём репозиторий
git init .

# Создаём первый коммит с единственным файлом
echo "some string" > file.txt
git add file.txt
git commit -m "Initial commit"

# Создаём две ветки, в которых будем играться изменениями
git branch branch-1
git branch branch-2

# Изменяем единственную строку в ветке branch-1
git checkout branch-1
echo "some string // changed" > file.txt
git add file.txt
git commit -m "Changed line 2 on branch 1"

# Изменяем единственную строку в ветке branch-2
git checkout branch-2
echo "some string // changed" > file.txt
git add file.txt
git commit -m "Changed line 2 on branch 2"

# Откатываем единственную строку в файле branch-2
echo "some string" > file.txt
git add file.txt
git commit -m "Reverted line 2 on branch 2"

# Создаём копию ветки для того, чтобы попробовать и merge и rebase
git branch branch-2-non-modified

# Делаем git merge
git merge --no-edit branch-1
cat file.txt
# Получаем some string // changed

# Возвращаем branch-2 к до-merge-состоянию
git reset --hard branch-2-non-modified

# Делаем git rebase
git rebase branch-1
cat file.txt
# Получаем some string

Интересно получилось? Вот и мне интересно. Особенно если кандидат про это расскажет (без листинга и подробностей, конечно) на собеседовании.

Почему так происходит? Думайте, гуглите, дальше мне лень простыню писать. Особенно с учётом того, что при описании такого последствия кандидатом дальнейших объяснений не потребуется — и так понятно, что кандидат с git работал достаточно глубоко.