1const SOFT_DIFF_LIMIT: usize = 15_000;
2
3#[derive(Debug, thiserror::Error)]
4pub enum PreflightError {
5 #[error("Not a git repository")]
6 NotGitRepo,
7 #[error("Git command failed: {command}")]
8 GitCommandFailed {
9 command: String,
10 source: std::io::Error,
11 },
12 #[error("No staged files")]
13 NoStagedFiles { unstaged: Vec<UnstagedFile> },
14 #[error("Diff too large: {size} chars")]
15 DiffTooLarge { size: usize },
16}
17
18#[derive(Debug, Clone)]
19pub struct UnstagedFile {
20 pub status: String,
21 pub path: String,
22}
23
24#[derive(Debug)]
25#[allow(dead_code)]
26pub struct PreflightSuccess {
27 pub diff_content: String,
28}
29
30fn is_git_repo() -> bool {
31 std::process::Command::new("git")
32 .args(["rev-parse", "--is-inside-work-tree"])
33 .output()
34 .map(|output| {
35 let stdout = String::from_utf8_lossy(&output.stdout);
36 stdout.trim() == "true"
37 })
38 .unwrap_or(false)
39}
40
41fn get_staged_files() -> Result<Vec<String>, std::io::Error> {
42 let output = std::process::Command::new("git")
43 .args(["diff", "--cached", "--name-only"])
44 .output()?;
45 Ok(String::from_utf8_lossy(&output.stdout)
46 .lines()
47 .map(String::from)
48 .collect())
49}
50
51fn get_unstaged_files() -> Result<Vec<UnstagedFile>, std::io::Error> {
52 let output = std::process::Command::new("git")
53 .args(["status", "-s"])
54 .output()?;
55 Ok(String::from_utf8_lossy(&output.stdout)
56 .lines()
57 .filter_map(|line| {
58 let mut parts = line.splitn(2, ' ');
59 let status = parts.next()?;
60 let path = parts.next().map(|p| p.trim()).unwrap_or("");
61 if status.is_empty() || path.is_empty() {
62 return None; }
64 Some(UnstagedFile {
65 status: status.to_string(),
66 path: path.to_string(),
67 })
68 })
69 .collect())
70}
71
72fn get_diff_content() -> Result<String, std::io::Error> {
73 let output = std::process::Command::new("git")
74 .args(["diff", "--cached"])
75 .output()?;
76 Ok(String::from_utf8_lossy(&output.stdout).to_string())
77}
78
79pub fn run() -> Result<PreflightSuccess, PreflightError> {
84 if !is_git_repo() {
85 return Err(PreflightError::NotGitRepo);
86 }
87
88 let staged = get_staged_files().map_err(|e| PreflightError::GitCommandFailed {
89 command: "git diff --cached --name-only".into(),
90 source: e,
91 })?;
92
93 if staged.is_empty() {
94 let unstaged = get_unstaged_files().map_err(|e| PreflightError::GitCommandFailed {
95 command: "git status -s".into(),
96 source: e,
97 })?;
98 return Err(PreflightError::NoStagedFiles { unstaged });
99 }
100
101 let diff_content = get_diff_content().map_err(|e| PreflightError::GitCommandFailed {
102 command: "git diff --cached".into(),
103 source: e,
104 })?;
105
106 if diff_content.len() > SOFT_DIFF_LIMIT {
107 return Err(PreflightError::DiffTooLarge {
108 size: diff_content.len(),
109 });
110 }
111
112 Ok(PreflightSuccess { diff_content })
113}
114
115pub fn run_with_diff_bypass() -> Result<PreflightSuccess, PreflightError> {
118 if !is_git_repo() {
119 return Err(PreflightError::NotGitRepo);
120 }
121 let staged = get_staged_files().map_err(|e| PreflightError::GitCommandFailed {
122 command: "git diff --cached --name-only".into(),
123 source: e,
124 })?;
125 if staged.is_empty() {
126 let unstaged = get_unstaged_files().map_err(|e| PreflightError::GitCommandFailed {
127 command: "git status -s".into(),
128 source: e,
129 })?;
130 return Err(PreflightError::NoStagedFiles { unstaged });
131 }
132 let diff_content = get_diff_content().map_err(|e| PreflightError::GitCommandFailed {
133 command: "git diff --cached".into(),
134 source: e,
135 })?;
136 Ok(PreflightSuccess { diff_content })
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn test_unstaged_file_parse_status_M() {
145 let file = UnstagedFile {
146 status: "M".to_string(),
147 path: "src/main.rs".to_string(),
148 };
149 assert_eq!(file.status, "M");
150 assert_eq!(file.path, "src/main.rs");
151 }
152
153 #[test]
154 fn test_unstaged_file_parse_status_UU() {
155 let file = UnstagedFile {
156 status: "??".to_string(),
157 path: ".env.example".to_string(),
158 };
159 assert_eq!(file.status, "??");
160 assert_eq!(file.path, ".env.example");
161 }
162
163 #[test]
164 fn test_preflight_error_display() {
165 let err = PreflightError::NotGitRepo;
166 assert_eq!(err.to_string(), "Not a git repository");
167 }
168
169 #[test]
170 fn test_diff_too_large_error_display() {
171 let err = PreflightError::DiffTooLarge { size: 23450 };
172 assert_eq!(err.to_string(), "Diff too large: 23450 chars");
173 }
174}