git_editor/utils/
validator.rs

1use crate::args::Args;
2use crate::utils::types::Result;
3use regex::Regex;
4use url::Url;
5
6pub fn validate_inputs(args: &Args) -> Result<()> {
7    // Always validate repo_path since it's required for all operations
8    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    // Skip validation for email, name, start, end if using show_history, pick_specific_commits, range, or simulate
29    if args.show_history || args.pick_specific_commits || args.range || args.simulate {
30        return Ok(());
31    }
32
33    // Validate email, name, start, end only for full rewrite operations
34    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        // Initialize git repo
80        let repo = git2::Repository::init(&repo_path).unwrap();
81
82        // Create a test file
83        let file_path = temp_dir.path().join("test.txt");
84        fs::write(&file_path, "test content").unwrap();
85
86        // Add and commit file
87        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        // This test would normally call process::exit, so we can't test it directly
195        // without mocking. We'll test the regex separately.
196        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        // This would normally call process::exit, so we test the path validation logic
244        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        // Valid emails
253        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        // Invalid emails
259        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        // Valid datetime formats
271        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        // Invalid datetime formats
276        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        // Initialize git repo
288        let repo = git2::Repository::init(&repo_path).unwrap();
289
290        // Create multiple commits
291        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}