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(),