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

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? ".


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

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

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

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.


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.


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 !!


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

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 
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.