В каждом проекте наступает момент, когда правки ещё не готовы к коммиту, а рабочую копию нужно срочно очистить для другой задачи. Механизм stash в Git решает именно эту задачу, но пользуются им чаще всего наугад: сохранили, забыли, а через неделю не могут вспомнить, что лежит в третьей записи стека. Ниже разобран рабочий подход с именованными сохранениями через git stash push -m, аккуратный выбор между apply и pop, а также сценарии, при которых запись теряется, и способы этого избежать. Всё на реальных командах, с выводом и пояснениями по строкам.
Сохранение изменений с именем через git stash push -m
Классическая команда git stash сохраняет и неиндексированные, и подготовленные изменения, возвращая рабочую копию к состоянию последнего коммита. Современная форма записывается так:
$ git stash push -m "черновик фильтрации по дате"
Saved working directory and index state On main: черновик фильтрации по дате
Разбор вывода построчно. Первая строка это сама команда: подкоманда push, флаг -m задаёт текстовое сообщение, которое дальше будет видно в списке. Вторая строка подтверждает, что рабочая копия и индекс зафиксированы, а префикс "On main" показывает ветку, в которой делалось сохранение. Заметьте, что сообщение в выводе идёт после двоеточия, и именно по нему запись опознаётся позже.
Если нужно захватить и файлы, которые Git ещё не отслеживает, добавляется флаг -u, он же --include-untracked:
$ git stash push -u -m "скрипт миграции плюс правки конфига"
Saved working directory and index state On main: скрипт миграции плюс правки конфига
Без -u новые файлы останутся в рабочей копии, и после очистки стека их можно случайно закоммитить не в ту ветку. Есть и более жёсткий вариант --all, который забирает даже игнорируемые файлы; на практике он нужен редко, потому что утаскивает с собой каталоги сборок и зависимости, а возвращать это обратно потом утомительно.
Полезные частичные сохранения делаются через путь:
$ git stash push -m "только модуль оплаты" -- src/payments/
Всё, что вне указанного пути, останется на месте. Это удобно, когда в рабочей копии смешались две независимые правки и одну хочется отложить, не трогая вторую. Ещё один недооценённый флаг это --keep-index: он сохраняет в stash вообще все изменения, но оставляет в рабочей копии те, что уже добавлены в индекс. Так откладывают черновик, а готовую часть тут же коммитят. Набор опций у push шире, чем у старого git stash save, и команда save официально объявлена устаревшей именно потому, что не умеет частичные сохранения по путям.
Чтение стека через git stash list и адресация записей по индексу
Сохранения складываются в стек: последняя запись всегда под индексом ноль. Список смотрится так:
$ git stash list
stash@{0}: On main: черновик фильтрации по дате
stash@{1}: On main: скрипт миграции плюс правки конфига
stash@{2}: WIP on feature/api: 3fa9c21 Merge branch 'main' into feature/api
Построчно: stash@{0} это самая свежая запись с человекочитаемым именем из -m; запись под индексом 1 взяла имя из второго сохранения; третья запись создавалась командой git stash без сообщения, поэтому получила автоматическую метку WIP с именем ветки и хешем коммита, на котором она основана. Разница очевидна: именованные записи читаются и через месяц, а "WIP on ..." расшифровке не поддаётся, особенно когда таких записей пять штук на одной ветке.
Индексы в фигурных скобках не постоянны. Если удалить запись ноль, старая единица съедет на её место. Поэтому адресоваться к записям по индексу стоит сразу после list, а не по памяти. Перед применением чего-то нетривиального запись осматривают командой show:
$ git stash show stash@{1}
src/migrate.sh | 12 ++++++++++++
src/config.js | 4 ++--
2 files changed, 14 insertions(+), 2 deletions(-)
По умолчанию show выводит только статистику diffstat: первая строка говорит, что добавлен новый файл со скриптом, вторая показывает точечную правку конфига, третья суммирует масштаб. Чтобы увидеть сам патч, добавляют -p:
$ git stash show -p stash@{1}
diff --git a/src/config.js b/src/config.js
index 8f3c2a1..9b1d4e7 100644
--- a/src/config.js
+++ b/src/config.js
@@ -14,2 +14,2 @@
-timeout: 3000,
+timeout: 8000,
Важный нюанс: неотслеживаемые файлы, сохранённые с -u, в обычный вывод show не попадают. Их содержимое живёт в третьем родительском коммите записи, и посмотреть его можно через git show "stash@{1}^3". Об этом забывают даже опытные разработчики, а потом удивляются, куда делся только что созданный файл. Для быстрого поиска записи по тексту сообщения пригодится фильтрация списка через git stash list | grep фильтрация, а относительная адресация stash@{1}~ работает здесь не так, как в обычных ссылках на коммиты, лучше её не использовать.
Если просмотр списка хочется сделать нагляднее, у list есть форматирование как у git log:
$ git stash list --stat --date=relative
Флаг --stat добавляет к каждой записи сокращённую статистику файлов, а --date=relative показывает возраст словами вроде "3 days ago". По возрасту сразу видно, какие записи уже превратились в балласт. Удаляется конкретная запись командой git stash drop stash@{3}, а полная очистка делается через git stash clear, и clear вопросов не задаёт, так что перед ней список листают ещё раз свежим list.
Выбор между apply и pop и поведение при конфликте
Обе команды накатывают сохранённые изменения на текущую рабочую копию. Разница одна, но принципиальная: apply оставляет запись в стеке, pop удаляет её после успешного применения.
$ git stash apply stash@{0}
On branch main
Changes not staged for commit:
modified: src/dates.js
Вывод после применения повторяет привычный git status: ветка, список затронутых файлов и их состояние. Если изменения могут понадобиться повторно, например патч надо раскатать на несколько веток, используется именно apply. Без аргумента обе команды берут stash@{0}, конкретную запись лучше указывать явно.
Главный подводный камень pop вскрывается при конфликте. Когда рабочая копия ушла вперёд и патч не накладывается чисто, Git оставляет в файлах конфликтные маркеры:
$ git stash pop
Auto-merging src/config.js
CONFLICT (content): Merge conflict in src/config.js
The stash entry is kept in case you need it again.
Последняя строка ключевая: запись из стека не удаляется, пока слияние не завершилось чисто. То есть сам по себе pop при конфликте ничего не теряет. Потеря случается руками: человек видит маркеры, нервничает и инстинктивно откатывает всё через git checkout -- . или git reset --hard. После чистого сброса рабочей копии применённый патч стирается, а если запись до этого была удалена из стека командой drop, изменения уходят окончательно. Отсюда профилактика:
- Перед pop на неактуальную копию сначала выполнить git status и git log -1 --oneline, чтобы понять базу, к которой применится патч;
- При любом сомнении заменить pop на apply, разобрать конфликт, закоммитить результат и только потом удалить запись через git stash drop stash@{0};
- Никогда не чистить рабочую копию разрушающими командами, пока конфликт от stash не разрешён и не проверен;
- Хранить в стеке только реально живые задачи на ближайшие дни, старое вычищать git stash drop по одной записи или git stash clear полностью.
Даже удалённая по ошибке запись не всегда пропадает безвозвратно. Запись стека это обычный коммит с двумя или тремя родителями, и пока сборщик мусора его не удалил, хеш находится через git fsck --unreachable, а изменения возвращаются командой git stash apply <хеш>. Срок жизни недостижимых объектов по умолчанию около двух недель, так что действовать стоит без паники, но и без месячного затягивания.
Ещё одна рабочая мелочь: если после apply нужно вернуть и состояние индекса (что было в stage, то и должно вернуться в stage), используется флаг --index:
$ git stash apply --index stash@{0}
Без --index всё сваливается в неиндексированные изменения, и аккуратно разложенное разграничение между "готово" и "черновик" теряется. Флаг капризничает, если в текущем индексе уже что-то лежит, поэтому его дают на чистый индекс.
Изредка возникает обратная задача: посмотреть, что изменится, если накатить запись, но не накатывать. Это делает git stash show -p в связке с выводом в файл: git stash show -p stash@{0} > /tmp/check.patch, затем patch читается глазами или пробуется через git apply --check /tmp/check.patch. Команда git apply --check ничего не меняет, а только отвечает, наложится ли патч без конфликтов. Дёшево, безопасно и честно показывает, стоит ли сейчас делать pop.
Страховка через git stash branch для долгоживущих правок
Если отложенные изменения провисели в стеке дольше пары дней и база успела уехать, лучший способ вернуть их в работу это не pop на новую голову, а создание отдельной ветки от исторического коммита:
$ git stash branch fix-payments stash@{1}
Switched to a new branch 'fix-payments'
On branch fix-payments
Changes not staged for commit:
modified: src/config.js
Dropped refs/stash@{1} (c3d9f0a...)
Построчно: первая строка сообщает о переключении на свежую ветку fix-payments, созданную от того коммита, на котором запись делалась. Дальше идёт привычный статус с применёнными изменениями. Последняя строка с Dropped подтверждает, что запись удалена из стека, а хеш рядом это хеш самой записи. Поскольку база в точности совпадает с исходной, конфликтов не возникает вовсе, а дальше ветку можно ребейзить на актуальный main уже в спокойном режиме и с ревью. Честно говоря, для всего, что старше недели, git stash branch это единственный здравый путь.
Именованные записи как личная очередь задач и типовые ошибки
Смысл флага -m раскрывается не в одиночных сохранениях, а в очереди из нескольких отложенных направлений. Типичный сценарий: разработчик ведёт фичу, прилетает срочный баг, потом запрос на ревью чужой ветки. Сначала недоделанная фича уходит в стек с говорящим именем: git stash push -u -m "фича рейтингов, не компилируется тест". Затем для бага создаётся чистая ветка, правка отправляется. Во время ревью чужие правки тоже можно временно убрать: git stash push -m "правки после ревью, ждут ответа". Когда очередь разгребается, git stash list показывает осмысленные подписи вместо пяти одинаковых WIP, и каждая запись применяется адресно.
Для удобства многие заводят псевдонимы в конфиге, чтобы команды стека были короче:
[alias]
ss = stash push -u
sl = stash list --date=local
sa = stash apply
sb = stash branch
Разбор буквально: ss сохраняет всё вместе с новыми файлами, сообщение дописывается руками как ss -m "текст"; sl печатает список с локальными датами, что помогает оценить возраст записей; sa и sb сокращают два самых длинных глагола. Псевдонимы хранятся в файле ~/.gitconfig и заодно служат памяткой о рабочем процессе.
Типичные ошибки вокруг stash держать в голове стоит отдельно: применение pop на устаревшую голову вместо stash branch; забытый флаг -u, из-за которого новые файлы вернулись сами собой и попали в чужой коммит; адресация по индексу по памяти, когда стек уже перестроился после drop или нового push; попытка применить запись на ветку, ушедшую далеко от исходной базы, хотя diffstat давно намекал на конфликт. И самая коварная привычка это использование stash как долгосрочного хранилища вместо ветки. Стек не попадает в push и живёт только локально в refs/stash внутри каталога .git: переустановка системы, удаление клона или случайный git stash clear уничтожат все записи, а удалённой копии у них нет в принципе. Многие разработчики узнают об этом ровно один раз, поздно вечером перед дедлайном.
И последний практический сценарий: срочный хотфикс прямо из стека. Представим, в stash лежит правка логирования, а её внезапно попросили выкатить отдельно от основной фичи. Последовательность такая: от актуального main создаётся ветка hotfix/logging, на ней делается git stash apply stash@{2}, изменения коммитятся, ветка пушится и уходит в ревью. Запись в стеке при этом зеркально остаётся, потому что использовался apply, и позже из неё поднимается основная фича без правки логирования, которую из неё теперь можно вычистить.
Профилактика проста и скучна, как и положено хорошей дисциплине. Имя через -m у каждой записи, apply вместо pop всегда, когда ход неочевиден, git stash branch для всего, что старше нескольких дней, и регулярная чистка списка. При таком порядке stash перестаёт быть ящиком с сюрпризами и становится предсказуемым инструментом: сохранил, подписал, вернул, удалил. Ровно та последовательность, которая отличает спокойную работу от вечернего поиска потерянного патча по недостижимым хешам.