Git cherry-pick: несколько коммитов, ветки, конфликты
Git cherry-pick: как скопировать коммит из другой ветки, перенести несколько коммитов или диапазон, разрешить конфликты и когда лучше merge или rebase.
TL;DR: git cherry-pick <sha> копирует коммит из любой ветки в вашу текущую — тот же патч, новый SHA, история никуда не сдвигается. Для нескольких коммитов перечислите SHA или возьмите диапазон (git cherry-pick A..B); учтите, что диапазон исключает A, так что пишите A^..B, если его нужно включить. Если пик остановился на конфликте — разрешите его, git add, затем git cherry-pick --continue. Cherry-pick — чтобы перенести конкретный фикс; когда нужна вся ветка целиком, берите merge или rebase.
Что git cherry-pick делает на самом деле?
Cherry-pick берёт существующий коммит и применяет его diff как новый коммит в вашей текущей ветке. Оригинал остаётся где был; копия получает свежий SHA. Git ничего не «перемещает» — те, кто потом удивляется, почему коммит всё ещё виден в старой ветке, видят ровно это.
Модель «копия, а не перенос» определяет, когда cherry-pick — правильный инструмент:
- Нужен один фикс из фичевой ветки на
mainпрямо сейчас, без мержа остального. - Хотфикс, закоммиченный не в ту ветку, должен попасть в нужную.
- Патч надо воспроизвести на релизной ветке, которая никогда не мержит из
main.
Смежный навык — умение откатить коммит, который оказался ошибкой; механика этого разобрана в git undo last commit: keep the changes.
Как cherry-pickнуть коммит из другой ветки?
Найдите SHA, переключитесь на целевую ветку, пикайте:
# 1. Найдите коммит в исходной ветке
git log feature/payment-fix --oneline -5
# 2. Переключитесь на ветку, которая должна его получить
git switch main
# 3. Скопируйте его
git cherry-pick 1a2b3c4
Два удобства, которые стоит знать:
git cherry-pick <branch>пикает верхний (tip) коммит ветки — удобно, но легко сработать случайно, держа в голове устаревшее представление о том, где этот tip.- После пика
git log -1 --statподтверждает, что приземлилось. Одна секунда чтения — и revert не понадобится.
Коммиты остаются в feature/payment-fix; удаляйте ветку когда угодно — скопированный коммит на main имеет собственный SHA и никак не зависит от старого.
Как перенести несколько коммитов?
Три формы, по убыванию частоты:
Все три работают и между ветками: SHA разрешаются по всему репозиторию, так что коммиты могут приходить из любой ветки — вы просто должны находиться на ветке, которая их принимает.
# 1. Явный список — применяется в порядке перечисления
git cherry-pick 1a2b3c4 5d6e7f8
# 2. Диапазон — всё после A вплоть до B включительно
git cherry-pick A..B
# 3. Диапазон вместе с A
git cherry-pick A^..B
Различие A..B против A^..B — классический сюрприз: A..B исключает A. Если вы визуализируете диапазон из git log и пикаете oldest..newest, вы молча пропускаете самый старый коммит. Когда цель — «несколько самых старых, по порядку», пишите oldest^..newest, и off-by-one исчезает.
Чтобы слить несколько пиков в один коммит вместо трёх, стейджируйте без коммита с -n / --no-commit и закоммитьте один раз:
git cherry-pick -n 1a2b3c4 5d6e7f8
git commit -m "Backport: payment retry fixes"
Почему git cherry-pick не работает?
Четыре реальные причины, по убыванию частоты:
1. Пик остановился на конфликте. Git применяет патч, натыкается на строку, изменённую в обеих ветках, и ставит последовательность на паузу:
# разрешите файлы, затем:
git add <resolved-files>
git cherry-pick --continue # или --abort, чтобы вернуться к состоянию до пика
--continue не опционален и не подразумевается — пока вы его не выполнили, вы внутри приостановленной последовательности cherry-pick, и git status будет об этом напоминать.
2. Пик пуст («The previous cherry-pick is now empty»). Изменение уже есть в этой ветке — обычно из более раннего пика или сквош-мержа. Пропустите через git cherry-pick --skip или форсируйте пустой коммит через --allow-empty, если маркер действительно нужен.
3. Коммит — merge-коммит. У мержа два родителя, поэтому «примени этот diff» неоднозначно — git отказывается, а не угадывает. Скажите, с каким родителем считать diff:
git cherry-pick -m 1 <merge-sha> # родитель 1 = ветка, *в* которую вы мержили
4. Не то рабочее дерево или detached HEAD. Пик приземлится туда, куда указывает HEAD. git branch --show-current перед пиком; если вывод пуст, вы в detached HEAD, и коммит осиротеет при переключении.
Cherry-pick vs merge vs rebase: что и когда?
| Команда | Что приземляется в целевой ветке | История | Когда использовать |
|---|---|---|---|
git cherry-pick <sha> |
Только названные коммиты | Копия, новые SHA | Один конкретный фикс должен переехать сейчас |
git merge <branch> |
Всё, что есть в ветке | Merge-коммит или fast-forward | Нужна вся ветка, расхождение видно |
git rebase <base> |
Все коммиты ветки, воспроизведённые заново | Линейная, SHA переписаны | Нужна ветка как чистая линейная история |
git revert <sha> |
Обратный коммит | Добавляет undo-коммит | Ушедший коммит надо отменить в общей истории |
Правило в одну строку: cherry-pick переносит выборку; merge и rebase переносят всё. Тянуться к cherry-pick, чтобы «синхронизироваться» с веткой, — признак, что вам на самом деле нужен merge; а если ветка — main вашего форка против upstream, полный ритуал описан в sync a fork with upstream, step by step.
Привычка, от которой стоит отказаться: вечно держать один и тот же коммит в нескольких ветках. Каждый будущий фикс в исходной ветке потребует нового пика, и ветки рано или поздно разъезжаются. Бэкпорты в релизные ветки — нормальный паттерн; постоянная параллельная вселенная — нет.
Workflow cherry-pick: кратко
git log <source-branch> --oneline -5 # найдите SHA(ы)
git switch <target-branch> # окажитесь в нужном месте
git cherry-pick A^..B # диапазон, список или одиночный SHA
# при конфликте: resolve → git add → git cherry-pick --continue
git log -1 --stat # проверьте, что приземлилось
Найти, переключиться, пикнуть, проверить. У команды репутация опасной, которой она не заслуживает: diff либо применяется, либо всё останавливается и объясняет почему. Единственная по-настоящему разрушительная ошибка — пикнуть не в ту ветку, и git log -1 перед пушем делает её трудно прозевать.