git_editor/utils/
datetime.rs

1use crate::args::Args;
2use crate::utils::types::Result;
3use chrono::{Duration, NaiveDateTime};
4use colored::*;
5use rand::Rng;
6use uuid::Uuid;
7
8pub fn generate_timestamps(args: &mut Args) -> Result<Vec<NaiveDateTime>> {
9    let start_dt =
10        NaiveDateTime::parse_from_str(args.start.as_ref().unwrap(), "%Y-%m-%d %H:%M:%S")?;
11    let end_dt = NaiveDateTime::parse_from_str(args.end.as_ref().unwrap(), "%Y-%m-%d %H:%M:%S")?;
12
13    if start_dt >= end_dt {
14        eprintln!(
15            "{}",
16            "Start datetime must be before end datetime".red().bold()
17        );
18        std::process::exit(1);
19    }
20
21    if url::Url::parse(args.repo_path.as_ref().unwrap()).is_ok()
22        && !std::path::Path::new(args.repo_path.as_ref().unwrap()).exists()
23    {
24        let tmp_dir = std::env::temp_dir().join(format!("git_editor_{}", Uuid::new_v4()));
25        std::fs::create_dir_all(&tmp_dir)?;
26
27        let status = std::process::Command::new("git")
28            .args([
29                "clone",
30                args.repo_path.as_ref().unwrap(),
31                &tmp_dir.to_string_lossy(),
32            ])
33            .status()?;
34
35        if !status.success() {
36            eprintln!("{}", "Failed to clone repository".red().bold());
37            std::process::exit(1);
38        }
39
40        // Update repo_path to point to the cloned repository
41        args.repo_path = Some(tmp_dir.to_string_lossy().to_string());
42    }
43    let total_commits = count_commits(args.repo_path.as_ref().unwrap())?;
44    if total_commits == 0 {
45        eprintln!("{}", "No commits found in repository".red().bold());
46        std::process::exit(1);
47    }
48
49    let min_span = Duration::hours(3 * (total_commits as i64 - 1));
50    let total_span = end_dt - start_dt;
51
52    if total_span < min_span {
53        eprintln!(
54            "{} {} {}",
55            "Date range too small for".red().bold(),
56            total_commits.to_string().yellow(),
57            "commits".red().bold()
58        );
59        std::process::exit(1);
60    }
61
62    let slack = total_span - min_span;
63    let mut rng = rand::rng();
64    let mut weights: Vec<f64> = (0..(total_commits - 1)).map(|_| rng.random()).collect();
65    let sum: f64 = weights.iter().sum();
66
67    for w in &mut weights {
68        *w = (*w / sum) * slack.num_seconds() as f64;
69    }
70
71    let mut timestamps = Vec::with_capacity(total_commits);
72    let mut current = start_dt;
73    timestamps.push(current);
74
75    for w in &weights {
76        let secs = w.round() as i64 + 3 * 3600;
77        current += Duration::seconds(secs);
78        timestamps.push(current);
79    }
80
81    Ok(timestamps)
82}
83
84fn count_commits(repo_path: &str) -> Result<usize> {
85    let repo = git2::Repository::open(repo_path)?;
86    let mut revwalk = repo.revwalk()?;
87    revwalk.push_head()?;
88    revwalk.set_sorting(git2::Sort::TOPOLOGICAL | git2::Sort::TIME)?;
89    Ok(revwalk.count())
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use std::fs;
96    use tempfile::TempDir;
97
98    fn create_test_repo() -> (TempDir, String) {
99        let temp_dir = TempDir::new().unwrap();
100        let repo_path = temp_dir.path().to_str().unwrap().to_string();
101
102        // Initialize git repo
103        let repo = git2::Repository::init(&repo_path).unwrap();
104
105        // Create a test file
106        let file_path = temp_dir.path().join("test.txt");
107        fs::write(&file_path, "test content").unwrap();
108
109        // Add and commit file
110        let mut index = repo.index().unwrap();
111        index.add_path(std::path::Path::new("test.txt")).unwrap();
112        index.write().unwrap();
113
114        let tree_id = index.write_tree().unwrap();
115        let tree = repo.find_tree(tree_id).unwrap();
116
117        let sig = git2::Signature::new(
118            "Test User",
119            "test@example.com",
120            &git2::Time::new(1234567890, 0),
121        )
122        .unwrap();
123        repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])
124            .unwrap();
125
126        (temp_dir, repo_path)
127    }
128
129    #[test]
130    fn test_count_commits() {
131        let (_temp_dir, repo_path) = create_test_repo();
132        let count = count_commits(&repo_path).unwrap();
133        assert_eq!(count, 1);
134    }
135
136    #[test]
137    fn test_generate_timestamps_invalid_date_format() {
138        let (_temp_dir, repo_path) = create_test_repo();
139        let mut args = Args {
140            repo_path: Some(repo_path),
141            email: Some("test@example.com".to_string()),
142            name: Some("Test User".to_string()),
143            start: Some("invalid-date".to_string()),
144            end: Some("2023-01-02 00:00:00".to_string()),
145            show_history: false,
146            pic_specific_commits: false,
147            range: false,
148        };
149
150        let result = generate_timestamps(&mut args);
151        assert!(result.is_err());
152    }
153
154    #[test]
155    fn test_generate_timestamps_valid_range() {
156        let (_temp_dir, repo_path) = create_test_repo();
157        let mut args = Args {
158            repo_path: Some(repo_path),
159            email: Some("test@example.com".to_string()),
160            name: Some("Test User".to_string()),
161            start: Some("2023-01-01 00:00:00".to_string()),
162            end: Some("2023-01-10 00:00:00".to_string()),
163            show_history: false,
164            pic_specific_commits: false,
165            range: false,
166        };
167
168        let result = generate_timestamps(&mut args);
169        assert!(result.is_ok());
170
171        let timestamps = result.unwrap();
172        assert_eq!(timestamps.len(), 1); // One commit in test repo
173
174        let start_dt =
175            NaiveDateTime::parse_from_str("2023-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
176        let end_dt =
177            NaiveDateTime::parse_from_str("2023-01-10 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
178
179        assert!(timestamps[0] >= start_dt);
180        assert!(timestamps[0] <= end_dt);
181    }
182
183    #[test]
184    fn test_generate_timestamps_preserves_order() {
185        let (_temp_dir, repo_path) = create_test_repo();
186        let mut args = Args {
187            repo_path: Some(repo_path),
188            email: Some("test@example.com".to_string()),
189            name: Some("Test User".to_string()),
190            start: Some("2023-01-01 00:00:00".to_string()),
191            end: Some("2023-01-10 00:00:00".to_string()),
192            show_history: false,
193            pic_specific_commits: false,
194            range: false,
195        };
196
197        let result = generate_timestamps(&mut args);
198        assert!(result.is_ok());
199
200        let timestamps = result.unwrap();
201
202        // Check that timestamps are in ascending order
203        for i in 1..timestamps.len() {
204            assert!(timestamps[i] >= timestamps[i - 1]);
205        }
206    }
207}