git_revise/
config.rs

1use std::{collections::HashMap, path::PathBuf, str::FromStr, sync::OnceLock};
2
3use colored::Colorize;
4use realme::{Adaptor, EnvParser, EnvSource, FileSource, Realme, TomlParser};
5use serde::Deserialize;
6
7use crate::{
8    error::ReviseResult,
9    git::{repo::GitRepository, GitUtils},
10    hook::HookType,
11};
12
13pub static CFG: OnceLock<ReviseConfig> = OnceLock::new();
14
15pub fn initialize_config() -> ReviseResult<ReviseConfig> {
16    let config = CFG.get_or_init(|| {
17        dotenvy::dotenv().ok();
18        ReviseConfig::load_config().unwrap_or_else(|e| {
19            eprintln!("Load config err: {e}");
20            std::process::exit(exitcode::CONFIG);
21        })
22    });
23    Ok(config.clone())
24}
25
26pub fn get_config() -> &'static ReviseConfig {
27    CFG.get().unwrap()
28}
29
30#[derive(Deserialize, Debug, Clone)]
31pub struct ReviseConfig {
32    pub template: String,
33    pub types: Vec<Type>,
34    pub emojis: Vec<Emoji>,
35    pub scopes: Vec<String>,
36    pub auto: Auto,
37    #[serde(default)]
38    pub api_key: HashMap<String, String>,
39    #[serde(deserialize_with = "deserialize_hooks")]
40    pub hooks: HashMap<HookType, Vec<Hook>>,
41    #[serde(default)]
42    pub exclude_files: Vec<String>,
43}
44
45#[derive(Deserialize, Debug, Clone, Default)]
46pub struct Render {}
47
48#[derive(Deserialize, Debug, Clone, Default)]
49pub struct Emoji {
50    pub key: String,
51    pub value: String,
52}
53
54#[derive(Deserialize, Debug, Clone, Default)]
55pub struct Type {
56    pub key: String,
57    pub value: String,
58}
59
60#[derive(Deserialize, Debug, Clone, Default)]
61pub struct Auto {
62    pub git: AutoGit,
63    pub commit: AutoCommit,
64}
65
66#[derive(Deserialize, Debug, Clone, Default)]
67pub struct Hook {
68    pub command: String,
69    pub order: Option<u32>,
70    pub skip: Option<bool>,
71}
72
73#[allow(clippy::struct_excessive_bools)]
74#[derive(Deserialize, Debug, Clone, Copy, Default)]
75pub struct AutoGit {
76    pub add: bool,
77    pub push: bool,
78    pub diff: bool,
79    pub footer: bool,
80}
81
82#[derive(Deserialize, Debug, Clone, Copy, Default)]
83pub struct AutoCommit {
84    pub content: bool,
85    pub footer: bool,
86}
87
88fn deserialize_hooks<'de, D>(
89    deserializer: D,
90) -> Result<HashMap<HookType, Vec<Hook>>, D::Error>
91where
92    D: serde::Deserializer<'de>,
93{
94    let raw_hooks: HashMap<String, Vec<Hook>> =
95        HashMap::deserialize(deserializer)?;
96    let mut hooks = HashMap::new();
97
98    for (key, value) in raw_hooks {
99        if let Ok(hook_type) = HookType::from_str(&key) {
100            hooks.insert(hook_type, value);
101        }
102    }
103
104    Ok(hooks)
105}
106
107impl ReviseConfig {
108    pub fn new() -> Self {
109        Self::default()
110    }
111
112    pub fn get_types(&self) -> Vec<String> {
113        let types = self.types.clone();
114        let max_key_len = types.iter().map(|t| t.key.len()).max().unwrap_or(5);
115        types
116            .into_iter()
117            .map(|t| {
118                let padding = " ".repeat(max_key_len - t.key.len() + 1);
119                format!("{}:{}{}", t.key, padding, t.value)
120            })
121            .collect()
122    }
123
124    pub fn get_type_key(&self, idx: usize) -> Option<String> {
125        let types = self.types.clone();
126        if let Some(t) = types.get(idx) {
127            return Some(t.key.clone());
128        }
129        None
130    }
131
132    pub fn get_emoji(&self, key: &str) -> Option<String> {
133        self.emojis
134            .iter()
135            .find(|e| e.key == key)
136            .map(|e| e.value.clone())
137    }
138
139    pub fn get_scopes(&self) -> Vec<String> {
140        self.scopes.clone()
141    }
142
143    pub fn get_config_path() -> ReviseResult<Option<PathBuf>> {
144        let mut config_paths = Vec::new();
145
146        if let Ok(repo) = GitUtils::git_repo() {
147            config_paths.push(PathBuf::from(repo).join("revise.toml"));
148        }
149
150        if let Some(config_dir) = dirs::config_local_dir() {
151            config_paths.push(config_dir.join("revise").join("revise.toml"));
152        }
153
154        let config_path = config_paths
155            .into_iter()
156            .find(|path| path.try_exists().unwrap_or(false));
157
158        Ok(config_path)
159    }
160
161    pub fn load_config() -> ReviseResult<Self> {
162        let config_path = Self::get_config_path()?;
163        let config = match config_path {
164            Some(path) => {
165                return Realme::builder()
166                    .load(Adaptor::new(Box::new(EnvSource::<EnvParser>::new(
167                        "REVISE_",
168                    ))))
169                    .load(Adaptor::new(Box::new(
170                        FileSource::<TomlParser>::new(path),
171                    )))
172                    .build()?
173                    .try_deserialize()
174                    .map_err(|e| anyhow::anyhow!(e.to_string()));
175            }
176            None => Self::default(),
177        };
178
179        let msg = format!(
180            "{}",
181            "Read config file failed, loading default config!!!!!"
182                .red()
183                .on_black()
184        );
185        println!("{msg}");
186        Ok(config)
187    }
188}
189
190#[allow(clippy::too_many_lines)]
191impl Default for ReviseConfig {
192    fn default() -> Self {
193        Self {
194            types: vec![
195                Type {
196                    key: "feat".to_owned(),
197                    value: "A new feature".to_owned(),
198                },
199                Type {
200                    key: "fix".to_owned(),
201                    value: "A bug fix".to_owned(),
202                },
203                Type {
204                    key: "docs".to_owned(),
205                    value: "Documentation only changes".to_owned(),
206                },
207                Type {
208                    key: "style".to_owned(),
209                    value: "Changes that do not affect the meaning of the code".to_owned(),
210                },
211                Type {
212                    key: "refactor".to_owned(),
213                    value: "A code change that neither fixes a bug nor adds a feature".to_owned(),
214                },
215                Type {
216                    key: "perf".to_owned(),
217                    value: "A code change that improves performance".to_owned(),
218                },
219                Type {
220                    key: "test".to_owned(),
221                    value: "Adding missing tests or correcting existing tests".to_owned(),
222                },
223                Type {
224                    key: "build".to_owned(),
225                    value: "Changes that affect the build system or external dependencies"
226                        .to_owned(),
227                },
228                Type {
229                    key: "ci".to_owned(),
230                    value: "Changes to our CI configuration files and scripts".to_owned(),
231                },
232                Type {
233                    key: "chore".to_owned(),
234                    value: "Other changes that don\"t modify src or test files".to_owned(),
235                },
236                Type {
237                    key: "revert".to_owned(),
238                    value: "Reverts a previous commit".to_owned(),
239                },
240            ],
241            emojis: vec![
242                Emoji {
243                    key: "feat".to_owned(),
244                    value: "✨".to_owned(),
245                },
246                Emoji {
247                    key: "fix".to_owned(),
248                    value: "🐛".to_owned(),
249                },
250                Emoji {
251                    key: "docs".to_owned(),
252                    value: "📚".to_owned(),
253                },
254                Emoji {
255                    key: "style".to_owned(),
256                    value: "🎨".to_owned(),
257                },
258                Emoji {
259                    key: "refactor".to_owned(),
260                    value: "♻️".to_owned(),
261                },
262                Emoji {
263                    key: "perf".to_owned(),
264                    value: "⚡️".to_owned(),
265                },
266                Emoji {
267                    key: "test".to_owned(),
268                    value: "✅".to_owned(),
269                },
270                Emoji {
271                    key: "build".to_owned(),
272                    value: "📦️".to_owned(),
273                },
274                Emoji {
275                    key: "ci".to_owned(),
276                    value: "⚙️".to_owned(),
277                },
278                Emoji {
279                    key: "chore".to_owned(),
280                    value: "🔨".to_owned(),
281                },
282                Emoji {
283                    key: "revert".to_owned(),
284                    value: "🔙".to_owned(),
285                },
286            ],
287            scopes: Vec::new(),
288            auto: Auto {
289                git: AutoGit::default(),
290                commit: AutoCommit::default(),
291            },
292            api_key: HashMap::new(),
293            hooks: HashMap::new(),
294            exclude_files: Vec::new(),
295            template: String::from("
296{{commit_icon}} {{ commit_type }}{% if commit_scope %}({{commit_scope}}){% endif %}{% if commit_breaking %}!{% endif %}: {{ commit_subject }}{% if commit_issue %}({{commit_issue}}){% endif %}   
297{% if commit_body %}\n{{ commit_body }}{% endif %}
298{% if commit_breaking %}\nBREAKING CHANGE: {{ commit_breaking }}{% endif %}"),
299        }
300    }
301}