1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7pub enum DiffType {
8 Added,
9 Removed,
10 Modified,
11 Unchanged,
12 TypeChanged,
13 Unreadable,
14 Incomparable,
15}
16
17impl std::fmt::Display for DiffType {
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 let s = match self {
20 DiffType::Added => "Added",
21 DiffType::Removed => "Removed",
22 DiffType::Modified => "Modified",
23 DiffType::Unchanged => "Unchanged",
24 DiffType::TypeChanged => "TypeChanged",
25 DiffType::Unreadable => "Unreadable",
26 DiffType::Incomparable => "Incomparable",
27 };
28 write!(f, "{s}")
29 }
30}
31
32impl DiffType {
33 pub fn is_changed(self) -> bool {
34 !matches!(self, DiffType::Unchanged)
35 }
36 pub fn is_error(self) -> bool {
37 matches!(self, DiffType::Unreadable | DiffType::Incomparable)
38 }
39}
40
41#[derive(Debug, Clone, Default)]
43pub struct DiffStats {
44 pub lines_added: usize,
45 pub lines_removed: usize,
46 pub lines_unchanged: usize,
47}
48
49impl DiffStats {
50 pub fn lines_changed(&self) -> usize {
51 self.lines_added + self.lines_removed
52 }
53
54 pub fn compute(before: &str, after: &str) -> Self {
56 use similar::{ChangeTag, TextDiff};
57 let td = TextDiff::from_lines(before, after);
58 let mut stats = DiffStats::default();
59 for change in td.iter_all_changes() {
60 match change.tag() {
61 ChangeTag::Insert => stats.lines_added += 1,
62 ChangeTag::Delete => stats.lines_removed += 1,
63 ChangeTag::Equal => stats.lines_unchanged += 1,
64 }
65 }
66 stats
67 }
68}
69
70#[derive(Debug, Clone)]
72pub struct DiffEntry {
73 pub path: String,
75 pub diff_type: DiffType,
76 pub is_dir: bool,
77
78 pub before_text: Option<String>,
80 pub after_text: Option<String>,
81
82 pub is_binary: bool,
85 pub before_size: Option<u64>,
87 pub after_size: Option<u64>,
89
90 pub before_sha256: Option<String>,
93 pub after_sha256: Option<String>,
95
96 pub stats: Option<DiffStats>,
99
100 pub error_detail: Option<String>,
101}
102
103impl DiffEntry {
104 pub fn has_text_diff(&self) -> bool {
105 !self.is_binary && (self.before_text.is_some() || self.after_text.is_some())
106 }
107
108 pub fn size_change_label(&self) -> Option<String> {
110 match (self.before_size, self.after_size) {
111 (Some(b), Some(a)) => Some(format!("{} → {}", fmt_size(b), fmt_size(a))),
112 (None, Some(a)) => Some(format!("(new) {}", fmt_size(a))),
113 (Some(b), None) => Some(format!("{} (removed)", fmt_size(b))),
114 (None, None) => None,
115 }
116 }
117}
118
119pub fn fmt_size(bytes: u64) -> String {
121 if bytes < 1_024 {
122 format!("{bytes} B")
123 } else if bytes < 1_024 * 1_024 {
124 format!("{:.1} KB", bytes as f64 / 1_024.0)
125 } else if bytes < 1_024 * 1_024 * 1_024 {
126 format!("{:.1} MB", bytes as f64 / (1_024.0 * 1_024.0))
127 } else {
128 format!("{:.2} GB", bytes as f64 / (1_024.0 * 1_024.0 * 1_024.0))
129 }
130}
131
132pub const LARGE_FILE_THRESHOLD: u64 = 1_024 * 1_024;