1use crate::args::Args;
2use crate::utils::types::Result;
3use colored::*;
4use regex::Regex;
5use std::process;
6use url::Url;
7
8pub fn validate_inputs(args: &Args) -> Result<()> {
9 let repo_path = args.repo_path.as_ref().unwrap();
11
12 if repo_path.is_empty() {
13 eprintln!("Repository path cannot be empty");
14 process::exit(1);
15 }
16 if Url::parse(repo_path).is_err() && !std::path::Path::new(repo_path).exists() {
17 eprintln!(
18 "{} {}",
19 "Invalid repository path or URL".red().bold(),
20 repo_path.yellow()
21 );
22 process::exit(1);
23 }
24 if std::path::Path::new(repo_path).exists() {
25 if !std::path::Path::new(repo_path).is_dir() {
26 eprintln!("Repository path is not a directory {repo_path}");
27 process::exit(1);
28 }
29 if !std::path::Path::new(repo_path).join(".git").exists() {
30 eprintln!("Repository path does not contain a valid Git repository {repo_path}");
31 process::exit(1);
32 }
33 }
34
35 if args.show_history || args.pic_specific_commits || args.range {
37 return Ok(());
38 }
39
40 let email = args.email.as_ref().unwrap();
42 let name = args.name.as_ref().unwrap();
43 let start = args.start.as_ref().unwrap();
44 let end = args.end.as_ref().unwrap();
45
46 let email_re = Regex::new(r"(?i)^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$")?;
47 if !email_re.is_match(email) {
48 eprintln!("{} {}", "Invalid email format".red().bold(), email.yellow());
49 process::exit(1);
50 }
51
52 if name.trim().is_empty() {
53 eprintln!("{}", "Name cannot be empty".red().bold());
54 process::exit(1);
55 }
56
57 let start_re = Regex::new(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$")?;
58 if !start_re.is_match(start) {
59 eprintln!(
60 "{} {}",
61 "Invalid start date format".red().bold(),
62 start.yellow()
63 );
64 process::exit(1);
65 }
66
67 let end_re = Regex::new(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$")?;
68 if !end_re.is_match(end) {
69 eprintln!(
70 "{} {}",
71 "Invalid end date format".red().bold(),
72 end.yellow()
73 );
74 process::exit(1);
75 }
76
77 if start >= end {
78 eprintln!("Start date must be before end date");
79 process::exit(1);
80 }
81
82 Ok(())
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use std::fs;
89 use tempfile::TempDir;
90
91 fn create_test_repo() -> (TempDir, String) {
92 let temp_dir = TempDir::new().unwrap();
93 let repo_path = temp_dir.path().to_str().unwrap().to_string();
94
95 let repo = git2::Repository::init(&repo_path).unwrap();
97
98 let file_path = temp_dir.path().join("test.txt");
100 fs::write(&file_path, "test content").unwrap();
101
102 let mut index = repo.index().unwrap();
104 index.add_path(std::path::Path::new("test.txt")).unwrap();
105 index.write().unwrap();
106
107 let tree_id = index.write_tree().unwrap();
108 let tree = repo.find_tree(tree_id).unwrap();
109
110 let sig = git2::Signature::new(
111 "Test User",
112 "test@example.com",
113 &git2::Time::new(1234567890, 0),
114 )
115 .unwrap();
116 repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])
117 .unwrap();
118
119 (temp_dir, repo_path)
120 }
121
122 #[test]
123 fn test_validate_inputs_show_history_mode() {
124 let (_temp_dir, repo_path) = create_test_repo();
125 let args = Args {
126 repo_path: Some(repo_path),
127 email: None,
128 name: None,
129 start: None,
130 end: None,
131 show_history: true,
132 pic_specific_commits: false,
133 range: false,
134 };
135
136 let result = validate_inputs(&args);
137 assert!(result.is_ok());
138 }
139
140 #[test]
141 fn test_validate_inputs_pick_specific_commits_mode() {
142 let (_temp_dir, repo_path) = create_test_repo();
143 let args = Args {
144 repo_path: Some(repo_path),
145 email: None,
146 name: None,
147 start: None,
148 end: None,
149 show_history: false,
150 pic_specific_commits: true,
151 range: false,
152 };
153
154 let result = validate_inputs(&args);
155 assert!(result.is_ok());
156 }
157
158 #[test]
159 fn test_validate_inputs_full_rewrite_valid() {
160 let (_temp_dir, repo_path) = create_test_repo();
161 let args = Args {
162 repo_path: Some(repo_path),
163 email: Some("test@example.com".to_string()),
164 name: Some("Test User".to_string()),
165 start: Some("2023-01-01 00:00:00".to_string()),
166 end: Some("2023-01-02 00:00:00".to_string()),
167 show_history: false,
168 pic_specific_commits: false,
169 range: false,
170 };
171
172 let result = validate_inputs(&args);
173 assert!(result.is_ok());
174 }
175
176 #[test]
177 fn test_validate_inputs_invalid_email() {
178 let (_temp_dir, repo_path) = create_test_repo();
179 let _args = Args {
180 repo_path: Some(repo_path),
181 email: Some("invalid-email".to_string()),
182 name: Some("Test User".to_string()),
183 start: Some("2023-01-01 00:00:00".to_string()),
184 end: Some("2023-01-02 00:00:00".to_string()),
185 show_history: false,
186 pic_specific_commits: false,
187 range: false,
188 };
189
190 let email_re = Regex::new(r"(?i)^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$").unwrap();
193 assert!(!email_re.is_match("invalid-email"));
194 assert!(email_re.is_match("test@example.com"));
195 }
196
197 #[test]
198 fn test_validate_inputs_invalid_date_format() {
199 let (_temp_dir, repo_path) = create_test_repo();
200 let _args = Args {
201 repo_path: Some(repo_path),
202 email: Some("test@example.com".to_string()),
203 name: Some("Test User".to_string()),
204 start: Some("invalid-date".to_string()),
205 end: Some("2023-01-02 00:00:00".to_string()),
206 show_history: false,
207 pic_specific_commits: false,
208 range: false,
209 };
210
211 let start_re = Regex::new(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$").unwrap();
212 assert!(!start_re.is_match("invalid-date"));
213 assert!(start_re.is_match("2023-01-01 00:00:00"));
214 }
215
216 #[test]
217 fn test_validate_inputs_nonexistent_repo() {
218 let _args = Args {
219 repo_path: Some("/nonexistent/path".to_string()),
220 email: Some("test@example.com".to_string()),
221 name: Some("Test User".to_string()),
222 start: Some("2023-01-01 00:00:00".to_string()),
223 end: Some("2023-01-02 00:00:00".to_string()),
224 show_history: false,
225 pic_specific_commits: false,
226 range: false,
227 };
228
229 let repo_path = "/nonexistent/path";
231 assert!(!std::path::Path::new(repo_path).exists());
232 }
233
234 #[test]
235 fn test_email_regex_patterns() {
236 let email_re = Regex::new(r"(?i)^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$").unwrap();
237
238 assert!(email_re.is_match("test@example.com"));
240 assert!(email_re.is_match("user.name@domain.org"));
241 assert!(email_re.is_match("user+tag@example.co.uk"));
242 assert!(email_re.is_match("123@test.io"));
243
244 assert!(!email_re.is_match("invalid-email"));
246 assert!(!email_re.is_match("@domain.com"));
247 assert!(!email_re.is_match("user@"));
248 assert!(!email_re.is_match("user@domain"));
249 assert!(!email_re.is_match("user@domain."));
250 }
251
252 #[test]
253 fn test_datetime_regex_patterns() {
254 let datetime_re = Regex::new(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$").unwrap();
255
256 assert!(datetime_re.is_match("2023-01-01 00:00:00"));
258 assert!(datetime_re.is_match("2023-12-31 23:59:59"));
259 assert!(datetime_re.is_match("2023-06-15 12:30:45"));
260
261 assert!(!datetime_re.is_match("2023-1-1 0:0:0"));
263 assert!(!datetime_re.is_match("2023/01/01 00:00:00"));
264 assert!(!datetime_re.is_match("2023-01-01T00:00:00"));
265 assert!(!datetime_re.is_match("23-01-01 00:00:00"));
266 assert!(!datetime_re.is_match("2023-01-01 00:00"));
267 }
268
269 fn create_test_repo_with_commits() -> (TempDir, String) {
270 let temp_dir = TempDir::new().unwrap();
271 let repo_path = temp_dir.path().to_str().unwrap().to_string();
272
273 let repo = git2::Repository::init(&repo_path).unwrap();
275
276 for i in 1..=3 {
278 let file_path = temp_dir.path().join(format!("test{i}.txt"));
279 std::fs::write(&file_path, format!("test content {i}")).unwrap();
280
281 let mut index = repo.index().unwrap();
282 index
283 .add_path(std::path::Path::new(&format!("test{i}.txt")))
284 .unwrap();
285 index.write().unwrap();
286
287 let tree_id = index.write_tree().unwrap();
288 let tree = repo.find_tree(tree_id).unwrap();
289
290 let sig = git2::Signature::new(
291 "Test User",
292 "test@example.com",
293 &git2::Time::new(1234567890 + i as i64 * 3600, 0),
294 )
295 .unwrap();
296
297 let parents = if i == 1 {
298 vec![]
299 } else {
300 let head = repo.head().unwrap();
301 let parent_commit = head.peel_to_commit().unwrap();
302 vec![parent_commit]
303 };
304
305 repo.commit(
306 Some("HEAD"),
307 &sig,
308 &sig,
309 &format!("Commit {i}"),
310 &tree,
311 &parents.iter().collect::<Vec<_>>(),
312 )
313 .unwrap();
314 }
315
316 (temp_dir, repo_path)
317 }
318
319 #[test]
320 fn test_validate_inputs_range_mode() {
321 let (_temp_dir, repo_path) = create_test_repo_with_commits();
322 let args = Args {
323 repo_path: Some(repo_path),
324 email: None,
325 name: None,
326 start: None,
327 end: None,
328 show_history: false,
329 pic_specific_commits: false,
330 range: true,
331 };
332
333 let result = validate_inputs(&args);
334 assert!(result.is_ok());
335 }
336}