Как git-cherry-pick только изменения для определенных файлов?

587

Если я хочу объединить ветку Git с изменениями, внесенными только в некоторые файлы, измененные в конкретном коммите, который включает в себя изменения в нескольких файлах, как это может быть достигнуто?

Предположим , что Git коммит называется stuffимеет изменения в файлах A, B, Cи , Dно я хочу , чтобы объединить только stuffизменения «s к файлам Aи B. Звучит как работа, git cherry-pickно cherry-pickзнает, как объединить все коммиты, а не подмножество файлов.

Тобиас Кинцлер
источник

Ответы:

689

Я бы сделал это с cherry-pick -n( --no-commit), который позволяет вам проверить (и изменить) результат перед фиксацией:

git cherry-pick -n <commit>

# unstage modifications you don't want to keep, and remove the
# modifications from the work tree as well.
# this does work recursively!
git checkout HEAD <path>

# commit; the message will have been stored for you by cherry-pick
git commit

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

# unstage everything
git reset HEAD

# stage the modifications you do want
git add <path>

# make the work tree match the index
# (do this from the top level of the repo)
git checkout .
Cascabel
источник
10
В дополнение к этому git checkout .я бы порекомендовал также git clean -fудалить все новые, но нежелательные файлы, введенные вишневым коммитом.
rlat
4
Дополнительное примечание к последнему методу: я использую метод, git add -pкоторый позволяет вам в интерактивном режиме решить, какие изменения вы хотите добавить в индекс для каждого файла
Матфей
6
Это не так здорово в том случае, если выбранный вишневый коммит не применяется к текущей рабочей копии, потому что он настолько отличается, но один файл будет применяться корректно.
Ограниченное искупление
3
Вы также можете удалить выборочно с git reset -p HEAD. Это эквивалент, add -pно очень немногие знают, что он существует.
Патрик Шлютер
1
Очень полезный трюк. Я поместил это в суть на тот случай, если кому-то понадобится быстрый скрипт gist.github.com/PiDayDev/68c39b305ab9d61ed8bb2a1195ee1afc
Дамиано,
146

Другие методы не работали для меня, так как в коммите было много изменений и конфликтов с множеством других файлов. То, что я придумал, было просто

git show SHA -- file1.txt file2.txt | git apply -

На самом деле addэто не файлы или коммит для вас, поэтому вам может потребоваться

git add file1.txt file2.txt
git commit -c SHA

Или, если вы хотите пропустить добавление, вы можете использовать --cachedаргумент дляgit apply

git show SHA -- file1.txt file2.txt | git apply --cached -

Вы также можете сделать то же самое для целых каталогов

git show SHA -- dir1 dir2 | git apply -
Майкл Андерсон
источник
2
Интересный способ, спасибо. Но разве show SHA -- file | applyэто не так, checkout SHA -- fileкак в ответе Марка Лонгаира ?
Тобиас Кинцлер
4
Нет, checkout SHA -- fileбудет проверять именно версию в SHA, в то время как show SHA -- file | applyбудут применены только изменения в SHA (точно так же, как cherry-pick). Имеет значение, если (а) существует более одного коммита, изменяющего данный файл в исходной ветке, или (б) есть коммит, изменяющий файл в вашей текущей целевой ветке.
Майкл Андерсон
9
Просто нашел другое отличное применение для этого: выборочный возврат, когда вы хотите вернуть только один файл (так как git revertотменяет весь коммит). В этом случае просто используйтеgit show -R SHA -- file1.txt file2.txt | git apply -
Майкл Андерсон
2
@RoeiBahumi, у которого совсем другое значение. git diff SHA -- file1.txt file2.txt | git apply -означает применить все различия между текущей версией файла и версией в SHA к текущей версии. По сути это так же, как git checkout SHA -- file1.txt file2.txt. Смотрите мой предыдущий комментарий, почему это отличается от того, что git showверсия.
Майкл Андерсон
5
Если вам нужно разрешить конфликты, используйте git apply -3 -вместо просто git apply -, тогда, если конфликт возникает, вы можете использовать свою стандартную технику разрешения конфликтов, включая использование git mergetool.
qwertzguy
87

Я обычно использую -pфлаг с git checkout из другой ветки, который я считаю более простым и более детальным, чем большинство других методов, с которыми я сталкивался.

В общем:

git checkout <other_branch_name> <files/to/grab in/list/separated/by/spaces> -p

пример:

git checkout mybranch config/important.yml app/models/important.rb -p

Затем вы получите диалоговое окно, в котором вас спросят, какие изменения вы хотите внести в «BLOB-объекты», и это в значительной степени работает с каждым фрагментом непрерывного изменения кода, который затем можно сигнализировать y(Да) n(Нет) и т. Д. Для каждого фрагмента кода.

Опция -por patchработает с множеством команд в git, в том числе git stash save -pпозволяет выбрать то, что вы хотите сохранить в своей текущей работе.

Я иногда использую эту технику, когда проделал большую работу и хотел бы выделить ее и зафиксировать в коммитах на основе нескольких тем, используя git add -pи выбирая то, что я хочу для каждого коммита :)

Тайрон Уилсон
источник
3
Я регулярно использую git-add -p, но я не знал, git-checkoutтакже есть -pфлаг - это решает проблемы слияния, которые есть у -pответа ?
Тобиас Кинцлер
1
по крайней мере -p, допускается ручное редактирование для такого противоречивого раздела, что cherry-pick, вероятно, в любом случае также приведет к. Я проверю это в следующий раз, когда мне это понадобится, безусловно, интересный подход
Тобиас Кинцлер,
2
Один из двух лучших ответов, которые не убивают параллельные изменения в ветвях.
Акостадинов
1
Посмотрите этот ответ, чтобы узнать, как выбрать те блоки, которые нужно применить: stackoverflow.com/a/10605465/4816250 В частности, опция 's' была очень полезна.
jvd10
1
git reset -p HEADтакже позволяет -pлегко выйти из него, если вы хотите удалить только некоторые патчи из индекса.
Патрик Шлютер
42

Возможно, преимущество этого метода перед ответом Джефроми в том, что вам не нужно помнить, какое поведение git reset является правильным :)

 # Create a branch to throw away, on which we'll do the cherry-pick:
 git checkout -b to-discard

 # Do the cherry-pick:
 git cherry-pick stuff

 # Switch back to the branch you were previously on:
 git checkout -

 # Update the working tree and the index with the versions of A and B
 # from the to-discard branch:
 git checkout to-discard -- A B

 # Commit those changes:
 git commit -m "Cherry-picked changes to A and B from [stuff]"

 # Delete the temporary branch:
 git branch -D to-discard
Марк Лонгэйр
источник
2
спасибо за Ваш ответ. Теперь это вдохновило меня задуматься, почему бы не пропустить cherry-pickи напрямую использовать git checkout stuff -- A B? И с git commit -C stuffсообщением фиксации тоже останется то же самое
Тобиас Кинцлер
8
@Tobias: Это будет работать, только если измененные файлы stuffне были изменены в вашей текущей ветке или где-либо между общим предком HEADи stuffи верхушкой stuff. Если они есть, то cherry-pickсоздает правильный результат (по сути, результат слияния), в то время как ваш метод отбрасывает изменения в текущей ветви и сохраняет все изменения от общего предка до stuff- не только те, что в одиночный коммит.
Каскабель
2
@Tobias Kienzler: Я предполагал, что ваша отправная точка достаточно сильно отличается от родительской, stuffпоскольку результат выбора вишни уйдет Aи будет Bотличаться от их содержания в коммите stuff. Однако, если бы это было так же, вы правы - вы могли бы просто сделать, как вы говорите.
Марк Лонгэйр,
@Jeromi, @Mark: спасибо за ваш отзыв, в моем случае я лечу ветки совершенно несвязанными файлами, что привело меня к моему предложению. Но на самом деле у меня рано или поздно возникли проблемы с этим, так что спасибо, что подняли этот вопрос
Тобиас Кинцлер,
Я думаю, что мой ответ в этой другой теме может быть то, что вы после.
Ян
30

Cherry pick - это выбор изменений из определенного «коммита». Самое простое решение состоит в том, чтобы выбрать все изменения определенных файлов, это использовать

 git checkout source_branch <paths>...

В примере:

$ git branch
* master
  twitter_integration
$ git checkout twitter_integration app/models/avatar.rb db/migrate/20090223104419_create_avatars.rb test/unit/models/avatar_test.rb test/functional/models/avatar_test.rb
$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   new file:   app/models/avatar.rb
#   new file:   db/migrate/20090223104419_create_avatars.rb
#   new file:   test/functional/models/avatar_test.rb
#   new file:   test/unit/models/avatar_test.rb
#
$ git commit -m "'Merge' avatar code from 'twitter_integration' branch"
[master]: created 4d3e37b: "'Merge' avatar code from 'twitter_integration' branch"
4 files changed, 72 insertions(+), 0 deletions(-)
create mode 100644 app/models/avatar.rb
create mode 100644 db/migrate/20090223104419_create_avatars.rb
create mode 100644 test/functional/models/avatar_test.rb
create mode 100644 test/unit/models/avatar_test.rb

Источники и полное объяснение http://jasonrudolph.com/blog/2009/02/25/git-tip-how-to-merge-specific-files-from-another-branch/

ОБНОВИТЬ:

С помощью этого метода git не будет MERGE файл, он просто переопределит любые другие изменения, сделанные в целевой ветви. Вам нужно будет объединить изменения вручную:

$ git diff HEAD filename

cminatti
источник
5
Я тоже так думал , но это ужасно терпит неудачу, если файлы изменились в обеих ветвях, так как отбрасывает изменения вашей текущей ветки
Тобиас Кинцлер
Вы правы, необходимо уточнить, что таким образом git не MERGE, он просто переопределяет. Затем вы можете сделать «git diff HEAD filename», чтобы увидеть, что изменилось, и выполнить слияние вручную.
Cminatti
18

Ситуация:

Допустим, вы в своей ветке, masterи у вас есть коммит в любой другой ветке. Вы должны выбрать только один файл из этого конкретного коммита.

Подход:

Шаг 1: Оформить заказ на нужную ветку.

git checkout master

Шаг 2: Убедитесь, что вы скопировали необходимый хеш коммита.

git checkout commit_hash path\to\file

Шаг 3: Теперь у вас есть изменения нужного файла в нужной ветке. Вам просто нужно добавить и зафиксировать их.

git add path\to\file
git commit -m "Your commit message"
techdreams
источник
1
Потрясающие! Также работал для всех изменений в каталоге с \ path \ to \ directory \ для меня
zaggi
13

Я бы просто все выбрал, а потом сделал бы это:

git reset --soft HEAD^

Затем я отменил бы изменения, которые мне не нужны, а затем сделал бы новый коммит.

funroll
источник
11

Используйте git merge --squash branch_nameэто, чтобы получить все изменения из другой ветки и подготовить коммит для вас. Теперь удалите все ненужные изменения и оставьте тот, который вы хотите. И мерзавец не узнает, что произошло слияние.

неукротимый
источник
Спасибо, я не знал об этом варианте слияния. Это жизнеспособная альтернатива, если вы хотите выбрать вишню большую часть всей ветви (но в отличие от вишни она не будет работать, если нет общего предка)
Тобиас Кинцлер,
4

Я нашел другой способ, который предотвращает любое противоречивое слияние при сборке вишни, которое IMO легко запомнить и понять. Поскольку на самом деле вы выбираете не коммит, а его часть, вам нужно сначала разделить его, а затем создать коммит, который соответствует вашим потребностям, и выбрать его.

Сначала создайте ветку из коммита, который вы хотите разделить, и извлеките его:

$ git checkout COMMIT-TO-SPLIT-SHA -b temp

Затем верните предыдущий коммит:

$ git reset HEAD~1

Затем добавьте файлы / изменения, которые вы хотите в cherry-pick:

$ git add FILE

и совершить это:

$ git commit -m "pick me"

обратите внимание на хэш коммита, давайте назовем его PICK-SHA и вернемся к вашей основной ветке, например, master для принудительной проверки:

$ git checkout -f master

и черри выбирай коммит:

$ git cherry-pick PICK-SHA

Теперь вы можете удалить временную ветку:

$ git branch -d temp -f
Алексей
источник
2

Объедините ветку в новую (сквош) и удалите ненужные файлы:

git checkout master
git checkout -b <branch>
git merge --squash <source-branch-with-many-commits>
git reset HEAD <not-needed-file-1>
git checkout -- <not-needed-file-1>
git reset HEAD <not-needed-file-2>
git checkout -- <not-needed-file-2>
git commit
ПНВ
источник
2

Для полноты картины лучше всего мне подходит:

git show YOURHASH --no-color -- file1.txt file2.txt dir3 dir4 | git apply -3 --index -

git status

Он делает именно то, что хочет ОП. Он делает разрешение конфликтов, когда это необходимо, так же, как mergeэто делает. Это делает, addно не commitваши новые изменения.

kubanczyk
источник
1

Ты можешь использовать:

git diff <commit>^ <commit> -- <path> | git apply

Обозначение <commit>^указывает (первого) родителя <commit>. Следовательно, эта команда diff выбирает изменения, сделанные <path>в коммите <commit>.

Обратите внимание, что это еще ничего не зафиксирует (как это git cherry-pickделает). Так что если вы хотите этого, вам придется сделать:

git add <path>
git commit
Garogolun
источник