diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bdad9ba8f..6bb2cb6b51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixes * 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)) ## [0.28.1] - 2026-03-21 diff --git a/asyncgit/src/sync/staging/mod.rs b/asyncgit/src/sync/staging/mod.rs index fc16340811..3070292964 100644 --- a/asyncgit/src/sync/staging/mod.rs +++ b/asyncgit/src/sync/staging/mod.rs @@ -39,8 +39,10 @@ impl NewFromOldContent { } fn add_old_line(&mut self, old_lines: &[&str]) { - self.lines.push(old_lines[self.old_index].to_string()); - self.old_index += 1; + if let Some(line) = old_lines.get(self.old_index) { + self.lines.push((*line).to_string()); + self.old_index += 1; + } } fn catchup_to_hunkstart( @@ -48,7 +50,9 @@ impl NewFromOldContent { hunk_start: usize, old_lines: &[&str], ) { - while hunk_start > self.old_index + 1 { + while hunk_start > self.old_index + 1 + && self.old_index < old_lines.len() + { self.add_old_line(old_lines); } } @@ -182,3 +186,35 @@ pub fn load_file( Ok(res) } + +#[cfg(test)] +mod tests { + use super::NewFromOldContent; + + // Regression for #2953: indexing old_lines past its length used to panic + // in add_old_line / catchup_to_hunkstart when a hunk_start pointed past + // the end of the working copy. The bounds-checked helpers must stop + // catching up at end-of-buffer instead of panicking. + #[test] + fn catchup_to_hunkstart_past_end_does_not_panic() { + let old_lines = ["a", "b", "c"]; + let mut content = NewFromOldContent::default(); + + content.catchup_to_hunkstart(99, &old_lines); + + assert_eq!(content.old_index, old_lines.len()); + assert_eq!(content.lines, vec!["a", "b", "c"]); + } + + #[test] + fn add_old_line_at_end_is_noop() { + let old_lines = ["only-line"]; + let mut content = NewFromOldContent::default(); + content.add_old_line(&old_lines); + assert_eq!(content.old_index, 1); + + content.add_old_line(&old_lines); + assert_eq!(content.old_index, 1); + assert_eq!(content.lines, vec!["only-line"]); + } +}