Skip to content

fix: survive stashing untracked files from inside the current directory - #3015

Open
VXNCXNX wants to merge 1 commit into
gitui-org:masterfrom
VXNCXNX:fix/stash-deletes-cwd-2651
Open

fix: survive stashing untracked files from inside the current directory#3015
VXNCXNX wants to merge 1 commit into
gitui-org:masterfrom
VXNCXNX:fix/stash-deletes-cwd-2651

Conversation

@VXNCXNX

@VXNCXNXVXNCXNX commented Aug 15, 2026

Copy link
Copy Markdown

This Pull Request fixes/closes#2651.

It changes the following:

  • RepoPath::Path is resolved once at startup to the repository workdir root, so stashing untracked files from inside an untracked subdirectory no longer leaves every later operation reopening a deleted inode.
  • As a side effect, --file from a subdirectory now produces a path relative to the repo root rather than to the cwd. See the note below, this overlaps fix: resolve --file from repository subdirectories #2989.

What's broken

Stashing with untracked files included, while the cwd is an untracked subdirectory, deletes that directory and then every later operation fails:

Error: git error:failed to resolve path '.': No such file or directory; class=Os (2); code=NotFound (-3)

Worth noting that git stash -u refuses this outright, printing "Refusing to remove current working directory" and keeping the directory. libgit2 has no such guard, which is why gitui hits it and the CLI does not.

The cause

DEFAULT_GIT_DIR is the literal ".", and that string is what lands in RepoPath::Path. repository::repo() reopens from it on every operation. stash_save passes INCLUDE_UNTRACKED to stash_save2, which removes the now-empty untracked directory that is the process cwd, so the next reopen resolves "." against a deleted inode.

The fix

Resolve the path once at startup to the repository workdir root, and store that.

Canonicalizing the cwd is not enough, which I checked before writing this: the canonical path is the very directory that gets deleted, so it fails identically. Only the workdir root survives.

RepoPath::Workdir (from --workdir/--directory) is returned untouched. Bare repos fall back to repo.path(), since workdir() is None there.

What this does to --file, since it overlaps #2989

Please look at this part specifically.

app.rs relativizes the selected file by stripping gitpath() from it, so making the root absolute changes what that produces from a subdirectory:

from repo ROOT:
before: sub/subfile.txt after: sub/subfile.txt (identical)
from a SUBDIRECTORY:
before: subfile.txt after: sub/subfile.txt

The old value was relative to the cwd rather than to the repo, which is not a valid git path, and is why --file currently does nothing from a subdirectory. So this incidentally fixes that too, which is the same ground #2989 is on. If you would rather keep the two separate I can gate the resolution so --file behaviour is untouched here, just say which you prefer.

start_terminal also canonicalizes the path for the window title. That becomes a no-op, since the path is already an absolute existing directory.

Verification

cargo fmt -- --check clean, cargo clippy --workspace clean, and the asyncgit suite is 178 passed, 0 failed.

The new test creates an untracked subdirectory, stashes with untracked included, and asserts a later get_stashes still works. Reverting the resolution with the test kept reproduces the issue's error exactly:

Error: Git(Error { code: -3, klass: 2, message: "failed to resolve path '/tmp/gituimpXLlK/untracked_sub': No such file or directory" })

One note on how that test is written. My first version changed the process cwd, which passed on its own but broke test_merge_fastforward when the suite ran in parallel, since cwd is process-wide. The version here passes the doomed directory as the stored path instead, which is the same trap without touching global state.

Separate bug found while testing, not fixed here

Opening the Log tab after this scenario still panics, via gix rather than git2:

revlog.rs:186 failed to fetch: Gix(Discover(...CurrentDir(NotFound)))

I confirmed it is independent by reproducing it on an unmodified binary using an explicit absolute -d, a path this change never touches. gix_repo discovery consults the process cwd itself, so RepoPath cannot fix it. Happy to file it separately.

I followed the checklist:

  • I added unittests (in asyncgit/src/sync/stash.rs, described above)
  • I ran make check without errors (fmt and clippy clean; cargo nextest run --workspace is 318/319, the one failure being git2-hooks tests::test_pre_commit_py, which needs a python binary and fails identically on a clean master checkout here. sort and deny need tombi and cargo-deny, which are not installed in this environment)
  • I tested the overall application (verified at the asyncgit layer; I did not drive the whole TUI through this scenario, and the separate gix Log-tab panic noted above is still present)
  • I added an appropriate item to the changelog

Disclosure: written with AI assistance (Claude Code). I reproduced the issue, ran the change and the verification myself.

The startup path was stored as a literal "." and reopened on every
operation, so stashing with untracked files included deleted the cwd and
every later call failed to resolve it. Resolve to the repo workdir root
once at startup instead.

@CAOShurongCAOShurong left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I independently validated exact head 5c3d858a: cargo fmt -- --check passed; cargo nextest run -p asyncgit stash passed 14/14; full asyncgit nextest passed 173/173; and a separate pygit2 reproduction showed the baseline stored subdirectory becomes unreopenable after libgit2 stashes/removes it, while resolving to the workdir root keeps the stash reopenable. The fix itself looks correct for the git2 path.

One follow-up: the PR description says opening the Log tab in the same post-stash scenario still panics through gix_repo (CurrentDir(NotFound)). If that remains reproducible on this head, could you please file/link a separate issue (or narrow the PR's scope explicitly)? I don't think it invalidates this focused git2 fix, but linking it would make the remaining user-visible limitation actionable.

AI disclosure: this review was prepared with agent assistance; I independently ran the commands above and checked the exact head.

@VXNCXNX

Copy link
Copy Markdown
Author

Thanks for running it independently, and for the disclosure.

Filed as #3017. It does still reproduce on this head. I re-checked it two ways before filing: with an explicit absolute repo root, and with RepoPath::resolve_root() applied exactly as main.rs does at startup. Identical panic both times, so it is independent of which path this PR stores.

The reason it cannot be fixed from the path side is that gix reads the cwd before it looks at the path it was handed:

gix/src/discover.rs:45 options.current_dir = Some(gix_fs::current_dir(false).map_err(upwards::Error::CurrentDir)?);
gix-fs/src/lib.rs:56 let cwd = std::env::current_dir()?;

Two things I did not have in the PR description and put in the issue: the panic aborts the whole process through rayon rather than just the log thread, and the filtered log path does not hit it, since that one goes through git2. So it only fires with no commit filter active.

Happy to narrow this PR's description if you would rather it not mention the gix bug at all now that it has its own issue.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Crash when stashing current directory

2 participants

@VXNCXNX@CAOShurong