¿Cómo git reset --hard un subdirectorio?

197

ACTUALIZACIÓN² : con Git 2.23 (agosto de 2019), hay un nuevo comando git restoreque hace esto, vea la respuesta aceptada .

ACTUALIZACIÓN : Esto funcionará de manera más intuitiva a partir de Git 1.8.3, consulte mi propia respuesta .

Imagine el siguiente caso de uso: quiero deshacerme de todos los cambios en un subdirectorio específico de mi árbol de trabajo Git, dejando intactos todos los demás subdirectorios.

¿Cuál es el comando Git adecuado para esta operación?

El siguiente script ilustra el problema. Inserte el comando adecuado debajo del How to make filescomentario: el comando actual restaurará el archivo a/c/acque se supone que está excluido por el pago escaso. Tenga en cuenta que no quiero restaurar explícitamente a/ay a/bsolo "sé" ay quiero restaurar todo lo siguiente. EDITAR : Y tampoco "sé" b, o qué otros directorios residen en el mismo nivel que a.

#!/bin/sh

rm -rf repo; git init repo; cd repo
for f in a b; do
  for g in a b c; do
    mkdir -p $f/$g
    touch $f/$g/$f$g
    git add $f/$g
    git commit -m "added $f/$g"
  done
done
git config core.sparsecheckout true
echo a/a > .git/info/sparse-checkout
echo a/b >> .git/info/sparse-checkout
echo b/a >> .git/info/sparse-checkout
git read-tree -m -u HEAD
echo "After read-tree:"
find * -type f

rm a/a/aa
rm a/b/ab
echo >> b/a/ba
echo "After modifying:"
find * -type f
git status

# How to make files a/* reappear without changing b and without recreating a/c?
git checkout -- a

echo "After checkout:"
git status
find * -type f
krlmlr
fuente
3
¿Qué tal un git stash && git stash drop?
CharlesB
1
¿qué pasa git checkout -- /path/to/subdir/?
iberbeu
3
@CharlesB: git stashno acepta un argumento de ruta ...
krlmlr
@iberbeu: No. También agregará archivos excluidos por un pago escaso.
krlmlr
1
@CharlesBailey: Entonces, ¿por qué hay un botón de radio que dice "Buscando un dibujo de respuesta de fuentes confiables y / u oficiales". en el diálogo de recompensa? ¡No escribí eso yo mismo! También intente buscar en Google el "subdirectorio git reset" (sin las comillas) y vea qué hay en las primeras 3 posiciones. Seguro que un mensaje a la lista de correo kernel.org será más difícil de encontrar. - Además, para mí aún no está claro si este comportamiento es un error o una característica.
krlmlr

Respuestas:

162

Con Git 2.23 (agosto de 2019), tiene el nuevo comandogit restore

git restore --source=HEAD --staged --worktree -- aDirectory
# or, shorter
git restore -s@ -SW  -- aDirectory

Eso reemplazaría tanto el índice como el árbol de trabajo con HEADcontenido, como lo reset --hardharía, pero para una ruta específica.


Respuesta original (2013)

Nota (como se ha comentado por Dan Fabulich ) que:

  • git checkout -- <path> no hace un restablecimiento completo: reemplaza el contenido del árbol de trabajo con el contenido en etapas.
  • git checkout HEAD -- <path>realiza un restablecimiento completo para una ruta, reemplazando tanto el índice como el árbol de trabajo con la versión de la HEADconfirmación.

Como contestado por Ajedi32 , ambas formas de pago y envío no eliminan los archivos que fueron borrados en la revisión destino .
Si tiene archivos adicionales en el árbol de trabajo que no existen en HEAD, a git checkout HEAD -- <path>no los eliminará.

Nota: Con git checkout --overlay HEAD -- <path>(Git 2.22, Q1 2019) , los archivos que aparecen en el índice y el árbol de trabajo, pero no en, <tree-ish>se eliminan, para que coincidan <tree-ish>exactamente.

Pero ese pago puede respetar a git update-index --skip-worktree(para aquellos directorios que desea ignorar), como se menciona en " ¿Por qué los archivos excluidos siguen apareciendo en mi pago escaso de git? ".

VonC
fuente
1
Por favor aclarar Después git checkout HEAD -- ., reaparecen los archivos excluidos por pago escaso. ¿Qué se git update-index --skip-worktreesupone que debe hacer?
krlmlr
@krlmlr skip-worktree o asumir-sin cambios son las dos formas de tratar de hacer una entrada en el índice "invisible" para git: fallengamer.livejournal.com/93321.html , stackoverflow.com/q/13630849/6309 y stackoverflow. com / a / 6139470/6309
VonC
@krlmlr esos enlaces son solo punteros para que usted pruebe y vea si un proceso de pago aún restauraría esas entradas, una vez que se hayan marcado como 'skipped-worktree'.
VonC
Lo siento, pero eso es demasiado complicado para la tarea en cuestión. Quiero un reinicio inclusivo, no exclusivo. ¿Realmente no hay una buena manera de hacer esto en Git?
krlmlr
@krlmlr no: es mejor hacer lo siguiente git checkout HEAD -- <path>y luego eliminar los directorios que se restauraron (pero que todavía se declaran en el pago escaso).
VonC
125

Según el desarrollador de Git, Duy Nguyen, quien implementó amablemente la función y un interruptor de compatibilidad , lo siguiente funciona como se esperaba a partir de Git 1.8.3 :

git checkout -- a

(donde aestá el directorio que desea restablecer). Se puede acceder al comportamiento original a través de

git checkout --ignore-skip-worktree-bits -- a
krlmlr
fuente
55
Gracias por su esfuerzo de seguimiento con el equipo de desarrollo de Git, que resultó en este cambio en Git.
Dan Cruz
13
Y tenga en cuenta que "a" en este caso significa el directorio que desea revertir, por lo que si está en el directorio que desea revertir, el comando debe estar git checkout -- .donde .significa el directorio actual.
TheWestIsThe ...
55
Un comentario de mi parte es que primero debe desestabilizar la carpeta con git reset -- a(donde a es el directorio que desea restablecer)
Boyan
¿No es eso cierto también si quieres git reset --hardtodo el repositorio?
krlmlr
Y si agregó archivos nuevos a ese directorio, haga un rm -rf aantes.
Tobias Feil
30

Intenta cambiar

git checkout -- a

a

git checkout -- `git ls-files -m -- a`

Desde la versión 1.7.0, Git's honra la bandera skip-worktree .ls-files

El funcionamiento de su script de prueba (con algunos ajustes menores cambiando git commit... a git commit -qy git statusa git status --short) Salidas:

Initialized empty Git repository in /home/user/repo/.git/
After read-tree:
a/a/aa
a/b/ab
b/a/ba
After modifying:
b/a/ba
 D a/a/aa
 D a/b/ab
 M b/a/ba
After checkout:
 M b/a/ba
a/a/aa
a/c/ac
a/b/ab
b/a/ba

Ejecutando su script de prueba con las checkoutsalidas de cambio propuestas :

Initialized empty Git repository in /home/user/repo/.git/
After read-tree:
a/a/aa
a/b/ab
b/a/ba
After modifying:
b/a/ba
 D a/a/aa
 D a/b/ab
 M b/a/ba
After checkout:
 M b/a/ba
a/a/aa
a/b/ab
b/a/ba
Dan Cruz
fuente
Suena bien. ¿Pero no debería git checkoutrespetar el bit "skip-worktree" en primer lugar?
krlmlr
Una revisión rápida de checkout.cy tree.cno revela que se usa el indicador skip-worktree.
Dan Cruz
Esto es lo suficientemente simple como para ser útil en la práctica, incluso si tendré que configurar un alias bash para este comando. Duy Nguyen ha respondido a mi mensaje a la lista de correo de Git, veamos si pronto aparecerá una alternativa más fácil de usar.
krlmlr
18

Para el caso de simplemente descartar cambios, los comandos git checkout -- path/o git checkout HEAD -- path/sugeridos por otras respuestas funcionan muy bien. Sin embargo, cuando desea restablecer un directorio a una revisión que no sea HEAD, esa solución tiene un problema importante: no elimina los archivos que se eliminaron en la revisión de destino.

Entonces, en cambio, comencé a usar el siguiente comando:

git diff --cached commit -- subdir | git apply -R --index

Esto funciona encontrando la diferencia entre el compromiso de destino y el índice, luego aplicando esa diferencia en reversa al directorio e índice de trabajo. Básicamente, esto significa que hace que el contenido del índice coincida con el contenido de la revisión que especificó. El hecho de que git difftome un argumento de ruta le permite limitar este efecto a un archivo o directorio específico.

Dado que este comando es bastante largo y planeo usarlo con frecuencia, he configurado un alias para él que llamé reset-checkout:

git config --global alias.reset-checkout '!f() { git diff --cached "$@" | git apply -R --index; }; f'

Puedes usarlo así:

git reset-checkout 451a9a4 -- path/to/directory

O solo:

git reset-checkout 451a9a4
Ajedi32
fuente
Vi tu comentario ayer y lo experimenté hoy. Tu alias es útil. +1
VonC
¿Cómo se compara esta opción con el git checkout --overlay HEAD -- <path>comando que @VonC menciona en su respuesta?
Ehtesh Choudhury
1
@EhteshChoudhury Nota que git checkout --overlay HEAD -- <path>aún no se lanzó (Git 2.22 se lanzará en el segundo trimestre de 2019)
VonC
5

Voy a ofrecer una opción terrible aquí, ya que no tengo idea de cómo hacer nada con git excepto add commity push, así es como "revertí" un subdirectorio:

Comencé un nuevo repositorio en mi PC local, volví todo al commit del que quería copiar el código y luego copié esos archivos en mi directorio de trabajo, add commit pushy listo. No odies al jugador, odia al señor Torvalds por ser más listo que todos nosotros.

Abraham Brookes
fuente
4

Un restablecimiento normalmente cambiará todo, pero puede usarlo git stashpara elegir lo que desea conservar. Como mencionó, stashno acepta una ruta directamente, pero aún puede usarse para mantener una ruta específica con la --keep-indexbandera. En su ejemplo, escondería el directorio b y luego restablecería todo lo demás.

# How to make files a/* reappear without changing b and without recreating a/c?
git add b               #add the directory you want to keep
git stash --keep-index  #stash anything that isn't added
git reset               #unstage the b directory
git stash drop          #clean up the stash (optional)

Esto lo lleva a un punto donde la última parte de su script generará esto:

After checkout:
# On branch master
# Changes not staged for commit:
#
#   modified:   b/a/ba
#
no changes added to commit (use "git add" and/or "git commit -a")
a/a/aa
a/b/ab
b/a/ba

Creo que este fue el resultado objetivo (b permanece modificado, los archivos a / * están de vuelta, a / c no se recrea).

Este enfoque tiene el beneficio adicional de ser muy flexible; puede obtener la precisión que desee al agregar archivos específicos, pero no otros, en un directorio.

Jonathan Wren
fuente
Eso es bueno, pero tendría que hacer git addtodo excepto a, ¿verdad? Suena difícil en la práctica.
krlmlr
1
@krlmlr No realmente. git add .Luego puede git reset aagregar todo excepto a.
Jonathan Wren el
1
@krlmlr Además, vale la pena señalar que git addno agrega archivos eliminados. Entonces, si solo está recuperando archivos eliminados, git add .agregará todos los archivos modificados, pero no los eliminados.
Jonathan Wren el
3

Si el tamaño del subdirectorio no es particularmente grande, Y desea mantenerse alejado de la CLI, aquí hay una solución rápida para restablecer manualmente el subdirectorio:

  1. Cambie a la rama maestra y copie el subdirectorio para restablecerlo.
  2. Ahora vuelva a su rama de características y reemplace el subdirectorio con la copia que acaba de crear en el paso 1.
  3. Cometer los cambios.

Salud. ¡Simplemente restablece manualmente un subdirectorio en su rama de características para que sea el mismo que el de la rama maestra!

Mehul Parmar
fuente
No vi ningún voto para esta respuesta, pero a veces esta es la ruta más simple garantizada hacia el éxito.
Sue Spence el
1

La respuesta de Ajedi32 es lo que estaba buscando, pero para algunas confirmaciones me encontré con este error:

error: cannot apply binary patch to 'path/to/directory' without full index line

Puede ser porque algunos archivos del directorio son archivos binarios. La adición de la opción '--binary' al comando git diff lo arregló:

git diff --binary --cached commit -- path/to/directory | git apply -R --index
khelkun
fuente
0

Qué pasa

subdir=thesubdir
for fn in $(find $subdir); do
  git ls-files --error-unmatch $fn 2>/dev/null >/dev/null;
  if [ "$?" = "1" ]; then
    continue;
  fi
  echo "Restoring $fn";
  git show HEAD:$fn > $fn;
done 
Cochise Ruhulessin
fuente