¿Cómo git-cherry-pick solo cambia a ciertos archivos?

587

Si quiero fusionarme en una rama de Git, los cambios realizados solo en algunos de los archivos cambiaron en una confirmación particular que incluye cambios en varios archivos, ¿cómo se puede lograr esto?

Supongamos que el git commit llamada stufftiene cambios en los archivos A, B, C, y Dpero quiero fusionar sólo stuffcambios 's a archivos Ay B. Suena como un trabajo para, git cherry-pickpero cherry-picksolo sabe cómo combinar confirmaciones completas, no un subconjunto de los archivos.

Tobias Kienzler
fuente

Respuestas:

689

Lo haría con cherry-pick -n( --no-commit), que le permite inspeccionar (y modificar) el 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

Si la gran mayoría de las modificaciones son cosas que no desea, en lugar de verificar rutas individuales (el paso intermedio), puede restablecer todo y luego agregar lo que desee:

# 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
fuente
10
Además de git checkout ., también recomendaría git clean -feliminar cualquier archivo nuevo pero no deseado introducido por el commit seleccionado.
rlat
44
Nota adicional para el último método: uso el git add -pque le permite decidir de forma interactiva qué cambios desea agregar al índice por archivo
matthaeus
66
Esto no es tan grande en el caso de que el palmitas cometen no se aplica a la copia de trabajo actual porque es muy diferente, pero que un archivo sería aplicar limpiamente.
Expiación limitada el
3
También puedes desestabilizar selectivamente con git reset -p HEAD. Es el equivalente add -ppero muy pocos saben que existe.
Patrick Schlüter
1
Truco muy útil. Lo puse en un resumen en caso de que alguien lo necesite como un script rápido gist.github.com/PiDayDev/68c39b305ab9d61ed8bb2a1195ee1afc
Damiano
146

Los otros métodos no funcionaron para mí, ya que la confirmación tuvo muchos cambios y conflictos con muchos otros archivos. Lo que se me ocurrió fue simplemente

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

En realidad, no contiene addlos archivos ni realiza una confirmación por usted, por lo que es posible que deba seguir con

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

O si desea omitir el complemento, puede usar el --cachedargumento paragit apply

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

También puede hacer lo mismo para directorios completos

git show SHA -- dir1 dir2 | git apply -
Michael Anderson
fuente
2
Método interesante, gracias. ¿Pero no hace show SHA -- file | applybásicamente lo mismo checkout SHA -- fileque en la respuesta de Mark Longair ?
Tobias Kienzler
44
No, checkout SHA -- filepagará exactamente la versión en SHA, mientras show SHA -- file | applyque solo aplicará los cambios en SHA (al igual que cherry-pick). Importa si (a) hay más de una confirmación que cambia el archivo dado en la rama de origen, o (b) hay una confirmación que cambia el archivo en su rama de destino actual.
Michael Anderson
99
Acabo de encontrar otro gran uso para esto: reversión selectiva, para cuando solo quieres revertir un archivo (ya que git revertdeshace todo el commit). En ese caso, solo usegit show -R SHA -- file1.txt file2.txt | git apply -
Michael Anderson el
2
@RoeiBahumi que tiene un significado bastante diferente. git diff SHA -- file1.txt file2.txt | git apply -significa aplicar todas las diferencias entre la versión actual del archivo y la versión en SHA a la versión actual. En esencia es lo mismo que git checkout SHA -- file1.txt file2.txt. Vea mi comentario anterior de por qué eso es diferente a cuál es la git showversión.
Michael Anderson
55
En caso de que tenga que resolver conflictos, use en git apply -3 -lugar de solo git apply -, luego, si ocurre un conflicto, puede usar su técnica estándar de resolución de conflictos, incluido el uso git mergetool.
qwertzguy
87

Por lo general, uso la -pbandera con un pago git de la otra rama, que me parece más fácil y más detallado que la mayoría de los otros métodos que he encontrado.

En principio:

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

ejemplo:

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

Luego, aparece un cuadro de diálogo que le pregunta qué cambios desea en los "blobs", esto prácticamente funciona para cada fragmento de cambio continuo de código que luego puede señalar y(Sí) n(No), etc. para cada fragmento de código.

La opción -po patchfunciona para una variedad de comandos en git, incluido el git stash save -pque le permite elegir qué desea guardar de su trabajo actual

A veces uso esta técnica cuando he hecho mucho trabajo y me gustaría separarla y comprometerme en más confirmaciones basadas en temas usando git add -py eligiendo lo que quiero para cada confirmación :)

Tyrone Wilson
fuente
3
Uso habitualmente git-add -p, pero no sabía que git-checkouttambién tiene una -pbandera: ¿eso soluciona los problemas de fusión que tiene la no -prespuesta ?
Tobias Kienzler
1
al menos -ppermitiría una edición manual para una sección tan conflictiva, que cherry-pickprobablemente también cedería de todos modos.
Probaré
2
Una de las dos mejores respuestas que no eliminan los cambios simultáneos en las ramas.
akostadinov
1
Consulte esta respuesta para saber cómo seleccionar qué trozos aplicar: stackoverflow.com/a/10605465/4816250 La opción 's' en particular fue muy útil.
jvd10
1
git reset -p HEADtambién permite -pque se pueda dejar de usar cuando solo desea eliminar algunos parches del índice.
Patrick Schlüter
42

Quizás la ventaja de este método sobre la respuesta de Jefromi es que no tienes que recordar qué comportamiento de git reset es el correcto :)

 # 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
fuente
2
gracias por tu respuesta. Ahora que me inspiró a pensar, ¿por qué no omitir cherry-picky usar directamente git checkout stuff -- A B? Y con git commit -C stuffel mensaje de confirmación seguiría siendo el mismo también
Tobias Kienzler
8
@Tobias: Eso funcionaría solo si los archivos modificados en stuffno se han modificado en su rama actual o en cualquier lugar entre el ancestro común de HEADy stuffy la punta de stuff. Si lo han hecho, cherry-pickcrea el resultado correcto (esencialmente el resultado de una fusión), mientras que su método descartaría los cambios en la rama actual y mantendría todos los cambios del ancestro común hasta stuff, no solo los que están en ese Compromiso único.
Cascabel
2
@Tobias Kienzler: Estaba asumiendo que su punto de partida era lo suficientemente diferente del padre de stuffque el resultado de la selección de cereza se iría Ay Bcon un contenido diferente de su contenido en el compromiso stuff. Sin embargo, si fuera lo mismo, tienes razón: podrías hacer lo que dices.
Mark Longair
@Jeromi, @Mark: gracias por sus comentarios, en mi caso estoy tratando a las sucursales con archivos completamente disjuntos que me llevaron a mi sugerencia. Pero, de hecho, me había topado con problemas tarde o temprano, así que gracias por mencionarlo
Tobias Kienzler
Creo que mi respuesta en este otro hilo puede ser lo que buscas.
Ian
30

La selección de cereza es elegir cambios de un "compromiso" específico. La solución más simple es elegir todos los cambios de ciertos archivos es usar

 git checkout source_branch <paths>...

Por ejemplo:

$ 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

Fuentes y explicación completa http://jasonrudolph.com/blog/2009/02/25/git-tip-how-to-merge-specific-files-from-another-branch/

ACTUALIZAR:

Con este método, git no FUSIONARÁ el archivo, simplemente anulará cualquier otro cambio realizado en la rama de destino. Deberá fusionar los cambios manualmente:

$ git diff HEAD nombre de archivo

cminatti
fuente
55
Yo también lo pensé , pero esto falla horriblemente si los archivos han cambiado en ambas ramas ya que descarta los cambios de su rama actual
Tobias Kienzler
Tienes razón, es imprescindible aclarar que de esta manera git no se fusiona, simplemente anula. Luego puede hacer "git diff HEAD filename" para ver qué cambió y hacer la fusión manualmente.
cminatti
18

La situación:

Estás en tu rama, digamos mastery tienes tu compromiso en cualquier otra rama. Debe elegir solo un archivo de esa confirmación en particular.

El enfoque:

Paso 1: Pago en la sucursal requerida.

git checkout master

Paso 2: asegúrese de haber copiado el hash de confirmación requerido.

git checkout commit_hash path\to\file

Paso 3: Ahora tiene los cambios del archivo requerido en la rama deseada. Solo necesita agregarlos y confirmarlos.

git add path\to\file
git commit -m "Your commit message"
sueños tecnológicos
fuente
1
¡Increíble! También funcionó para todos los cambios en un directorio con \ ruta \ a \ directorio \ para mí
zaggi
13

Simplemente elegiría todo, luego haría esto:

git reset --soft HEAD^

Luego revertiría los cambios que no quiero y luego realizaría una nueva confirmación.

funroll
fuente
11

Use git merge --squash branch_nameesto para obtener todos los cambios de la otra rama y preparará una confirmación para usted. Ahora elimine todos los cambios innecesarios y deje el que desee. Y git no sabrá que hubo una fusión.

Indomable
fuente
Gracias, no sabía sobre esa opción de fusión. Es una alternativa viable si desea seleccionar la mayor parte de una rama completa (pero en contraste con la selección de cereza, no funcionará si no hay un ancestro común)
Tobias Kienzler
4

Encontré otra forma que evita cualquier fusión conflictiva en la selección de cerezas, que en mi opinión es algo fácil de recordar y entender. Dado que en realidad no está seleccionando una confirmación, sino parte de ella, primero debe dividirla y luego crear una confirmación que se adapte a sus necesidades y seleccionarla.

Primero cree una rama desde el commit que desea dividir y pruébela:

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

Luego revierte el commit anterior:

$ git reset HEAD~1

Luego agregue los archivos / cambios que desea seleccionar:

$ git add FILE

y comprometerlo:

$ git commit -m "pick me"

tenga en cuenta el hash de confirmación, llamémoslo PICK-SHA y regresemos a su rama principal, master, por ejemplo, forzando el pago:

$ git checkout -f master

y escoge el commit:

$ git cherry-pick PICK-SHA

ahora puedes eliminar la rama temporal:

$ git branch -d temp -f
Alexey
fuente
2

Combine una rama en una nueva (squash) y elimine los archivos que no necesita:

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
fuente
2

En aras de la integridad, lo que funciona mejor para mí es:

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

git status

Hace exactamente lo que OP quiere. Resuelve conflictos cuando es necesario, de manera similar, ¿cómo lo mergehace? Lo hace addpero no committus nuevos cambios.

kubanczyk
fuente
1

Puedes usar:

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

La notación <commit>^especifica el (primer) padre de <commit>. Por lo tanto, este comando diff selecciona los cambios realizados <path>en el commit <commit>.

Tenga en cuenta que esto todavía no confirmará nada (como lo git cherry-pickhace). Entonces, si quieres eso, tendrás que hacer:

git add <path>
git commit
Garogolun
fuente