Como o git-cherry-pick altera apenas alguns arquivos?

587

Se eu quiser mesclar em uma ramificação Git, as alterações feitas apenas em alguns dos arquivos alterados em uma confirmação específica que inclui alterações em vários arquivos, como isso pode ser alcançado?

Suponha que o git commit chamada stufftem alterações em arquivos A, B, C, e D, mas eu quero fundir apenas stuffmudanças 's para arquivos Ae B. Parece um trabalho para, git cherry-pickmas cherry-picksó sabe mesclar confirmações inteiras, não um subconjunto dos arquivos.

Tobias Kienzler
fonte

Respostas:

689

Eu faria isso com cherry-pick -n( --no-commit), que permite inspecionar (e modificar) o resultado antes de confirmar:

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

Se a grande maioria das modificações são coisas que você não deseja, em vez de verificar caminhos individuais (a etapa do meio), você pode redefinir tudo de volta e adicionar o que deseja:

# 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
fonte
10
Além de git checkout .recomendar, também git clean -fremova todos os arquivos novos, porém indesejados, introduzidos pelo commit escolhido pela cereja.
rlat
4
Nota adicional para o último método: use eu git add -pque lhe permite decidir de forma interactiva que muda você deseja adicionar ao índice por ficheiro
matthaeus
6
Isso não é tão bom no caso de o commit escolhido por cereja não se aplicar à cópia de trabalho atual, porque é muito diferente, mas um arquivo seria aplicado de maneira limpa.
Expiação limitada
3
Você também pode desfilar seletivamente com git reset -p HEAD. É o equivalente a, add -pmas muito poucos sabem que ele existe.
Patrick Schlüter
1
Truque muito útil. Eu colocá-lo em uma essência no caso de alguém precisa dele como um script rápido gist.github.com/PiDayDev/68c39b305ab9d61ed8bb2a1195ee1afc
Damiano
146

Os outros métodos não funcionaram para mim, pois o commit teve muitas alterações e conflitos em muitos outros arquivos. O que eu vim com foi simplesmente

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

Na verdade, ele não copia addos arquivos ou faz um commit para você; portanto, talvez seja necessário segui-lo

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

Ou, se você quiser pular a adição, use o --cachedargumento paragit apply

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

Você também pode fazer o mesmo por diretórios inteiros

git show SHA -- dir1 dir2 | git apply -
Michael Anderson
fonte
2
Método interessante, obrigado. Mas, show SHA -- file | applybasicamente, não faz o mesmo checkout SHA -- fileque na resposta de Mark Longair ?
Tobias KIENZLER
4
Não, checkout SHA -- filefará o checkout exatamente da versão no SHA, enquanto show SHA -- file | applyaplicará apenas as alterações no SHA (assim como o cherry-pick faz). É importante se (a) houver mais de uma confirmação alterando o arquivo especificado na ramificação de origem ou (b) houver uma confirmação alterando o arquivo em sua ramificação de destino atual.
22615 Michael Anderson
9
Acabei de encontrar outro ótimo uso para isso: reversão seletiva, para quando você deseja reverter apenas um arquivo (pois git revertdesfaz todo o commit). Nesse caso, basta usargit show -R SHA -- file1.txt file2.txt | git apply -
Michael Anderson
2
@ RoeiBahumi que tem um significado bem diferente. git diff SHA -- file1.txt file2.txt | git apply -significa aplicar todas as diferenças entre a versão atual do arquivo e a versão no SHA para a versão atual. Em essência, é o mesmo que git checkout SHA -- file1.txt file2.txt. Veja meu comentário anterior para saber por que isso é diferente do que a git showversão.
Michael Anderson
5
Caso você precise resolver conflitos, use em git apply -3 -vez de apenas git apply -; se ocorrer um conflito, você poderá usar sua técnica padrão de resolução de conflitos, incluindo o uso git mergetool.
Qwertzguy
87

Normalmente, uso a -pflag com um checkout git do outro ramo, que acho mais fácil e mais granular do que a maioria dos outros métodos que encontrei.

Em princípio:

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

exemplo:

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

Você então recebe uma caixa de diálogo perguntando quais alterações você deseja em "blobs", isso praticamente funciona em cada parte da mudança contínua de código que você pode sinalizar y(Sim) n(Não) etc para cada parte do código.

A opção -pou patchfunciona para uma variedade de comandos no git, incluindo o git stash save -pque permite que você escolha o que deseja esconder do seu trabalho atual

Às vezes uso essa técnica quando tenho feito muito trabalho e gostaria de separá-la e confirmar em mais confirmações baseadas em tópicos, usando git add -pe escolhendo o que eu quero para cada confirmação :)

Tyrone Wilson
fonte
3
Uso regularmente git-add -p, mas não sabia que git-checkouttambém tem uma -pbandeira - isso corrige os problemas de fusão que a não -presposta tem?
Tobias Kienzler
1
pelo menos -ppermitiria uma edição manual para uma seção tão conflitante, que cherry-pickprovavelmente também produziria de qualquer maneira. Eu vou testar esta próxima vez que eu precisar dele, definitivamente uma abordagem interessante
Tobias KIENZLER
2
Uma das duas melhores respostas que não matam alterações simultâneas nos ramos.
Akostadinov 17/11
1
Veja esta resposta para saber como selecionar quais blocos aplicar: stackoverflow.com/a/10605465/4816250 A opção 's' em particular foi muito útil.
jvd10
1
git reset -p HEADtambém permite o -pque pode ser útil quando você deseja remover apenas alguns patches do índice.
Patrick Schlüter
42

Talvez a vantagem desse método sobre a resposta de Jefromi seja que você não precisa se lembrar de qual comportamento do git reset é o correto :)

 # 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
Mark Longair
fonte
2
obrigado pela sua resposta. Agora que me inspirou a pensar, por que não pular cherry-picke usar diretamente git checkout stuff -- A B? E com git commit -C stuffa mensagem de commit permaneceria o mesmo bem
Tobias KIENZLER
8
@Tobias: Isso só funcionaria se os arquivos modificados em stuffnão foram modificados em seu ramo atual ou em qualquer lugar entre o ancestral comum de HEADe stuffe a ponta stuff. Se houver, cherry-pickcria o resultado correto (essencialmente o resultado de uma mesclagem), enquanto seu método descartaria as alterações no ramo atual e manteria todas as alterações do ancestral comum até stuff- e não apenas as que confirmação única.
Cascabel
2
@Tobias Kienzler: Eu estava assumindo que seu ponto de partida era suficientemente diferente do pai, stuffque o resultado da escolha da cereja sairia Ae Bcom conteúdo diferente do conteúdo no commit stuff. No entanto, se for o mesmo, você está certo - você pode fazer o que diz.
precisa saber é o seguinte
@ Jeromi, @ Mark: obrigado pelo seu feedback, no meu caso, estou tratando ramos com arquivos totalmente disjuntos, o que me levou à minha sugestão. Mas na verdade eu tinha executado em problemas com ele mais cedo ou mais tarde, por isso, obrigado por trazer este acima
Tobias KIENZLER
Acho que minha resposta nesse outro tópico pode ser o que você procura.
Ian
30

Escolha de cereja é escolher as alterações de um "commit" específico. A solução mais simples é escolher todas as alterações de determinados arquivos é usar

 git checkout source_branch <paths>...

No exemplo:

$ 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

Fontes e explicação completa http://jasonrudolph.com/blog/2009/02/25/git-tip-how-to-merge-specific-files-from-another-branch/

ATUALIZAR:

Com esse método, o git não MERGE o arquivo, substituirá qualquer outra alteração feita na ramificação de destino. Você precisará mesclar as alterações manualmente:

Nome do arquivo $ git diff HEAD

cminatti
fonte
5
Eu também pensava assim , mas isso falhar terrivelmente se os arquivos foram alterados em ambos os ramos, uma vez que descarta as alterações da sua filial atual
Tobias KIENZLER
Você está certo, é necessário esclarecer que dessa maneira o git não MERGE, apenas substitui. Você pode fazer "git diff HEAD filename" para ver o que mudou e fazer a mesclagem manualmente.
Cminatti
18

A situação:

Você está no seu ramo, digamos, mastere você tem seu commit em qualquer outro ramo. Você precisa escolher apenas um arquivo dessa confirmação específica.

A abordagem:

Etapa 1: finalize a compra na filial necessária.

git checkout master

Etapa 2: verifique se você copiou o hash de confirmação necessário.

git checkout commit_hash path\to\file

Etapa 3: Agora você tem as alterações do arquivo necessário no seu ramo desejado. Você só precisa adicionar e confirmar.

git add path\to\file
git commit -m "Your commit message"
techdreams
fonte
1
Impressionante! Também funcionou para todas as alterações em um diretório com \ path \ to \ directory \ para mim
zaggi 03/04
13

Gostaria apenas de escolher tudo de cereja e fazer o seguinte:

git reset --soft HEAD^

Depois, reverteria as alterações que não quero e, em seguida, faria um novo commit.

funroll
fonte
11

Use git merge --squash branch_nameisso para obter todas as alterações do outro ramo e preparará uma confirmação para você. Agora remova todas as alterações desnecessárias e deixe a que você deseja. E o git não saberá que houve uma fusão.

Indomável
fonte
Obrigado, eu não sabia sobre essa opção de mesclagem. É uma alternativa viável se você quiser cherry-pick mais de um ramo inteiro (mas em contraste com cherry-pick não vai funcionar se não houver ancestral comum)
Tobias KIENZLER
4

Eu encontrei uma outra maneira que impede qualquer fusão conflitante na escolha da cereja, que é fácil de lembrar e entender da IMO. Como na verdade você não escolhe um commit, mas faz parte dele, é necessário dividi-lo primeiro e depois criar um commit que atenda às suas necessidades e selecioná-lo.

Primeiro, crie uma ramificação a partir do commit que você deseja dividir e faça o checkout:

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

Reverta a confirmação anterior:

$ git reset HEAD~1

Em seguida, adicione os arquivos / alterações que deseja escolher:

$ git add FILE

e comprometa-o:

$ git commit -m "pick me"

observe o hash de confirmação, vamos chamá-lo de PICK-SHA e voltar para sua ramificação principal, mestre por exemplo, forçando o checkout:

$ git checkout -f master

e escolha a submissão:

$ git cherry-pick PICK-SHA

agora você pode excluir o ramo temporário:

$ git branch -d temp -f
Alexey
fonte
2

Mesclar uma ramificação em uma nova (squash) e remover os arquivos não necessários:

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
nvd
fonte
2

Por uma questão de completude, o que funciona melhor para mim é:

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

git status

Faz exatamente o que o OP quer. Faz a resolução de conflitos quando necessário, da mesma forma que mergefaz. Faz, addmas não commitsuas novas alterações.

kubanczyk
fonte
1

Você pode usar:

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

A notação <commit>^especifica o (primeiro) pai de <commit>. Portanto, este comando diff seleciona as alterações feitas <path>no commit <commit>.

Observe que isso ainda não confirma nada (como git cherry-pickfaz). Então, se você quiser, terá que fazer:

git add <path>
git commit
Garogolun
fonte