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 };
148
149 let result = generate_timestamps(&mut args);
150 assert!(result.is_err());
151 }
152
153 #[test]
154 fn test_generate_timestamps_valid_range() {
155 let (_temp_dir, repo_path) = create_test_repo();
156 let mut args = Args {
157 repo_path: Some(repo_path),
158 email: Some("test@example.com".to_string()),
159 name: Some("Test User".to_string()),
160 start: Some("2023-01-01 00:00:00".to_string()),
161 end: Some("2023-01-10 00:00:00".to_string()),
162 show_history: false,
163 pic_specific_commits: false,
164 };
165
166 let result = generate_timestamps(&mut args);
167 assert!(result.is_ok());
168
169 let timestamps = result.unwrap();
170 assert_eq!(timestamps.len(), 1); let start_dt =
173 NaiveDateTime::parse_from_str("2023-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
174 let end_dt =
175 NaiveDateTime::parse_from_str("2023-01-10 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
176
177 assert!(timestamps[0] >= start_dt);
178 assert!(timestamps[0] <= end_dt);
179 }
180
181 #[test]
182 fn test_generate_timestamps_preserves_order() {
183 let (_temp_dir, repo_path) = create_test_repo();
184 let mut args = Args {
185 repo_path: Some(repo_path),
186 email: Some("test@example.com".to_string()),
187 name: Some("Test User".to_string()),
188 start: Some("2023-01-01 00:00:00".to_string()),
189 end: Some("2023-01-10 00:00:00".to_string()),
190 show_history: false,
191 pic_specific_commits: false,
192 };
193
194 let result = generate_timestamps(&mut args);
195 assert!(result.is_ok());
196
197 let timestamps = result.unwrap();
198
199 for i in 1..timestamps.len() {
201 assert!(timestamps[i] >= timestamps[i - 1]);
202 }
203 }
204}