1use std::path::Path;
8
9use crate::condition::Condition;
10use crate::config::ConfigDirective;
11use crate::toml_config::TomlGit;
12
13const CAUTIOUS_TOML: &str = include_str!("stdlib/git_cautious.toml");
14const STANDARD_TOML: &str = include_str!("stdlib/git_standard.toml");
15const PERMISSIVE_TOML: &str = include_str!("stdlib/git_permissive.toml");
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum GitStyle {
20 Cautious,
21 Standard,
22 Permissive,
23}
24
25impl GitStyle {
26 pub fn parse(s: &str) -> Result<Self, String> {
32 match s {
33 "cautious" => Ok(Self::Cautious),
34 "standard" => Ok(Self::Standard),
35 "permissive" => Ok(Self::Permissive),
36 other => Err(format!(
37 "unknown git style: {other} (expected cautious, standard, or permissive)"
38 )),
39 }
40 }
41
42 const fn toml_source(self) -> &'static str {
43 match self {
44 Self::Cautious => CAUTIOUS_TOML,
45 Self::Standard => STANDARD_TOML,
46 Self::Permissive => PERMISSIVE_TOML,
47 }
48 }
49
50 const fn label(self) -> &'static str {
51 match self {
52 Self::Cautious => "cautious",
53 Self::Standard => "standard",
54 Self::Permissive => "permissive",
55 }
56 }
57}
58
59pub fn expand_git_config(git: &TomlGit) -> Result<Vec<ConfigDirective>, String> {
70 let mut directives = Vec::new();
71
72 if let Some(style_name) = &git.style {
73 let style = GitStyle::parse(style_name)?;
74 directives.extend(parse_style_rules(style)?);
75 }
76
77 for branch in &git.branches {
78 if branch.pattern.is_empty() {
79 return Err("git.branches entry has empty pattern".into());
80 }
81 let style = GitStyle::parse(&branch.style)?;
82 let rules = parse_style_rules(style)?;
83 directives.extend(add_branch_condition(rules, &branch.pattern));
84 }
85
86 Ok(directives)
87}
88
89fn parse_style_rules(style: GitStyle) -> Result<Vec<ConfigDirective>, String> {
91 let source = style.toml_source();
92 let label = format!("(git-style:{style_name})", style_name = style.label());
93 crate::toml_config::parse_toml_config(source, Path::new(&label))
94 .map_err(|e| format!("error parsing git style {}: {e}", style.label()))
95}
96
97fn add_branch_condition(directives: Vec<ConfigDirective>, pattern: &str) -> Vec<ConfigDirective> {
99 directives
100 .into_iter()
101 .map(|d| match d {
102 ConfigDirective::Rule(mut rule) => {
103 rule.conditions
104 .push(Condition::BranchMatch(pattern.to_string()));
105 ConfigDirective::Rule(rule)
106 }
107 other => other,
108 })
109 .collect()
110}
111
112#[cfg(test)]
113#[allow(clippy::unwrap_used)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn cautious_toml_parses() {
119 let rules = parse_style_rules(GitStyle::Cautious).unwrap();
120 assert!(!rules.is_empty());
121 }
122
123 #[test]
124 fn standard_toml_parses() {
125 let rules = parse_style_rules(GitStyle::Standard).unwrap();
126 assert!(!rules.is_empty());
127 }
128
129 #[test]
130 fn permissive_toml_parses() {
131 let rules = parse_style_rules(GitStyle::Permissive).unwrap();
132 assert!(!rules.is_empty());
133 }
134
135 #[test]
136 fn unknown_style_errors() {
137 assert!(GitStyle::parse("yolo").is_err());
138 }
139
140 #[test]
141 fn expand_default_style_only() {
142 let git = TomlGit {
143 style: Some("standard".into()),
144 branches: vec![],
145 };
146 let directives = expand_git_config(&git).unwrap();
147 assert!(!directives.is_empty());
148 for d in &directives {
150 if let ConfigDirective::Rule(r) = d {
151 assert!(r.conditions.is_empty());
152 }
153 }
154 }
155
156 #[test]
157 fn expand_branch_override_adds_conditions() {
158 let git = TomlGit {
159 style: None,
160 branches: vec![crate::toml_config::TomlGitBranch {
161 pattern: "agent/*".into(),
162 style: "permissive".into(),
163 }],
164 };
165 let directives = expand_git_config(&git).unwrap();
166 assert!(!directives.is_empty());
167 for d in &directives {
168 if let ConfigDirective::Rule(r) = d {
169 assert!(
170 r.conditions
171 .iter()
172 .any(|c| matches!(c, Condition::BranchMatch(p) if p == "agent/*")),
173 "expected BranchMatch condition on rule"
174 );
175 }
176 }
177 }
178
179 #[test]
180 fn expand_default_plus_branch_override() {
181 let git = TomlGit {
182 style: Some("standard".into()),
183 branches: vec![crate::toml_config::TomlGitBranch {
184 pattern: "main".into(),
185 style: "cautious".into(),
186 }],
187 };
188 let directives = expand_git_config(&git).unwrap();
189 let unconditional = directives
191 .iter()
192 .filter(|d| matches!(d, ConfigDirective::Rule(r) if r.conditions.is_empty()))
193 .count();
194 let conditioned = directives
195 .iter()
196 .filter(|d| matches!(d, ConfigDirective::Rule(r) if !r.conditions.is_empty()))
197 .count();
198 assert!(unconditional > 0, "expected unconditional standard rules");
199 assert!(conditioned > 0, "expected conditioned cautious rules");
200 }
201
202 #[test]
203 fn empty_git_section_produces_no_directives() {
204 let git = TomlGit {
205 style: None,
206 branches: vec![],
207 };
208 let directives = expand_git_config(&git).unwrap();
209 assert!(directives.is_empty());
210 }
211
212 #[test]
213 fn empty_branch_pattern_errors() {
214 let git = TomlGit {
215 style: None,
216 branches: vec![crate::toml_config::TomlGitBranch {
217 pattern: String::new(),
218 style: "standard".into(),
219 }],
220 };
221 assert!(expand_git_config(&git).is_err());
222 }
223}