1#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct LfsFilterStatus {
15 pub smudge_installed: bool,
16 pub clean_installed: bool,
17 pub smudge_value: Option<String>,
18 pub clean_value: Option<String>,
19}
20
21impl LfsFilterStatus {
22 pub fn is_healthy(&self) -> bool {
23 self.smudge_installed && self.clean_installed
24 }
25
26 pub fn issues(&self) -> Vec<String> {
27 let mut issues = Vec::new();
28 if !self.smudge_installed {
29 issues.push("LFS smudge filter not configured".to_string());
30 }
31 if !self.clean_installed {
32 issues.push("LFS clean filter not configured".to_string());
33 }
34 issues
35 }
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Default)]
39pub struct LfsStatusSummary {
40 pub staged_lfs: Vec<String>,
41 pub staged_not_lfs: Vec<String>,
42 pub unstaged_lfs: Vec<String>,
43 pub untracked_attributes: Vec<String>,
44}
45
46impl LfsStatusSummary {
47 pub fn is_clean(&self) -> bool {
48 self.staged_not_lfs.is_empty()
49 && self.untracked_attributes.is_empty()
50 && self.unstaged_lfs.is_empty()
51 }
52
53 pub fn issue_descriptions(&self) -> Vec<String> {
54 let mut issues = Vec::new();
55 if !self.staged_not_lfs.is_empty() {
56 issues.push(format!(
57 "Files staged as regular files but should be LFS: {}",
58 self.staged_not_lfs.join(", ")
59 ));
60 }
61 if !self.untracked_attributes.is_empty() {
62 issues.push(format!(
63 "Files match .gitattributes LFS patterns but are not tracked by LFS: {}",
64 self.untracked_attributes.join(", ")
65 ));
66 }
67 if !self.unstaged_lfs.is_empty() {
68 issues.push(format!(
69 "Modified LFS files not staged: {}",
70 self.unstaged_lfs.join(", ")
71 ));
72 }
73 issues
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub enum LfsPointerIssue {
79 InvalidPointer {
80 path: String,
81 reason: String,
82 },
83 BinaryContent {
84 path: String,
85 },
86 Corrupted {
87 path: String,
88 content_preview: String,
89 },
90}
91
92impl LfsPointerIssue {
93 pub fn path(&self) -> &str {
94 match self {
95 Self::InvalidPointer { path, .. }
96 | Self::BinaryContent { path }
97 | Self::Corrupted { path, .. } => path,
98 }
99 }
100
101 pub fn description(&self) -> String {
102 match self {
103 Self::InvalidPointer { path, reason } => {
104 format!("Invalid LFS pointer for '{}': {}", path, reason)
105 }
106 Self::BinaryContent { path } => format!(
107 "'{}' contains binary content but should be an LFS pointer (smudge filter may not be working)",
108 path
109 ),
110 Self::Corrupted {
111 path,
112 content_preview,
113 } => {
114 format!(
115 "Corrupted LFS pointer for '{}': preview='{}'",
116 path, content_preview
117 )
118 }
119 }
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Default)]
124pub struct LfsHealthReport {
125 pub lfs_initialized: bool,
126 pub filter_status: Option<LfsFilterStatus>,
127 pub status_summary: Option<LfsStatusSummary>,
128 pub pointer_issues: Vec<LfsPointerIssue>,
129}
130
131impl LfsHealthReport {
132 pub fn is_healthy(&self) -> bool {
133 if !self.lfs_initialized {
134 return true;
135 }
136
137 let Some(filter) = &self.filter_status else {
138 return false;
139 };
140 if !filter.is_healthy() {
141 return false;
142 }
143
144 let Some(status) = &self.status_summary else {
145 return false;
146 };
147 if !status.is_clean() {
148 return false;
149 }
150
151 self.pointer_issues.is_empty()
152 }
153
154 pub fn all_issues(&self) -> Vec<String> {
155 let mut issues = Vec::new();
156 if let Some(filter) = &self.filter_status {
157 issues.extend(filter.issues());
158 }
159 if let Some(status) = &self.status_summary {
160 issues.extend(status.issue_descriptions());
161 }
162 issues.extend(self.pointer_issues.iter().map(LfsPointerIssue::description));
163 issues
164 }
165}