1pub mod accounts;
2pub mod format;
3pub mod migrations;
4pub mod secure_storage;
5
6use anyhow::{Context, Result};
7use colored::Colorize;
8use dirs::home_dir;
9use serde::{Deserialize, Serialize};
10use std::env;
11use std::path::PathBuf;
12
13#[derive(Debug, Serialize, Deserialize, Clone)]
14pub struct Config {
15 pub api_key: Option<String>,
17 pub api_url: Option<String>,
18 pub ai_provider: Option<String>,
19 pub model: Option<String>,
20
21 pub tokens_max_input: Option<usize>,
23 pub tokens_max_output: Option<u32>,
24
25 pub commit_type: Option<String>,
27 pub emoji: Option<bool>,
28 pub description: Option<bool>,
29 pub description_capitalize: Option<bool>,
30 pub description_add_period: Option<bool>,
31 pub description_max_length: Option<usize>,
32
33 pub language: Option<String>,
35 pub message_template_placeholder: Option<String>,
36 pub prompt_module: Option<String>,
37
38 pub gitpush: Option<bool>,
40 pub remote: Option<String>,
41 pub one_line_commit: Option<bool>,
42 pub why: Option<bool>,
43 pub omit_scope: Option<bool>,
44 pub generate_count: Option<u8>,
45 pub clipboard_on_timeout: Option<bool>,
46
47 pub action_enabled: Option<bool>,
49
50 pub test_mock_type: Option<String>,
52
53 pub hook_auto_uncomment: Option<bool>,
55 pub pre_gen_hook: Option<Vec<String>>,
56 pub pre_commit_hook: Option<Vec<String>>,
57 pub post_commit_hook: Option<Vec<String>>,
58 pub hook_strict: Option<bool>,
59 pub hook_timeout_ms: Option<u64>,
60
61 pub commitlint_config: Option<String>,
63 pub custom_prompt: Option<String>,
64 pub prompt_file: Option<String>,
65
66 pub learn_from_history: Option<bool>,
68 pub history_commits_count: Option<usize>,
69 pub style_profile: Option<String>,
70
71 pub enable_commit_body: Option<bool>,
73}
74
75impl Default for Config {
76 fn default() -> Self {
77 Self {
78 api_key: None,
79 api_url: None,
80 ai_provider: Some("openai".to_string()),
81 model: Some("gpt-3.5-turbo".to_string()),
82 tokens_max_input: Some(4096),
83 tokens_max_output: Some(500),
84 commit_type: Some("conventional".to_string()),
85 emoji: Some(false),
86 description: Some(false),
87 description_capitalize: Some(true),
88 description_add_period: Some(false),
89 description_max_length: Some(100),
90 language: Some("en".to_string()),
91 message_template_placeholder: Some("$msg".to_string()),
92 prompt_module: Some("conventional-commit".to_string()),
93 gitpush: Some(false),
94 remote: None,
95 one_line_commit: Some(false),
96 why: Some(false),
97 omit_scope: Some(false),
98 generate_count: Some(1),
99 clipboard_on_timeout: Some(true),
100 action_enabled: Some(false),
101 test_mock_type: None,
102 hook_auto_uncomment: Some(false),
103 pre_gen_hook: None,
104 pre_commit_hook: None,
105 post_commit_hook: None,
106 hook_strict: Some(true),
107 hook_timeout_ms: Some(30000),
108 commitlint_config: None,
109 custom_prompt: None,
110 prompt_file: None,
111 learn_from_history: Some(false),
112 history_commits_count: Some(50),
113 style_profile: None,
114 enable_commit_body: Some(false),
115 }
116 }
117}
118
119impl Config {
120 #[allow(dead_code)]
122 pub fn global_config_path() -> Result<PathBuf> {
123 if let Ok(config_home) = env::var("RCO_CONFIG_HOME") {
124 Ok(PathBuf::from(config_home).join("config.toml"))
125 } else {
126 let home = home_dir().context("Could not find home directory")?;
127 Ok(home.join(".config").join("rustycommit").join("config.toml"))
128 }
129 }
130
131 pub fn load() -> Result<Self> {
133 format::ConfigLocations::load_merged()
135 }
136
137 pub fn save(&self) -> Result<()> {
138 self.save_to(format::ConfigLocation::Global)
140 }
141
142 pub fn save_to(&self, location: format::ConfigLocation) -> Result<()> {
144 let mut save_config = self.clone();
146
147 if let Some(ref api_key) = self.api_key {
149 if secure_storage::is_available() {
150 match secure_storage::store_secret("RCO_API_KEY", api_key) {
151 Ok(_) => {
152 save_config.api_key = None;
154 }
155 Err(e) => {
156 eprintln!("Warning: Secure storage unavailable, falling back to file: {e}");
158 }
159 }
160 }
161 }
162
163 format::ConfigLocations::save(&save_config, location)
164 }
165
166 fn get_env_var(base_name: &str) -> Option<String> {
168 let rco_key = format!("RCO_{}", base_name);
169
170 env::var(&rco_key).ok()
172 }
173
174 pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
175 if value == "undefined" || value == "null" {
177 return Ok(());
178 }
179
180 match key {
181 "RCO_API_KEY" => {
183 self.api_key = Some(value.to_string());
184 if secure_storage::is_available() {
186 let _ = secure_storage::store_secret("RCO_API_KEY", value);
187 }
188 }
189 "RCO_API_URL" => self.api_url = Some(value.to_string()),
190 "RCO_AI_PROVIDER" => self.ai_provider = Some(value.to_string()),
191 "RCO_MODEL" => self.model = Some(value.to_string()),
192 "RCO_TOKENS_MAX_INPUT" => {
193 self.tokens_max_input = Some(
194 value
195 .parse()
196 .context("Invalid number for TOKENS_MAX_INPUT")?,
197 );
198 }
199 "RCO_TOKENS_MAX_OUTPUT" => {
200 self.tokens_max_output = Some(
201 value
202 .parse()
203 .context("Invalid number for TOKENS_MAX_OUTPUT")?,
204 );
205 }
206 "RCO_COMMIT_TYPE" => {
207 self.commit_type = Some(value.to_string());
208 }
209 "RCO_PROMPT_MODULE" => {
210 let commit_type = match value {
212 "conventional-commit" => "conventional",
213 _ => value,
214 };
215 self.commit_type = Some(commit_type.to_string());
216 self.prompt_module = Some(value.to_string());
217 }
218 "RCO_EMOJI" => {
219 self.emoji = Some(value.parse().context("Invalid boolean for EMOJI")?);
220 }
221 "RCO_DESCRIPTION_CAPITALIZE" => {
222 self.description_capitalize = Some(
223 value
224 .parse()
225 .context("Invalid boolean for DESCRIPTION_CAPITALIZE")?,
226 );
227 }
228 "RCO_DESCRIPTION_ADD_PERIOD" => {
229 self.description_add_period = Some(
230 value
231 .parse()
232 .context("Invalid boolean for DESCRIPTION_ADD_PERIOD")?,
233 );
234 }
235 "RCO_DESCRIPTION_MAX_LENGTH" => {
236 self.description_max_length = Some(
237 value
238 .parse()
239 .context("Invalid number for DESCRIPTION_MAX_LENGTH")?,
240 );
241 }
242 "RCO_LANGUAGE" => self.language = Some(value.to_string()),
243 "RCO_MESSAGE_TEMPLATE_PLACEHOLDER" => {
244 self.message_template_placeholder = Some(value.to_string());
245 }
246 "RCO_GITPUSH" => {
247 self.gitpush = Some(value.parse().context("Invalid boolean for GITPUSH")?);
248 }
249 "RCO_REMOTE" => self.remote = Some(value.to_string()),
250 "RCO_ONE_LINE_COMMIT" => {
251 self.one_line_commit = Some(
252 value
253 .parse()
254 .context("Invalid boolean for ONE_LINE_COMMIT")?,
255 );
256 }
257 "RCO_ACTION_ENABLED" => {
258 self.action_enabled = Some(
259 value
260 .parse()
261 .context("Invalid boolean for ACTION_ENABLED")?,
262 );
263 }
264 "RCO_DESCRIPTION" => {
265 self.description = Some(value.parse().context("Invalid boolean for DESCRIPTION")?);
266 }
267 "RCO_WHY" => {
268 self.why = Some(value.parse().context("Invalid boolean for WHY")?);
269 }
270 "RCO_OMIT_SCOPE" => {
271 self.omit_scope = Some(value.parse().context("Invalid boolean for OMIT_SCOPE")?);
272 }
273 "RCO_TEST_MOCK_TYPE" => {
274 self.test_mock_type = Some(value.to_string());
275 }
276 "RCO_HOOK_AUTO_UNCOMMENT" => {
277 self.hook_auto_uncomment = Some(
278 value
279 .parse()
280 .context("Invalid boolean for HOOK_AUTO_UNCOMMENT")?,
281 );
282 }
283 "RCO_PRE_GEN_HOOK" => {
284 let items = value
285 .split(';')
286 .map(|s| s.trim().to_string())
287 .filter(|s| !s.is_empty())
288 .collect();
289 self.pre_gen_hook = Some(items);
290 }
291 "RCO_PRE_COMMIT_HOOK" => {
292 let items = value
293 .split(';')
294 .map(|s| s.trim().to_string())
295 .filter(|s| !s.is_empty())
296 .collect();
297 self.pre_commit_hook = Some(items);
298 }
299 "RCO_POST_COMMIT_HOOK" => {
300 let items = value
301 .split(';')
302 .map(|s| s.trim().to_string())
303 .filter(|s| !s.is_empty())
304 .collect();
305 self.post_commit_hook = Some(items);
306 }
307 "RCO_HOOK_STRICT" => {
308 self.hook_strict = Some(value.parse().context("Invalid boolean for HOOK_STRICT")?);
309 }
310 "RCO_HOOK_TIMEOUT_MS" => {
311 self.hook_timeout_ms = Some(
312 value
313 .parse()
314 .context("Invalid number for HOOK_TIMEOUT_MS")?,
315 );
316 }
317 "RCO_COMMITLINT_CONFIG" => {
318 self.commitlint_config = Some(value.to_string());
319 }
320 "RCO_CUSTOM_PROMPT" => {
321 self.custom_prompt = Some(value.to_string());
322 }
323 "RCO_PROMPT_FILE" => {
324 self.prompt_file = Some(value.to_string());
325 }
326 "RCO_GENERATE_COUNT" => {
327 self.generate_count = Some(
328 value
329 .parse()
330 .context("Invalid number for GENERATE_COUNT (1-5)")?,
331 );
332 }
333 "RCO_CLIPBOARD_ON_TIMEOUT" => {
334 self.clipboard_on_timeout = Some(
335 value
336 .parse()
337 .context("Invalid boolean for CLIPBOARD_ON_TIMEOUT")?,
338 );
339 }
340 "RCO_LEARN_FROM_HISTORY" => {
341 self.learn_from_history = Some(
342 value
343 .parse()
344 .context("Invalid boolean for LEARN_FROM_HISTORY")?,
345 );
346 }
347 "RCO_HISTORY_COMMITS_COUNT" => {
348 self.history_commits_count = Some(
349 value
350 .parse()
351 .context("Invalid number for HISTORY_COMMITS_COUNT")?,
352 );
353 }
354 "RCO_STYLE_PROFILE" => {
355 self.style_profile = Some(value.to_string());
356 }
357 "RCO_ENABLE_COMMIT_BODY" => {
358 self.enable_commit_body = Some(
359 value
360 .parse()
361 .context("Invalid boolean for ENABLE_COMMIT_BODY")?,
362 );
363 }
364 "RCO_API_CUSTOM_HEADERS" => {
366 return Ok(());
368 }
369 _ => anyhow::bail!("Unknown configuration key: {}", key),
370 }
371
372 self.save()?;
373 Ok(())
374 }
375
376 pub fn get(&self, key: &str) -> Result<String> {
377 let value = match key {
378 "RCO_API_KEY" => {
379 self.api_key
381 .as_ref()
382 .map(|s| s.to_string())
383 .or_else(|| secure_storage::get_secret("RCO_API_KEY").ok().flatten())
384 }
385 "RCO_API_URL" => self.api_url.as_ref().map(|s| s.to_string()),
386 "RCO_AI_PROVIDER" => self.ai_provider.as_ref().map(|s| s.to_string()),
387 "RCO_MODEL" => self.model.as_ref().map(|s| s.to_string()),
388 "RCO_TOKENS_MAX_INPUT" => self.tokens_max_input.map(|v| v.to_string()),
389 "RCO_TOKENS_MAX_OUTPUT" => self.tokens_max_output.map(|v| v.to_string()),
390 "RCO_COMMIT_TYPE" => self.commit_type.as_ref().map(|s| s.to_string()),
391 "RCO_EMOJI" => self.emoji.map(|v| v.to_string()),
392 "RCO_DESCRIPTION_CAPITALIZE" => self.description_capitalize.map(|v| v.to_string()),
393 "RCO_DESCRIPTION_ADD_PERIOD" => self.description_add_period.map(|v| v.to_string()),
394 "RCO_DESCRIPTION_MAX_LENGTH" => self.description_max_length.map(|v| v.to_string()),
395 "RCO_LANGUAGE" => self.language.as_ref().map(|s| s.to_string()),
396 "RCO_MESSAGE_TEMPLATE_PLACEHOLDER" => self
397 .message_template_placeholder
398 .as_ref()
399 .map(|s| s.to_string()),
400 "RCO_GITPUSH" => self.gitpush.map(|v| v.to_string()),
401 "RCO_REMOTE" => self.remote.as_ref().map(|s| s.to_string()),
402 "RCO_ONE_LINE_COMMIT" => self.one_line_commit.map(|v| v.to_string()),
403 "RCO_ACTION_ENABLED" => self.action_enabled.map(|v| v.to_string()),
404 "RCO_COMMITLINT_CONFIG" => self.commitlint_config.as_ref().map(|s| s.to_string()),
405 "RCO_CUSTOM_PROMPT" => self.custom_prompt.as_ref().map(|s| s.to_string()),
406 "RCO_PROMPT_FILE" => self.prompt_file.as_ref().map(|s| s.to_string()),
407 "RCO_GENERATE_COUNT" => self.generate_count.map(|v| v.to_string()),
408 "RCO_CLIPBOARD_ON_TIMEOUT" => self.clipboard_on_timeout.map(|v| v.to_string()),
409 _ => None,
410 };
411
412 value.ok_or_else(|| anyhow::anyhow!("Configuration key '{}' not found or not set", key))
413 }
414
415 pub fn reset(&mut self, keys: Option<&[String]>) -> Result<()> {
416 if let Some(key_list) = keys {
417 let default = Self::default();
418 for key in key_list {
419 match key.as_str() {
420 "RCO_API_KEY" => {
421 self.api_key = default.api_key.clone();
422 let _ = secure_storage::delete_secret("RCO_API_KEY");
424 }
425 "RCO_API_URL" => self.api_url = default.api_url.clone(),
426 "RCO_AI_PROVIDER" => self.ai_provider = default.ai_provider.clone(),
427 "RCO_MODEL" => self.model = default.model.clone(),
428 "RCO_TOKENS_MAX_INPUT" => self.tokens_max_input = default.tokens_max_input,
429 "RCO_TOKENS_MAX_OUTPUT" => self.tokens_max_output = default.tokens_max_output,
430 "RCO_COMMIT_TYPE" => self.commit_type = default.commit_type.clone(),
431 "RCO_EMOJI" => self.emoji = default.emoji,
432 "RCO_DESCRIPTION_CAPITALIZE" => {
433 self.description_capitalize = default.description_capitalize
434 }
435 "RCO_DESCRIPTION_ADD_PERIOD" => {
436 self.description_add_period = default.description_add_period
437 }
438 "RCO_DESCRIPTION_MAX_LENGTH" => {
439 self.description_max_length = default.description_max_length
440 }
441 "RCO_LANGUAGE" => self.language = default.language.clone(),
442 "RCO_MESSAGE_TEMPLATE_PLACEHOLDER" => {
443 self.message_template_placeholder =
444 default.message_template_placeholder.clone()
445 }
446 "RCO_GITPUSH" => self.gitpush = default.gitpush,
447 "RCO_REMOTE" => self.remote = default.remote.clone(),
448 "RCO_ONE_LINE_COMMIT" => self.one_line_commit = default.one_line_commit,
449 "RCO_ACTION_ENABLED" => self.action_enabled = default.action_enabled,
450 "RCO_PRE_GEN_HOOK" => self.pre_gen_hook = default.pre_gen_hook.clone(),
451 "RCO_PRE_COMMIT_HOOK" => self.pre_commit_hook = default.pre_commit_hook.clone(),
452 "RCO_POST_COMMIT_HOOK" => {
453 self.post_commit_hook = default.post_commit_hook.clone()
454 }
455 "RCO_HOOK_STRICT" => self.hook_strict = default.hook_strict,
456 "RCO_HOOK_TIMEOUT_MS" => self.hook_timeout_ms = default.hook_timeout_ms,
457 "RCO_GENERATE_COUNT" => self.generate_count = default.generate_count,
458 "RCO_CLIPBOARD_ON_TIMEOUT" => {
459 self.clipboard_on_timeout = default.clipboard_on_timeout
460 }
461 _ => anyhow::bail!("Unknown configuration key: {}", key),
462 }
463 }
464 } else {
465 *self = Self::default();
466 }
467
468 self.save()?;
469 Ok(())
470 }
471
472 pub fn load_with_commitlint(&mut self) -> Result<()> {
474 if let Ok(commitlint_path) = env::var("COMMITLINT_CONFIG") {
476 self.commitlint_config = Some(commitlint_path);
477 }
478
479 if self.commitlint_config.is_none() {
481 let home = home_dir().context("Could not find home directory")?;
482
483 let possible_paths = [
485 home.join(".commitlintrc.js"),
486 home.join(".commitlintrc.json"),
487 home.join(".commitlintrc.yml"),
488 home.join(".commitlintrc.yaml"),
489 home.join("commitlint.config.js"),
490 ];
491
492 for path in &possible_paths {
493 if path.exists() {
494 self.commitlint_config = Some(path.to_string_lossy().to_string());
495 break;
496 }
497 }
498 }
499
500 Ok(())
501 }
502
503 pub fn apply_commitlint_rules(&mut self) -> Result<()> {
505 if let Some(ref config_path) = self.commitlint_config.clone() {
506 let path = PathBuf::from(config_path);
507 if path.exists() {
508 if self.commit_type.is_none() {
511 self.commit_type = Some("conventional".to_string());
512 }
513
514 println!("📋 Found commitlint config at: {}", config_path);
517 println!("🔧 Using conventional commit format for consistency");
518 }
519 }
520 Ok(())
521 }
522
523 pub fn get_effective_prompt(
525 &self,
526 diff: &str,
527 context: Option<&str>,
528 full_gitmoji: bool,
529 ) -> String {
530 let custom_prompt_template = if let Some(ref prompt_file) = self.prompt_file {
532 match Self::load_prompt_file(prompt_file) {
533 Ok(content) => {
534 tracing::info!("Loaded custom prompt from file: {}", prompt_file);
535 Some(content)
536 }
537 Err(e) => {
538 eprintln!(
539 "{}",
540 format!(
541 "Warning: Failed to load prompt file '{}': {}. Using fallback.",
542 prompt_file, e
543 )
544 .yellow()
545 );
546 self.custom_prompt.clone()
547 }
548 }
549 } else {
550 self.custom_prompt.clone()
551 };
552
553 if let Some(template) = custom_prompt_template {
554 tracing::warn!(
556 "SECURITY: Using custom prompt template - full diff content will be included in the prompt. \
557 Only use custom prompts from trusted sources. Malicious prompts could exfiltrate code."
558 );
559 eprintln!(
560 "{}",
561 "⚠️ SECURITY WARNING: Using custom prompt template."
562 .yellow()
563 .bold()
564 );
565 eprintln!(
566 "{}",
567 " Your diff content (potentially including sensitive code) will be sent to the AI provider."
568 .yellow()
569 );
570 eprintln!(
571 "{}",
572 " Only use custom prompts from trusted sources.".yellow()
573 );
574
575 Self::replace_placeholders(&template, diff, context, self)
577 } else {
578 super::providers::build_prompt(diff, context, self, full_gitmoji)
580 }
581 }
582
583 fn load_prompt_file(path: &str) -> Result<String> {
585 let expanded_path = if path.starts_with("~") {
586 if let Some(home) = home_dir() {
587 home.join(path.strip_prefix("~/").unwrap_or(path))
588 } else {
589 PathBuf::from(path)
590 }
591 } else {
592 PathBuf::from(path)
593 };
594
595 std::fs::read_to_string(&expanded_path)
596 .with_context(|| format!("Failed to read prompt file: {}", expanded_path.display()))
597 }
598
599 fn replace_placeholders(
602 template: &str,
603 diff: &str,
604 context: Option<&str>,
605 config: &Config,
606 ) -> String {
607 let mut result = template.to_string();
608
609 let language = config.language.as_deref().unwrap_or("en");
611 let commit_type = config.commit_type.as_deref().unwrap_or("conventional");
612 let max_length = config.description_max_length.unwrap_or(100).to_string();
613 let emoji = config.emoji.unwrap_or(false).to_string();
614 let description = config.description.unwrap_or(false).to_string();
615
616 let context_str = context.unwrap_or("");
618
619 result = result.replace("{diff}", diff);
621 result = result.replace("{context}", context_str);
622 result = result.replace("{language}", language);
623 result = result.replace("{commit_type}", commit_type);
624 result = result.replace("{max_length}", &max_length);
625 result = result.replace("{emoji}", &emoji);
626 result = result.replace("{description}", &description);
627
628 result = result.replace("$diff", diff);
630 result = result.replace("$context", context_str);
631 result = result.replace("$language", language);
632 result = result.replace("$commit_type", commit_type);
633 result = result.replace("$max_length", &max_length);
634 result = result.replace("$emoji", &emoji);
635 result = result.replace("$description", &description);
636
637 result
638 }
639
640 pub fn set_prompt_file(&mut self, path: Option<String>) {
642 self.prompt_file = path;
643 }
644
645 pub fn merge(&mut self, other: Config) {
647 macro_rules! merge_field {
648 ($field:ident) => {
649 if other.$field.is_some() {
650 self.$field = other.$field;
651 }
652 };
653 }
654
655 merge_field!(api_key);
656 merge_field!(api_url);
657 merge_field!(ai_provider);
658 merge_field!(model);
659 merge_field!(tokens_max_input);
660 merge_field!(tokens_max_output);
661 merge_field!(commit_type);
662 merge_field!(emoji);
663 merge_field!(description);
664 merge_field!(description_capitalize);
665 merge_field!(description_add_period);
666 merge_field!(description_max_length);
667 merge_field!(language);
668 merge_field!(message_template_placeholder);
669 merge_field!(prompt_module);
670 merge_field!(gitpush);
671 merge_field!(remote);
672 merge_field!(one_line_commit);
673 merge_field!(why);
674 merge_field!(omit_scope);
675 merge_field!(action_enabled);
676 merge_field!(test_mock_type);
677 merge_field!(hook_auto_uncomment);
678 merge_field!(pre_gen_hook);
679 merge_field!(pre_commit_hook);
680 merge_field!(post_commit_hook);
681 merge_field!(hook_strict);
682 merge_field!(hook_timeout_ms);
683 merge_field!(commitlint_config);
684 merge_field!(custom_prompt);
685 merge_field!(prompt_file);
686 merge_field!(generate_count);
687 merge_field!(clipboard_on_timeout);
688 merge_field!(learn_from_history);
689 merge_field!(history_commits_count);
690 merge_field!(style_profile);
691 }
692
693 pub fn load_from_environment(&mut self) {
696 macro_rules! load_env_var {
698 ($field:ident, $base_name:expr) => {
699 if let Some(value) = Self::get_env_var($base_name) {
700 self.$field = Some(value);
701 }
702 };
703 }
704
705 macro_rules! load_env_var_parse {
706 ($field:ident, $base_name:expr, $type:ty) => {
707 if let Some(value) = Self::get_env_var($base_name) {
708 if let Ok(parsed) = value.parse::<$type>() {
709 self.$field = Some(parsed);
710 }
711 }
712 };
713 }
714
715 load_env_var!(api_key, "API_KEY");
716 load_env_var!(api_url, "API_URL");
717 load_env_var!(ai_provider, "AI_PROVIDER");
718 load_env_var!(model, "MODEL");
719 load_env_var_parse!(tokens_max_input, "TOKENS_MAX_INPUT", usize);
720 load_env_var_parse!(tokens_max_output, "TOKENS_MAX_OUTPUT", u32);
721 load_env_var!(commit_type, "COMMIT_TYPE");
722 load_env_var_parse!(emoji, "EMOJI", bool);
723 load_env_var_parse!(description, "DESCRIPTION", bool);
724 load_env_var_parse!(description_capitalize, "DESCRIPTION_CAPITALIZE", bool);
725 load_env_var_parse!(description_add_period, "DESCRIPTION_ADD_PERIOD", bool);
726 load_env_var_parse!(description_max_length, "DESCRIPTION_MAX_LENGTH", usize);
727 load_env_var!(language, "LANGUAGE");
728 load_env_var!(message_template_placeholder, "MESSAGE_TEMPLATE_PLACEHOLDER");
729 load_env_var!(prompt_module, "PROMPT_MODULE");
730 load_env_var_parse!(gitpush, "GITPUSH", bool);
731 load_env_var!(remote, "REMOTE");
732 load_env_var_parse!(one_line_commit, "ONE_LINE_COMMIT", bool);
733 load_env_var_parse!(why, "WHY", bool);
734 load_env_var_parse!(omit_scope, "OMIT_SCOPE", bool);
735 load_env_var_parse!(action_enabled, "ACTION_ENABLED", bool);
736 load_env_var!(test_mock_type, "TEST_MOCK_TYPE");
737 load_env_var_parse!(hook_auto_uncomment, "HOOK_AUTO_UNCOMMENT", bool);
738 load_env_var!(commitlint_config, "COMMITLINT_CONFIG");
739 load_env_var!(custom_prompt, "CUSTOM_PROMPT");
740 load_env_var!(prompt_file, "PROMPT_FILE");
741 load_env_var_parse!(generate_count, "GENERATE_COUNT", u8);
742 load_env_var_parse!(clipboard_on_timeout, "CLIPBOARD_ON_TIMEOUT", bool);
743 load_env_var_parse!(learn_from_history, "LEARN_FROM_HISTORY", bool);
744 load_env_var_parse!(history_commits_count, "HISTORY_COMMITS_COUNT", usize);
745 load_env_var!(style_profile, "STYLE_PROFILE");
746 load_env_var_parse!(enable_commit_body, "ENABLE_COMMIT_BODY", bool);
747 }
748}
749
750#[allow(dead_code)]
755impl Config {
756 pub fn get_active_account(&self) -> Result<Option<accounts::AccountConfig>> {
758 if let Some(accounts_config) = accounts::AccountsConfig::load()? {
759 if let Some(account) = accounts_config.get_active_account() {
760 return Ok(Some(account.clone()));
761 }
762 }
763 Ok(None)
764 }
765
766 pub fn has_accounts(&self) -> bool {
768 accounts::AccountsConfig::load()
769 .map(|c| c.map(|ac| !ac.accounts.is_empty()).unwrap_or(false))
770 .unwrap_or(false)
771 }
772
773 pub fn get_account(&self, alias: &str) -> Result<Option<accounts::AccountConfig>> {
775 if let Some(accounts_config) = accounts::AccountsConfig::load()? {
776 if let Some(account) = accounts_config.get_account(alias) {
777 return Ok(Some(account.clone()));
778 }
779 }
780 Ok(None)
781 }
782
783 pub fn list_accounts(&self) -> Result<Vec<accounts::AccountConfig>> {
785 if let Some(accounts_config) = accounts::AccountsConfig::load()? {
786 Ok(accounts_config
787 .list_accounts()
788 .into_iter()
789 .cloned()
790 .collect())
791 } else {
792 Ok(Vec::new())
793 }
794 }
795
796 pub fn set_default_account(&mut self, alias: &str) -> Result<()> {
798 let mut accounts_config = accounts::AccountsConfig::load()?.unwrap_or_default();
799 accounts_config.set_active_account(alias)?;
800 accounts_config.save()?;
801 Ok(())
802 }
803
804 pub fn remove_account(&mut self, alias: &str) -> Result<()> {
806 let mut accounts_config = accounts::AccountsConfig::load()?.unwrap_or_default();
807 if accounts_config.remove_account(alias) {
808 accounts_config.save()?;
809 }
810 Ok(())
811 }
812}