Repository navigation
How Stashes Work
A stash puts your uncommitted work aside so you can switch branches or pull, and brings it back later. In Nibtree you stash from the Git menu (Uncommitted Changes) or a repository row's ... > Stash in Changes, and apply, pop or drop from the Branches panel, those menus, or the Git menu's Unstash Changes. For the user side, see Stashes.
People often need a clean work tree in the middle of something: a quick fix on another branch, a pull that would touch the same files. Stashing must be fast, must not lose work, and bringing work back must handle conflicts like any other merge.
Stashes follow the same pattern as branches: git2 reads the list, the git CLI does every change, and repoStore refreshes after each action.
sequenceDiagram
participant H as gitActions.stash
participant D as dialogs
participant RS as repoStore
participant API as api.ts
participant CMD as commands/stash.rs
H->>D: prompt message, "Include untracked files"
D-->>H: value "WIP", checked true
H->>RS: run("Stash", work)
RS->>API: stashPush(repoPath, message, includeUntracked)
API->>CMD: invoke stash_push
CMD->>CMD: git stash push --include-untracked -m message
CMD-->>RS: ok
RS->>RS: refreshRepo, which also runs refreshActive
RS->>API: getStashes(repoRoot)
API-->>RS: StashEntry list
get_stashes calls git::stash::list, which uses git2's stash_foreach. It needs a mutable repository handle, which is why the command opens one with &mut. Each entry is a StashEntry with index, message and shortId, newest first. repoStore.refreshActive() loads stashes together with branches, only for the active repository, and the Branches panel shows them as the last section (kind: "stash" rows from tree.ts). If reading stashes fails, the list is empty and a "Could not read stashes" toast shows the error once, not on every refresh, until stashes load again.
| Action | Where | Command | git |
|---|---|---|---|
| Stash | Git menu or row menu | stash_push |
stash push, plus --include-untracked and -m <message> when given; an error when refs/stash did not change (nothing to stash) |
| Apply | Stash menu or row button |
stash_apply with pop: false
|
stash apply stash@{N} |
| Pop | Stash menu or row button |
stash_apply with pop: true
|
stash pop stash@{N} |
| Drop | Stash menu or row button | stash_drop |
stash drop stash@{N} |
| Drop All Stashes | Row menu | stash_clear |
stash clear |
stash_ref(stash_index) builds the stash@{N} name. The prompt starts with "WIP", so a default stash reads "On main: WIP"; an empty field leaves the message out and git writes "WIP on main".
stash(repoRoot?, includeUntracked?) in views/gitActions.ts is shared. The Git menu passes nothing: the active repository, and the prompt shows the Include untracked files checkbox. The row's Stash and Stash (Include Untracked) pass both, so there is no checkbox. applyStash and dropStash in sidebar/actions.ts take an optional repoRoot the same way. The row menu adds Apply and Pop Latest Stash (index 0), pickers built by stashPickItems in repoPickers.ts, and Drop All Stashes, which confirms (danger) and runs stash_clear (git stash clear). Git > Unstash Changes... picks a stash, then asks Pop or Apply with dialogs.choose.
Apply and pop can conflict with changes in the work tree. They run through run_op, so conflicts come back as OpOutcome { conflicts: true }, not as an error:
stateDiagram-v2
[*] --> Stashed: stash push
Stashed --> Applied: apply, clean
Stashed --> Conflicted: apply or pop, conflicts
Stashed --> Gone: pop, clean
Stashed --> Gone: drop, after confirm
Applied --> Gone: drop, after confirm
Conflicted --> Resolved: resolve in the Conflicts dialog or merge tool
Resolved --> Gone: drop when done
Gone --> [*]
On conflicts, runOp makes that repository active and opens the Conflicts dialog. When a pop stops on conflicts, git keeps the stash, so nothing is lost. The user resolves the files and drops the stash when they are happy.
Drop asks first with dialogs.confirm({ danger: true }), because a dropped stash is hard to recover. Apply and Pop do not ask: they never throw work away.
| File | What it does |
|---|---|
src-tauri/src/commands/stash.rs |
get_stashes, stash_push, stash_apply, stash_drop, stash_clear, stash_ref
|
src-tauri/src/git/stash.rs |
StashEntry and the git2 reader |
src/lib/views/gitActions.ts |
stash: the prompt and the "nothing to stash" note |
src/lib/views/changes/repoActions.ts, views/git/gitMenuActions.ts
|
Row stash items, dropAllStashes, unstashFromMenu
|
src/lib/views/sidebar/actions.ts |
stashMenu, applyStash, dropStash
|
src/lib/views/sidebar/tree.ts |
Stash rows in the Branches panel |
src/lib/views/sidebar/RowContent.svelte |
The Apply, Pop and Drop hover buttons of a stash row |
src/lib/stores/repo.svelte.ts |
stashes, refreshActive, stashErrorRepo, run, runOp
|
src/lib/api.ts |
getStashes, stashPush, stashApply, stashDrop, stashClear
|
Address stashes by index. Git's own commands take stash@{N}, so the index is the natural handle. Indexes shift when a stash is added or dropped, so the list is reloaded after every action, and menus act on the row you clicked in the fresh list. Addressing by commit id was rejected: git stash drop wants a stash ref, and mapping ids back to indexes adds work for no gain.
Include untracked files by default. New files are usually part of the work you want to set aside. Leaving them behind surprises people when they switch branches. The checkbox in the prompt lets you turn it off.
Apply and pop share one command. They differ only in the git verb and both can conflict, so one stash_apply with a pop flag keeps the code and the tests in one place.
Writes through the git CLI. git2 has stash functions, but the CLI keeps behavior identical to the terminal, including the user's config.
Stash with nothing to stash said "Changes stashed".
- The issue: stashing a clean repository showed a success note, but no stash appeared.
-
Why it happened:
git stash pushprints "No local changes to save" and still exits with 0. -
The fix and why we chose it:
stash_pushcomparesrefs/stashbefore and after and returns "No local changes to stash" as an error when nothing was added. When only new files exist and Include untracked files is off, it says to turn that on. We compare the ref rather than git's message because git translates its messages.stash()ingitActions.tsshows that error as an info note, not a failure.
Stash errors were hidden.
- The issue: when git could not read the stash list, the Stashes section was just empty, with no hint that something was wrong.
-
Why it happened:
refreshActivecaught the error ofgetStashesand returned an empty list without a message. -
The fix and why we chose it: the list still falls back to empty so the panel stays usable, but a "Could not read stashes" toast now shows the error. Refreshes run on every file change, so the store remembers the failing repository (
stashErrorRepo) and shows the toast only once until stashes load again.
-
src-tauri/src/commands/tests.rs:stash_push_apply_with_conflict_and_dropstashes tracked and untracked work, commits a conflicting change, pops and checks that conflicts are reported and the stash is kept, then drops it and checks that applying a missing stash is an error;stash_apply_clean_reports_no_conflictschecks the clean path. -
src-tauri/src/commands/stash.rs:stashing_nothing_is_an_error_not_a_success,stash_clear_drops_every_stash_and_pop_by_index_keeps_the_rest. -
src/lib/views/changes/repoPickers.test.ts: stash pick items;repoMenu.test.ts: when the Stash items are disabled. -
src-tauri/src/git/tests.rs:stash_list_newest_firstchecks the order and fields ofStashEntry.
When you add a stash feature, add a Rust test on a real repository from test_support.rs.
- Update this page when
commands/stash.rs,git/stash.rsor the stash actions change. - Update Stashes for any change to the prompt or the menu.
- Retake
stashes.pngwhen the stash section or menu looks different. - Related: How Branches and Tags Work, How Conflict Resolution Works.
These pages are generated from docs/wiki. To correct something, open a pull request against the source file; edits made here are overwritten.
Using Nibtree
- Getting Started
- Welcome Screen
- Workspaces
- Files Panel
- File Operations
- File Icons
- Rounded Panels
- Editor and Tabs
- Pin Reorder and Wrap Tabs
- Single Tab Title
- Split Editor
- New File and Unsaved Changes
- Unload Hidden Tabs
- Editing Code
- Code Appearance
- Markdown Editor
- Image and PDF Preview
- Search Everywhere
- Recent Files
- Navigation Bar
- Find and Replace
- Changes and Commits
- Sign In Prompts
- Commit Box Layout
- Repository Actions
- Commit Options
- Ignoring Files
- Diffs
- Inline Diffs
- Staging View
- Revert a Change
- Resolving Conflicts
- Merge Tool
- Blame
- History and Log
- Merge Commits
- Branches and Tags
- Branches Popup
- Remotes
- Stashes
- Shelf
- Git Menu
- Git Dialogs
- Interactive Rebase
- Worktrees
- AI Agents
- AI Tasks
- AI Usage
- Keep Computer Awake
- Submodules
- Git LFS
- GitHub
- Git Console
- Terminal
- Terminal Features
- Scripts
- Navigation
- Menus
- Settings
- Settings Search
- Color Themes
- Settings Terminal and Automation
- Settings Files
- Updates
- Status Bar and Help
- Notifications
- Memory Use
- Clear Cache
- MCP and CLI
- Git Mergetool
- Keyboard Shortcuts
- Keyboard Shortcuts Editor
- Troubleshooting
- Troubleshooting Git
Developers
- Developer Guide
- Architecture
- Project Layout
- Backend
- How Folder Watching Works
- Folder Watching Bugs We Fixed
- How Workspace Files Work
- How the Preview Scheme Works
- Backend Services
- How Terminal Features Work
- Terminal Bugs We Fixed
- Frontend
- How the Rich Markdown Editor Works
- Rich Markdown Bugs We Fixed
- Git Menu Links
- Menu Keys and Routing
- Frontend Modules
- Blame Bugs We Fixed
- Project Color And Title Bar
- One Diff View
- Diff Bugs We Fixed
- Merge Tool Layouts
- Merge Tool Bugs We Fixed
- Color Theme Bugs We Fixed
- How the Connected Diff Works
- Rounded Panels Bugs We Fixed
- Update Bugs We Fixed
- Status Bar Bugs We Fixed
- MCP and CLI Bugs We Fixed
- Repository Actions Bugs We Fixed
- How the Changes Tab Works
- CodeMirror Patterns
- How the Cursor Works
- How Editor Features Work
- How the Path Bar Works
- Commands and Events
- Settings Reference
- Commands Workspace and Changes
- Commands Branches and Remotes
- Commands History and Extras
- Commands App and Tools
- Testing
- Test Suites
- Debugging
- Debugging with MCP
- Measuring Setting Memory
- How Terminal Memory Is Measured
- How Markdown Diagrams Work
- Releases and CI
- Publishing the Wiki
- Versioning and Changelog
- Browser Version Packages
- Browser Release Checks
- Docker Image
- Platforms and Signing
- Windows Support
- Windows Bugs We Fixed
- Windows MCP and CLI
- Docs and Screenshots
- UI Library
- UI Library MCP
- UI Library Bugs
- UI Library Migration
- Contributing
How features work
- Welcome screen
- Workspaces and folders
- Files panel
- File operations
- File icons
- Editor and tabs
- Pin, reorder and wrap tabs
- Single tab title
- Split editor
- New File and unsaved changes
- Unload hidden tabs
- Editing code
- Code appearance
- Markdown editor
- Image and PDF preview
- Search Everywhere
- Recent Files
- Navigation Bar
- Find and replace
- Changes and commits
- Sign-in prompts
- Commit box layout
- Repository actions
- Commit options
- Ignoring files
- Diffs
- Inline diffs
- Staging view
- Revert a change
- Resolving conflicts
- Merge tool
- Blame
- History and log
- Merge commits
- Commit tabs
- Branches and tags
- Branches popup
- Remotes
- Stashes
- Shelf
- Git menu
- Git dialogs
- Interactive rebase
- Worktrees
- AI agents
- AI usage
- Keep computer awake
- AI tasks
- Submodules
- Git LFS
- GitHub account
- Git Console
- Back and Forward
- Integrated terminal
- Scripts tool window
- Menu bar
- Settings
- Search in Settings
- Color themes
- Rounded panels
- Updates and release channels
- Status bar and help links
- Notifications
- Memory readout and memory log
- Clear Cache
- MCP server and command line tool
- git mergetool mode