From 5c3d858a3525c52ef296a032b8a7e5e9eccb3863 Mon Sep 17 00:00:00 2001 From: VXNCXNX Date: Sat, 15 Aug 2026 08:30:09 +0000 Subject: [PATCH] fix: survive stashing untracked files from inside the current directory 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. --- CHANGELOG.md | 1 + asyncgit/src/sync/repository.rs | 17 +++++++++++++++++ asyncgit/src/sync/stash.rs | 23 ++++++++++++++++++++++- src/main.rs | 4 +++- 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1ca0eaac2..555e28b22a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * crash when opening submodule ([#2895](https://github.com/gitui-org/gitui/issues/2895)) * when staging the last file in a directory, the first item after the directory is no longer skipped [[@Tillerino](https://github.com/Tillerino)] ([#2748](https://github.com/gitui-org/gitui/issues/2748)) * index-out-of-bounds panic when unstaging lines near the end of a diff ([#2953](https://github.com/gitui-org/gitui/issues/2953)) +* crash after stashing untracked files when the current directory is one of them ([#2651](https://github.com/gitui-org/gitui/issues/2651)) ## [0.28.1] - 2026-03-21 diff --git a/asyncgit/src/sync/repository.rs b/asyncgit/src/sync/repository.rs index c49795fb7c..ab9f77cc39 100644 --- a/asyncgit/src/sync/repository.rs +++ b/asyncgit/src/sync/repository.rs @@ -40,6 +40,23 @@ impl RepoPath { Self::Workdir { workdir, .. } => Some(workdir.as_path()), } } + + /// resolve to the absolute repo root so later operations survive + /// the deletion of the process cwd (see #2651) + pub fn resolve_root(&self) -> Result { + match self { + Self::Path(_) => { + let repo = repo(self)?; + // bare repos have no workdir + let root = repo + .workdir() + .unwrap_or_else(|| repo.path()) + .to_path_buf(); + Ok(Self::Path(root)) + } + Self::Workdir { .. } => Ok(self.clone()), + } + } } impl From for RepoPath { diff --git a/asyncgit/src/sync/stash.rs b/asyncgit/src/sync/stash.rs index 8a65f92feb..125d979903 100644 --- a/asyncgit/src/sync/stash.rs +++ b/asyncgit/src/sync/stash.rs @@ -135,7 +135,28 @@ mod tests { }, utils::{repo_read_file, repo_write_file}, }; - use std::{fs::File, io::Write, path::Path}; + use std::{fs, fs::File, io::Write, path::Path}; + + #[test] + fn test_stash_untracked_removes_stored_repo_path() -> Result<()> { + let (_td, repo) = repo_init()?; + let root = repo.path().parent().unwrap(); + + let sub_dir = root.join("untracked_sub"); + fs::create_dir(&sub_dir)?; + File::create(sub_dir.join("foo.txt"))?.write_all(b"foo")?; + + // gitui stores the startup path and reopens from it on every call, so a + // path inside the untracked dir is the same trap as a relative "." there + let repo_path = RepoPath::Path(sub_dir).resolve_root()?; + + stash_save(&repo_path, None, true, false)?; + + // stashing deleted that dir, further operations must still work + assert_eq!(get_stashes(&repo_path)?.len(), 1); + + Ok(()) + } #[test] fn test_smoke() { diff --git a/src/main.rs b/src/main.rs index fd662950a2..52b462d3c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -161,10 +161,12 @@ macro_rules! log_eprintln { fn main() -> Result<()> { let app_start = Instant::now(); - let cliargs = process_cmdline()?; + let mut cliargs = process_cmdline()?; asyncgit::register_tracing_logging(); ensure_valid_path(&cliargs.repo_path)?; + // pin to the repo root so operations survive deletion of the cwd + cliargs.repo_path = cliargs.repo_path.resolve_root()?; let key_config = KeyConfig::init( cliargs.key_bindings_path.as_ref(),