git_editor/utils/
datetime.rs1use 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 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 let repo = git2::Repository::init(&repo_path).unwrap();
95
96 let file_path = temp_dir.path().join("test.txt");
98 fs::write(&file_path, "test content").unwrap();
99
100 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 edit_message: false,
142 edit_author: false,
143 edit_time: false,
144 };
145
146 let result = generate_timestamps(&mut args);
147 assert!(result.is_err());
148 }
149
150 #[test]
151 fn test_generate_timestamps_valid_range() {
152 let (_temp_dir, repo_path) = create_test_repo();
153 let mut args = Args {
154 repo_path: Some(repo_path),
155 email: Some("test@example.com".to_string()),
156 name: Some("Test User".to_string()),
157 start: Some("2023-01-01 00:00:00".to_string()),
158 end: Some("2023-01-10 00:00:00".to_string()),
159 show_history: false,
160 pick_specific_commits: false,
161 range: false,
162 simulate: false,
163 show_diff: false,
164 edit_message: false,
165 edit_author: false,
166 edit_time: false,
167 };
168
169 let result = generate_timestamps(&mut args);
170 assert!(result.is_ok());
171
172 let timestamps = result.unwrap();
173 assert_eq!(timestamps.len(), 1); let start_dt =
176 NaiveDateTime::parse_from_str("2023-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
177 let end_dt =
178 NaiveDateTime::parse_from_str("2023-01-10 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
179
180 assert!(timestamps[0] >= start_dt);
181 assert!(timestamps[0] <= end_dt);
182 }
183
184 #[test]
185 fn test_generate_timestamps_preserves_order() {
186 let (_temp_dir, repo_path) = create_test_repo();
187 let mut args = Args {
188 repo_path: Some(repo_path),
189 email: Some("test@example.com".to_string()),
190 name: Some("Test User".to_string()),
191 start: Some("2023-01-01 00:00:00".to_string()),
192 end: Some("2023-01-10 00:00:00".to_string()),
193 show_history: false,
194 pick_specific_commits: false,
195 range: false,
196 simulate: false,
197 show_diff: false,
198 edit_message: false,
199 edit_author: false,
200 edit_time: false,
201 };
202
203 let result = generate_timestamps(&mut args);
204 assert!(result.is_ok());
205
206 let timestamps = result.unwrap();
207
208 for i in 1..timestamps.len() {
210 assert!(timestamps[i] >= timestamps[i - 1]);
211 }
212 }
213}