Skip to main content

ralph/git/lfs/
types.rs

1//! Git LFS data types.
2//!
3//! Responsibilities:
4//! - Define LFS filter, status, pointer, and health-report models.
5//! - Provide convenience helpers for health/issue reporting.
6//!
7//! Not handled here:
8//! - Running git commands or parsing command output.
9//!
10//! Invariants/assumptions:
11//! - `lfs_initialized = false` means the repository should be treated as healthy.
12
13#[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}