web-dev-qa-db-fra.com

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

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

Imaginons le cas d'utilisation suivant: je souhaite supprimer toutes les modifications apportées à 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 commentaire How to make files - la commande en cours restaure le fichier a/c/ac qui est supposé être exclu par le contrôle clairsemé. Notez que je ne souhaite pas restaurer explicitement a/a et a/b, je ne fais que "connaître" a et je souhaite tout restaurer ci-dessous. EDIT: Et je ne sais pas non plus "b, ou quels autres répertoires se trouvent 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
166
krlmlr

Selon le développeur de 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. :

git checkout -- a

(où a est le répertoire que vous voulez réinitialiser) Le comportement initial peut être consulté via

git checkout --ignore-skip-worktree-bits -- a
108
krlmlr

Notez (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 mis en scène.
  • git checkout HEAD -- <path> effectue une réinitialisation matérielle pour un chemin, en remplaçant l'index et l'arbre de travail par la version issue du HEAD commit.

Comme répond par Ajedi32 , les deux formulaires de commande ne suppriment pas les fichiers supprimés dans la révision cible .
Si vous avez des fichiers supplémentaires dans l’arbre de travail qui n’existent pas dans HEAD, un 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 dans l'arbre 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 sparse? ".

122
VonC

Essayez de changer

_git checkout -- a
_

à

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

Depuis la version 1.7.0, Git ls-fileshonore le drapeau skip-worktree .

L’exécution de votre script de test (avec quelques modifications mineures modifiant les valeurs de _git commit_... à _git commit -q_ et _git status_ à _git status --short_):

_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écuter votre script de test avec les sorties checkout change 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
_
28
Dan Cruz

Pour le cas de simplement ignorer les modifications, les commandes _git checkout -- path/_ ou _git checkout HEAD -- path/_ suggérées par d'autres réponses fonctionnent très bien. Toutefois, lorsque vous souhaitez réinitialiser un répertoire vers une révision autre que HEAD, cette solution présente un problème important: elle ne supprime pas les fichiers supprimés dans la révision cible.

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

git diff _--cached_ commit _--_ subdir _|_ git apply _-R --index_

Cela fonctionne en recherchant le diff entre le commit cible et l'index, puis en appliquant ce diff de manière inverse au répertoire de travail et à l'index. En gros, cela signifie que le contenu de l'index correspond au contenu de la révision que vous avez spécifiée. Le fait que _git diff_ prenne un argument de chemin permet de limiter cet effet à un fichier ou à un répertoire spécifique.

Comme cette commande est assez longue et que je prévois l’utiliser fréquemment, j’ai créé un alias pour cela, 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
_
17
Ajedi32

Je vais proposer une très mauvaise option ici, car je ne sais pas comment faire quoi que ce soit avec git sauf addcommit et Push, voici comment j'ai "retourné" un sous-répertoire:

J'ai commencé un nouveau référentiel sur mon ordinateur local, j'ai rétabli le commit dans lequel je voulais copier le code, puis copié ces fichiers dans mon répertoire de travail, addcommitPush et le tour est joué Ne déteste pas le joueur, déteste M. Torvalds pour être plus intelligent que nous tous.

4
Abraham Brookes

Une réinitialisation va normalement tout changer, mais vous pouvez utiliser git stash pour choisir ce que vous souhaitez conserver. Comme vous l'avez mentionné, stash n'accepte pas directement un chemin, mais il peut toujours être utilisé pour conserver un chemin spécifique avec l'indicateur _--keep-index_. Dans votre exemple, vous cacheriez le répertoire b, puis réinitialisez 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 va générer 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 recherché (b reste modifié, les fichiers/* sont de retour, a/c n’est pas recréé).

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

4
Jonathan Wren

Si la taille du sous-répertoire n'est pas particulièrement énorme ET si vous souhaitez rester à l'écart de la CLI, voici une solution rapide pour manuellement réinitialiser 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. Commettez les modifications.

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

3
Mehul Parmar

Ajedi32 's answer 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
1
khelkun