mit_commit_message_lints/mit/cmd/
rotate_authors.rs1use miette::Result;
2use rand::seq::SliceRandom;
3
4use crate::external::Vcs;
5use crate::mit::cmd::set_commit_authors::{remove_coauthors, set_vcs_coauthor, set_vcs_user};
6use crate::mit::{cmd::vcs::get_vcs_coauthors_config, Author};
7
8pub fn rotate_authors(config: &mut dyn Vcs, strategy: crate::mit::RotationOption) -> Result<()> {
29 let primary_name = config.get_str("user.name")?.map(String::from);
31 let primary_email = config.get_str("user.email")?.map(String::from);
32 let primary_signingkey = config.get_str("user.signingkey")?.map(String::from);
33
34 let primary = match (primary_name, primary_email, primary_signingkey) {
35 (Some(name), Some(email), signingkey) => Some(Author::new(
36 name.into(),
37 email.into(),
38 signingkey.map(Into::into),
39 )),
40 _ => return Ok(()), };
42
43 let coauthor_emails: Vec<String> = get_vcs_coauthors_config(config, "email")?
45 .into_iter()
46 .filter_map(|x| x.map(|s| s.to_string()))
47 .collect();
48
49 let coauthors: Vec<Author> = get_vcs_coauthors_config(config, "name")?
50 .into_iter()
51 .filter_map(|x| x.map(|s| s.to_string()))
52 .zip(coauthor_emails)
53 .filter_map(|(name, email)| {
54 if name.is_empty() || email.is_empty() {
55 None
56 } else {
57 Some(Author::new(name.into(), email.into(), None))
58 }
59 })
60 .collect();
61
62 let mut all_authors: Vec<Author> = vec![primary.unwrap()];
64 all_authors.extend(coauthors);
65
66 if all_authors.len() <= 1 {
68 return Ok(());
69 }
70
71 match strategy {
73 crate::mit::RotationOption::Off => return Ok(()),
74 crate::mit::RotationOption::RoundRobin => {
75 all_authors.rotate_left(1);
76 }
77 crate::mit::RotationOption::Random => {
78 all_authors.shuffle(&mut rand::rng());
79 }
80 }
81
82 remove_coauthors(config)?;
84 set_vcs_user(config, &all_authors[0])?;
85 all_authors[1..]
86 .iter()
87 .enumerate()
88 .try_for_each(|(index, author)| set_vcs_coauthor(config, index, author))?;
89
90 Ok(())
91}
92
93#[cfg(test)]
94mod tests {
95 use std::collections::BTreeMap;
96 use std::time::Duration;
97
98 use miette::Result;
99
100 use crate::external::InMemory;
101 use crate::mit::{set_commit_authors, Author};
102
103 #[test]
104 fn rotate_authors_rotates_three_authors() -> Result<()> {
105 let mut buffer = BTreeMap::new();
106 {
107 let mut vcs_config = InMemory::new(&mut buffer);
108
109 let author_1 = Author::new("Billie Thompson".into(), "billie@example.com".into(), None);
110 let author_2 = Author::new("Somebody Else".into(), "someone@example.com".into(), None);
111 let author_3 = Author::new("Annie Example".into(), "annie@example.com".into(), None);
112
113 set_commit_authors(
114 &mut vcs_config,
115 &[&author_1, &author_2, &author_3],
116 Duration::from_hours(1),
117 )?;
118 }
119
120 assert_eq!(
122 buffer.get("user.name").map(String::as_str),
123 Some("Billie Thompson"),
124 "Expected the initial primary author to be Billie Thompson"
125 );
126 assert_eq!(
127 buffer
128 .get("mit.author.coauthors.0.name")
129 .map(String::as_str),
130 Some("Somebody Else"),
131 "Expected the first coauthor to be Somebody Else before rotation"
132 );
133 assert_eq!(
134 buffer
135 .get("mit.author.coauthors.1.name")
136 .map(String::as_str),
137 Some("Annie Example"),
138 "Expected the second coauthor to be Annie Example before rotation"
139 );
140
141 {
143 let mut vcs_config = InMemory::new(&mut buffer);
144 crate::mit::cmd::rotate_authors::rotate_authors(
145 &mut vcs_config,
146 crate::mit::RotationOption::RoundRobin,
147 )?;
148 }
149
150 assert_eq!(
151 buffer.get("user.name").map(String::as_str),
152 Some("Somebody Else"),
153 "Expected the primary author to be Somebody Else after rotation"
154 );
155 assert_eq!(
156 buffer
157 .get("mit.author.coauthors.0.name")
158 .map(String::as_str),
159 Some("Annie Example"),
160 "Expected the first coauthor to be Annie Example after rotation"
161 );
162 assert_eq!(
163 buffer
164 .get("mit.author.coauthors.1.name")
165 .map(String::as_str),
166 Some("Billie Thompson"),
167 "Expected the second coauthor to be Billie Thompson after rotation"
168 );
169
170 Ok(())
171 }
172
173 #[test]
174 fn rotate_authors_noops_with_single_author() -> Result<()> {
175 let mut buffer = BTreeMap::new();
176 {
177 let mut vcs_config = InMemory::new(&mut buffer);
178 let author = Author::new("Billie Thompson".into(), "billie@example.com".into(), None);
179 set_commit_authors(&mut vcs_config, &[&author], Duration::from_hours(1))?;
180 }
181
182 {
183 let mut vcs_config = InMemory::new(&mut buffer);
184 crate::mit::cmd::rotate_authors::rotate_authors(
185 &mut vcs_config,
186 crate::mit::RotationOption::RoundRobin,
187 )?;
188 }
189
190 assert_eq!(
192 buffer.get("user.name").map(String::as_str),
193 Some("Billie Thompson"),
194 "Expected user.name to be unchanged with a single author"
195 );
196 assert_eq!(
197 buffer.get("user.email").map(String::as_str),
198 Some("billie@example.com"),
199 "Expected user.email to be unchanged with a single author"
200 );
201 assert!(
202 !buffer.contains_key("mit.author.coauthors.0.name"),
203 "Expected no coauthors to be set with a single author"
204 );
205
206 Ok(())
207 }
208
209 #[test]
210 fn rotate_authors_noops_with_zero_authors() -> Result<()> {
211 let mut buffer = BTreeMap::new();
212
213 {
214 let mut vcs_config = InMemory::new(&mut buffer);
215 crate::mit::cmd::rotate_authors::rotate_authors(
216 &mut vcs_config,
217 crate::mit::RotationOption::RoundRobin,
218 )?;
219 }
220
221 assert!(
223 !buffer.contains_key("user.name"),
224 "Expected no user.name to be set when there are no authors"
225 );
226
227 Ok(())
228 }
229
230 #[test]
231 fn rotate_authors_rotates_two_authors() -> Result<()> {
232 let mut buffer = BTreeMap::new();
233 {
234 let mut vcs_config = InMemory::new(&mut buffer);
235
236 let author_1 = Author::new("Billie Thompson".into(), "billie@example.com".into(), None);
237 let author_2 = Author::new("Somebody Else".into(), "someone@example.com".into(), None);
238
239 set_commit_authors(
240 &mut vcs_config,
241 &[&author_1, &author_2],
242 Duration::from_hours(1),
243 )?;
244 }
245
246 assert_eq!(
248 buffer.get("user.name").map(String::as_str),
249 Some("Billie Thompson"),
250 "Expected the initial primary author to be Billie Thompson"
251 );
252 assert_eq!(
253 buffer
254 .get("mit.author.coauthors.0.name")
255 .map(String::as_str),
256 Some("Somebody Else"),
257 "Expected the first coauthor to be Somebody Else before rotation"
258 );
259
260 {
262 let mut vcs_config = InMemory::new(&mut buffer);
263 crate::mit::cmd::rotate_authors::rotate_authors(
264 &mut vcs_config,
265 crate::mit::RotationOption::RoundRobin,
266 )?;
267 }
268
269 assert_eq!(
270 buffer.get("user.name").map(String::as_str),
271 Some("Somebody Else"),
272 "Expected the primary author to be Somebody Else after first rotation"
273 );
274 assert_eq!(
275 buffer
276 .get("mit.author.coauthors.0.name")
277 .map(String::as_str),
278 Some("Billie Thompson"),
279 "Expected the first coauthor to be Billie Thompson after first rotation"
280 );
281
282 {
284 let mut vcs_config = InMemory::new(&mut buffer);
285 crate::mit::cmd::rotate_authors::rotate_authors(
286 &mut vcs_config,
287 crate::mit::RotationOption::RoundRobin,
288 )?;
289 }
290
291 assert_eq!(
292 buffer.get("user.name").map(String::as_str),
293 Some("Billie Thompson"),
294 "Expected the primary author to be Billie Thompson after second rotation"
295 );
296 assert_eq!(
297 buffer
298 .get("mit.author.coauthors.0.name")
299 .map(String::as_str),
300 Some("Somebody Else"),
301 "Expected the first coauthor to be Somebody Else after second rotation"
302 );
303
304 Ok(())
305 }
306
307 #[test]
308 fn rotate_authors_random_produces_valid_permutation() -> Result<()> {
309 let mut buffer = BTreeMap::new();
310 {
311 let mut vcs_config = InMemory::new(&mut buffer);
312
313 let author_1 = Author::new("Billie Thompson".into(), "billie@example.com".into(), None);
314 let author_2 = Author::new("Somebody Else".into(), "someone@example.com".into(), None);
315 let author_3 = Author::new("Annie Example".into(), "annie@example.com".into(), None);
316
317 set_commit_authors(
318 &mut vcs_config,
319 &[&author_1, &author_2, &author_3],
320 Duration::from_hours(1),
321 )?;
322 }
323
324 let mut original: Vec<String> = vec![buffer.get("user.name").cloned().unwrap_or_default()];
326 for i in 0..3 {
327 if let Some(name) = buffer.get(&format!("mit.author.coauthors.{i}.name")) {
328 original.push(name.clone());
329 }
330 }
331 original.sort();
332
333 {
335 let mut vcs_config = InMemory::new(&mut buffer);
336 crate::mit::cmd::rotate_authors::rotate_authors(
337 &mut vcs_config,
338 crate::mit::RotationOption::Random,
339 )?;
340 }
341
342 let mut result: Vec<String> = vec![buffer.get("user.name").cloned().unwrap_or_default()];
344 for i in 0..3 {
345 if let Some(name) = buffer.get(&format!("mit.author.coauthors.{i}.name")) {
346 result.push(name.clone());
347 }
348 }
349 result.sort();
350
351 assert_eq!(
353 result, original,
354 "Expected random rotation to produce a valid permutation of the original authors"
355 );
356
357 Ok(())
358 }
359
360 #[test]
361 fn rotate_authors_random_noops_with_single_author() -> Result<()> {
362 let mut buffer = BTreeMap::new();
363 {
364 let mut vcs_config = InMemory::new(&mut buffer);
365 let author = Author::new("Billie Thompson".into(), "billie@example.com".into(), None);
366 set_commit_authors(&mut vcs_config, &[&author], Duration::from_hours(1))?;
367 }
368
369 {
370 let mut vcs_config = InMemory::new(&mut buffer);
371 crate::mit::cmd::rotate_authors::rotate_authors(
372 &mut vcs_config,
373 crate::mit::RotationOption::Random,
374 )?;
375 }
376
377 assert_eq!(
378 buffer.get("user.name").map(String::as_str),
379 Some("Billie Thompson"),
380 "Expected user.name to be unchanged with a single author under random rotation"
381 );
382 assert_eq!(
383 buffer.get("user.email").map(String::as_str),
384 Some("billie@example.com"),
385 "Expected user.email to be unchanged with a single author under random rotation"
386 );
387
388 Ok(())
389 }
390
391 #[test]
392 fn rotate_authors_ignores_coauthor_with_empty_name() -> Result<()> {
393 let mut buffer = BTreeMap::new();
394 buffer.insert("user.name".into(), "Billie Thompson".into());
395 buffer.insert("user.email".into(), "billie@example.com".into());
396 buffer.insert("mit.author.coauthors.0.name".into(), String::new());
398 buffer.insert(
399 "mit.author.coauthors.0.email".into(),
400 "ghost@example.com".into(),
401 );
402
403 {
404 let mut vcs_config = InMemory::new(&mut buffer);
405 crate::mit::cmd::rotate_authors::rotate_authors(
406 &mut vcs_config,
407 crate::mit::RotationOption::RoundRobin,
408 )?;
409 }
410
411 assert_eq!(
414 buffer.get("user.name").map(String::as_str),
415 Some("Billie Thompson"),
416 "Expected user.name to be unchanged when the only coauthor has an empty name"
417 );
418
419 Ok(())
420 }
421}