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.
Je peux faire git checkout .
, mais git checkout. Ajoute des répertoires exclus par le contrôle clairsemé
Il y a git reset --hard
, mais cela ne me permet pas de le faire pour un sous-répertoire:
> git reset --hard .
fatal: Cannot do hard reset with paths.
Encore une fois: Pourquoi git ne peut pas faire de réinitialisations dures/douces par chemin?
Je peux inverser le patch sur l'état actuel en utilisant git diff subdir | patch -p1 -R
, mais c'est une façon assez étrange de le faire.
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
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
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? ".
Essayez de changer
_git checkout -- a
_
à
_git checkout -- `git ls-files -m -- a`
_
Depuis la version 1.7.0, Git ls-files
honore 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
_
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
_
Je vais proposer une très mauvaise option ici, car je ne sais pas comment faire quoi que ce soit avec git sauf add
commit
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, add
commit
Push
et le tour est joué Ne déteste pas le joueur, déteste M. Torvalds pour être plus intelligent que nous tous.
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.
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:
À 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 !!
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