Как сделать git reset --hard подкаталога?

199

ОБНОВЛЕНИЕ² : В Git 2.23 (август 2019 г.) есть новая команда, git restoreкоторая делает это, см. Принятый ответ .

ОБНОВЛЕНИЕ : Это будет работать более интуитивно, начиная с Git 1.8.3, смотрите мой собственный ответ .

Представьте себе следующий вариант использования: я хочу избавиться от всех изменений в конкретном подкаталоге моего рабочего дерева Git, оставив все остальные подкаталоги без изменений.

Какова правильная команда Git для этой операции?

Сценарий ниже иллюстрирует проблему. Вставьте соответствующую команду под How to make filesкомментарием - текущая команда восстановит файл, a/c/acкоторый должен быть исключен из-за разреженной проверки. Обратите внимание, что я не хочу явно восстанавливать, a/aи a/bя только «знаю» aи хочу восстановить все ниже. РЕДАКТИРОВАТЬ : И я также не "знаю" b, или какие другие каталоги находятся на том же уровне, что и 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
источник
3
а как насчет git stash && git stash drop?
CharlesB
1
о чем git checkout -- /path/to/subdir/?
Ибербеу,
3
@CharlesB: git stashне принимает аргумент пути ...
krlmlr
@iberbeu: Нет. Также добавит файлы, исключенные из-за разреженной проверки.
krlmlr
1
@CharlesBailey: Тогда почему радиокнопка с надписью «В поисках ответа из достоверных и / или официальных источников». в диалоге щедрости? Я не печатал это сам! Также попробуйте поискать «подкаталог git reset» (без кавычек) и посмотрите, что находится на первых 3 позициях. Наверняка будет сложнее найти сообщение в списке рассылки kernel.org. - Кроме того, для меня еще не ясно, является ли это поведение ошибкой или особенностью.
krlmlr

Ответы:

165

С Git 2.23 (август 2019) у вас есть новая командаgit restore

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

Это заменило бы и индекс, и рабочее дерево HEADсодержимым, как если reset --hardбы, но для определенного пути.


Оригинальный ответ (2013)

Примечание (как заметил по Dan Fabulich ) , что:

  • git checkout -- <path> не выполняет полный сброс: он заменяет содержимое рабочего дерева на поэтапное.
  • git checkout HEAD -- <path>выполняет полный сброс пути, заменяя индекс и рабочее дерево версией из HEADкоммита.

Как ответил на Ajedi32 , обе формы заказа не удалять файлы , которые были удалены в целевой пересмотре .
Если у вас есть дополнительные файлы в рабочем дереве, которых нет в HEAD, git checkout HEAD -- <path>они не будут удалены.

Примечание: С помощью git checkout --overlay HEAD -- <path>(Git 2.22, Q1 2019) файлы, которые появляются в индексе и рабочем дереве, но не в нем <tree-ish>, удаляются, чтобы они <tree-ish>точно совпадали .

Но эта проверка может относиться к git update-index --skip-worktree(для тех каталогов, которые вы хотите игнорировать), как указано в разделе « Почему исключенные файлы продолжают появляться в моей git sparse checkout? ».

VonC
источник
1
Просьба уточнить. После git checkout HEAD -- .этого файлы, исключенные из-за разреженной проверки, появляются снова. Что git update-index --skip-worktreeдолжен делать?
krlmlr
@krlmlr skip-worktree или предположить-без изменений - это два способа сделать запись в индексе «невидимой» для git: fallengamer.livejournal.com/93321.html , stackoverflow.com/q/13630849/6309 и stackoverflow. com / a / 6139470/6309
VonC
@krlmlr эти ссылки - всего лишь указатели, чтобы вы могли попробовать проверить, восстановит ли извлечение эти записи, как только они будут помечены как «skipped-worktree».
VonC
Извините, но это слишком сложно для поставленной задачи. Я хочу инклюзивный сброс, а не эксклюзивный. Неужели нет хорошего способа сделать это в Git?
krlmlr
@krlmlr нет: лучше всего сделать git checkout HEAD -- <path>, а затем удалить каталоги, которые были восстановлены (но которые все еще объявляются в редкой проверке).
VonC
126

По словам разработчика Git Дуи Нгуена (Duy Nguyen), который любезно реализовал функцию и переключатель совместимости , следующее, как и ожидалось, начиная с Git 1.8.3 :

git checkout -- a

(где aнаходится каталог, который вы хотите выполнить с полной перезагрузкой). К исходному поведению можно получить доступ через

git checkout --ignore-skip-worktree-bits -- a
krlmlr
источник
5
Спасибо за ваши усилия по развитию команды разработчиков Git, которые привели к этим изменениям в Git.
Дан Круз,
13
И обратите внимание, что «a» в данном случае означает каталог, который вы хотите вернуть, поэтому, если вы находитесь в каталоге, который хотите вернуть, команда должна быть git checkout -- .где .означает текущий каталог.
TheWestIsThe ...
5
Один комментарий с моей стороны заключается в том, что сначала вы должны удалить папку из папки git reset -- a(где a - каталог, который вы хотите сбросить)
Boyan
Разве это не правда, если вы хотите git reset --hardвесь репо?
krlmlr
И если вы добавили какие-либо новые файлы в этот каталог, сделайте это rm -rf aраньше.
Тобиас Фейл
30

Попробуйте изменить

git checkout -- a

в

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

Начиная с версии 1.7.0, Git отмечает флаг skip-worktree .ls-files

Запуск вашего тестового скрипта (с небольшими изменениями, меняющими git commit... на git commit -qи 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

Запустите ваш тестовый скрипт с предлагаемыми checkoutвыходами изменений:

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
Дэн Круз
источник
Звучит хорошо. Но не стоит ли git checkoutв первую очередь уважать бит "skip-worktree"?
krlmlr
Быстрый обзор checkout.cи tree.cне показывает, что используется флаг пропуска рабочего дерева.
Дан Круз
Это достаточно просто, чтобы быть полезным на практике, даже если мне придется настроить псевдоним bash для этой команды. Duy Nguyen ответил на мое сообщение в списке рассылки Git, посмотрим, появится ли более удобная альтернатива в ближайшее время.
krlmlr
18

В случае простого отказа от изменений, команды git checkout -- path/или, git checkout HEAD -- path/предложенные другими ответами, прекрасно работают. Однако, когда вы хотите сбросить каталог на версию, отличную от HEAD, это решение имеет существенную проблему: оно не удаляет файлы, которые были удалены в целевой редакции.

Поэтому вместо этого я начал использовать следующую команду:

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

Это работает путем нахождения разницы между целевым коммитом и индексом, затем применяя эту разность обратно к рабочему каталогу и индексу. По сути, это означает, что содержимое индекса совпадает с содержимым указанной вами ревизии. Тот факт, что git diffиспользуется аргумент пути, позволяет ограничить этот эффект определенным файлом или каталогом.

Поскольку эта команда довольно длинная, и я планирую использовать ее часто, я создал для нее псевдоним, который я назвал reset-checkout:

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

Вы можете использовать это так:

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

Или просто:

git reset-checkout 451a9a4
Ajedi32
источник
Я видел ваш комментарий вчера и экспериментировал сегодня. Ваш псевдоним полезен. +1
VonC
Как этот параметр сравнивается с git checkout --overlay HEAD -- <path>командой, которую @VonC упоминает в своем ответе?
Эхтеш Чоудхури
1
@EhteshChoudhury Примечание, которое git checkout --overlay HEAD -- <path>еще не выпущено (Git 2.22 будет выпущен во втором квартале 2019 года)
VonC
5

Я собираюсь предложить ужасный вариант здесь, так как я понятия не имею, как сделать что-то с git, кроме как, add commitи pushвот как я «перевернул» подкаталог:

Я запустил новый репозиторий на своем локальном компьютере, вернул все это к коммиту, из которого я хотел скопировать код, а затем скопировал эти файлы в мой рабочий каталог add commit pushи так далее. Не ненавидь игрока, ненавидь мистера Торвальдса за то, что он умнее всех нас.

Авраам Брукс
источник
4

Сброс обычно меняет все, но вы можете использовать, git stashчтобы выбрать то, что вы хотите сохранить. Как вы упомянули, stashон не принимает путь напрямую, но его все равно можно использовать для сохранения определенного пути с --keep-indexфлагом. В вашем примере вы бы спрятали каталог b, а затем сбросили все остальное.

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

Это приведет вас к точке, где последняя часть вашего скрипта выведет это:

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

Я считаю, что это был целевой результат (b остается измененным, файлы / * возвращаются, / / ​​не воссоздается).

Этот подход имеет дополнительное преимущество, поскольку он очень гибкий; вы можете получить как можно более мелкие детали, добавив определенные файлы, но не другие, в каталог.

Джонатан Рен
источник
Это хорошо, но мне придется на git addвсе, кроме a, верно? Звучит сложно на практике.
krlmlr
1
@krlmlr Не совсем. git add .Затем вы можете git reset aдобавить все, кроме a.
Джонатан Рен
1
@krlmlr Также стоит отметить, что git addне добавляет удаленные файлы. Итак, если вы восстанавливаете только удаленные файлы, git add .добавьте все измененные файлы, но не удаленные.
Джонатан Рен
3

Если размер подкаталога не особенно велик, и вы хотите держаться подальше от CLI, вот быстрое решение для ручного сброса подкаталога:

  1. Переключитесь на главную ветку и скопируйте подкаталог для сброса.
  2. Теперь вернитесь к своей ветви функций и замените подкаталог копией, которую вы только что создали на шаге 1.
  3. Зафиксируйте изменения.

Приветствия. Вы просто вручную сбрасываете подкаталог в вашей ветви функций, чтобы он был таким же, как и в основной ветви !!

Мехул Пармар
источник
Я не видел голосов за этот ответ, но иногда это самый простой гарантированный путь к успеху.
Сью Спенс
1

Ajedi32 «S ответа является то , что я искал , но для некоторых коммитов я столкнулся с этой ошибкой:

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

Может быть потому, что некоторые файлы каталога являются двоичными файлами. Добавление опции --binary к команде git diff исправило это:

git diff --binary --cached commit -- path/to/directory | git apply -R --index
khelkun
источник
0

Что о

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 
Кочиз Рухулессин
источник