Skip to content

Commit 8979838

Browse files
committed
refactor: extract generate_default_config method
Moves hardcoded config template from create_default() into a separate generate_default_config() method for reusability. Adds tests to verify generated config round-trips to defaults and includes all expected fields.
1 parent c3650e4 commit 8979838

2 files changed

Lines changed: 320 additions & 76 deletions

File tree

src/config.rs

Lines changed: 228 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ pub struct Config {
9393
#[serde(default = "default_timeout_secs")]
9494
pub timeout_secs: u64,
9595

96-
/// LLM temperature (0.0-2.0, default 0.3)
96+
/// LLM temperature (0.0-1.0, default 0.3)
9797
#[serde(default = "default_temperature")]
9898
pub temperature: f32,
9999

@@ -430,7 +430,7 @@ impl Config {
430430

431431
if !(0.0..=2.0).contains(&self.temperature) {
432432
return Err(Error::Config(format!(
433-
"temperature must be 0.0–2.0, got {}",
433+
"temperature must be 0.0–1.0, got {}",
434434
self.temperature
435435
)));
436436
}
@@ -485,81 +485,9 @@ impl Config {
485485
fs::create_dir_all(&dir)?;
486486

487487
let path = dir.join("config.toml");
488-
let content = r#"# CommitBee Configuration
488+
let content = Self::generate_default_config();
489489

490-
# LLM provider: ollama, openai, anthropic
491-
provider = "ollama"
492-
493-
# Model name (for Ollama, use `ollama list` to see available)
494-
model = "qwen3.5:4b"
495-
496-
# Ollama server URL
497-
ollama_host = "http://localhost:11434"
498-
499-
# Maximum lines of diff to include in prompt
500-
max_diff_lines = 500
501-
502-
# Maximum lines per file in diff
503-
max_file_lines = 100
504-
505-
# Maximum tokens to generate (default 256)
506-
# Increase to 8192+ if using thinking models with think = true
507-
# num_predict = 256
508-
509-
# Enable thinking/reasoning for Ollama models (default: false)
510-
# When enabled, models like qwen3 will reason before responding.
511-
# Requires higher num_predict (8192+) to accommodate thinking tokens.
512-
# think = false
513-
514-
# Maximum context characters for LLM prompt (~4 chars per token)
515-
# Increase for larger models (e.g., 48000 for 16K context)
516-
# max_context_chars = 24000
517-
518-
# Rename detection similarity threshold (0-100, default 70)
519-
# Set to 0 to disable rename detection
520-
# rename_threshold = 70
521-
522-
# Language for commit message generation (ISO 639-1 code)
523-
# Type and scope remain in English per Conventional Commits spec.
524-
# locale = "de"
525-
526-
# Custom secret patterns (additional regex patterns for secret scanning)
527-
# custom_secret_patterns = ["CUSTOM_KEY_[a-zA-Z0-9]{32}"]
528-
529-
# Disable built-in secret patterns by name
530-
# disabled_secret_patterns = ["Generic Secret (unquoted)"]
531-
532-
# Experimental: learn commit style from repository history (default: false)
533-
# Analyzes recent commits to learn scope naming, type patterns, and
534-
# subject phrasing conventions for the repository.
535-
# learn_from_history = false
536-
537-
# Number of recent commits to sample for style learning (default: 50)
538-
# history_sample_size = 50
539-
540-
# Exclude files matching glob patterns from analysis and diff context
541-
# Excluded files are listed in output but not sent to the LLM.
542-
# exclude_patterns = ["*.lock", "**/*.generated.*"]
543-
544-
# Custom system prompt file (overrides built-in prompt)
545-
# system_prompt_path = "/path/to/system_prompt.txt"
546-
547-
# Custom user prompt template file (supports {{diff}}, {{symbols}}, {{files}} variables)
548-
# template_path = "/path/to/template.txt"
549-
550-
# Commit message format options
551-
[format]
552-
# Include body/description in commit message
553-
include_body = true
554-
555-
# Include scope in commit type, e.g., feat(scope): subject
556-
include_scope = true
557-
558-
# Enforce lowercase first character of subject (conventional commits best practice)
559-
lowercase_subject = true
560-
"#;
561-
562-
fs::write(&path, content)?;
490+
fs::write(&path, &content)?;
563491

564492
// Set secure permissions (0600)
565493
#[cfg(unix)]
@@ -572,4 +500,228 @@ lowercase_subject = true
572500

573501
Ok(path)
574502
}
503+
504+
/// Generate the default config TOML string with descriptive comments.
505+
///
506+
/// Values are pulled from `Config::default()` so the template never
507+
/// drifts from the struct defaults. Comments are maintained alongside
508+
/// field descriptors in a single list.
509+
pub fn generate_default_config() -> String {
510+
let default = Config::default();
511+
let table: toml::Table = {
512+
let s = toml::to_string(&default).expect("Config serializes to TOML");
513+
toml::from_str(&s).expect("round-trips as TOML table")
514+
};
515+
516+
/// Whether to show the field commented-out (advanced/optional settings)
517+
/// or active (core settings the user should see immediately).
518+
#[derive(Clone, Copy)]
519+
enum Show {
520+
Active,
521+
CommentedOut,
522+
}
523+
524+
struct Field {
525+
key: &'static str,
526+
comment: &'static str,
527+
show: Show,
528+
/// Override value for Option/Vec fields that serialize to nothing
529+
example: Option<&'static str>,
530+
}
531+
532+
let fields: &[Field] = &[
533+
Field {
534+
key: "provider",
535+
comment: "LLM provider: ollama, openai, anthropic",
536+
show: Show::Active,
537+
example: None,
538+
},
539+
Field {
540+
key: "model",
541+
comment: "Model name (for Ollama, use `ollama list` to see available)",
542+
show: Show::Active,
543+
example: None,
544+
},
545+
Field {
546+
key: "ollama_host",
547+
comment: "Ollama server URL",
548+
show: Show::Active,
549+
example: None,
550+
},
551+
Field {
552+
key: "max_diff_lines",
553+
comment: "Maximum lines of diff to include in prompt",
554+
show: Show::Active,
555+
example: None,
556+
},
557+
Field {
558+
key: "max_file_lines",
559+
comment: "Maximum lines per file in diff",
560+
show: Show::Active,
561+
example: None,
562+
},
563+
Field {
564+
key: "num_predict",
565+
comment: "Maximum tokens to generate\n\
566+
Increase to 8192+ if using thinking models with think = true",
567+
show: Show::CommentedOut,
568+
example: None,
569+
},
570+
Field {
571+
key: "think",
572+
comment: "Enable thinking/reasoning for Ollama models\n\
573+
When enabled, models like qwen3 will reason before responding.\n\
574+
Requires higher num_predict (8192+) to accommodate thinking tokens.",
575+
show: Show::CommentedOut,
576+
example: None,
577+
},
578+
Field {
579+
key: "max_context_chars",
580+
comment: "Maximum context characters for LLM prompt (~4 chars per token)\n\
581+
Increase for larger models (e.g., 48000 for 16K context)",
582+
show: Show::CommentedOut,
583+
example: None,
584+
},
585+
Field {
586+
key: "timeout_secs",
587+
comment: "Request timeout in seconds",
588+
show: Show::CommentedOut,
589+
example: None,
590+
},
591+
Field {
592+
key: "temperature",
593+
comment: "LLM temperature (0.0-1.0, default 0.3)",
594+
show: Show::CommentedOut,
595+
example: None,
596+
},
597+
Field {
598+
key: "rename_threshold",
599+
comment: "Rename detection similarity threshold (0-100)\n\
600+
Set to 0 to disable rename detection",
601+
show: Show::CommentedOut,
602+
example: None,
603+
},
604+
Field {
605+
key: "locale",
606+
comment: "Language for commit message generation (ISO 639-1 code)\n\
607+
Type and scope remain in English per Conventional Commits spec.",
608+
show: Show::CommentedOut,
609+
example: Some("\"de\""),
610+
},
611+
Field {
612+
key: "custom_secret_patterns",
613+
comment: "Custom secret patterns (additional regex patterns for secret scanning)",
614+
show: Show::CommentedOut,
615+
example: Some("[\"CUSTOM_KEY_[a-zA-Z0-9]{32}\"]"),
616+
},
617+
Field {
618+
key: "disabled_secret_patterns",
619+
comment: "Disable built-in secret patterns by name",
620+
show: Show::CommentedOut,
621+
example: Some("[\"Generic Secret (unquoted)\"]"),
622+
},
623+
Field {
624+
key: "learn_from_history",
625+
comment: "Experimental: learn commit style from repository history\n\
626+
Analyzes recent commits to learn scope naming, type patterns, and\n\
627+
subject phrasing conventions for the repository.",
628+
show: Show::CommentedOut,
629+
example: None,
630+
},
631+
Field {
632+
key: "history_sample_size",
633+
comment: "Number of recent commits to sample for style learning",
634+
show: Show::CommentedOut,
635+
example: None,
636+
},
637+
Field {
638+
key: "exclude_patterns",
639+
comment: "Exclude files matching glob patterns from analysis and diff context\n\
640+
Excluded files are listed in output but not sent to the LLM.",
641+
show: Show::CommentedOut,
642+
example: Some("[\"*.lock\", \"**/*.generated.*\"]"),
643+
},
644+
Field {
645+
key: "openai_base_url",
646+
comment: "Base URL for OpenAI-compatible APIs",
647+
show: Show::CommentedOut,
648+
example: Some("\"https://api.openai.com/v1\""),
649+
},
650+
Field {
651+
key: "anthropic_base_url",
652+
comment: "Base URL for Anthropic API",
653+
show: Show::CommentedOut,
654+
example: Some("\"https://api.anthropic.com/v1\""),
655+
},
656+
Field {
657+
key: "system_prompt_path",
658+
comment: "Custom system prompt file (overrides built-in prompt)",
659+
show: Show::CommentedOut,
660+
example: Some("\"/path/to/system_prompt.txt\""),
661+
},
662+
Field {
663+
key: "template_path",
664+
comment: "Custom user prompt template file\n\
665+
Supports {{diff}}, {{symbols}}, {{files}} variables",
666+
show: Show::CommentedOut,
667+
example: Some("\"/path/to/template.txt\""),
668+
},
669+
];
670+
671+
let format_fields: &[(&str, &str)] = &[
672+
("include_body", "Include body/description in commit message"),
673+
(
674+
"include_scope",
675+
"Include scope in commit type, e.g., feat(scope): subject",
676+
),
677+
(
678+
"lowercase_subject",
679+
"Enforce lowercase first character of subject (conventional commits best practice)",
680+
),
681+
];
682+
683+
let mut out = String::from("# CommitBee Configuration\n");
684+
685+
for field in fields {
686+
out.push('\n');
687+
for line in field.comment.lines() {
688+
out.push_str("# ");
689+
out.push_str(line);
690+
out.push('\n');
691+
}
692+
693+
// Resolve the display value: serialized default, or example for Option/Vec fields
694+
let val_str = if let Some(v) = table.get(field.key) {
695+
// f32 round-trips through f64 in TOML and gains precision noise;
696+
// format from the struct directly for float fields
697+
if v.is_float() {
698+
format!("{} = {}", field.key, default.temperature)
699+
} else {
700+
format!("{} = {v}", field.key)
701+
}
702+
} else if let Some(ex) = field.example {
703+
format!("{} = {ex}", field.key)
704+
} else {
705+
continue;
706+
};
707+
708+
if matches!(field.show, Show::CommentedOut) {
709+
out.push_str("# ");
710+
}
711+
out.push_str(&val_str);
712+
out.push('\n');
713+
}
714+
715+
// [format] section
716+
out.push_str("\n# Commit message format options\n[format]\n");
717+
if let Some(toml::Value::Table(fmt)) = table.get("format") {
718+
for (key, comment) in format_fields {
719+
if let Some(v) = fmt.get(*key) {
720+
out.push_str(&format!("# {comment}\n{key} = {v}\n"));
721+
}
722+
}
723+
}
724+
725+
out
726+
}
575727
}

0 commit comments

Comments
 (0)