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}