git_editor/utils/
datetime.rs1use 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 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 let repo = git2::Repository::init(&repo_path).unwrap();
104
105 let file_path = temp_dir.path().join("test.txt");
107 fs::write(&file_path, "test content").unwrap();
108
109 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); 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 for i in 1..timestamps.len() {
204 assert!(timestamps[i] >= timestamps[i - 1]);
205 }
206 }
207}