spin_templates/
environment.rs

1use std::collections::HashMap;
2
3use anyhow::Result;
4use tokio::process::Command;
5
6use crate::git::UnderstandGitResult;
7
8#[derive(Debug, Default)]
9pub(crate) struct Authors {
10    pub author: String,
11    pub username: String,
12}
13
14type GitConfig = HashMap<String, String>;
15
16/// Heavily adapted from cargo, portions (c) 2020 Cargo Developers
17///
18/// cf. <https://github.com/rust-lang/cargo/blob/2d5c2381e4e50484bf281fc1bfe19743aa9eb37a/src/cargo/ops/cargo_new.rs#L769-L851>
19pub(crate) async fn get_authors() -> Result<Authors> {
20    fn get_environment_variable(variables: &[&str]) -> Option<String> {
21        variables
22            .iter()
23            .filter_map(|var| std::env::var(var).ok())
24            .next()
25    }
26
27    async fn discover_author() -> Result<(String, Option<String>)> {
28        let git_config = find_real_git_config().await;
29
30        let name_variables = ["GIT_AUTHOR_NAME", "GIT_COMMITTER_NAME"];
31        let backup_name_variables = ["USER", "USERNAME", "NAME"];
32        let name = get_environment_variable(&name_variables)
33            .or_else(|| git_config.get("user.name").map(|s| s.to_owned()))
34            .or_else(|| get_environment_variable(&backup_name_variables));
35
36        let name = match name {
37            Some(name) => name,
38            None => {
39                let username_var = if cfg!(windows) { "USERNAME" } else { "USER" };
40                anyhow::bail!(
41                    "could not determine the current user, please set ${}",
42                    username_var
43                )
44            }
45        };
46        let email_variables = ["GIT_AUTHOR_EMAIL", "GIT_COMMITTER_EMAIL", "EMAIL"];
47        let email = get_environment_variable(&email_variables[0..3])
48            .or_else(|| git_config.get("user.email").map(|s| s.to_owned()))
49            .or_else(|| get_environment_variable(&email_variables[3..]));
50
51        let name = name.trim().to_string();
52        let email = email.map(|s| {
53            let mut s = s.trim();
54
55            // In some cases emails will already have <> remove them since they
56            // are already added when needed.
57            if s.starts_with('<') && s.ends_with('>') {
58                s = &s[1..s.len() - 1];
59            }
60
61            s.to_string()
62        });
63
64        Ok((name, email))
65    }
66
67    async fn find_real_git_config() -> GitConfig {
68        find_real_git_config_inner().await.unwrap_or_default()
69    }
70
71    async fn find_real_git_config_inner() -> Option<GitConfig> {
72        Command::new("git")
73            .arg("config")
74            .arg("--list")
75            .output()
76            .await
77            .understand_git_result()
78            .ok()
79            .and_then(|stdout| try_parse_git_config(&stdout))
80    }
81
82    let author = match discover_author().await? {
83        (name, Some(email)) => Authors {
84            author: format!("{} <{}>", name, email),
85            username: name,
86        },
87        (name, None) => Authors {
88            author: name.clone(),
89            username: name,
90        },
91    };
92
93    Ok(author)
94}
95
96fn try_parse_git_config(stdout: &[u8]) -> Option<GitConfig> {
97    std::str::from_utf8(stdout).ok().map(parse_git_config_text)
98}
99
100fn parse_git_config_text(text: &str) -> GitConfig {
101    text.lines().filter_map(try_parse_git_config_line).collect()
102}
103
104fn try_parse_git_config_line(line: &str) -> Option<(String, String)> {
105    line.split_once('=')
106        .map(|(k, v)| (k.to_owned(), v.to_owned()))
107}