Представьте ситуацию: вы нашли критический баг в проекте, исправили его в feature-ветке, но до полного слияния ещё далеко. Или вам срочно нужно перенести одно конкретное изменение из текущей ветки в другую. В таких случаях git cherry-pick становится вашим секретным оружием.

Впервые сам я узнал о cherry-pick несколько лет назад от своего руководителя, будучи еще в 1С, и моя искренняя реакция тогда была: "Да ладно, а что, так правда можно было?!" Оказывается, можно)) При этом я обратил внимание, что эта команда редко освещается в базовых пособиях про git, а те, кто сталкиваются с ней впервые (как и я сам когда-то), могут упустить некоторые важные нюансы её использования. Поэтому было решено посвятить команде cherry-pick отдельный пост.

Что такое git cherry-pick и как он работает изнутри

Git cherry-pick – это как хирургический пинцет для вашего кода. Он позволяет взять конкретный коммит из любой ветки и применить его там, где нужно. Название cherry-pick (дословно "сбор вишен") отлично отражает суть операции – вы выбираете только те "вишенки" (коммиты), которые вам действительно нужны.

Под капотом cherry-pick работает следующим образом:

  1. Git создаёт патч (diff) выбранного коммита

  2. Сохраняет метаданные оригинального коммита (временную метку, автора) для поддержания хронологии

  3. Анализирует состояние файлов в целевой ветке

  4. Пытается применить изменения к текущему состоянию (при конфликтах требуется ручное разрешение)

  5. Создаёт новый коммит с уникальным хешем (из-за нового родительского коммита и времени создания)

Важное отличие от merge

В отличие от merge, который создает новый коммит слияния, сохраняя историю обеих веток, команды cherry-pick и rebase создают совершенно новые коммиты с новыми хешами. Это происходит потому, что хеш коммита в Git зависит от:

  • Содержимого изменений

  • Данных автора и времени коммита

  • Хеша родительского коммита

  • Сообщения коммита

  • Временной метки оригинального коммита

Основное отличие cherry-pick от rebase заключается в том, что cherry-pick переносит отдельно выбранные коммиты (их оригиналы при этом остаются нетронутыми, а в новой ветке создается их "копия" с новыми хешами), в то время как rebase переносит целую последовательность коммитов, перестраивая историю веток (чем-то напоминая операцию "вырезать - вставить").

Практическое применение

Подготовка к cherry-pick

Прежде чем применять cherry-pick, важно:

1. Убедиться, что рабочая директория чиста:

git status
# nothing to commit, working tree clean

2. Определить точный коммит для переноса. Для этого могут пригодиться следующие команды:

# Просмотр последних коммитов с графом веток
git log --oneline --graph --decorate --all -n 10

# Поиск коммита по ключевому слову
git log --grep="bug fix"

# Просмотр изменений конкретного коммита
git show abc123

# Проверка, какие коммиты уже были перенесены в ветку master из ветки feature
# Знак "-" будет означать, что коммит уже есть в master
git cherry -v master feature
#- cccc000... commit C  # коммит уже перенесен в master
#+ bbbb000... commit B  # коммит еще не перенесен в master
#- aaaa000... commit A  # коммит уже перенесен в master

Базовое использование

Рассмотрим типичный сценарий: у нас есть баг-фикс в feature-ветке, который срочно нужен в релизной ветке version/2.0. Чтобы точечно перенести нужные изменения:

  1. Находим нужный коммит:

    git log feature --oneline
    # abc123 fix: Critical null pointer exception in user service
    # def456 test: Add test cases
    # ghi789 fix: Handle edge cases
    # jkl012 feat: Add new user registration flow
    # ...
  2. Переключаемся в целевую ветку version/2.0, в которую хотим перенести баг-фикс:

    git checkout version/2.0
  3. Применяем нужный коммит (с автоматическим добавлением информации об оригинальном коммите):

    git cherry-pick -x abc123

Перенос коммитов с созданием новой ветки

Этот подход особенно полезен в командной разработке и при работе с критически важным кодом. В данном случае, коммит переносится через создание отдельной ветки (по аналогии с тем, как используются отдельные feature-ветки для добавления новой функциональности). В таком сценарии, последовательность действий будет выглядеть следующим образом:

  1. От целевой ветки, в которую планируется перенос изменений (например, от main), следует создать резервную ветку:

    # Создаем резервную ветку от ветки main и сразу переключаемся в эту ветку
    git checkout -b backup/cherry-pick-fix main
  2. Перенести коммит из ветки feature в резервную ветку:

    # Переносим коммит из ветки feature в резервную ветку
    git cherry-pick -x abc123
  3. Смерджить резервную ветку в ветку main с созданием нового merge-коммита:

    # Переключаемся в ветку main
    git checkout main
    
    # Мерджим резервную ветку в ветку main с созданием нового коммита слияния
    git merge backup/cherry-pick-fix --no-ff

Создание отдельной ветки для cherry-pick является хорошей практикой. Основными преимуществами такого подхода являются:

  1. Безопасность и прозрачность

    • Если что-то пойдет не так при cherry-pick, основная ветка останется нетронутой. Всегда можно легко отменить изменения, просто не выполняя merge.

    • Флаг --no-ff создает отдельный коммит слияния.

    • История git наглядно показывает, какие изменения были перенесены из другой ветки, когда это произошло и откуда именно были взяты правки.

  2. Возможность для код-ревью

    • Можно создать отдельный pull-request, чтобы дать другим разработчикам возможность проверить корректность переносимых изменений.

  3. Возможность доработки

    • Если нужно внести дополнительные изменения после cherry-pick, то можно сделать это в резервной ветке до слияния с main.

Перенос нескольких коммитов

С помощью команды cherry-pick можно переносить несколько коммитов за один раз. Перед тем как приступить к переносу, получим список коммитов из ветки feature:

git log --oneline feature
# abc123 fix: Critical null pointer exception in user service
# def456 test: Add test cases
# ghi789 fix: Handle edge cases
# jkl012 feat: Add new user registration flow
# ...

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

# Переносим отдельные коммиты
git cherry-pick def456 abc123

Можно также переносить диапазон коммитов. Для выделения диапазона следует указать хэш начального и конечного коммитов с .. между ними. Однако в этом диапазоне начальный коммит НЕ включается. Чтобы включить начальный коммит, нужно указать на коммит, идущий непосредственно перед ним. Это можно сделать с помощью символа ~, например так: def456~ , что будет значить: «коммит, предшествующий коммиту def456» (в нашем примере — ghi789).

# Переносим диапазон коммитов (где ghi789 — более старый коммит, чем abc123)
git cherry-pick ghi789..abc123  # от ghi789 до abc123, не включая ghi789
git cherry-pick ghi789~..abc123  # от ghi789 до abc123, включая ghi789

Полезные опции

Есть несколько полезных опций, который можно использовать с командой cherry-pick.

  1. Перенос изменений без автоматического коммита.
    Эта опция позволяет перенести изменения из нужного коммита в рабочую директорию, при этом не создавая самой фиксации.

    # Перенос без автоматического коммита
    git cherry-pick -n abc123
  2. Автоматическое добавление информации об оригинальном коммите.
    Git может автоматически добавлять примечание к сообщению коммита вида: cherry picked from commit abc123...). Это полезно при переносе исправлений между публичными ветками, например, когда вы портируете баг-фикс из основной ветки разработки в старую версию продукта. Такое сообщение поможет другим разработчикам отследить историю изменений. Важно, что информация будет добавлена только для успешных cherry-pick'ов без конфликтов.

    # Автоматическое добавление информации об оригинальном коммите
    git cherry-pick -x abc123  # добавит к сообщению "cherry-picked from commit ..."
  3. Изменение сообщения коммита.
    Вы также можете оставить произвольное сообщение к cherry-pick коммиту. Чтобы это сделать, используйте флаг -e.

    # Ручное добавление информации об оригинальном коммите
    git cherry-pick -e abc123

Работа с конфликтами

Конфликты при cherry-pick могут возникать чаще, чем при обычном merge, потому что контекст изменений может сильно отличаться. Вот пошаговое руководство по их разрешению:

  1. Анализ конфликта

git status  # смотрим конфликтующие файлы
git diff    # детальный просмотр конфликтующих изменений
  1. Стратегии разрешения

# Использование изменений из ветки, в которую мы переносим коммит
git checkout --ours path/to/file

# Использование изменений из коммита, который мы переносим
git checkout --theirs path/to/file

# Ручное редактирование (файл откроется в редакторе nano)
nano path/to/file
  1. Продолжение операции cherry-pick

# После того как мы разрешили конфликты, добавляем файлы в индекс
git add .

# Продолжаем процесс cherry-pick
git cherry-pick --continue

# Пропуск проблемного коммита при массовом переносе
git cherry-pick --skip

# Отмена операции
git cherry-pick --abort

Типичные проблемы и как их избежать

  1. Дублирование кода

При неправильном использовании cherry-pick можно случайно применить одни и те же изменения дважды. Чтобы этого не произошло, полезно заранее проверять наличие похожих изменений в целевой ветке:

# Проверка наличия похожих изменений по названию коммита
git log --grep="fix: Critical bug"

# Проверка, какие коммиты уже были перенесены в ветку master из ветки feature, а какие нет
# Знак "-" будет означать, что коммит уже есть в master
git cherry -v master feature
#- cccc000... commit C  # коммит уже перенесен в master
#+ bbbb000... commit B  # коммит еще не перенесен в master
#- aaaa000... commit A  # коммит уже перенесен в master
  1. Потеря контекста

Cherry-picked коммиты теряют связь с оригинальной веткой. Чтобы этого не происходило, следует оставлять "следы" в виде понятных сообщений и ссылок:

# Перенос коммита с автоматическим добавлением ссылки на оригинальный коммит
git cherry-pick -x abc123

# Перенос коммита с ручным добавлением детального описания
git cherry-pick -e abc123

В сообщении коммита постарайтесь указывать:

  • Описание переноса

  • Номер тикета/issue

  • Ссылку на оригинальный коммит

Например:

# Critical null pointer exception in user service fix from feature branch
# Ticket: PROJ-123
# Original commit: abc123

Можно также добавлять теги либо заметки к cherry-pick коммитам:

# Добавление тега для отслеживания
git tag -a cherrypick/fix-123 -m "Cherry-picked from feature branch"

# Использование notes для документирования
git notes add -m "Cherry-picked from commit abc123" HEAD

Когда использовать cherry-pick

Подходящие случаи:

  • Срочный перенос исправлений багов

  • Перенос отдельных функций в нужную ветку

  • Восстановление случайно удалённых изменений

  • Бэкпортирование в старые версии

  • Создание hotfix-релизов

  • Перенос экспериментальных фич в отдельную ветку для тестирования

  • Создание чистой версии фичи из "грязной" ветки с временными фиксами

Когда лучше воздержаться:

  • Если можно использовать обычный merge или rebase

  • При переносе большого количества связанных коммитов

  • Когда важно сохранить полную историю изменений

  • В случае сильной связности кода между коммитами

  • Для регулярного переноса изменений между длительно живущими ветками

  • При работе с коммитами, имеющими сложные зависимости от других изменений

Итог

Git cherry-pick – мощный инструмент для точечного переноса изменений. Его главные преимущества:

  • Точность и контроль над переносимыми изменениями

  • Возможность быстрого исправления критических ошибок

  • Гибкость в управлении историей коммитов

Однако важно понимать, что частое использование cherry-pick может привести к дублированию коммитов и усложнению истории git. Используйте его как скальпель, а не как топор – только когда действительно необходимо выполнить точечную операцию.

Еще больше полезных статей про разработку и не только – публикую в своем канале.

Комментарии (10)


  1. NeriaLab
    21.01.2025 15:30

    Уважаемый автор, попробуйте использовать GitKraken (Git GUI). Он тоже так умеет


    1. trabl
      21.01.2025 15:30

      Автор написал как сделать всё из консоли средствами git, без доп утилит, за что большой респект. Информация действительно полезная. За git kraken тоже спасибо, будем посмотреть.


      1. NeriaLab
        21.01.2025 15:30

        Просто я совсем разленился, поэтому с удовольствием юзаю Git GUI. Из самых лучших и с большим набором функций, меня устроил GitKraken


    1. Stanislav9801 Автор
      21.01.2025 15:30

      Спасибо за дополнение!


    1. coodi
      21.01.2025 15:30

      Он много чего умеет, но он платный и закрытый. И это немного ограничивает его использование


  1. checkpoint
    21.01.2025 15:30

    Вопрос на засыпку. Допустим я надергал из ветки feature в ветку main багфиксов. Далее я продолжаю разработку в ветке feature. В какойто момент разрабатываемая фича признается годной для вноса её в основную ветку и в этом случае обычно делается merge ветки feature с веткой main. Но, в основной ветке уже есть кучка надерганных ранее коммитов из ветки feature. Возникнет ли в этом случае конфликт и как его правильно разрезолвить ? Логично было бы удалить из главной ветки то, что было надергано и продолжить merge, но, на сколько я понимаю, постого способа для этого нет. Или есть ?


    1. coodi
      21.01.2025 15:30

      Если из главной удалить фиксы, то при таком мерже они не восстановятся. Я как то раз переносил черрипиками кучу коммитов, потомучто ветки давно разошлись и смержить их было невозможно. Зато от этой главной ветки все остальные подтянули изменения без проблем


    1. feelamee
      21.01.2025 15:30

      по-моему гит сам это резолвит.

      Но у меня появились сомнения, т.к. как автор и сказал - новый коммит теряет связь со старым. Но гит ведь все же узнает как-то перенесен ли уже коммит в команде `git checkout -v master feature`. Возможно сравнением исходников просто.


    1. Stanislav9801 Автор
      21.01.2025 15:30

      Интересный вопрос! Спасибо что спросили) Сами по себе конфликты возникают, если в одной и той же строке кода были сделаны изменения, и при этом, эти изменения расходятся. В самом простом и идеальном случае, если черри-пикнуть из feature ветки коммит в main, затем добавить еще коммитов в feature, после чего смерджить, то конфликтов возникнуть не должно, так как bugfix-коммит в feature ветке и bugfix-коммит в main ветке будут содержать идентичные изменения. Но это в идеально случае.
      В реальности же, вероятнее всего, конфликты могут быть, просто потому что те же самые строки впоследствие могут быть снова изменены в ветке feature.
      На черри-пик коммит в этом случае можно смотреть просто как на коммит, который кто-то сделал в ветке main. И если ваши финальные изменения, которые мерджатся из ветки feature никак не конфликтуют с изменениями в коммитах main, то все будет ок.

      Решил для верности потестировать (репо):
      - мердж ветки feature прошел с конфликтом из-за того, что нечаянно где-то тронул пустые строки (дифф)
      - мердж ветки feature_2 прошел без конфликтов (тот самый "идеальный" случай)
      - мердж ветки feature_3 прошел с конфликтом, так как в финальных изменениях ветки feature "перезаписывались" те же строчки кода, которые ранее были перенесены коммитом в main (дифф).

      На всю историю репозитория удобнее всего посмотреть во вкладке Repository graph


      1. checkpoint
        21.01.2025 15:30

        Большое спасибо. Будем иметь в виду.