git_editor/utils/
datetime.rs

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