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!("Warning: Failed to load prompt file '{}': {}. Using fallback.", prompt_file, e)
541 .yellow()
542 );
543 self.custom_prompt.clone()
544 }
545 }
546 } else {
547 self.custom_prompt.clone()
548 };
549
550 if let Some(template) = custom_prompt_template {
551 tracing::warn!(
553 "SECURITY: Using custom prompt template - full diff content will be included in the prompt. \
554 Only use custom prompts from trusted sources. Malicious prompts could exfiltrate code."
555 );
556 eprintln!(
557 "{}",
558 "⚠️ SECURITY WARNING: Using custom prompt template.".yellow().bold()
559 );
560 eprintln!(
561 "{}",
562 " Your diff content (potentially including sensitive code) will be sent to the AI provider."
563 .yellow()
564 );
565 eprintln!(
566 "{}",
567 " Only use custom prompts from trusted sources.".yellow()
568 );
569
570 Self::replace_placeholders(&template, diff, context, self)
572 } else {
573 super::providers::build_prompt(diff, context, self, full_gitmoji)
575 }
576 }
577
578 fn load_prompt_file(path: &str) -> Result<String> {
580 let expanded_path = if path.starts_with("~") {
581 if let Some(home) = home_dir() {
582 home.join(path.strip_prefix("~/").unwrap_or(path))
583 } else {
584 PathBuf::from(path)
585 }
586 } else {
587 PathBuf::from(path)
588 };
589
590 std::fs::read_to_string(&expanded_path)
591 .with_context(|| format!("Failed to read prompt file: {}", expanded_path.display()))
592 }
593
594 fn replace_placeholders(
597 template: &str,
598 diff: &str,
599 context: Option<&str>,
600 config: &Config,
601 ) -> String {
602 let mut result = template.to_string();
603
604 let language = config.language.as_deref().unwrap_or("en");
606 let commit_type = config.commit_type.as_deref().unwrap_or("conventional");
607 let max_length = config.description_max_length.unwrap_or(100).to_string();
608 let emoji = config.emoji.unwrap_or(false).to_string();
609 let description = config.description.unwrap_or(false).to_string();
610
611 let context_str = context.unwrap_or("");
613
614 result = result.replace("{diff}", diff);
616 result = result.replace("{context}", context_str);
617 result = result.replace("{language}", language);
618 result = result.replace("{commit_type}", commit_type);
619 result = result.replace("{max_length}", &max_length);
620 result = result.replace("{emoji}", &emoji);
621 result = result.replace("{description}", &description);
622
623 result = result.replace("$diff", diff);
625 result = result.replace("$context", context_str);
626 result = result.replace("$language", language);
627 result = result.replace("$commit_type", commit_type);
628 result = result.replace("$max_length", &max_length);
629 result = result.replace("$emoji", &emoji);
630 result = result.replace("$description", &description);
631
632 result
633 }
634
635 pub fn set_prompt_file(&mut self, path: Option<String>) {
637 self.prompt_file = path;
638 }
639
640 pub fn merge(&mut self, other: Config) {
642 macro_rules! merge_field {
643 ($field:ident) => {
644 if other.$field.is_some() {
645 self.$field = other.$field;
646 }
647 };
648 }
649
650 merge_field!(api_key);
651 merge_field!(api_url);
652 merge_field!(ai_provider);
653 merge_field!(model);
654 merge_field!(tokens_max_input);
655 merge_field!(tokens_max_output);
656 merge_field!(commit_type);
657 merge_field!(emoji);
658 merge_field!(description);
659 merge_field!(description_capitalize);
660 merge_field!(description_add_period);
661 merge_field!(description_max_length);
662 merge_field!(language);
663 merge_field!(message_template_placeholder);
664 merge_field!(prompt_module);
665 merge_field!(gitpush);
666 merge_field!(remote);
667 merge_field!(one_line_commit);
668 merge_field!(why);
669 merge_field!(omit_scope);
670 merge_field!(action_enabled);
671 merge_field!(test_mock_type);
672 merge_field!(hook_auto_uncomment);
673 merge_field!(pre_gen_hook);
674 merge_field!(pre_commit_hook);
675 merge_field!(post_commit_hook);
676 merge_field!(hook_strict);
677 merge_field!(hook_timeout_ms);
678 merge_field!(commitlint_config);
679 merge_field!(custom_prompt);
680 merge_field!(prompt_file);
681 merge_field!(generate_count);
682 merge_field!(clipboard_on_timeout);
683 merge_field!(learn_from_history);
684 merge_field!(history_commits_count);
685 merge_field!(style_profile);
686 }
687
688 pub fn load_from_environment(&mut self) {
691 macro_rules! load_env_var {
693 ($field:ident, $base_name:expr) => {
694 if let Some(value) = Self::get_env_var($base_name) {
695 self.$field = Some(value);
696 }
697 };
698 }
699
700 macro_rules! load_env_var_parse {
701 ($field:ident, $base_name:expr, $type:ty) => {
702 if let Some(value) = Self::get_env_var($base_name) {
703 if let Ok(parsed) = value.parse::<$type>() {
704 self.$field = Some(parsed);
705 }
706 }
707 };
708 }
709
710 load_env_var!(api_key, "API_KEY");
711 load_env_var!(api_url, "API_URL");
712 load_env_var!(ai_provider, "AI_PROVIDER");
713 load_env_var!(model, "MODEL");
714 load_env_var_parse!(tokens_max_input, "TOKENS_MAX_INPUT", usize);
715 load_env_var_parse!(tokens_max_output, "TOKENS_MAX_OUTPUT", u32);
716 load_env_var!(commit_type, "COMMIT_TYPE");
717 load_env_var_parse!(emoji, "EMOJI", bool);
718 load_env_var_parse!(description, "DESCRIPTION", bool);
719 load_env_var_parse!(description_capitalize, "DESCRIPTION_CAPITALIZE", bool);
720 load_env_var_parse!(description_add_period, "DESCRIPTION_ADD_PERIOD", bool);
721 load_env_var_parse!(description_max_length, "DESCRIPTION_MAX_LENGTH", usize);
722 load_env_var!(language, "LANGUAGE");
723 load_env_var!(message_template_placeholder, "MESSAGE_TEMPLATE_PLACEHOLDER");
724 load_env_var!(prompt_module, "PROMPT_MODULE");
725 load_env_var_parse!(gitpush, "GITPUSH", bool);
726 load_env_var!(remote, "REMOTE");
727 load_env_var_parse!(one_line_commit, "ONE_LINE_COMMIT", bool);
728 load_env_var_parse!(why, "WHY", bool);
729 load_env_var_parse!(omit_scope, "OMIT_SCOPE", bool);
730 load_env_var_parse!(action_enabled, "ACTION_ENABLED", bool);
731 load_env_var!(test_mock_type, "TEST_MOCK_TYPE");
732 load_env_var_parse!(hook_auto_uncomment, "HOOK_AUTO_UNCOMMENT", bool);
733 load_env_var!(commitlint_config, "COMMITLINT_CONFIG");
734 load_env_var!(custom_prompt, "CUSTOM_PROMPT");
735 load_env_var!(prompt_file, "PROMPT_FILE");
736 load_env_var_parse!(generate_count, "GENERATE_COUNT", u8);
737 load_env_var_parse!(clipboard_on_timeout, "CLIPBOARD_ON_TIMEOUT", bool);
738 load_env_var_parse!(learn_from_history, "LEARN_FROM_HISTORY", bool);
739 load_env_var_parse!(history_commits_count, "HISTORY_COMMITS_COUNT", usize);
740 load_env_var!(style_profile, "STYLE_PROFILE");
741 load_env_var_parse!(enable_commit_body, "ENABLE_COMMIT_BODY", bool);
742 }
743}
744
745#[allow(dead_code)]
750impl Config {
751 pub fn get_active_account(&self) -> Result<Option<accounts::AccountConfig>> {
753 if let Some(accounts_config) = accounts::AccountsConfig::load()? {
754 if let Some(account) = accounts_config.get_active_account() {
755 return Ok(Some(account.clone()));
756 }
757 }
758 Ok(None)
759 }
760
761 pub fn has_accounts(&self) -> bool {
763 accounts::AccountsConfig::load()
764 .map(|c| c.map(|ac| !ac.accounts.is_empty()).unwrap_or(false))
765 .unwrap_or(false)
766 }
767
768 pub fn get_account(&self, alias: &str) -> Result<Option<accounts::AccountConfig>> {
770 if let Some(accounts_config) = accounts::AccountsConfig::load()? {
771 if let Some(account) = accounts_config.get_account(alias) {
772 return Ok(Some(account.clone()));
773 }
774 }
775 Ok(None)
776 }
777
778 pub fn list_accounts(&self) -> Result<Vec<accounts::AccountConfig>> {
780 if let Some(accounts_config) = accounts::AccountsConfig::load()? {
781 Ok(accounts_config
782 .list_accounts()
783 .into_iter()
784 .cloned()
785 .collect())
786 } else {
787 Ok(Vec::new())
788 }
789 }
790
791 pub fn set_default_account(&mut self, alias: &str) -> Result<()> {
793 let mut accounts_config = accounts::AccountsConfig::load()?.unwrap_or_default();
794 accounts_config.set_active_account(alias)?;
795 accounts_config.save()?;
796 Ok(())
797 }
798
799 pub fn remove_account(&mut self, alias: &str) -> Result<()> {
801 let mut accounts_config = accounts::AccountsConfig::load()?.unwrap_or_default();
802 if accounts_config.remove_account(alias) {
803 accounts_config.save()?;
804 }
805 Ok(())
806 }
807}