diff --git a/asyncgit/src/sync/staging/mod.rs b/asyncgit/src/sync/staging/mod.rs index fc16340811..57b9b0532c 100644 --- a/asyncgit/src/sync/staging/mod.rs +++ b/asyncgit/src/sync/staging/mod.rs @@ -13,10 +13,20 @@ use std::{collections::HashSet, fs::File, io::Read}; const NEWLINE: char = '\n'; -#[derive(Default)] struct NewFromOldContent { lines: Vec, old_index: usize, + trailing_newline: bool, +} + +impl Default for NewFromOldContent { + fn default() -> Self { + Self { + lines: Vec::new(), + old_index: 0, + trailing_newline: true, + } + } } impl NewFromOldContent { @@ -57,14 +67,11 @@ impl NewFromOldContent { for line in old_lines.iter().skip(self.old_index) { self.lines.push((*line).to_string()); } - let lines = self.lines.join("\n"); - if lines.ends_with(NEWLINE) { - lines - } else { - let mut lines = lines; + let mut lines = self.lines.join("\n"); + if self.trailing_newline && !lines.ends_with(NEWLINE) { lines.push(NEWLINE); - lines } + lines } } @@ -133,6 +140,18 @@ pub fn apply_selection( || hunk_line.origin_value() == DiffLineType::AddEOFNL { + let applying = (is_staged && !selected_line) + || (!is_staged && selected_line); + new_content.trailing_newline = + if hunk_line.origin_value() + == DiffLineType::DeleteEOFNL + { + // old had newline, new doesn't; applying removal → no newline + !applying + } else { + // AddEOFNL: old had no newline, new does; applying addition → newline + applying + }; break; } diff --git a/asyncgit/src/sync/staging/stage_tracked.rs b/asyncgit/src/sync/staging/stage_tracked.rs index 891f61bef1..84de71991b 100644 --- a/asyncgit/src/sync/staging/stage_tracked.rs +++ b/asyncgit/src/sync/staging/stage_tracked.rs @@ -142,6 +142,48 @@ c = 4"; assert_eq!(&*diff.hunks[0].lines[0].content, "@@ -1,2 +1 @@"); } + #[test] + fn test_stage_preserves_no_trailing_newline() { + // Both files have no trailing newline + static FILE_1: &str = "line1\nline2"; + static FILE_2: &str = "line1_changed\nline2"; + + let (path, repo) = repo_init().unwrap(); + let path: &RepoPath = &path.path().to_str().unwrap().into(); + + write_commit_file(&repo, "test.txt", FILE_1, "c1"); + repo_write_file(&repo, "test.txt", FILE_2).unwrap(); + + // Stage only the changed first line + stage_lines( + path, + "test.txt", + false, + &[DiffLinePosition { + old_lineno: Some(1), + new_lineno: Some(1), + }], + ) + .unwrap(); + + // Read the staged blob and verify it has no trailing newline + let git_repo = repo(path).unwrap(); + let mut index = git_repo.index().unwrap(); + index.read(true).unwrap(); + let entry = index + .get_path(Path::new("test.txt"), 0) + .expect("entry not found"); + let blob = git_repo.find_blob(entry.id).unwrap(); + let staged = std::str::from_utf8(blob.content()).unwrap(); + + assert!( + !staged.ends_with('\n'), + "staged content must not gain a trailing newline; got: {:?}", + staged + ); + assert_eq!(staged, "line1_changed\nline2"); + } + #[test] fn test_unstage() { static FILE_1: &str = r"0