Как оправиться от "git stash save --all"?

Я хотел спрятать ненужные файлы, но я продолжаю передавать неправильный вариант. Для меня это звучит правильно:

git stash save [-a|--all]

но на самом деле это также игнорирует файлы. Правильный:

git stash save [-u|--include-untracked]

Когда я запускаю git stash save -a и пытаюсь выполнить git stash pop, я получаю бесчисленные ошибки для всех игнорируемых файлов:

path/to/file1.ext already exists, no checkout
path/to/file1.ext already exists, no checkout
path/to/file1.ext already exists, no checkout
...
Could not restore untracked files from stash

поэтому команда не работает.

Как мне вернуть мои отслеживаемые и неотслеживаемые спрятанные изменения? git reflog не хранит команды штампа.

Ответ 1

TL; версия DR:

Вам нужно, чтобы каталог был чистым (в терминах git clean) для правильного применения кошелька. Это означает, что вы выполняете git clean -f или даже git clean -fdx, что является своего рода уродливой вещью, потому что некоторые из неперехваченных или не отслеживаемых и проигнорированных файлов/каталогов могут быть элементами, которые вы хотите сохранить, а не удалять полностью. (Если это так, вы должны переместить их за пределами своего рабочего дерева вместо git clean - отбросить их. Помните, что файлы, удаляемые git clean, - это те, которые вы не можете получить из Git!)

Чтобы понять, почему, посмотрите на шаг 3 в описании "apply". Обратите внимание, что нет возможности пропустить ненужные и/или проигнорированные файлы в тире.

Основные факты о самом кошельке

Когда вы используете git stash save с помощью -u или -a, stash script записывает свой пакетный портфель в качестве трехстороннего фиксации а не обычное двух-родительское.

Диаграммно, "сумка с чемоданом" обычно выглядит так: с точки зрения графика фиксации:

o--o--C     <-- HEAD (typically, a branch)
      |\
      i-w   <-- stash

o - это любые старые обычные узлы фиксации, как и C. Node C (для Commit) есть письмо, чтобы мы могли назвать его: там, где висит "сумка для хранения".

Сам портфель представляет собой маленький треугольный мешок, висящий от C, и он содержит две коммиты: w - это фиксация дерева работы, а i - фиксация индекса. (Не показано, потому что это просто сложно для диаграммы, является тот факт, что w первый родительский C, а второй его родительский i.)

С --untracked или --all есть третий родительский элемент для w, поэтому диаграмма выглядит примерно так:

o--o--C     <-- HEAD
      |\
      i-w   <-- stash
       /
      u

(эти диаграммы действительно должны быть изображениями, чтобы они могли иметь стрелки, а не ASCII-искусство, где стрелки трудно включить). В этом случае stash - commit w, stash^ - commit C (еще еще HEAD), stash^2 - commit i, а stash^3 - commit u, который содержит файлы "без следа" или даже "не отслеживаемые и проигнорированные" файлы. (На самом деле это не важно, насколько я могу судить, но я добавлю, что i имеет C в качестве родительского коммита, а u - безболезненный или root-commit. для этой причины это просто то, что делает script, но это объясняет, почему "стрелки" (линии) такие же, как на диаграмме.)

Различные параметры в save время

В режиме экономии времени вы можете указать любые или все из следующих параметров:

  • -p, --patch
  • -k, --keep-index, --no-keep-index
  • -q, --quiet
  • -u, --include-untracked
  • -a, --all

Некоторые из них подразумевают, переопределяют или отключают другие. Например, использование -p полностью изменяет алгоритм, используемый script для создания stash, а также включает --keep-index, заставляя вас использовать --no-keep-index, чтобы отключить его, если вы этого не хотите. Он несовместим с -a и -u и будет выходить из строя, если дано какое-либо из них.

В противном случае между -a и -u, какой бы ни был установлен последний, сохраняется.

В этот момент script создается либо одна, либо две коммиты:

  • один для текущего индекса (даже если он не содержит изменений), с parent commit C
  • с -u или -a, безпользовательская фиксация, содержащая (только) либо невоспроизводимые файлы, либо все (незатребованные и проигнорированные) файлы.

stash script затем сохраняет ваше текущее дерево работы. Он делает это с временным индексным файлом (в основном, новой промежуточной областью). С помощью -p script считывает фиксацию HEAD в новую промежуточную область, тогда эффективно 1 запускает git add -i --patch, так что этот индекс завершается выбранными патчами. Без -p он просто разворачивает рабочий каталог с сохраненным индексом для поиска измененных файлов. 2 В любом случае он записывает древовидный объект из временного индекса. Это дерево будет деревом для commit w.

В качестве последнего шага создания шага, script использует только что сохраненное дерево, parent commit C, фиксация индекса и корневой фиксации для необработанных файлов, если он существует, для создания окончательной фиксации stash w. Тем не менее, script затем выполняет еще несколько шагов, которые влияют на ваш рабочий каталог, в зависимости от того, используете ли вы -a, -u, -p и/или --keep-index (и помните, что -p подразумевает --keep-index):

  • С -p:

    • "Обратный патч" рабочего каталога, чтобы удалить разницу между HEAD и типом. По сути, это оставляет рабочий каталог только с теми изменениями, которые не были спрятаны (в частности, те, которые не заключены в commit w, здесь все в commit i).

    • Только если вы указали --no-keep-index: запустите git reset (без каких-либо параметров вообще, т.е. git reset --mixed). Это очищает состояние "быть совершенным" для всего, не меняя ничего другого. (Конечно, любые частичные изменения, которые вы поставили перед запуском git stash save -p, с git add или git add -p, сохраняются в commit i.)

  • Без -p:

    • Запустите git reset --hard (с помощью -q, если вы тоже это указали). Это устанавливает дерево работы в состояние в HEAD commit.

    • Только если вы указали -a или -u: запустите git clean --force --quiet -d (с помощью -x, если -a, или без него, если -u). Это удаляет все неиспользуемые файлы, включая неподготовленные каталоги; с -x (т.е. в режиме -a), он также удаляет все проигнорированные файлы.

    • Только если вы указали -k/--keep-index: используйте git read-tree --reset -u $i_tree, чтобы "вернуть" сохраненный индекс как "изменения, которые необходимо зафиксировать", которые также отображаются в дереве работ. (--reset не должен иметь эффекта, так как шаг 1 очистил дерево работы.)

Различные параметры в apply время

Двумя основными подкомандами, восстанавливающими тайник, являются apply и pop. Код pop запускает только apply, а затем, если apply преуспевает, запускается drop, поэтому фактически существует только apply. (Ну, есть также branch, что немного сложнее, но в конце он также использует apply.)

Когда вы применяете stash-любой "похожий на stash-объект", на самом деле, то есть все, что stash script может обрабатывать как сумка-стоп-ловушка, есть только две специфические опции:

  • -q, --quiet
  • --index (не --keep-index!)

Другие флаги накапливаются, но в любом случае их все равно игнорируют. (Тот же код синтаксического анализа используется для show, а здесь остальные флаги передаются на git diff.)

Все остальное контролируется содержимым складок и статусом рабочего дерева и индекса. Как и выше, я буду использовать метки w, i и u для обозначения различных коммитов в кошельке и C для обозначения фиксации, из которой висит сумка.

Последовательность apply выглядит так: если все идет хорошо (если что-то не срабатывает раньше, например, мы находимся в середине слияния или git apply --cached не работает, ошибки script в этой точке)

  • записать текущий индекс в дерево, убедившись, что мы не находимся в середине слияния
  • только если --index: diff commit i против commit C, pipe to git apply --cached, сохраните полученное дерево и используйте git reset, чтобы отключить его
  • только если u существует: используйте git read-tree и git checkout-index --all с временным индексом, чтобы восстановить дерево u
  • используйте git merge-recursive, чтобы объединить дерево для C ( "база" ) с тем, что было написано на шаге 1 ( "обновлено вверх по течению" ) и деревом в w ( "спрятанные изменения" )

После этого момента он немного усложняется:-), поскольку это зависит от того, прошел ли слияние на шаге 4. Но сначала немного расширьте это.

Шаг 1 довольно прост: script просто запускает git write-tree, что не удается, если в индексе есть несвязанные записи. Если дерево записи работает, результатом является идентификатор дерева ($c_tree в script).

Шаг 2 является более сложным, так как он проверяет не только параметр --index, но также и $b_tree != $i_tree (т.е. существует разница между деревом для C и деревом для i) и что $c_tree!= $i_tree (т.е. существует разница между деревом, выписанным на шаге 1, и деревом для i). Тест для $b_tree != $i_tree имеет смысл: он проверяет, есть ли какие-либо изменения для применения. Если нет изменений - если дерево для i соответствует значению для C - нет никакого индекса для восстановления, а --index не нужен в конце концов. Однако, если $i_tree соответствует $c_tree, это означает, что текущий индекс уже содержит изменения, которые нужно восстановить с помощью --index. Верно, что в этом случае мы не хотим git apply те изменения; но мы хотим, чтобы они оставались "восстановленными". (Возможно, что пункт кода я не совсем понимаю ниже. Вероятнее всего, здесь есть небольшая ошибка.)

В любом случае, если шаг 2 должен запускаться git apply --cached, он также запускает git write-tree для записи дерева, сохраняя это в переменной script $unstashed_index_tree. В противном случае $unstashed_index_tree остается пустым.

Шаг 3 - это то, что происходит в "нечистом" каталоге. Если в stash присутствует фиксация u, script настаивает на ее извлечении, но git checkout-index --all не удастся, если какой-либо из этих файлов будет перезаписан. (Обратите внимание, что это делается с временным файлом индекса, который затем удаляется: шаг 3 вообще не использует обычную промежуточную область.)

(В шаге 4 используются три "волшебные" переменные среды, которые я не видел документально: $GITHEAD_t предоставляет "имя" объединяемых деревьев. Для запуска git merge-recursive script содержит четыре аргумента: $b_tree -- $c_tree $w_tree. Как уже отмечалось, это деревья для базового commit C, index-at-start-of-apply и зафиксированная работа commit w. string-names для каждого из этих деревьев, git merge-recursive ищет в среде имена, сформированные путем добавления GITHEAD_ к необработанному SHA-1 для каждого дерева. script не передает никаких аргументов стратегии git merge-recursive, ни пусть вы выберете любую стратегию, отличную от recursive. Вероятно, она должна.)

Если у слияния есть конфликт, stash script запускает git rerere (q.v.), и если --index сообщает вам, что индекс не был восстановлен и завершен с статусом слияния-конфликта. (Как и в случае других ранних выходов, это предотвращает отбрасывание тэга pop.)

Если слияние завершено успешно, выполните следующие действия:

  • Если у нас есть $unstashed_index_tree -ie, мы делаем --index, и все остальные тесты на шаге 2 тоже прошли, тогда нам нужно восстановить состояние индекса, созданное на шаге 2. В этом случай простой git read-tree $unstashed_index_tree (без параметров) делает трюк.

  • Если у нас нет чего-то в $unstashed_index_tree, script использует git diff-index --cached --name-only --diff-filter=A $c_tree для поиска файлов для добавления, запускает git read-tree --reset $c_tree для слияния одного дерева с исходным сохраненным индексом и затем git update-index --add с именами файлов из более раннего diff-index. Я не совсем уверен, почему он подходит к этим длинам (есть подсказка на странице руководства git-read-tree, об избежании ложных ударов для модифицированных файлов, которые могут это объяснить), но что он делает.

Наконец, script запускает git status (с выходом, отправленным в /dev/null для режима -q, не уверен, почему он вообще работает под -q).

Несколько слов на git stash branch

Если у вас возникли проблемы с применением кошелька, вы можете превратить его в "реальную ветвь", что делает его гарантированным для восстановления (за исключением, как обычно, проблемы с приложением, содержащим фиксацию u не применяя, если вы сначала очищаете неустановленные и, возможно, даже игнорируемые файлы).

Трюк здесь - начать с проверки commit C (например, git checkout stash^). Это, конечно, приводит к "отсоединенной HEAD", поэтому вам нужно создать новую ветку, которую вы можете комбинировать с шагом, который проверяет commit C:

git checkout -b new_branch stash^

Теперь вы можете применить stash, даже с --index, и он должен работать, так как он будет применяться к одному и тому же фиксатору. Шкатулка зависает от:

git stash apply --index

В этот момент все более ранние поэтапные изменения должны быть снова поставлены, а любые ранее не прошедшие этап (но отслеживаемые) файлы будут иметь свои неустановленные, но отслеживаемые изменения в рабочем каталоге. Теперь безопасно оставить бросок:

git stash drop

Использование:

git stash branch new_branch

просто выполняет указанную выше последовательность. Он буквально запускает git checkout -b, и если это удастся, применяется тайник (с --index) и затем его удаляет.

После этого вы можете зафиксировать индекс (если хотите), затем добавить и зафиксировать оставшиеся файлы, сделать два (или один, если вы оставите первый, индекс, фиксацию), "обычный", зафиксирует "регулярная" ветвь:

o-o-C-o-...   <-- some_branch
     \
      I-W     <-- new_branch

и вы конвертировали сумку i и w, которая совершает обычные, на ветке фиксирует i и w.


1 Вернее, он запускает git add-interactive --patch=stash --, который напрямую вызывает perl script для интерактивного добавления, со специальным набором магии для стирки. Есть еще несколько магических режимов --patch; см. script.

2 Здесь очень небольшая ошибка: git считывает $i_tree, зафиксированное дерево индексов во временный индекс, но затем разграничивает рабочий каталог на HEAD. Это означает, что если вы изменили какой-либо файл f в индексе, а затем изменили его обратно в соответствии с версией HEAD, дерево работы, хранящееся в w в сумке-контейнере, содержит индексную версию f вместо версии дерева f.

Ответ 2

Не зная, почему проблема возникает, я нашел быстрое решение:

git show -p --no-color [<stash>] | git apply

Параметр --no-color удаляет любые цвета из выходного сигнала diff, потому что они привинчивают команду git apply.

Однако было бы здорово, если бы кто-то мог отредактировать этот ответ, объяснив, почему git stash pop не удается.