git_editor/utils/
validator.rs

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    // Always validate repo_path since it's required for all operations
10    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    // Skip validation for email, name, start, end if using show_history or pic_specific_commits
36    if args.show_history || args.pic_specific_commits {
37        return Ok(());
38    }
39
40    // Validate email, name, start, end only for full rewrite operations
41    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        // Initialize git repo
96        let repo = git2::Repository::init(&repo_path).unwrap();
97
98        // Create a test file
99        let file_path = temp_dir.path().join("test.txt");
100        fs::write(&file_path, "test content").unwrap();
101
102        // Add and commit file
103        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        };
134
135        let result = validate_inputs(&args);
136        assert!(result.is_ok());
137    }
138
139    #[test]
140    fn test_validate_inputs_pick_specific_commits_mode() {
141        let (_temp_dir, repo_path) = create_test_repo();
142        let args = Args {
143            repo_path: Some(repo_path),
144            email: None,
145            name: None,
146            start: None,
147            end: None,
148            show_history: false,
149            pic_specific_commits: true,
150        };
151
152        let result = validate_inputs(&args);
153        assert!(result.is_ok());
154    }
155
156    #[test]
157    fn test_validate_inputs_full_rewrite_valid() {
158        let (_temp_dir, repo_path) = create_test_repo();
159        let args = Args {
160            repo_path: Some(repo_path),
161            email: Some("test@example.com".to_string()),
162            name: Some("Test User".to_string()),
163            start: Some("2023-01-01 00:00:00".to_string()),
164            end: Some("2023-01-02 00:00:00".to_string()),
165            show_history: false,
166            pic_specific_commits: false,
167        };
168
169        let result = validate_inputs(&args);
170        assert!(result.is_ok());
171    }
172
173    #[test]
174    fn test_validate_inputs_invalid_email() {
175        let (_temp_dir, repo_path) = create_test_repo();
176        let _args = Args {
177            repo_path: Some(repo_path),
178            email: Some("invalid-email".to_string()),
179            name: Some("Test User".to_string()),
180            start: Some("2023-01-01 00:00:00".to_string()),
181            end: Some("2023-01-02 00:00:00".to_string()),
182            show_history: false,
183            pic_specific_commits: false,
184        };
185
186        // This test would normally call process::exit, so we can't test it directly
187        // without mocking. We'll test the regex separately.
188        let email_re = Regex::new(r"(?i)^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$").unwrap();
189        assert!(!email_re.is_match("invalid-email"));
190        assert!(email_re.is_match("test@example.com"));
191    }
192
193    #[test]
194    fn test_validate_inputs_invalid_date_format() {
195        let (_temp_dir, repo_path) = create_test_repo();
196        let _args = Args {
197            repo_path: Some(repo_path),
198            email: Some("test@example.com".to_string()),
199            name: Some("Test User".to_string()),
200            start: Some("invalid-date".to_string()),
201            end: Some("2023-01-02 00:00:00".to_string()),
202            show_history: false,
203            pic_specific_commits: false,
204        };
205
206        let start_re = Regex::new(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$").unwrap();
207        assert!(!start_re.is_match("invalid-date"));
208        assert!(start_re.is_match("2023-01-01 00:00:00"));
209    }
210
211    #[test]
212    fn test_validate_inputs_nonexistent_repo() {
213        let _args = Args {
214            repo_path: Some("/nonexistent/path".to_string()),
215            email: Some("test@example.com".to_string()),
216            name: Some("Test User".to_string()),
217            start: Some("2023-01-01 00:00:00".to_string()),
218            end: Some("2023-01-02 00:00:00".to_string()),
219            show_history: false,
220            pic_specific_commits: false,
221        };
222
223        // This would normally call process::exit, so we test the path validation logic
224        let repo_path = "/nonexistent/path";
225        assert!(!std::path::Path::new(repo_path).exists());
226    }
227
228    #[test]
229    fn test_email_regex_patterns() {
230        let email_re = Regex::new(r"(?i)^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$").unwrap();
231
232        // Valid emails
233        assert!(email_re.is_match("test@example.com"));
234        assert!(email_re.is_match("user.name@domain.org"));
235        assert!(email_re.is_match("user+tag@example.co.uk"));
236        assert!(email_re.is_match("123@test.io"));
237
238        // Invalid emails
239        assert!(!email_re.is_match("invalid-email"));
240        assert!(!email_re.is_match("@domain.com"));
241        assert!(!email_re.is_match("user@"));
242        assert!(!email_re.is_match("user@domain"));
243        assert!(!email_re.is_match("user@domain."));
244    }
245
246    #[test]
247    fn test_datetime_regex_patterns() {
248        let datetime_re = Regex::new(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$").unwrap();
249
250        // Valid datetime formats
251        assert!(datetime_re.is_match("2023-01-01 00:00:00"));
252        assert!(datetime_re.is_match("2023-12-31 23:59:59"));
253        assert!(datetime_re.is_match("2023-06-15 12:30:45"));
254
255        // Invalid datetime formats
256        assert!(!datetime_re.is_match("2023-1-1 0:0:0"));
257        assert!(!datetime_re.is_match("2023/01/01 00:00:00"));
258        assert!(!datetime_re.is_match("2023-01-01T00:00:00"));
259        assert!(!datetime_re.is_match("23-01-01 00:00:00"));
260        assert!(!datetime_re.is_match("2023-01-01 00:00"));
261    }
262}