1use similar::{ChangeTag, TextDiff};
4
5pub struct DiffStats {
6 pub changed_lines: usize,
7 pub ratio: f64,
8}
9
10pub fn diff_stats(prev: &str, curr: &str) -> DiffStats {
13 let diff = TextDiff::from_lines(prev, curr);
14 let mut changed = 0usize;
15 for change in diff.iter_all_changes() {
16 match change.tag() {
17 ChangeTag::Insert | ChangeTag::Delete => changed += 1,
18 ChangeTag::Equal => {}
19 }
20 }
21 let prev_lines = prev.lines().count();
22 let curr_lines = curr.lines().count();
23 let denom = prev_lines.max(curr_lines).max(1);
24 DiffStats {
25 changed_lines: changed,
26 ratio: changed as f64 / denom as f64,
27 }
28}
29
30pub fn unified_diff(prev: &str, curr: &str, context: usize, max_lines: usize) -> String {
32 let diff = TextDiff::from_lines(prev, curr);
33 let mut out: Vec<String> = Vec::new();
34 'outer: for group in diff.grouped_ops(context) {
35 for op in group {
36 for change in diff.iter_changes(&op) {
37 let sign = match change.tag() {
38 ChangeTag::Delete => '-',
39 ChangeTag::Insert => '+',
40 ChangeTag::Equal => ' ',
41 };
42 let value = change.value();
43 let value = value.strip_suffix('\n').unwrap_or(value);
44 out.push(format!("{sign}{value}"));
45 if out.len() >= max_lines {
46 out.push("... (diff truncated)".to_string());
47 break 'outer;
48 }
49 }
50 }
51 }
52 out.join("\n")
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[test]
60 fn identical_has_no_changes() {
61 let stats = diff_stats("a\nb\nc\n", "a\nb\nc\n");
62 assert_eq!(stats.changed_lines, 0);
63 assert_eq!(stats.ratio, 0.0);
64 }
65
66 #[test]
67 fn one_line_change_is_small() {
68 let stats = diff_stats("a\nb\nc\n", "a\nB\nc\n");
69 assert_eq!(stats.changed_lines, 2);
71 assert!(stats.ratio < 0.8);
72 }
73
74 #[test]
75 fn unified_diff_shows_the_change() {
76 let d = unified_diff("a\nb\nc\n", "a\nB\nc\n", 1, 100);
77 assert!(d.contains("-b"));
78 assert!(d.contains("+B"));
79 }
80
81 #[test]
82 fn unified_diff_caps_lines() {
83 let prev: String = (0..100).map(|i| format!("line {i}\n")).collect();
84 let curr: String = (0..100).map(|i| format!("changed {i}\n")).collect();
85 let d = unified_diff(&prev, &curr, 3, 10);
86 assert!(d.lines().count() <= 11);
87 assert!(d.contains("truncated"));
88 }
89}