Como git reset --hard um subdiretório?

197

ATUALIZAÇÃO² : Com o Git 2.23 (agosto de 2019), há um novo comando git restoreque faz isso, veja a resposta aceita .

ATUALIZAÇÃO : Isso funcionará de forma mais intuitiva a partir do Git 1.8.3, veja minha própria resposta .

Imagine o seguinte caso de uso: Quero me livrar de todas as alterações em um subdiretório específico da minha árvore de trabalho Git, deixando todos os outros subdiretórios intactos.

Qual é o comando Git apropriado para esta operação?

O script abaixo ilustra o problema. Insira o comando apropriado abaixo do How to make filescomentário - o comando atual restaurará o arquivo a/c/acque deveria ser excluído pelo checkout esparso. Observe que eu não quero restaurar explicitamente a/ae a/b, apenas "sei" ae quero restaurar tudo abaixo. EDIT : E eu também não "sei" b, ou quais outros diretórios residem no mesmo nível 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
fonte
3
que tal um git stash && git stash drop?
precisa
1
que tal git checkout -- /path/to/subdir/?
Iberbeu
3
@CharlesB: git stashnão aceita um argumento caminho ...
krlmlr
@iberbeu: Não. Também adicionará arquivos excluídos pelo checkout esparso.
krlmlr
1
@CharlesBailey: Então, por que existe um botão de opção com a mensagem "Procurando uma resposta de fontes credíveis e / ou oficiais". no diálogo de recompensa? Eu não digitei isso sozinho! Tente também pesquisar no "subdiretório git reset" do Google (sem as aspas) e veja o que está nas três primeiras posições. Com certeza, será mais difícil encontrar uma mensagem para uma lista de discussão do kernel.org. - Além disso, para mim ainda não está claro se esse comportamento é um bug ou um recurso.
krlmlr

Respostas:

162

Com o Git 2.23 (agosto de 2019), você tem o novo comandogit restore

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

Isso substituiria o índice e a árvore de trabalho pelo HEADconteúdo, como reset --hardfaria, mas por um caminho específico.


Resposta original (2013)

Observe (como comentado por Dan Fabulich ) que:

  • git checkout -- <path> não faz uma reinicialização completa: substitui o conteúdo da árvore de trabalho pelo conteúdo preparado.
  • git checkout HEAD -- <path>faz uma redefinição definitiva para um caminho, substituindo o índice e a árvore de trabalho pela versão do HEADcommit.

Conforme respondido por Ajedi32 , os dois formulários de checkout não removem arquivos que foram excluídos na revisão de destino .
Se você tiver arquivos extras na árvore de trabalho que não existem no HEAD, a git checkout HEAD -- <path>não os removerá.

Nota: Com git checkout --overlay HEAD -- <path>(Git 2.22, primeiro trimestre de 2019) , os arquivos que aparecem no índice e na árvore de trabalho, mas não dentro, <tree-ish>são removidos, para fazer com que correspondam <tree-ish>exatamente.

Mas esse check-out pode respeitar um git update-index --skip-worktree(para os diretórios que você deseja ignorar), conforme mencionado em " Por que os arquivos excluídos continuam reaparecendo no meu check-out esparso do git? ".

VonC
fonte
1
Por favor, esclareça. Depois git checkout HEAD -- ., os arquivos excluídos pelo checkout esparso reaparecem. O que git update-index --skip-worktreedeveria fazer?
krlmlr
@krlmlr skip-worktree ou assume-inalterado são as duas maneiras de tentar tornar uma entrada no índice "invisível" para o git: fallengamer.livejournal.com/93321.html , stackoverflow.com/q/13630849/6309 e stackoverflow. com / a / 6139470/6309
VonC 14/03
@krlmlr esses links são apenas indicadores para você tentar e ver se um checkout ainda restauraria essas entradas, uma vez que elas foram marcadas como 'área de trabalho ignorada'.
VonC 14/03
Desculpe, mas isso é muito complicado para a tarefa em questão. Eu quero uma redefinição inclusiva, não exclusiva. Não há realmente nenhuma maneira legal de fazer isso no Git?
krlmlr
@krlmlr no: melhor para fazer isso git checkout HEAD -- <path>e, em seguida, exclua os diretórios que foram restaurados (mas que ainda são declarados no checkout esparso).
VonC 14/03
125

De acordo com o desenvolvedor do Git Duy Nguyen, que gentilmente implementou o recurso e um comutador de compatibilidade , o seguinte funciona como esperado no Git 1.8.3 :

git checkout -- a

(onde aé o diretório que você deseja restaurar). O comportamento original pode ser acessado via

git checkout --ignore-skip-worktree-bits -- a
krlmlr
fonte
5
Obrigado pelo seu esforço em acompanhar a equipe de desenvolvimento do Git, resultando nessa alteração no Git.
Dan Cruz
13
E observe que "a", neste caso, significa o diretório que você deseja reverter; portanto, se você estiver no diretório que deseja reverter, o comando deve estar git checkout -- .onde .significa o diretório atual.
TheWestIsThe ...
5
Um comentário do meu lado é que você deve unstage a pasta primeiro com git reset -- a(onde a é o diretório que você deseja redefinir)
Boyan
Isso também não é verdade se você deseja git reset --hardo repositório inteiro?
precisa saber é
E se você adicionou novos arquivos a esse diretório, faça um rm -rf aantes.
Tobias Feil
30

Tente mudar

git checkout -- a

para

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

Desde a versão 1.7.0, o Git honra o sinalizador skip-worktree .ls-files

Executando seu script de teste (com alguns pequenos ajustes mudando git commit... para git commit -qe git statuspara git status --short) saídas:

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

Executando seu script de teste com as checkoutsaídas de mudança propostas :

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
fonte
Parece bom. Mas não deveria git checkoutrespeitar o bit "skip-worktree" em primeiro lugar?
krlmlr
Uma rápida revisão checkout.ce tree.cnão revela que o sinalizador skip-worktree é usado.
Dan Cruz
Isso é simples o suficiente para ser útil na prática, mesmo que eu precise configurar um alias do bash para este comando. Duy Nguyen respondeu à minha mensagem na lista de discussão do Git, vamos ver se uma alternativa mais amigável aparecerá em breve.
krlmlr
18

No caso de simplesmente descartar alterações, os comandos git checkout -- path/ou git checkout HEAD -- path/sugeridos por outras respostas funcionam muito bem. No entanto, quando você deseja redefinir um diretório para uma revisão diferente de HEAD, essa solução tem um problema significativo: ela não remove os arquivos que foram excluídos na revisão de destino.

Então, em vez disso, comecei a usar o seguinte comando:

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

Isso funciona encontrando a diferença entre a confirmação de destino e o índice, aplicando essa diferença inversamente ao diretório e índice de trabalho. Basicamente, isso significa que faz com que o conteúdo do índice corresponda ao conteúdo da revisão que você especificou. O fato de git diffusar um argumento de caminho permite limitar esse efeito a um arquivo ou diretório específico.

Como esse comando é bastante longo e pretendo usá-lo com frequência, configurei um apelido para ele, que chamei de reset-checkout:

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

Você pode usá-lo assim:

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

Ou apenas:

git reset-checkout 451a9a4
Ajedi32
fonte
Vi seu comentário ontem e experimentei hoje. Seu alias é útil. 1
VonC 16/01/2015
Como essa opção se compara ao git checkout --overlay HEAD -- <path>comando que o @VonC menciona em sua resposta?
Ehtesh Choudhury
1
@EhteshChoudhury Observe que git checkout --overlay HEAD -- <path>ainda não foi lançado (o Git 2.22 será lançado no segundo trimestre de 2019)
VonC 03/04/19
5

Vou oferecer uma opção terrível aqui, já que não tenho idéia de como fazer algo com o git, exceto , add commite pushaqui está como eu "reverti" um subdiretório:

Iniciei um novo repositório no meu PC local, reverti tudo para o commit do qual eu queria copiar o código e depois copiei esses arquivos para o meu diretório de trabalho, add commit pushe pronto. Não odeie o jogador, odeie o Sr. Torvalds por ser mais esperto do que todos nós.

Abraham Brookes
fonte
4

Uma redefinição normalmente muda tudo, mas você pode usar git stashpara escolher o que deseja manter. Como você mencionou, stashnão aceita um caminho diretamente, mas ainda pode ser usado para manter um caminho específico com o --keep-indexsinalizador. No seu exemplo, você esconderia o diretório b e redefiniria todo o 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)

Isso leva você a um ponto em que a última parte do seu script produzirá isso:

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

Acredito que esse foi o resultado desejado (b permanece modificado, arquivos a / * estão de volta, a / c não é recriado).

Essa abordagem tem o benefício adicional de ser muito flexível; você pode obter a granulação mais fina que desejar adicionando arquivos específicos, mas não outros, em um diretório.

Jonathan Wren
fonte
Isso é legal, mas eu teria que fazer git addtudo a, exceto , certo? Parece difícil na prática.
krlmlr
1
@krlmlr Na verdade não. Você pode git add .então git reset aadicionar tudo, exceto a.
Jonathan Wren
1
@krlmlr Além disso, vale ressaltar que git addnão adiciona arquivos excluídos. Portanto, se você estiver apenas recuperando arquivos excluídos, git add .adicionará todos os arquivos modificados, mas não os excluídos.
Jonathan Wren
3

Se o tamanho do subdiretório não for particularmente grande E você desejar ficar longe da CLI, aqui está uma solução rápida para redefinir manualmente o subdiretório:

  1. Alterne para a ramificação principal e copie o subdiretório a ser redefinido.
  2. Agora volte para o ramo de recursos e substitua o subdiretório pela cópia que você acabou de criar na etapa 1.
  3. Confirme as alterações.

Felicidades. Você acabou de redefinir manualmente um subdiretório no ramo de recursos para ser o mesmo do ramo principal !!

Mehul Parmar
fonte
Não vi votos para esta resposta, mas às vezes esse é o caminho mais simples e garantido para o sucesso.
Sue Spence
1

A resposta do Ajedi32 é o que eu estava procurando, mas para alguns commits, encontrei este erro:

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

Pode ser porque alguns arquivos do diretório são arquivos binários. A adição da opção '--binary' ao comando git diff o corrigiu:

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

A respeito

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
fonte