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