Come ripristinare git --hard una sottodirectory?


197

AGGIORNAMENTO² : Con Git 2.23 (agosto 2019), c'è un nuovo comando git restoreche fa questo, vedi la risposta accettata .

AGGIORNAMENTO : Funzionerà in modo più intuitivo a partire da Git 1.8.3, vedi la mia risposta .

Immagina il seguente caso d'uso: voglio sbarazzarmi di tutte le modifiche in una specifica sottodirectory del mio albero di lavoro Git, lasciando intatte tutte le altre sottodirectory.

Qual è il comando Git corretto per questa operazione?

Lo script seguente illustra il problema. Inserisci il comando corretto sotto il How to make filescommento: il comando corrente ripristinerà il file a/c/acche dovrebbe essere escluso dal checkout sparse. Nota che non voglio ripristinare esplicitamente a/ae a/b, solo "so" ae voglio ripristinare tutto ciò che segue. EDIT : E anche io non "sapere" b, o quali altre directory risiedono allo stesso livello di 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

3
che dire di un git stash && git stash drop?
CharlesB

1
che dire git checkout -- /path/to/subdir/?
iberbeu,

3
@CharlesB: git stashnon accetta un argomento sul percorso ...
krlmlr

@iberbeu: No. Aggiungerà anche i file esclusi dal checkout sparse.
krlmlr

1
@CharlesBailey: Allora perché c'è un pulsante di opzione che legge "Alla ricerca di una risposta che attinge da fonti credibili e / o ufficiali". nella finestra di dialogo di taglie? Non l'ho scritto da solo! Prova anche a cercare "sottodirectory git reset" (senza virgolette) e guarda cosa c'è nelle prime 3 posizioni. Sicuramente un messaggio a una mailing list di kernel.org sarà più difficile da trovare. - Inoltre, per me non è ancora chiaro se questo comportamento sia un bug o una funzionalità.
krlmlr

Risposte:


162

Con Git 2.23 (agosto 2019), hai il nuovo comandogit restore

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

Ciò sostituirebbe sia l'indice che l'albero di lavoro con HEADcontenuti, come reset --hardfarebbe, ma per un percorso specifico.


Risposta originale (2013)

Nota (come commentato da Dan Fabulich ) che:

  • git checkout -- <path> non esegue un hard reset: sostituisce il contenuto dell'albero di lavoro con il contenuto graduale.
  • git checkout HEAD -- <path>esegue un hard reset per un percorso, sostituendo sia l'indice che l'albero di lavoro con la versione dal HEADcommit.

Come risposto da Ajedi32 , entrambe le forme checkout non rimuovere i file che sono stati eliminati nella revisione di destinazione .
Se nella struttura di lavoro sono presenti file aggiuntivi che non esistono in HEAD, git checkout HEAD -- <path>non verranno rimossi.

Nota: con git checkout --overlay HEAD -- <path>(Git 2.22, Q1 2019) , i file che appaiono nell'indice e nell'albero di lavoro, ma non in, <tree-ish>vengono rimossi per farli corrispondere <tree-ish>esattamente.

Ma quel checkout può rispettare un git update-index --skip-worktree(per quelle directory che vuoi ignorare), come menzionato in " Perché i file esclusi continuano a riapparire nel mio checkout sparso git? ".


1
Si prega di precisare. Successivamente git checkout HEAD -- ., ricompaiono i file esclusi dal checkout sparse. Cosa git update-index --skip-worktreedovrebbe fare?
krlmlr,

@krlmlr salta-worktree o assumere-invariati sono i due modo di cercare di fare una voce nell'indice "invisibile" per git: fallengamer.livejournal.com/93321.html , stackoverflow.com/q/13630849/6309 e StackOverflow. com / a / 6139470/6309
VonC

@krlmlr quei collegamenti sono solo dei suggerimenti per provare a vedere se un checkout ripristinerebbe comunque quelle voci, una volta che sono state contrassegnate come 'skpped-worktree'.
VonC

Siamo spiacenti, ma è troppo complicato per l'attività da svolgere. Voglio un ripristino inclusivo, non esclusivo. Non c'è davvero un bel modo per farlo in Git?
krlmlr

@krlmlr no: meglio fare il git checkout HEAD -- <path>, quindi eliminare le directory che sono state ripristinate (ma che sono ancora dichiarate nel checkout sparse).
VonC

125

Secondo lo sviluppatore di Git Duy Nguyen, che ha gentilmente implementato la funzionalità e un interruttore di compatibilità , i seguenti funzionano come previsto da Git 1.8.3 :

git checkout -- a

(dove si atrova la directory da ripristinare). È possibile accedere al comportamento originale tramite

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

5
Grazie per il tuo impegno nel dare seguito al team di sviluppo di Git, portando a questo cambiamento in Git.
Dan Cruz,

13
E nota che "a" in questo caso indica la directory che vuoi ripristinare, quindi se sei nella directory che vuoi ripristinare, il comando dovrebbe essere git checkout -- .dove .significa la directory corrente.
TheWestIsThe ...

5
Un commento da parte mia è che devi prima disinstallare la cartella con la cartella git reset -- a(dove a è la directory che vuoi ripristinare)
Boyan,

Non è vero anche se vuoi git reset --hardl'intero repository?
krlmlr,

E se hai aggiunto nuovi file a quella directory, fai rm -rf aprima.
Tobias Feil,

30

Prova a cambiare

git checkout -- a

per

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

Dalla versione 1.7.0, Git's onora la bandiera skip-worktree .ls-files

Esecuzione dello script di test (con alcune piccole modifiche che cambiano git commit... in git commit -qe git statusto git status --short) output:

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

Esecuzione dello script di test con gli checkoutoutput di modifica proposti :

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

Suona bene. Ma non dovremmo git checkoutrispettare il bit "skip-worktree" in primo luogo?
krlmlr

Una rapida revisione checkout.ce tree.cnon rivela che viene utilizzato il flag skip-worktree.
Dan Cruz,

Questo è abbastanza semplice per essere utile nella pratica, anche se dovrò impostare un alias bash per questo comando. Duy Nguyen ha risposto al mio messaggio alla mailing list di Git, vediamo se presto comparirà un'alternativa più user-friendly.
krlmlr,

18

Nel caso di scartare semplicemente le modifiche, i comandi git checkout -- path/o git checkout HEAD -- path/suggeriti da altre risposte funzionano alla grande. Tuttavia, quando si desidera reimpostare una directory su una revisione diversa da HEAD, quella soluzione presenta un problema significativo: non rimuove i file che sono stati eliminati nella revisione di destinazione.

Quindi, invece, ho iniziato a utilizzare il seguente comando:

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

Funziona individuando la differenza tra il commit di destinazione e l'indice, quindi applicando la differenza inversa alla directory e all'indice di lavoro. Fondamentalmente, questo significa che fa corrispondere il contenuto dell'indice al contenuto della revisione specificata. Il fatto che git diffaccetta un argomento path consente di limitare questo effetto a un file o una directory specifici.

Poiché questo comando è abbastanza lungo e ho intenzione di usarlo frequentemente, ho creato un alias per questo che ho chiamato reset-checkout:

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

Puoi usarlo in questo modo:

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

O semplicemente:

git reset-checkout 451a9a4

Ho visto il tuo commento ieri e l'ho sperimentato oggi. Il tuo alias è utile. +1
VonC

Come si confronta questa opzione con il git checkout --overlay HEAD -- <path>comando che @VonC menziona nella sua risposta?
Ehtesh Choudhury,

1
@EhteshChoudhury Nota che git checkout --overlay HEAD -- <path>non è stato ancora rilasciato (Git 2.22 sarà rilasciato nel secondo trimestre del 2019)
VonC

5

Offrirò una terribile opzione qui, dal momento che non ho idea di come fare qualsiasi cosa con git tranne , add commited pushecco come ho "ripristinato" una sottodirectory:

Ho avviato un nuovo repository sul mio PC locale, ho ripristinato il tutto sull'impegno da cui volevo copiare il codice e quindi ho copiato quei file nella mia directory di lavoro, add commit pushecc. Non odiare il giocatore, odia il signor Torvalds per essere più intelligente di tutti noi.


4

Un reset normalmente cambia tutto, ma è possibile utilizzare git stashper scegliere ciò che si desidera conservare. Come hai detto, stashnon accetta direttamente un percorso, ma può comunque essere utilizzato per mantenere un percorso specifico con la --keep-indexbandiera. Nel tuo esempio, devi nascondere la directory b, quindi reimpostare tutto il resto.

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

Questo ti porta a un punto in cui l'ultima parte dello script produrrà questo:

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

Credo che questo sia stato il risultato finale (b rimane modificato, i file a / * sono tornati, a / c non è stato ricreato).

Questo approccio ha l'ulteriore vantaggio di essere molto flessibile; puoi ottenere la granularità che desideri aggiungendo file specifici, ma non altri, in una directory.


È carino, ma dovrei fare git addtutto tranne a, giusto? Sembra difficile in pratica.
krlmlr

1
@krlmlr Non proprio. È git add .quindi possibile git reset aaggiungere tutto tranne a.
Jonathan Wren,

1
@krlmlr Inoltre, vale la pena notare che git addnon aggiunge file eliminati. Quindi, se stai solo recuperando i file eliminati, git add .aggiungerai tutti i file modificati, ma non quelli eliminati.
Jonathan Wren,

3

Se la dimensione della sottodirectory non è particolarmente grande, E si desidera stare lontani dalla CLI, ecco una rapida soluzione per ripristinare manualmente la sottodirectory:

  1. Passa al ramo principale e copia la sottodirectory da ripristinare.
  2. Ora torna al ramo delle funzionalità e sostituisci la sottodirectory con la copia appena creata nel passaggio 1.
  3. Conferma le modifiche.

Saluti. Hai appena ripristinato manualmente una sottodirectory nel tuo ramo delle caratteristiche per essere uguale a quella del ramo principale !!


Non ho visto alcun voto per questa risposta, ma a volte questa è la via più semplice garantita per il successo.
Sue Spence,

1

La risposta di Ajedi32 è ciò che stavo cercando, ma per alcuni commit ho riscontrato questo errore:

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

Può essere perché alcuni file della directory sono file binari. Aggiunta dell'opzione '--binary' al comando git diff risolto:

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

0

Che dire

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 
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.