Skip to content

Commit 2fbad93

Browse files
author
Austin Emmons
committed
fix: handle untracked diffs and hunk mapping after deletions
1 parent b910781 commit 2fbad93

2 files changed

Lines changed: 128 additions & 10 deletions

File tree

src-tauri/src/shared/git_ui_core/diff.rs

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -199,19 +199,32 @@ fn source_diff_for_path(
199199
path: &str,
200200
cached: bool,
201201
ignore_whitespace_changes: bool,
202+
is_untracked_worktree_file: bool,
202203
) -> Option<String> {
203204
let git_bin = resolve_git_binary().ok()?;
204205
let mut args = vec!["diff"];
205-
if cached {
206-
args.push("--cached");
207-
}
208-
args.push("--no-color");
209-
args.push("-U0");
210-
if ignore_whitespace_changes {
211-
args.push("-w");
206+
if is_untracked_worktree_file && !cached {
207+
args.push("--no-index");
208+
args.push("--no-color");
209+
args.push("-U0");
210+
if ignore_whitespace_changes {
211+
args.push("-w");
212+
}
213+
args.push("--");
214+
args.push(if cfg!(windows) { "NUL" } else { "/dev/null" });
215+
args.push(path);
216+
} else {
217+
if cached {
218+
args.push("--cached");
219+
}
220+
args.push("--no-color");
221+
args.push("-U0");
222+
if ignore_whitespace_changes {
223+
args.push("-w");
224+
}
225+
args.push("--");
226+
args.push(path);
212227
}
213-
args.push("--");
214-
args.push(path);
215228

216229
let output = std_command(git_bin)
217230
.args(args)
@@ -370,7 +383,7 @@ fn map_new_to_old_line_clamped(hunks: &[ParsedPatchHunk], new_line: usize) -> us
370383
if new_line < insertion_point {
371384
break;
372385
}
373-
delta -= hunk.old_count as isize;
386+
delta += hunk.old_count as isize;
374387
continue;
375388
}
376389

@@ -719,6 +732,42 @@ mod display_hunk_tests {
719732

720733
assert!(display_hunks[0].start_display_line_index < display_hunks[1].start_display_line_index);
721734
}
735+
736+
#[test]
737+
fn build_display_hunks_maps_unstaged_hunks_after_staged_deletions() {
738+
let diff = concat!(
739+
"@@ -2,1 +2,0 @@\n",
740+
"-line two\n",
741+
"@@ -5,1 +4,1 @@\n",
742+
"-line five\n",
743+
"+line five updated\n"
744+
);
745+
let staged_diff = concat!(
746+
"diff --git a/example.txt b/example.txt\n",
747+
"index 1111111..2222222 100644\n",
748+
"--- a/example.txt\n",
749+
"+++ b/example.txt\n",
750+
"@@ -2,1 +2,0 @@\n",
751+
"-line two\n"
752+
);
753+
let unstaged_diff = concat!(
754+
"diff --git a/example.txt b/example.txt\n",
755+
"index 2222222..3333333 100644\n",
756+
"--- a/example.txt\n",
757+
"+++ b/example.txt\n",
758+
"@@ -4,1 +4,1 @@\n",
759+
"-line five\n",
760+
"+line five updated\n"
761+
);
762+
763+
let display_hunks = build_display_hunks(diff, Some(staged_diff), Some(unstaged_diff));
764+
765+
assert_eq!(display_hunks.len(), 2);
766+
assert_eq!(display_hunks[0].id, "staged:2:1:2:0");
767+
assert_eq!(display_hunks[1].id, "unstaged:4:1:4:1");
768+
assert_eq!(display_hunks[1].start_display_line_index, 3);
769+
assert_eq!(display_hunks[1].end_display_line_index, 4);
770+
}
722771
}
723772

724773
fn has_ignored_parent_directory(repo: &Repository, path: &Path) -> bool {
@@ -1132,11 +1181,15 @@ pub(super) async fn get_git_diffs_inner(
11321181
let is_image = old_image_mime.is_some() || new_image_mime.is_some();
11331182
let is_deleted = delta.status() == git2::Delta::Deleted;
11341183
let is_added = delta.status() == git2::Delta::Added;
1184+
let file_status = repo.status_file(display_path).unwrap_or(Status::empty());
1185+
let is_untracked_worktree_file =
1186+
file_status.contains(Status::WT_NEW) && !file_status.contains(Status::INDEX_NEW);
11351187
let staged_diff = source_diff_for_path(
11361188
&repo_root,
11371189
normalized_path.as_str(),
11381190
true,
11391191
ignore_whitespace_changes,
1192+
is_untracked_worktree_file,
11401193
)
11411194
.and_then(|diff| {
11421195
if diff.trim().is_empty() {
@@ -1150,6 +1203,7 @@ pub(super) async fn get_git_diffs_inner(
11501203
normalized_path.as_str(),
11511204
false,
11521205
ignore_whitespace_changes,
1206+
is_untracked_worktree_file,
11531207
)
11541208
.and_then(|diff| {
11551209
if diff.trim().is_empty() {

src-tauri/src/shared/git_ui_core/tests.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,70 @@ fn get_git_diffs_omits_global_ignored_paths() {
226226
assert!(!has_ignored, "ignored files should not appear in diff list");
227227
}
228228

229+
#[test]
230+
fn get_git_diffs_populates_untracked_file_unstaged_diff_and_display_hunks() {
231+
let (root, repo) = create_temp_repo();
232+
let tracked_path = root.join("tracked.txt");
233+
fs::write(&tracked_path, "tracked\n").expect("write tracked file");
234+
let mut index = repo.index().expect("repo index");
235+
index.add_path(Path::new("tracked.txt")).expect("add tracked path");
236+
index.write().expect("write index");
237+
let tree_id = index.write_tree().expect("write tree");
238+
let tree = repo.find_tree(tree_id).expect("find tree");
239+
let sig = git2::Signature::now("Test", "test@example.com").expect("signature");
240+
repo.commit(Some("HEAD"), &sig, &sig, "init", &tree, &[])
241+
.expect("commit");
242+
243+
fs::write(root.join("new-file.txt"), "first line\nsecond line\n").expect("write new file");
244+
245+
let workspace = WorkspaceEntry {
246+
id: "w1".to_string(),
247+
name: "w1".to_string(),
248+
path: root.to_string_lossy().to_string(),
249+
kind: WorkspaceKind::Main,
250+
parent_id: None,
251+
worktree: None,
252+
settings: WorkspaceSettings::default(),
253+
};
254+
let mut entries = HashMap::new();
255+
entries.insert("w1".to_string(), workspace);
256+
let workspaces = Mutex::new(entries);
257+
let app_settings = Mutex::new(AppSettings::default());
258+
259+
let runtime = Runtime::new().expect("create tokio runtime");
260+
let diffs = runtime
261+
.block_on(diff::get_git_diffs_inner(
262+
&workspaces,
263+
&app_settings,
264+
"w1".to_string(),
265+
))
266+
.expect("get git diffs");
267+
268+
let diff = diffs
269+
.iter()
270+
.find(|diff| diff.path == "new-file.txt")
271+
.expect("find new file diff");
272+
let unstaged_diff = diff
273+
.unstaged_diff
274+
.as_deref()
275+
.expect("untracked file should have unstaged diff");
276+
277+
assert!(
278+
unstaged_diff.contains("+++ b/new-file.txt"),
279+
"expected relative untracked diff header, got: {unstaged_diff}"
280+
);
281+
assert!(
282+
unstaged_diff.contains("+first line\n+second line"),
283+
"expected untracked diff body, got: {unstaged_diff}"
284+
);
285+
assert!(
286+
!diff.display_hunks.is_empty(),
287+
"untracked file should expose display hunks"
288+
);
289+
assert_eq!(diff.display_hunks[0].source, "unstaged");
290+
assert_eq!(diff.display_hunks[0].action, "stage");
291+
}
292+
229293
#[test]
230294
fn check_ignore_with_git_respects_negated_rule_for_specific_file() {
231295
let (root, repo) = create_temp_repo();

0 commit comments

Comments
 (0)