1use anyhow::{Context, Result};
4use git2::{Diff, DiffFormat, DiffOptions, Repository};
5
6use super::types::{DiffFileEntry, DiffHunk, DiffInfo, DiffLine, FileStatus};
7
8pub fn get_working_dir_diff(repo: &Repository) -> Result<Vec<DiffInfo>> {
14 let mut opts = DiffOptions::new();
15 opts.include_untracked(true);
16 opts.recurse_untracked_dirs(true);
17
18 let diff = repo
19 .diff_index_to_workdir(None, Some(&mut opts))
20 .context("failed to diff working directory against index")?;
21 parse_diff(&diff)
22}
23
24pub fn get_staged_diff(repo: &Repository) -> Result<Vec<DiffInfo>> {
28 let head_tree = match repo.head() {
29 Ok(reference) => {
30 let commit = reference
31 .peel_to_commit()
32 .context("HEAD does not point to a commit")?;
33 Some(commit.tree().context("commit has no tree")?)
34 }
35 Err(_) => None,
37 };
38
39 let diff = repo
40 .diff_tree_to_index(head_tree.as_ref(), None, None)
41 .context("failed to diff index against HEAD tree")?;
42 parse_diff(&diff)
43}
44
45pub fn get_working_dir_file_list(repo: &Repository) -> Result<Vec<DiffFileEntry>> {
50 let mut opts = DiffOptions::new();
51 opts.include_untracked(true).recurse_untracked_dirs(true);
52 let diff = repo
53 .diff_index_to_workdir(None, Some(&mut opts))
54 .context("failed to diff index to workdir")?;
55 Ok(diff_file_entries(&diff))
56}
57
58pub fn get_working_dir_single_file_diff(repo: &Repository, file_path: &str) -> Result<DiffInfo> {
62 let mut opts = DiffOptions::new();
63 opts.include_untracked(true)
64 .recurse_untracked_dirs(true)
65 .pathspec(file_path);
66 let diff = repo
67 .diff_index_to_workdir(None, Some(&mut opts))
68 .context("failed to diff index to workdir for single file")?;
69 let infos = parse_diff(&diff)?;
70 infos
71 .into_iter()
72 .next()
73 .ok_or_else(|| anyhow::anyhow!("file '{}' not found in working-dir diff", file_path))
74}
75
76pub fn get_staged_single_file_diff(repo: &Repository, file_path: &str) -> Result<DiffInfo> {
80 let head_tree = repo.head().ok().and_then(|r| r.peel_to_tree().ok());
81 let mut opts = DiffOptions::new();
82 opts.pathspec(file_path);
83 let diff = repo
84 .diff_tree_to_index(head_tree.as_ref(), None, Some(&mut opts))
85 .context("failed to diff HEAD to index for single file")?;
86 let infos = parse_diff(&diff)?;
87 infos
88 .into_iter()
89 .next()
90 .ok_or_else(|| anyhow::anyhow!("file '{}' not found in staged diff", file_path))
91}
92
93pub fn get_staged_file_list(repo: &Repository) -> Result<Vec<DiffFileEntry>> {
98 let head_tree = repo.head().ok().and_then(|r| r.peel_to_tree().ok());
99 let diff = repo
100 .diff_tree_to_index(head_tree.as_ref(), None, None)
101 .context("failed to diff HEAD to index")?;
102 Ok(diff_file_entries(&diff))
103}
104
105pub fn get_commit_diff(repo: &Repository, oid_str: &str) -> Result<Vec<DiffInfo>> {
109 let oid =
110 git2::Oid::from_str(oid_str).with_context(|| format!("invalid OID string: {oid_str}"))?;
111 let commit = repo
112 .find_commit(oid)
113 .with_context(|| format!("commit {oid_str} not found"))?;
114 let commit_tree = commit.tree().context("commit has no tree")?;
115
116 let parent_tree = if commit.parent_count() > 0 {
117 let parent = commit.parent(0).context("failed to read parent commit")?;
118 Some(parent.tree().context("parent commit has no tree")?)
119 } else {
120 None
121 };
122
123 let mut opts = DiffOptions::new();
124 let diff = repo
125 .diff_tree_to_tree(parent_tree.as_ref(), Some(&commit_tree), Some(&mut opts))
126 .context("failed to diff commit against parent")?;
127 parse_diff(&diff)
128}
129
130pub fn get_commit_range_diff(
136 repo: &Repository,
137 oldest_oid_str: &str,
138 newest_oid_str: &str,
139) -> Result<Vec<DiffInfo>> {
140 let oldest_oid = git2::Oid::from_str(oldest_oid_str)
141 .with_context(|| format!("invalid OID: {oldest_oid_str}"))?;
142 let oldest_commit = repo
143 .find_commit(oldest_oid)
144 .with_context(|| format!("commit {oldest_oid_str} not found"))?;
145
146 let start_tree = if oldest_commit.parent_count() > 0 {
148 let parent = oldest_commit.parent(0).context("failed to read parent")?;
149 Some(parent.tree().context("parent has no tree")?)
150 } else {
151 None
152 };
153
154 let newest_oid = git2::Oid::from_str(newest_oid_str)
155 .with_context(|| format!("invalid OID: {newest_oid_str}"))?;
156 let newest_commit = repo
157 .find_commit(newest_oid)
158 .with_context(|| format!("commit {newest_oid_str} not found"))?;
159 let end_tree = newest_commit.tree().context("newest commit has no tree")?;
160
161 let diff = repo
162 .diff_tree_to_tree(start_tree.as_ref(), Some(&end_tree), None)
163 .context("failed to compute range diff")?;
164 parse_diff(&diff)
165}
166
167pub fn checkout_file_at_commit(repo: &Repository, oid_str: &str, file_path: &str) -> Result<()> {
172 let oid =
173 git2::Oid::from_str(oid_str).with_context(|| format!("invalid OID string: {oid_str}"))?;
174 let commit = repo
175 .find_commit(oid)
176 .with_context(|| format!("commit {oid_str} not found"))?;
177 let tree = commit.tree().context("commit has no tree")?;
178
179 let mut opts = git2::build::CheckoutBuilder::new();
180 opts.path(file_path).force().update_index(true);
181
182 repo.checkout_tree(&tree.into_object(), Some(&mut opts))
183 .with_context(|| format!("failed to checkout '{file_path}' from commit {oid_str}"))?;
184
185 Ok(())
186}
187
188pub fn get_commit_file_list(
194 repo: &Repository,
195 oid_str: &str,
196) -> Result<Vec<super::types::DiffFileEntry>> {
197 let oid =
198 git2::Oid::from_str(oid_str).with_context(|| format!("invalid OID string: {oid_str}"))?;
199 let commit = repo
200 .find_commit(oid)
201 .with_context(|| format!("commit {oid_str} not found"))?;
202 let commit_tree = commit.tree().context("commit has no tree")?;
203
204 let parent_tree = if commit.parent_count() > 0 {
205 let parent = commit.parent(0).context("failed to read parent commit")?;
206 Some(parent.tree().context("parent commit has no tree")?)
207 } else {
208 None
209 };
210
211 let diff = repo
212 .diff_tree_to_tree(parent_tree.as_ref(), Some(&commit_tree), None)
213 .context("failed to diff commit against parent")?;
214
215 Ok(diff_file_entries(&diff))
216}
217
218pub fn get_single_file_diff(repo: &Repository, oid_str: &str, file_path: &str) -> Result<DiffInfo> {
223 let oid =
224 git2::Oid::from_str(oid_str).with_context(|| format!("invalid OID string: {oid_str}"))?;
225 let commit = repo
226 .find_commit(oid)
227 .with_context(|| format!("commit {oid_str} not found"))?;
228 let commit_tree = commit.tree().context("commit has no tree")?;
229
230 let parent_tree = if commit.parent_count() > 0 {
231 let parent = commit.parent(0).context("failed to read parent commit")?;
232 Some(parent.tree().context("parent commit has no tree")?)
233 } else {
234 None
235 };
236
237 let mut opts = DiffOptions::new();
238 opts.pathspec(file_path);
239
240 let diff = repo
241 .diff_tree_to_tree(parent_tree.as_ref(), Some(&commit_tree), Some(&mut opts))
242 .context("failed to diff commit against parent for single file")?;
243
244 let infos = parse_diff(&diff)?;
245 infos
246 .into_iter()
247 .next()
248 .ok_or_else(|| anyhow::anyhow!("file '{}' not found in commit diff", file_path))
249}
250
251pub fn diff_file_commit_vs_workdir(
257 repo: &Repository,
258 oid_str: &str,
259 file_path: &str,
260) -> Result<DiffInfo> {
261 let oid =
262 git2::Oid::from_str(oid_str).with_context(|| format!("invalid OID string: {oid_str}"))?;
263 let commit = repo
264 .find_commit(oid)
265 .with_context(|| format!("commit {oid_str} not found"))?;
266 let commit_tree = commit.tree().context("commit has no tree")?;
267
268 let mut opts = DiffOptions::new();
269 opts.pathspec(file_path);
270
271 let diff = repo
273 .diff_tree_to_workdir_with_index(Some(&commit_tree), Some(&mut opts))
274 .context("failed to diff commit tree against working directory")?;
275
276 let infos = parse_diff(&diff)?;
277
278 if let Some(info) = infos.into_iter().next() {
279 return Ok(info);
280 }
281
282 let in_commit = commit_tree
284 .get_path(std::path::Path::new(file_path))
285 .is_ok();
286
287 let workdir = repo.workdir().context("bare repository")?;
289 let in_workdir = workdir.join(file_path).exists();
290
291 match (in_commit, in_workdir) {
292 (true, true) => {
293 Ok(DiffInfo {
295 old_file: file_path.to_string(),
296 new_file: file_path.to_string(),
297 status: FileStatus::Modified,
298 hunks: vec![DiffHunk {
299 header: "@@ No changes — file is identical @@".to_string(),
300 lines: vec![DiffLine::HunkHeader(
301 "@@ No changes — file is identical to working tree @@".to_string(),
302 )],
303 }],
304 })
305 }
306 (true, false) => {
307 let blob_entry = commit_tree.get_path(std::path::Path::new(file_path))?;
309 let mut hunks = Vec::new();
310 if let Ok(blob) = repo.find_blob(blob_entry.id()) {
311 let content = String::from_utf8_lossy(blob.content());
312 let lines: Vec<DiffLine> = std::iter::once(DiffLine::HunkHeader(format!(
313 "@@ File deleted since commit {} @@",
314 &oid_str[..7.min(oid_str.len())]
315 )))
316 .chain(content.lines().map(|l| DiffLine::Deletion(l.to_string())))
317 .collect();
318
319 hunks.push(DiffHunk {
320 header: lines
321 .first()
322 .map(|l| match l {
323 DiffLine::HunkHeader(h) => h.clone(),
324 _ => String::new(),
325 })
326 .unwrap_or_default(),
327 lines,
328 });
329 }
330
331 Ok(DiffInfo {
332 old_file: file_path.to_string(),
333 new_file: String::new(),
334 status: FileStatus::Deleted,
335 hunks,
336 })
337 }
338 (false, true) => {
339 Err(anyhow::anyhow!(
341 "file '{}' did not exist at commit {} — it was added later",
342 file_path,
343 &oid_str[..7.min(oid_str.len())]
344 ))
345 }
346 (false, false) => Err(anyhow::anyhow!(
347 "file '{}' not found in commit {} or working tree — it may have been renamed",
348 file_path,
349 &oid_str[..7.min(oid_str.len())]
350 )),
351 }
352}
353
354pub fn file_list_commit_vs_workdir(
356 repo: &Repository,
357 oid_str: &str,
358) -> Result<Vec<super::types::DiffFileEntry>> {
359 let oid =
360 git2::Oid::from_str(oid_str).with_context(|| format!("invalid OID string: {oid_str}"))?;
361 let commit = repo
362 .find_commit(oid)
363 .with_context(|| format!("commit {oid_str} not found"))?;
364 let commit_tree = commit.tree().context("commit has no tree")?;
365
366 let diff = repo
367 .diff_tree_to_workdir_with_index(Some(&commit_tree), None)
368 .context("failed to diff commit tree against working directory")?;
369
370 Ok(diff_file_entries(&diff))
371}
372
373fn diff_file_entries(diff: &Diff<'_>) -> Vec<DiffFileEntry> {
377 diff.deltas()
378 .map(|delta| DiffFileEntry {
379 old_file: delta
380 .old_file()
381 .path()
382 .map(|p| p.to_string_lossy().into_owned())
383 .unwrap_or_default(),
384 new_file: delta
385 .new_file()
386 .path()
387 .map(|p| p.to_string_lossy().into_owned())
388 .unwrap_or_default(),
389 status: FileStatus::from_delta(delta.status()),
390 })
391 .collect()
392}
393
394fn parse_diff(diff: &Diff<'_>) -> Result<Vec<DiffInfo>> {
397 let num_deltas = diff.deltas().len();
398 let mut infos: Vec<DiffInfo> = Vec::with_capacity(num_deltas);
399
400 const MAX_LINES_PER_FILE: usize = 10_000;
404
405 for delta in diff.deltas() {
408 let old_file = delta
409 .old_file()
410 .path()
411 .map(|p| p.to_string_lossy().into_owned())
412 .unwrap_or_default();
413 let new_file = delta
414 .new_file()
415 .path()
416 .map(|p| p.to_string_lossy().into_owned())
417 .unwrap_or_default();
418 let status = FileStatus::from_delta(delta.status());
419 infos.push(DiffInfo {
420 old_file,
421 new_file,
422 status,
423 hunks: Vec::new(),
424 });
425 }
426
427 let mut current_delta_idx: usize = 0;
430 let mut current_line_count: usize = 0;
431
432 diff.print(DiffFormat::Patch, |delta, maybe_hunk, line| {
433 let delta_new = delta
435 .new_file()
436 .path()
437 .map(|p| p.to_string_lossy().into_owned())
438 .unwrap_or_default();
439 let delta_old = delta
440 .old_file()
441 .path()
442 .map(|p| p.to_string_lossy().into_owned())
443 .unwrap_or_default();
444
445 let found_idx = infos[current_delta_idx..]
447 .iter()
448 .position(|info| info.new_file == delta_new && info.old_file == delta_old)
449 .map(|pos| pos + current_delta_idx)
450 .or_else(|| {
451 infos[..current_delta_idx]
453 .iter()
454 .position(|info| info.new_file == delta_new && info.old_file == delta_old)
455 });
456
457 let found = found_idx.is_some();
458 if let Some(idx) = found_idx {
459 if idx != current_delta_idx {
460 current_delta_idx = idx;
462 current_line_count = infos[idx].hunks.iter().map(|h| h.lines.len()).sum();
463 }
464 }
465 if !found {
466 return true; }
468
469 if current_line_count >= MAX_LINES_PER_FILE {
471 return true;
472 }
473
474 let info = &mut infos[current_delta_idx];
475
476 if let Some(hunk) = maybe_hunk {
478 let header = String::from_utf8_lossy(hunk.header())
479 .trim_end()
480 .to_string();
481
482 let needs_new = match info.hunks.last() {
484 Some(h) => h.header != header,
485 None => true,
486 };
487 if needs_new {
488 info.hunks.push(DiffHunk {
489 header: header.clone(),
490 lines: vec![DiffLine::HunkHeader(header)],
491 });
492 current_line_count += 1;
493 }
494 }
495
496 if let Some(hunk) = info.hunks.last_mut() {
498 let content = String::from_utf8_lossy(line.content())
499 .trim_end_matches('\n')
500 .trim_end_matches('\r')
501 .to_string();
502
503 let diff_line = match line.origin() {
504 '+' | '>' => DiffLine::Addition(content),
505 '-' | '<' => DiffLine::Deletion(content),
506 ' ' => DiffLine::Context(content),
507 _ => return true,
510 };
511 hunk.lines.push(diff_line);
512 current_line_count += 1;
513 }
514
515 true
516 })
517 .context("failed to walk diff")?;
518
519 Ok(infos)
520}
521
522#[cfg(test)]
523mod tests {
524 use super::*;
525 use std::fs;
526
527 fn init_repo_with_commit(dir: &std::path::Path) -> git2::Repository {
528 let repo = git2::Repository::init(dir).unwrap();
529 {
530 let file_path = dir.join("hello.txt");
531 fs::write(&file_path, "Hello, world!\n").unwrap();
532
533 let mut index = repo.index().unwrap();
534 index.add_path(std::path::Path::new("hello.txt")).unwrap();
535 index.write().unwrap();
536
537 let tree_oid = index.write_tree().unwrap();
538 let tree = repo.find_tree(tree_oid).unwrap();
539 let sig = git2::Signature::now("Test", "test@test.com").unwrap();
540 repo.commit(Some("HEAD"), &sig, &sig, "initial commit", &tree, &[])
541 .unwrap();
542 }
543 repo
544 }
545
546 #[test]
547 fn working_dir_diff_shows_changes() {
548 let tmp = tempfile::tempdir().unwrap();
549 let repo = init_repo_with_commit(tmp.path());
550
551 fs::write(tmp.path().join("hello.txt"), "Hello, modified!\n").unwrap();
553
554 let diffs = get_working_dir_diff(&repo).unwrap();
555 assert_eq!(diffs.len(), 1);
556 assert_eq!(diffs[0].new_file, "hello.txt");
557 assert_eq!(diffs[0].status, FileStatus::Modified);
558 assert!(!diffs[0].hunks.is_empty());
559 }
560
561 #[test]
562 fn staged_diff_shows_staged_changes() {
563 let tmp = tempfile::tempdir().unwrap();
564 let repo = init_repo_with_commit(tmp.path());
565
566 fs::write(tmp.path().join("hello.txt"), "Hello, staged!\n").unwrap();
568 let mut index = repo.index().unwrap();
569 index.add_path(std::path::Path::new("hello.txt")).unwrap();
570 index.write().unwrap();
571
572 let diffs = get_staged_diff(&repo).unwrap();
573 assert_eq!(diffs.len(), 1);
574 assert_eq!(diffs[0].new_file, "hello.txt");
575 assert_eq!(diffs[0].status, FileStatus::Modified);
576 }
577
578 #[test]
579 fn commit_diff_shows_initial_commit() {
580 let tmp = tempfile::tempdir().unwrap();
581 let repo = init_repo_with_commit(tmp.path());
582
583 let head_oid = repo.head().unwrap().target().unwrap().to_string();
584 let diffs = get_commit_diff(&repo, &head_oid).unwrap();
585 assert_eq!(diffs.len(), 1);
586 assert_eq!(diffs[0].new_file, "hello.txt");
587 assert_eq!(diffs[0].status, FileStatus::New);
588 }
589
590 #[test]
591 fn working_dir_diff_untracked_file() {
592 let tmp = tempfile::tempdir().unwrap();
593 let repo = init_repo_with_commit(tmp.path());
594
595 fs::write(tmp.path().join("new_file.txt"), "I am new!\n").unwrap();
597
598 let diffs = get_working_dir_diff(&repo).unwrap();
599 assert_eq!(diffs.len(), 1);
600 assert_eq!(diffs[0].new_file, "new_file.txt");
601 assert_eq!(diffs[0].status, FileStatus::Untracked);
602 }
603
604 #[test]
605 fn commit_file_list_returns_entries() {
606 let tmp = tempfile::tempdir().unwrap();
607 let repo = init_repo_with_commit(tmp.path());
608 let head_oid = repo.head().unwrap().target().unwrap().to_string();
609 let files = get_commit_file_list(&repo, &head_oid).unwrap();
610 assert_eq!(files.len(), 1);
611 assert_eq!(files[0].new_file, "hello.txt");
612 assert_eq!(files[0].status, FileStatus::New);
613 assert_eq!(files[0].display_path(), "hello.txt");
614 }
615
616 #[test]
617 fn single_file_diff_returns_correct_file() {
618 let tmp = tempfile::tempdir().unwrap();
619 let repo = init_repo_with_commit(tmp.path());
620 let head_oid = repo.head().unwrap().target().unwrap().to_string();
621 let diff = get_single_file_diff(&repo, &head_oid, "hello.txt").unwrap();
622 assert_eq!(diff.new_file, "hello.txt");
623 assert_eq!(diff.status, FileStatus::New);
624 assert!(!diff.hunks.is_empty());
625 }
626
627 #[test]
628 fn diff_file_commit_vs_workdir_shows_changes() {
629 let tmp = tempfile::tempdir().unwrap();
630 let repo = init_repo_with_commit(tmp.path());
631 let head_oid = repo.head().unwrap().target().unwrap().to_string();
632
633 std::fs::write(tmp.path().join("hello.txt"), "Modified content!\n").unwrap();
635
636 let diff = diff_file_commit_vs_workdir(&repo, &head_oid, "hello.txt").unwrap();
637 assert_eq!(diff.new_file, "hello.txt");
638 assert!(!diff.hunks.is_empty());
639 }
640
641 #[test]
642 fn file_list_commit_vs_workdir_detects_modified() {
643 let tmp = tempfile::tempdir().unwrap();
644 let repo = init_repo_with_commit(tmp.path());
645 let head_oid = repo.head().unwrap().target().unwrap().to_string();
646
647 std::fs::write(tmp.path().join("hello.txt"), "Changed!\n").unwrap();
649
650 let files = file_list_commit_vs_workdir(&repo, &head_oid).unwrap();
651 assert!(!files.is_empty());
652 assert_eq!(files[0].display_path(), "hello.txt");
653 assert_eq!(files[0].status, FileStatus::Modified);
654 }
655
656 #[test]
657 fn file_list_commit_vs_workdir_detects_new_file() {
658 let tmp = tempfile::tempdir().unwrap();
659 let repo = init_repo_with_commit(tmp.path());
660 let head_oid = repo.head().unwrap().target().unwrap().to_string();
661
662 std::fs::write(tmp.path().join("new_file.txt"), "new\n").unwrap();
664 let mut index = repo.index().unwrap();
665 index
666 .add_path(std::path::Path::new("new_file.txt"))
667 .unwrap();
668 index.write().unwrap();
669
670 let files = file_list_commit_vs_workdir(&repo, &head_oid).unwrap();
671 let new = files.iter().find(|f| f.display_path() == "new_file.txt");
672 assert!(new.is_some(), "new_file.txt should appear in the diff list");
673 }
674
675 #[test]
676 fn file_list_commit_vs_workdir_detects_deletion() {
677 let tmp = tempfile::tempdir().unwrap();
678 let repo = init_repo_with_commit(tmp.path());
679 let head_oid = repo.head().unwrap().target().unwrap().to_string();
680
681 std::fs::remove_file(tmp.path().join("hello.txt")).unwrap();
683
684 let files = file_list_commit_vs_workdir(&repo, &head_oid).unwrap();
685 let deleted = files.iter().find(|f| f.display_path() == "hello.txt");
686 assert!(deleted.is_some());
687 assert_eq!(deleted.unwrap().status, FileStatus::Deleted);
688 }
689
690 #[test]
691 fn file_list_commit_vs_workdir_empty_when_unchanged() {
692 let tmp = tempfile::tempdir().unwrap();
693 let repo = init_repo_with_commit(tmp.path());
694 let head_oid = repo.head().unwrap().target().unwrap().to_string();
695
696 let files = file_list_commit_vs_workdir(&repo, &head_oid).unwrap();
698 assert!(
699 files.is_empty(),
700 "should be empty when working tree matches commit"
701 );
702 }
703
704 #[test]
705 fn checkout_file_at_commit_restores_file() {
706 let dir = tempfile::tempdir().unwrap();
707 let repo = init_repo_with_commit(dir.path());
708 let head_oid = repo
710 .head()
711 .unwrap()
712 .peel_to_commit()
713 .unwrap()
714 .id()
715 .to_string();
716 std::fs::write(dir.path().join("hello.txt"), "modified content").unwrap();
718 checkout_file_at_commit(&repo, &head_oid, "hello.txt").unwrap();
720 let content = std::fs::read_to_string(dir.path().join("hello.txt")).unwrap();
721 assert_eq!(content, "Hello, world!\n");
722 }
723
724 #[test]
725 fn range_diff_single_commit_matches_commit_diff() {
726 let dir = tempfile::tempdir().unwrap();
727 let repo = init_repo_with_commit(dir.path());
728 let oid = repo
729 .head()
730 .unwrap()
731 .peel_to_commit()
732 .unwrap()
733 .id()
734 .to_string();
735 let range = get_commit_range_diff(&repo, &oid, &oid).unwrap();
737 let single = get_commit_diff(&repo, &oid).unwrap();
738 assert_eq!(range.len(), single.len());
739 }
740
741 #[test]
742 fn single_file_diff_not_found() {
743 let tmp = tempfile::tempdir().unwrap();
744 let repo = init_repo_with_commit(tmp.path());
745 let head_oid = repo.head().unwrap().target().unwrap().to_string();
746 let result = get_single_file_diff(&repo, &head_oid, "nonexistent.txt");
747 assert!(result.is_err());
748 }
749
750 #[test]
751 fn working_dir_file_list_returns_modified_files() {
752 let tmp = tempfile::tempdir().unwrap();
753 let repo = init_repo_with_commit(tmp.path());
754
755 fs::write(tmp.path().join("hello.txt"), "Hello, modified!\n").unwrap();
757
758 let files = get_working_dir_file_list(&repo).unwrap();
759 assert_eq!(files.len(), 1);
760 assert_eq!(files[0].new_file, "hello.txt");
761 }
762
763 #[test]
764 fn working_dir_file_list_empty_for_clean_repo() {
765 let tmp = tempfile::tempdir().unwrap();
766 let repo = init_repo_with_commit(tmp.path());
767
768 let files = get_working_dir_file_list(&repo).unwrap();
770 assert!(
771 files.is_empty(),
772 "clean repo should have no working dir changes"
773 );
774 }
775
776 #[test]
777 fn staged_file_list_returns_staged_files() {
778 let tmp = tempfile::tempdir().unwrap();
779 let repo = init_repo_with_commit(tmp.path());
780
781 fs::write(tmp.path().join("hello.txt"), "Hello, staged!\n").unwrap();
783 let mut index = repo.index().unwrap();
784 index.add_path(std::path::Path::new("hello.txt")).unwrap();
785 index.write().unwrap();
786
787 let files = get_staged_file_list(&repo).unwrap();
788 assert_eq!(files.len(), 1);
789 }
790
791 #[test]
792 fn staged_file_list_empty_when_nothing_staged() {
793 let tmp = tempfile::tempdir().unwrap();
794 let repo = init_repo_with_commit(tmp.path());
795
796 fs::write(tmp.path().join("hello.txt"), "Hello, unstaged!\n").unwrap();
798
799 let files = get_staged_file_list(&repo).unwrap();
800 assert!(
801 files.is_empty(),
802 "unstaged changes should not appear in staged file list"
803 );
804 }
805
806 #[test]
807 fn working_dir_single_file_diff_returns_diff_for_specific_file() {
808 let tmp = tempfile::tempdir().unwrap();
809 let repo = init_repo_with_commit(tmp.path());
810
811 fs::write(tmp.path().join("other.txt"), "Other content\n").unwrap();
813 let mut index = repo.index().unwrap();
814 index.add_path(std::path::Path::new("other.txt")).unwrap();
815 index.write().unwrap();
816 let tree_oid = index.write_tree().unwrap();
817 let tree = repo.find_tree(tree_oid).unwrap();
818 let sig = git2::Signature::now("Test", "test@test.com").unwrap();
819 let parent = repo.head().unwrap().peel_to_commit().unwrap();
820 repo.commit(Some("HEAD"), &sig, &sig, "add other.txt", &tree, &[&parent])
821 .unwrap();
822
823 fs::write(tmp.path().join("hello.txt"), "Hello, changed!\n").unwrap();
825 fs::write(tmp.path().join("other.txt"), "Other changed!\n").unwrap();
826
827 let diff = get_working_dir_single_file_diff(&repo, "hello.txt").unwrap();
828 assert_eq!(diff.new_file, "hello.txt");
829 assert!(!diff.hunks.is_empty());
830 }
831
832 #[test]
833 fn staged_single_file_diff_returns_diff_for_staged_file() {
834 let tmp = tempfile::tempdir().unwrap();
835 let repo = init_repo_with_commit(tmp.path());
836
837 fs::write(tmp.path().join("hello.txt"), "Hello, staged diff!\n").unwrap();
839 let mut index = repo.index().unwrap();
840 index.add_path(std::path::Path::new("hello.txt")).unwrap();
841 index.write().unwrap();
842
843 let diff = get_staged_single_file_diff(&repo, "hello.txt").unwrap();
844 assert!(!diff.hunks.is_empty());
845 }
846}