Как оправиться от "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
не хранит команды хранения.
2 ответа
TL;DR версия:
Вам нужен каталог, чтобы быть чистым (в git clean
условия) для тайника, чтобы применить должным образом. Это значит бегать git clean -f
, или даже git clean -fdx
Это довольно уродливо, поскольку некоторые из неотслеживаемых или не отслеживаемых и игнорируемых файлов / каталогов могут быть элементами, которые вы хотите сохранить, а не удалять полностью. (Если это так, вы должны переместить их за пределы своего рабочего дерева вместо git clean
отгоняя их. Помните, что файлы git clean
удаляет именно те, которые вы не можете вернуть из Git!)
Чтобы понять почему, посмотрите на шаг 3 в описании "применить". Обратите внимание, что нет возможности пропустить неотслеживаемые и / или проигнорированные файлы в тайнике.
Основные факты о самом тайнике
Когда вы используете git stash save
либо с -u
или же -a
Сценарий stash записывает свой "stash bag" как коммит с тремя родителями, а не как обычный коммит с двумя родителями.
Схематически "шкатулка" обычно выглядит так с точки зрения графика фиксации:
o--o--C <-- HEAD (typically, a branch)
|\
i-w <-- stash
o
s - это любые старые обычные узлы фиксации, как C
, Узел C
(для Commit) имеет букву, поэтому мы можем назвать ее: это то место, откуда висит "шкатулка".
Сама шкатулка - маленькая треугольная сумка, свисающая с C
и содержит две фиксации: w
это коммит рабочего дерева и i
это индекс фиксации. (Не показано, потому что это просто трудно изобразить, это тот факт, что w
первый родитель C
и его второй родитель i
.)
С --untracked
или же --all
есть третий родитель для w
поэтому диаграмма выглядит примерно так:
o--o--C <-- HEAD
|\
i-w <-- stash
/
u
(эти диаграммы действительно должны быть изображениями, чтобы они могли иметь стрелки, а не ASCII-art, где стрелки трудно включить). В этом случае, stash
это совершить w
, stash^
это совершить C
(все еще также HEAD
), stash^2
это совершить i
, а также stash^3
это совершить u
, который содержит "неотслеживаемые" или даже "неотслеживаемые и игнорируемые" файлы. (На самом деле это не важно, насколько я могу судить, но я добавлю, что i
имеет C
как родительский коммит, в то время как u
является коммитом без родительских прав, или root. Кажется, нет особой причины для этого, просто сценарий делает вещи, но он объясняет, почему "стрелки" (линии) такие же, как на диаграмме.)
Различные варианты на save
время
При сохранении времени вы можете указать любую или все следующие опции:
-p
,--patch
-k
,--keep-index
,--no-keep-index
-q
,--quiet
-u
,--include-untracked
-a
,--all
Некоторые из них подразумевают, отменяют или отключают другие. С помощью -p
например, полностью меняет алгоритм, используемый сценарием для создания тайника, а также включает --keep-index
, заставляя вас использовать --no-keep-index
чтобы отключить его, если вы этого не хотите. Это несовместимо с -a
а также -u
и выдаст ошибку, если какой-либо из них будет дан.
В противном случае между -a
а также -u
, какой бы ни был установлен последний, сохраняется.
В этот момент скрипт создает один или два коммита:
- один для текущего индекса (даже если он не содержит изменений), с родительским коммитом
C
- с
-u
или же-a
- фиксация без родителей, содержащая (только) либо неотслеживаемые файлы, либо все (неотслеживаемые и игнорируемые) файлы.
stash
Затем скрипт сохраняет ваше текущее рабочее дерево. Это делается с помощью временного индексного файла (в основном, свежей промежуточной области). С -p
скрипт считывает HEAD
совершить в новой области постановки, затем эффективно1 прогонов git add -i --patch
, чтобы этот индекс заканчивался выбранными вами патчами. Без -p
, он просто сравнивает рабочий каталог с сохраненным индексом, чтобы найти измененные файлы.2 В любом случае он записывает объект дерева из временного индекса. Это дерево будет деревом для коммита w
,
В качестве последнего шага создания тайника скрипт использует только что сохраненное дерево, родительский коммит C
, индексная фиксация и корневая фиксация для неотслеживаемых файлов, если они существуют, для создания финальной фиксации в тайнике w
, Однако сценарий затем выполняет еще несколько шагов, влияющих на ваш рабочий каталог, в зависимости от того, используете ли вы -a
, -u
, -p
и / или --keep-index
(и помните, что -p
подразумевает --keep-index
):
С
-p
:"Обратное исправление" рабочего каталога, чтобы устранить разницу между
HEAD
и заначка По сути, это оставляет рабочий каталог с только теми изменениями, которые не сохранены (в частности, те, которые не в коммитеw
; все в коммитеi
здесь игнорируется).Только если вы указали
--no-keep-index
: бежатьgit reset
(без опций вообще, т.е.git reset --mixed
). Это очищает состояние "быть преданным" для всего, ничего не меняя. (Конечно, любые частичные изменения, которые вы сделали перед запускомgit stash save -p
, сgit add
или жеgit add -p
, сохраняются в коммитеi
.)
Без
-p
:Бежать
git reset --hard
(с-q
если вы тоже это указали). Это устанавливает рабочее дерево обратно в состояние вHEAD
совершить.Только если вы указали
-a
или же-u
: бежатьgit clean --force --quiet -d
(с-x
если-a
или без него, если-u
). Это удаляет все неотслеживаемые файлы, включая неотслеживаемые каталоги; с-x
(т.е. под-a
режим), он также удаляет все игнорируемые файлы.Только если вы указали
-k
/--keep-index
: использоватьgit read-tree --reset -u $i_tree
"вернуть" спрятанный индекс как "изменения, которые нужно зафиксировать", которые также появляются в рабочем дереве. (The--reset
не должно иметь никакого эффекта, так как шаг 1 очистил рабочее дерево.)
Различные варианты на apply
время
Две основные подкоманды, которые восстанавливают тайник: apply
а также pop
, pop
код просто работает apply
а затем, если apply
успешно, работает drop
в сущности, apply
, (Ну, есть также branch
, что немного сложнее, но в конце концов, он тоже использует apply
.)
Когда вы применяете шкатулку - любой "объект, похожий на шкатулку", т. Е. Все, что сценарий шкатулки может рассматривать как шкатулку, - есть только две специфичные для шкатулки опции:
-q
,--quiet
--index
(не--keep-index
!)
Другие флаги накапливаются, но все равно быстро игнорируются. (Тот же код синтаксического анализа используется для show
и здесь другие флаги передаются git diff
.)
Все остальное контролируется содержимым шкатулки, а также состоянием рабочего дерева и индекса. Как и выше, я буду использовать ярлыки w
, i
, а также u
для обозначения различных коммитов в тайнике, и C
для обозначения коммита, на котором висит шкатулка.
apply
последовательность идет следующим образом, предполагая, что все идет хорошо (если что-то не получается рано, например, мы находимся в середине слияния, или git apply --cached
в этом случае ошибка скрипта):
- записать текущий индекс в дерево, убедившись, что мы не находимся в середине слияния
- только если
--index
: diff commiti
против совершенияC
, труба кgit apply --cached
, сохраните полученное дерево и используйтеgit reset
расстегнуть это - только если
u
существует: использоватьgit read-tree
а такжеgit checkout-index --all
с временным индексом, чтобы восстановитьu
дерево - использование
git merge-recursive
слить дерево дляC
("основание") с тем, что написано на шаге 1 ("обновленный восходящий поток") и деревом вw
("спрятанные изменения")
После этого все становится немного сложнее:-), поскольку все зависит от того, прошло ли слияние на шаге 4 хорошо. Но сначала давайте немного расширим вышесказанное.
Шаг 1 довольно прост: скрипт просто запускается git write-tree
, которая завершается ошибкой, если в индексе есть неотмеченные записи. Если дерево записи работает, результатом является идентификатор дерева ($c_tree
в сценарии).
Шаг 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
написать дерево, сохранив это в скрипте $unstashed_index_tree
переменная. Иначе $unstashed_index_tree
оставлено пустым.
Шаг 3 - это то, где что-то идет не так в "нечистом" каталоге. Если u
коммит существует в тайнике, скрипт требует его извлечения, но git checkout-index --all
потерпит неудачу, если любой из этих файлов будет перезаписан. (Обратите внимание, что это делается с помощью временного индексного файла, который впоследствии удаляется: шаг 3 вообще не использует обычную промежуточную область.)
(Шаг 4 использует три "волшебные" переменные среды, которые я не видел документированными: $GITHEAD_t
предоставляет "имя" объединяемых деревьев. Бежать git merge-recursive
скрипт предоставляет четыре аргумента: $b_tree
--
$c_tree
$w_tree
, Как уже отмечалось, это деревья для базового коммита C
, индекс в началеapply
и прятную работу делаете w
, Чтобы получить имена строк для каждого из этих деревьев, git merge-recursive
ищет в среде имена, образованные путем добавления GITHEAD_
в сырой SHA-1 для каждого дерева. Скрипт не передает никаких аргументов стратегии git merge-recursive
и не позволяйте вам выбирать какую-либо стратегию recursive
, Наверное, так и должно быть.)
Если слияние имеет конфликт, запускается скрипт git rerere
(см.) и, если --index
, сообщает, что индекс не был восстановлен и выходит со статусом конфликта слияния. (Как и в случае других ранних выходов, это предотвращает pop
от уронить тайник.)
Если слияние успешно, хотя:
Если у нас есть
$unstashed_index_tree
Мы делаем--index
и все остальные тесты на шаге 2 тоже пройдены - тогда нам нужно восстановить состояние индекса, созданное на шаге 2. В этом случае простоgit read-tree $unstashed_index_tree
(без вариантов) делает свое дело.Если у нас нет чего-то в
$unstashed_index_tree
Сценарий использует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
справочная страница, о том, как избежать ложных попаданий для измененных файлов, это может объяснить это), но это то, что он делает.
Наконец, скрипт запускается git status
(с выводом на /dev/null
за -q
Режим; не уверен, почему он работает под -q
).
Несколько слов о git stash branch
Если у вас возникают проблемы с применением тайника, вы можете превратить его в "реальную ветвь", что делает его гарантированно восстанавливаемым (за исключением, как обычно, проблемы с тайником, содержащим коммит u
не применяется, если вы сначала не удалили неустановленные и, возможно, даже проигнорированные файлы).
Хитрость в том, чтобы начать с проверки коммита C
(например, git checkout stash^
). Это, конечно, приводит к "отдельному HEAD", поэтому вам нужно создать новую ветку, которую вы можете объединить с шагом, который проверяет коммит C
:
git checkout -b new_branch 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-скрипт для интерактивного добавления, со специальным набором магии для сохранения. Есть несколько других магии --patch
режимы; увидеть сценарий
2 Здесь очень маленькая ошибка: git читает $i_tree
дерева подтвержденного индекса во временный индекс, но затем сравнивает рабочий каталог с HEAD
, Это означает, что если вы изменили какой-то файл f
в индексе, а затем изменил его обратно, чтобы соответствовать HEAD
ревизия, рабочее дерево хранится в w
в шкатулке содержится индекс версии f
вместо версии рабочего дерева f
,
Не полностью понимая причину возникновения проблемы, я нашел быстрое решение:
git show -p --no-color [<stash>] | git apply
--no-color
опция удаляет любые цвета из вывода diff, потому что они испортили git apply
команда.
Однако было бы здорово, если бы кто-то мог отредактировать этот ответ, объяснив, почему git stash pop
выходит из строя.