Skip to main content

mit_commit_message_lints/mit/cmd/
rotate_authors.rs

1use 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
8/// Rotate the primary author among configured authors
9///
10/// Moves the first coauthor to become the primary author,
11/// and demotes the current primary to be the last coauthor.
12/// This affects the NEXT commit (git reads user.name/email
13/// before the prepare-commit-msg hook runs).
14///
15/// # Errors
16///
17/// Returns an error if:
18/// - Reading git config (user.name, user.email, or coauthors) fails
19/// - Writing git config (removing old coauthors or setting new authors) fails
20///
21/// # Panics
22///
23/// This function will panic if the primary author (constructed from user.name
24/// and user.email) is None. However, this is prevented by an early return check:
25/// if either user.name or user.email is missing, the function returns Ok(())
26/// without attempting to unwrap. The unwrap on line 47 is safe because at that
27/// point we've confirmed both name and email exist.
28pub fn rotate_authors(config: &mut dyn Vcs, strategy: crate::mit::RotationOption) -> Result<()> {
29    // Read the current primary author
30    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(()), // No primary author, nothing to rotate
41    };
42
43    // Read coauthors
44    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    // Build full author list
63    let mut all_authors: Vec<Author> = vec![primary.unwrap()];
64    all_authors.extend(coauthors);
65
66    // If only 0 or 1 author, nothing to rotate
67    if all_authors.len() <= 1 {
68        return Ok(());
69    }
70
71    // Apply the rotation strategy
72    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    // Write back
83    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}