Звучит как магия "временного буфера", но никакой магии тут нет. stash - это обычные коммиты, просто спрятанные в отдельном уголке. Когда ты поймёшь его механику, ты перестанешь бояться потерять заначку и научишься доставать даже то, что вроде бы удалил. Поехали вглубь.
Спрятать изменения git: базовый цикл push, list, pop
Самое первое, что надо выучить - как спрятать изменения git и как их вернуть. Команда называется git stash push, хотя исторически работает и просто git stash (это синоним push без аргументов).
Код: Выделить всё
$ git status -s
M src/auth.py
M src/config.py
$ git stash push -m "недопиленный рефакторинг авторизации"
Saved working directory and index state On feature-auth: недопиленный рефакторинг авторизации
$ git status -s
$
Смотрим, что лежит в тайнике - git stash list:
Код: Выделить всё
$ git stash list
stash@{0}: On feature-auth: недопиленный рефакторинг авторизации
stash@{1}: WIP on main: 7a3f9c1 правка README
Достаём заначку обратно - git stash pop:
Код: Выделить всё
$ git stash pop
On branch feature-auth
Changes not staged for commit:
modified: src/auth.py
modified: src/config.py
Dropped refs/stash@{0} (a91b3e7c8d2f4a6b9e1c0d5f8a7b2e4c6d9f1a3b)

git stash apply против pop: когда что
Вот граблина, на которую напарываются почти все. git stash pop удаляет заначку сразу после наложения. А что если pop упрётся в конфликт? Тогда поведение зависит от ситуации, и легко остаться в подвешенном состоянии. Поэтому есть git stash apply - он накладывает изменения, но заначку НЕ трогает, она остаётся в стеке.
Код: Выделить всё
$ git stash apply stash@{0}
On branch feature-auth
Changes not staged for commit:
modified: src/auth.py
$ git stash list
stash@{0}: On feature-auth: недопиленный рефакторинг авторизации
Код: Выделить всё
$ git stash drop stash@{0}
Dropped stash@{0} (a91b3e7c8d2f4a6b9e1c0d5f8a7b2e4c6d9f1a3b)
А посмотреть, что именно лежит в заначке, не доставая её - git stash show:
Код: Выделить всё
$ git stash show stash@{0}
src/auth.py | 18 ++++++++++++------
src/config.py | 3 ++-
2 files changed, 13 insertions(+), 8 deletions(-)
$ git stash show -p stash@{0}
diff --git a/src/auth.py b/src/auth.py
index 3f9a1c2..7b4e8d0 100644
--- a/src/auth.py
+++ b/src/auth.py
@@ -41,9 +41,15 @@ def authenticate(user, token):
- if token.expired:
+ if token is None or token.expired:
Механика: stash - это коммиты в refs/stash
Теперь главное, ради чего этот урок. stash - это не отдельная подсистема и не временный файл с патчем. Это настоящие коммит-объекты в твоей базе объектов, спрятанные под особой ссылкой.
Когда ты делаешь git stash push, Git создаёт два коммита (а если есть что прятать сверх отслеживаемых файлов - три):
- Коммит индекса (staged). Снимок твоего индекса на момент заначки - то, что было в git add.
- Коммит рабочей копии (WIP). Это и есть голова заначки, то, на что указывает stash@{0}. У него хитрая родословная: первый родитель - коммит, на котором стоял HEAD; второй родитель - тот самый коммит индекса. Дерево этого WIP-коммита = состояние твоей рабочей копии.
- Коммит untracked (опционально, при -u или -a). Снимок неотслеживаемых файлов, цепляется третьим родителем.
Код: Выделить всё
$ cat .git/refs/stash
a91b3e7c8d2f4a6b9e1c0d5f8a7b2e4c6d9f1a3b
$ git cat-file -p stash@{0}
tree 6d8f1a3b9e2c4a7b0d5f8a3c6b9e1d4f7a2c5b8e
parent 7a3f9c1e4d2b8a6f0c9e3d1b5a8f2c7e4d6b9a1c
parent c4d9f1a3b6e8d2f4a7b9e1c0d5f8a3c6b9e1d4f7
author Khovanskiy <dev@cyberlake.ru> 1749900000 +0300
committer Khovanskiy <dev@cyberlake.ru> 1749900000 +0300
On feature-auth: недопиленный рефакторинг авторизации
А где же история заначек, весь стек? В reflog ссылки refs/stash. stash@{0}, stash@{1} - это синтаксис обращения к reflog: "запись 0 в журнале refs/stash", "запись 1". Сам файл .git/refs/stash хранит только верхушку. Загляни:
Код: Выделить всё
$ cat .git/logs/refs/stash
0000... a91b3e7c On feature-auth: недопиленный рефакторинг авторизации
a91b3e7c 4f8d2a1b WIP on main: правка README
Частичный stash, untracked и stash в новую ветку
Базу освоили. Теперь приёмы уровня профи.
Частичный тайник - git stash push --patch. Иногда спрятать надо не всё, а пару кусков. Флаг -p (или --patch) запускает интерактивный режим: Git показывает каждый ханк и спрашивает, прятать его или нет.
Код: Выделить всё
$ git stash push -p -m "только эксперимент с кешем"
diff --git a/src/cache.py b/src/cache.py
@@ -10,6 +10,8 @@
Stash this hunk [y,n,q,a,d,j,/,e,?]? y
Сохранить индекс - git stash push --keep-index. Флаг --keep-index прячет всё, но то, что было в git add, оставляет и в рабочей копии тоже. Классический сценарий: ты собрал staged-набор для коммита и хочешь прогнать тесты ровно на нём, без посторонних правок. Прячешь всё с --keep-index, гоняешь тесты на чистом staged-наборе, потом возвращаешь остальное.
Неотслеживаемые файлы. По умолчанию stash НЕ трогает untracked-файлы (новые, ещё не добавленные в git add). Это любимые грабли: спрятал, переключился, а новый файл models/new.py так и валяется в рабочей копии и мешает. Лечится флагами:
- -u (--include-untracked) - прячет и неотслеживаемые файлы тоже.
- -a (--all) - прячет ещё и игнорируемые (те, что под .gitignore). Применяй осторожно: легко утащить в заначку build/, node_modules/ и прочий мусор на гигабайты.
Код: Выделить всё
$ git stash push -u -m "фича + новый модуль"
Saved working directory and index state On feature-auth: фича + новый модуль
Код: Выделить всё
$ git stash branch experiment-cache stash@{0}
Switched to a new branch 'experiment-cache'
On branch experiment-cache
Changes not staged for commit:
modified: src/cache.py
Dropped refs/stash@{0} (a91b3e7c8d2f4a6b9e1c0d5f8a7b2e4c6d9f1a3b)
git stash export и import: перенос тайника между клонами (Git 2.51)
Исторически stash был приговорён жить на одной машине: refs/stash - локальная ссылка, push/fetch её не переносят. Хочешь перенести недострой с рабочего ноута на домашний - изворачивайся через git stash show -p > patch.diff и переноси патч руками, теряя разделение индекс/рабочая копия.
С Git 2.51 это в прошлом. Появились git stash export и git stash import. Идея: упаковать стек заначек в цепочку обычных коммитов, которую можно гонять штатными push/fetch.
Код: Выделить всё
$ git stash export --to-ref refs/stashes/laptop-wip
$ git push origin refs/stashes/laptop-wip
Код: Выделить всё
$ git fetch origin '+refs/stashes/*:refs/stashes/*'
$ git stash import refs/stashes/laptop-wip
Конфликты при pop и автостэш при rebase и pull
Что если pop наложился с конфликтом? Это нормально: заначка - это дифф, а код под ней мог уехать.
Код: Выделить всё
$ git stash pop
Auto-merging src/auth.py
CONFLICT (content): Merge conflict in src/auth.py
The stash entry is kept in case you need it again.
Теперь автостэш. У rebase и pull есть встроенный мини-stash для удобства - флаг --autostash. Допустим, у тебя грязная рабочая копия, а надо подтянуть изменения:
Код: Выделить всё
$ git pull --rebase --autostash
Created autostash: 3f8a1c2
...
Applied autostash.
Код: Выделить всё
$ git config --global rebase.autostash true
$ git config --global pull.rebase true
Восстановление удалённого тайника через fsck
Сделал git stash clear или drop, а через пять минут понял, что зря. Не паникуй. Коммит-объекты заначки ещё живут в базе как недостижимые (unreachable), пока их не собрал git gc. Ищем их через git fsck:
Код: Выделить всё
$ git fsck --no-reflogs | grep commit
dangling commit a91b3e7c8d2f4a6b9e1c0d5f8a7b2e4c6d9f1a3b
Код: Выделить всё
$ git stash apply a91b3e7c8d2f4a6b9e1c0d5f8a7b2e4c6d9f1a3b
Типичные грабли
- Забыл -u, untracked-файлы остались в рабочей копии и мешают переключению. Привыкай к git stash push -u, когда создаёшь новые файлы.
- git stash pop при конфликте оставляет заначку - и народ думает, что pop сломался. Нет, это by design, дропни вручную после разрешения.
- Несколько заначек, все без -m, в списке сплошные WIP on main. Через день не разберёшь. Всегда -m.
- git stash clear необратим через интерфейс stash. Перед clear лучше export или хотя бы git stash list скопировать.
- stash -a утаскивает игнорируемое - можно случайно спрятать build/ на гигабайты и раздуть репозиторий. Применяй -a осознанно.
- Заначка - это короткоживущий тайник, а не система версий. Если правки живут дольше дня - заведи нормальную ветку (git stash branch как раз для этого).
- Заведи песочницу: mkdir stash-lab, git init -b main, создай файл a.txt с парой строк, git add, git commit.
- Поменяй a.txt и создай новый b.txt (untracked). Сделай git stash push -m "проба". Проверь git status - b.txt остался? Да, untracked не спрятался.
- Верни заначку: git stash pop. Теперь спрячь с git stash push -u -m "проба2" и убедись, что b.txt тоже ушёл в тайник.
- Загляни внутрь: git cat-file -p stash@{0}. Найди оба parent и tree. Сколько parent при -u?
- Сделай git stash branch lab-branch stash@{0}. Где оказались твои правки? На какой ветке ты теперь?
- Создай новую заначку, дропни её через git stash drop, затем найди через git fsck --no-reflogs и восстанови git stash apply <id>.
- Бонус: git stash export --to-ref refs/stashes/test, затем git log refs/stashes/test - посмотри на цепочку коммитов, в которую упаковался стек.
- Сколько коммит-объектов создаёт git stash push без флагов и сколько с -u? Что хранится в каждом и кто кому родитель?
- Чем git stash pop отличается от git stash apply и в каком случае pop НЕ удаляет заначку?
- Где физически живёт стек заначек и почему stash@{0} и stash@{1} - это синтаксис обращения к reflog, а не к разным файлам?
- Зачем нужен git stash export и чем он принципиально лучше переноса через git stash show -p > patch.diff?
git stash - это не временный буфер, а аккуратно спрятанные коммиты под ссылкой refs/stash, со стеком в её reflog. Понял это - и тайник перестаёт быть чёрным ящиком: ты видишь два-три коммит-объекта, понимаешь, почему pop при конфликте бережёт заначку, умеешь достать "удалённое" через fsck. Базовый цикл push -m, list, show -p, pop против apply, drop закрывает 90 процентов задач. Частичный stash через -p, --keep-index, перенос в ветку через stash branch и перенос между машинами через export/import (Git 2.51) - это уже профессиональный уровень. И помни главное правило: тайник для срочного переключения на час, а не хранилище на неделю. Живёт дольше дня - заводи ветку.