Comment git reset --hard un sous-répertoire?

197

UPDATE² : Avec Git 2.23 (août 2019), il y a une nouvelle commande git restorequi fait cela, voir la réponse acceptée .

MISE À JOUR : Cela fonctionnera plus intuitivement à partir de Git 1.8.3, voir ma propre réponse .

Imaginez le cas d'utilisation suivant: Je veux me débarrasser de toutes les modifications dans un sous-répertoire spécifique de mon arbre de travail Git, en laissant tous les autres sous-répertoires intacts.

Quelle est la commande Git appropriée pour cette opération?

Le script ci-dessous illustre le problème. Insérez la commande appropriée sous le How to make filescommentaire - la commande en cours restaurera le fichier a/c/acqui est censé être exclu par la vérification clairsemée. Notez que je ne veux pas restaurer explicitement a/aet a/b, je "connais" seulement et je aveux tout restaurer ci-dessous. EDIT : Et je ne sais pas non plus b, ni quels autres répertoires résident au même niveau 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
la source
3
qu'en est-il d'un git stash && git stash drop?
CharlesB
1
qu'en est-il git checkout -- /path/to/subdir/?
iberbeu
3
@CharlesB: git stashn'accepte pas d'argument de chemin ...
krlmlr
@iberbeu: Non. Ajoutera également des fichiers exclus par une vérification clairsemée.
krlmlr
1
@CharlesBailey: Alors pourquoi y a-t-il un bouton radio indiquant "Vous cherchez une réponse tirée de sources crédibles et / ou officielles". dans la boîte de dialogue des primes? Je ne l'ai pas tapé moi-même! Essayez également de googler le "sous-répertoire git reset" (sans les guillemets) et voyez ce qui se trouve sur les 3 premières positions. Pour sûr, un message à une liste de diffusion kernel.org sera plus difficile à trouver. - De plus, pour moi, il n'est pas encore clair si ce comportement est un bug ou une fonctionnalité.
krlmlr

Réponses:

162

Avec Git 2.23 (août 2019), vous avez la nouvelle commandegit restore

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

Cela remplacerait à la fois l'index et l'arborescence de travail par du HEADcontenu, comme le reset --hardferait, mais pour un chemin spécifique.


Réponse originale (2013)

Note (comme commenté par Dan Fabulich ) que:

  • git checkout -- <path> ne fait pas de réinitialisation matérielle: il remplace le contenu de l'arborescence de travail par le contenu intermédiaire.
  • git checkout HEAD -- <path>effectue une réinitialisation matérielle pour un chemin d'accès, en remplaçant à la fois l'index et l'arborescence de travail par la version du HEADcommit.

Comme répondu par Ajedi32 , les deux formes de caisse ne suppriment pas les fichiers qui ont été supprimés dans la révision cible .
Si vous avez des fichiers supplémentaires dans l'arborescence de travail qui n'existent pas dans HEAD, a git checkout HEAD -- <path>ne les supprimera pas.

Remarque: Avec git checkout --overlay HEAD -- <path>(Git 2.22, Q1 2019) , les fichiers qui apparaissent dans l'index et l'arborescence de travail, mais pas dans <tree-ish>sont supprimés, pour les faire correspondre <tree-ish>exactement.

Mais cette extraction peut respecter un git update-index --skip-worktree(pour les répertoires que vous souhaitez ignorer), comme indiqué dans « Pourquoi les fichiers exclus continuent-ils de réapparaître dans mon extraction git clairsemée? ».

VonC
la source
1
Précisez s'il vous plaît. Après git checkout HEAD -- ., les fichiers exclus par une extraction clairsemée réapparaissent. Que faut-il git update-index --skip-worktreefaire?
krlmlr
@krlmlr skip-worktree ou assume-unchanged sont les deux façons d'essayer de rendre une entrée dans l'index "invisible" pour git: fallengamer.livejournal.com/93321.html , stackoverflow.com/q/13630849/6309 et stackoverflow. com / a / 6139470/6309
VonC
@krlmlr, ces liens ne sont que des pointeurs pour que vous essayiez de voir si une extraction restaurerait toujours ces entrées, une fois qu'elles ont été marquées comme 'skipped-worktree'.
VonC
Désolé, mais c'est trop compliqué pour la tâche à accomplir. Je veux une réinitialisation inclusive, pas exclusive. N'y a-t-il vraiment aucun moyen sympa de le faire dans Git?
krlmlr
@krlmlr no: il est préférable de faire le git checkout HEAD -- <path>, puis de supprimer les répertoires qui ont été restaurés (mais qui sont toujours déclarés lors de l'extraction clairsemée).
VonC
125

Selon le développeur Git Duy Nguyen qui a aimablement implémenté la fonctionnalité et un commutateur de compatibilité , les éléments suivants fonctionnent comme prévu à partir de Git 1.8.3 :

git checkout -- a

(où aest le répertoire que vous souhaitez réinitialiser). Le comportement d'origine est accessible via

git checkout --ignore-skip-worktree-bits -- a
krlmlr
la source
5
Merci pour vos efforts de suivi avec l'équipe de développement Git, ce qui a entraîné ce changement dans Git.
Dan Cruz
13
Et notez que "a" dans ce cas signifie le répertoire que vous souhaitez rétablir, donc si vous êtes dans le répertoire que vous souhaitez rétablir, la commande doit être git checkout -- ..signifie le répertoire en cours.
TheWestIsThe ...
5
Un commentaire de mon côté est que vous devez d'abord décompresser le dossier avec git reset -- a(où a est le répertoire que vous souhaitez réinitialiser)
Boyan
N'est-ce pas vrai aussi si vous voulez git reset --hardle repo entier?
krlmlr
Et si vous avez ajouté de nouveaux fichiers à ce répertoire, faites-le rm -rf aavant.
Tobias Feil
30

Essayez de changer

git checkout -- a

à

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

Depuis la version 1.7.0, Git's ls-files honore le drapeau skip-worktree .

Exécution de votre script de test (avec quelques modifications mineures changeant git commit... vers git commit -qet git statusvers git status --short) sorties:

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

Exécution de votre script de test avec les checkoutsorties de modification proposées :

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
la source
Ça m'a l'air bien. Mais ne devrait pas git checkoutrespecter le bit "skip-worktree" en premier lieu?
krlmlr
Un examen rapide checkout.cet tree.cne révèle pas que l'indicateur skip-worktree est utilisé.
Dan Cruz
C'est assez simple pour être utile dans la pratique, même si je vais devoir configurer un alias bash pour cette commande. Duy Nguyen a répondu à mon message sur la liste de diffusion Git, voyons si une alternative plus conviviale apparaîtra bientôt.
krlmlr
18

Dans le cas d'une simple suppression des modifications, le git checkout -- path/ougit checkout HEAD -- path/ commandes suggérées par d'autres réponses fonctionnent très bien. Cependant, lorsque vous souhaitez réinitialiser un répertoire dans une révision autre que HEAD, cette solution a un problème important: elle ne supprime pas les fichiers qui ont été supprimés dans la révision cible.

Au lieu de cela, j'ai commencé à utiliser la commande suivante:

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

Cela fonctionne en trouvant le diff entre le commit cible et l'index, puis en appliquant ce diff à l'envers au répertoire de travail et à l'index. Fondamentalement, cela signifie que le contenu de l'index correspond au contenu de la révision que vous avez spécifiée. Le fait quegit diff prendre un argument chemin vous permet de limiter cet effet à un fichier ou un répertoire spécifique.

Comme cette commande est assez longue et que je prévois de l'utiliser fréquemment, j'ai mis en place un alias que j'ai nommé reset-checkout:

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

Vous pouvez l'utiliser comme ceci:

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

Ou juste:

git reset-checkout 451a9a4
Ajedi32
la source
J'ai vu votre commentaire hier et je l'ai expérimenté aujourd'hui. Votre alias est utile. +1
VonC
Comment cette option se compare-t-elle à la git checkout --overlay HEAD -- <path>commande mentionnée par @VonC dans sa réponse?
Ehtesh Choudhury
1
@EhteshChoudhury Notez que ce git checkout --overlay HEAD -- <path>n'est pas encore sorti (Git 2.22 sera publié au T2 2019)
VonC
5

Je vais proposer une terrible option ici, car je n'ai aucune idée de comment faire quoi que ce soit avec git sauf add commitet push, voici comment j'ai "inversé" un sous-répertoire:

J'ai commencé un nouveau référentiel sur mon PC local, rétabli le tout au commit à partir duquel je voulais copier le code, puis copié ces fichiers dans mon répertoire de travail, add commit pushet le tour est joué. Ne détestez pas le joueur, détestez M. Torvalds pour être plus intelligent que nous tous.

Abraham Brookes
la source
4

Une réinitialisation changera normalement tout, mais vous pouvez utiliser git stashpour choisir ce que vous souhaitez conserver. Comme vous l'avez mentionné, stashn'accepte pas directement un chemin, mais il peut toujours être utilisé pour conserver un chemin spécifique avec l' --keep-indexindicateur. Dans votre exemple, vous devez ranger le répertoire b, puis réinitialiser tout le reste.

# 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)

Cela vous amène à un point où la dernière partie de votre script affichera ceci:

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

Je crois que c'était le résultat visé (b reste modifié, les fichiers a / * sont de retour, a / c n'est pas recréé).

Cette approche a l'avantage supplémentaire d'être très flexible; vous pouvez obtenir autant de détails que vous le souhaitez en ajoutant des fichiers spécifiques, mais pas d'autres, dans un répertoire.

Jonathan Wren
la source
C'est bien, mais je devrais git addtout sauf a, non? Cela semble difficile dans la pratique.
krlmlr
1
@krlmlr Pas vraiment. Vous pouvez git add .alors git reset atout ajouter sauf a.
Jonathan Wren
1
@krlmlr En outre, il convient de noter que git addn'ajoute pas de fichiers supprimés. Donc, si vous ne récupérez que des fichiers supprimés, git add .tous les fichiers modifiés seront ajoutés, mais pas ceux supprimés.
Jonathan Wren
3

Si la taille du sous-répertoire n'est pas particulièrement énorme ET que vous souhaitez rester à l'écart de la CLI, voici une solution rapide pour réinitialiser manuellement le sous-répertoire:

  1. Basculez vers la branche principale et copiez le sous-répertoire à réinitialiser.
  2. Revenez maintenant à votre branche de fonctionnalité et remplacez le sous-répertoire par la copie que vous venez de créer à l'étape 1.
  3. Validez les modifications.

À votre santé. Vous venez de réinitialiser manuellement un sous-répertoire dans votre branche de fonctionnalité pour qu'il soit identique à celui de la branche principale !!

Mehul Parmar
la source
Je n'ai vu aucun vote pour cette réponse, mais parfois c'est la voie la plus simple garantie vers le succès.
Sue Spence
1

La réponse d' Ajedi32 est ce que je cherchais, mais pour certains commits, j'ai rencontré cette erreur:

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

Peut-être parce que certains fichiers du répertoire sont des fichiers binaires. L'ajout de l'option '--binary' à la commande git diff l'a corrigé:

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

Qu'en est-il de

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
la source