1use super::model::*;
8use regex::Regex;
9
10pub fn find_branch_config<'a>(
13 config: &'a GitVersionConfiguration,
14 branch_name: &str,
15) -> Option<(String, &'a BranchConfiguration)> {
16 let short = branch_name.rsplit('/').next().unwrap_or(branch_name);
17 let mut unknown: Option<(String, &BranchConfiguration)> = None;
18 for (key, bc) in &config.branches {
19 let Some(re_src) = &bc.regex else { continue };
20 if re_src.is_empty() {
21 continue;
22 }
23 let Ok(re) = Regex::new(&format!("(?i){re_src}")) else {
24 continue;
25 };
26 if re.is_match(branch_name) || re.is_match(short) {
27 if key == "unknown" {
28 unknown = Some((key.clone(), bc));
29 } else {
30 return Some((key.clone(), bc));
31 }
32 }
33 }
34 unknown
35}
36
37fn normalize_next_version(value: &str) -> String {
40 match value.trim().parse::<i64>() {
41 Ok(major) => format!("{major}.0"),
42 Err(_) => value.to_string(),
43 }
44}
45
46fn resolve_label(label: &str, regex_src: &Option<String>, branch_name: &str) -> String {
55 let sanitize = |s: &str| {
56 Regex::new(r"[^a-zA-Z0-9-]")
57 .unwrap()
58 .replace_all(s, "-")
59 .into_owned()
60 };
61
62 let mut captures: std::collections::HashMap<String, String> = std::collections::HashMap::new();
64 if let Some(src) = regex_src {
65 if !src.trim().is_empty() && !branch_name.is_empty() {
66 if let Ok(re) = Regex::new(&format!("(?i){src}")) {
67 if let Some(caps) = re.captures(branch_name) {
68 for name in re.capture_names().flatten() {
69 if let Some(m) = caps.name(name) {
70 captures.insert(name.to_string(), sanitize(m.as_str()));
71 }
72 }
73 }
74 }
75 }
76 }
77
78 let token_re = Regex::new(r"\{([^}]+)\}").unwrap();
79 token_re
80 .replace_all(label, |c: ®ex::Captures| {
81 let whole = c[0].to_string();
82 let inner = c[1].trim();
83 let (expr, fallback) = match inner.split_once("??") {
85 Some((l, r)) => (l.trim(), Some(r.trim().trim_matches('"').to_string())),
86 None => (inner, None),
87 };
88 let name = expr.split(':').next().unwrap_or(expr).trim();
90 let resolved = if let Some(var) = expr.strip_prefix("env:") {
91 let var = var.split("??").next().unwrap_or(var).trim();
92 std::env::var(var).ok().filter(|v| !v.is_empty())
93 } else {
94 captures.get(name).cloned()
95 };
96 resolved.or(fallback).unwrap_or(whole)
98 })
99 .into_owned()
100}
101
102fn inherit_label(
107 config: &GitVersionConfiguration,
108 bc: &BranchConfiguration,
109 depth: usize,
110) -> Option<String> {
111 if let Some(l) = &bc.label {
112 return Some(l.clone());
113 }
114 if depth > 8 {
115 return None;
116 }
117 for src in &bc.source_branches {
118 if let Some(src_bc) = config.branches.get(src) {
119 if let Some(l) = inherit_label(config, src_bc, depth + 1) {
120 return Some(l);
121 }
122 }
123 }
124 None
125}
126
127pub(crate) fn resolve_increment(
129 config: &GitVersionConfiguration,
130 bc: &BranchConfiguration,
131 depth: usize,
132) -> IncrementStrategy {
133 let own = bc
134 .increment
135 .or(config.increment)
136 .unwrap_or(IncrementStrategy::Inherit);
137 if own != IncrementStrategy::Inherit {
138 return own;
139 }
140 if depth > 8 {
145 return IncrementStrategy::None;
146 }
147 for src in &bc.source_branches {
148 if let Some(src_bc) = config.branches.get(src) {
149 let resolved = resolve_increment(config, src_bc, depth + 1);
150 if resolved != IncrementStrategy::Inherit {
151 return resolved;
152 }
153 }
154 }
155 IncrementStrategy::None
156}
157
158#[derive(Debug, Clone)]
160pub struct EffectiveConfiguration {
161 pub branch_key: String,
162 pub deployment_mode: DeploymentMode,
163 pub label: String,
164 pub increment: IncrementStrategy,
165 pub regex: Option<String>,
166 pub prevent_increment_of_merged_branch: bool,
167 pub prevent_increment_when_branch_merged: bool,
168 pub prevent_increment_when_current_commit_tagged: bool,
169 pub track_merge_target: bool,
170 pub track_merge_message: bool,
171 pub tracks_release_branches: bool,
172 pub is_release_branch: bool,
173 pub is_main_branch: bool,
174 pub pre_release_weight: i64,
175 pub tag_pre_release_weight: i64,
176 pub commit_message_incrementing: CommitMessageIncrementMode,
177 pub major_bump_message: String,
178 pub minor_bump_message: String,
179 pub patch_bump_message: String,
180 pub no_bump_message: String,
181 pub tag_prefix: String,
182 pub version_in_branch_pattern: String,
183 pub next_version: Option<String>,
184 pub semantic_version_format: SemanticVersionFormat,
185 pub commit_date_format: String,
186 pub assembly_versioning_scheme: VersioningScheme,
187 pub assembly_file_versioning_scheme: VersioningScheme,
188 pub assembly_informational_format: String,
189 pub assembly_versioning_format: Option<String>,
190 pub assembly_file_versioning_format: Option<String>,
191 pub merge_message_formats: std::collections::BTreeMap<String, String>,
192 pub source_branches: Vec<String>,
193 pub label_number_pattern: String,
195}
196
197impl EffectiveConfiguration {
198 pub fn resolve(config: &GitVersionConfiguration, branch_name: &str) -> Self {
200 let matched = find_branch_config(config, branch_name);
201 let (branch_key, bc): (String, BranchConfiguration) = match matched {
202 Some((k, b)) => (k, b.clone()),
203 None => ("unknown".into(), BranchConfiguration::default()),
204 };
205
206 let pi_branch = bc.prevent_increment.clone().unwrap_or_default();
208 let pi_global = config.prevent_increment.clone().unwrap_or_default();
209 let coalesce_bool = |b: Option<bool>, g: Option<bool>| b.or(g).unwrap_or(false);
210
211 let raw_label = inherit_label(config, &bc, 0)
212 .or_else(|| config.label.clone())
213 .unwrap_or_default();
214 let label = resolve_label(&raw_label, &bc.regex, branch_name);
215
216 EffectiveConfiguration {
217 deployment_mode: bc
218 .mode
219 .or(config.mode)
220 .unwrap_or(DeploymentMode::ContinuousDelivery),
221 label,
222 increment: resolve_increment(config, &bc, 0),
223 regex: bc.regex.clone(),
224 prevent_increment_of_merged_branch: coalesce_bool(
225 pi_branch.of_merged_branch,
226 pi_global.of_merged_branch,
227 ),
228 prevent_increment_when_branch_merged: coalesce_bool(
229 pi_branch.when_branch_merged,
230 pi_global.when_branch_merged,
231 ),
232 prevent_increment_when_current_commit_tagged: pi_branch
233 .when_current_commit_tagged
234 .or(pi_global.when_current_commit_tagged)
235 .unwrap_or(true),
236 track_merge_target: coalesce_bool(bc.track_merge_target, config.track_merge_target),
237 track_merge_message: bc
238 .track_merge_message
239 .or(config.track_merge_message)
240 .unwrap_or(true),
241 tracks_release_branches: coalesce_bool(
242 bc.tracks_release_branches,
243 config.tracks_release_branches,
244 ),
245 is_release_branch: coalesce_bool(bc.is_release_branch, config.is_release_branch),
246 is_main_branch: coalesce_bool(bc.is_main_branch, config.is_main_branch),
247 pre_release_weight: bc
248 .pre_release_weight
249 .or(config.pre_release_weight)
250 .unwrap_or(0),
251 tag_pre_release_weight: config.tag_pre_release_weight.unwrap_or(60000),
252 commit_message_incrementing: bc
253 .commit_message_incrementing
254 .or(config.commit_message_incrementing)
255 .unwrap_or(CommitMessageIncrementMode::Enabled),
256 major_bump_message: config
257 .major_version_bump_message
258 .clone()
259 .unwrap_or_else(|| r"\+semver:\s?(breaking|major)".into()),
260 minor_bump_message: config
261 .minor_version_bump_message
262 .clone()
263 .unwrap_or_else(|| r"\+semver:\s?(feature|minor)".into()),
264 patch_bump_message: config
265 .patch_version_bump_message
266 .clone()
267 .unwrap_or_else(|| r"\+semver:\s?(fix|patch)".into()),
268 no_bump_message: config
269 .no_bump_message
270 .clone()
271 .unwrap_or_else(|| r"\+semver:\s?(none|skip)".into()),
272 tag_prefix: config.tag_prefix.clone().unwrap_or_else(|| "[vV]?".into()),
273 version_in_branch_pattern: config
274 .version_in_branch_pattern
275 .clone()
276 .unwrap_or_else(|| r"(?<version>[vV]?\d+(\.\d+)?(\.\d+)?).*".into()),
277 next_version: config.next_version.as_deref().map(normalize_next_version),
278 semantic_version_format: config
279 .semantic_version_format
280 .unwrap_or(SemanticVersionFormat::Strict),
281 commit_date_format: config
282 .commit_date_format
283 .clone()
284 .unwrap_or_else(|| "yyyy-MM-dd".into()),
285 assembly_versioning_scheme: config
286 .assembly_versioning_scheme
287 .unwrap_or(VersioningScheme::MajorMinorPatch),
288 assembly_file_versioning_scheme: config
289 .assembly_file_versioning_scheme
290 .unwrap_or(VersioningScheme::MajorMinorPatch),
291 assembly_informational_format: config
292 .assembly_informational_format
293 .clone()
294 .unwrap_or_else(|| "{InformationalVersion}".into()),
295 assembly_versioning_format: config.assembly_versioning_format.clone(),
296 assembly_file_versioning_format: config.assembly_file_versioning_format.clone(),
297 merge_message_formats: config.merge_message_formats.clone(),
298 source_branches: bc.source_branches.clone(),
299 label_number_pattern: bc
300 .label_number_pattern
301 .clone()
302 .or_else(|| config.label_number_pattern.clone())
303 .unwrap_or_else(|| r"(?<name>.*?)\.?(?<number>\d+)?$".into()),
304 branch_key,
305 }
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312 use crate::config::defaults;
313
314 #[test]
315 fn find_branch_config_main_matches() {
316 let cfg = defaults::gitflow();
317 let (key, _) = find_branch_config(&cfg, "main").unwrap();
318 assert_eq!(key, "main");
319 }
320
321 #[test]
322 fn find_branch_config_feature_matches() {
323 let cfg = defaults::gitflow();
324 let (key, _) = find_branch_config(&cfg, "feature/foo").unwrap();
325 assert_eq!(key, "feature");
326 }
327
328 #[test]
329 fn find_branch_config_no_match_returns_unknown() {
330 let cfg = defaults::gitflow();
331 let result = find_branch_config(&cfg, "totally-unknown-xyz-branch");
333 if let Some((key, _)) = result {
335 assert_eq!(key, "unknown");
336 }
337 }
338
339 #[test]
340 fn find_branch_config_short_name_matching() {
341 let cfg = defaults::gitflow();
342 let result = find_branch_config(&cfg, "refs/heads/develop");
344 assert!(result.is_some());
345 let (key, _) = result.unwrap();
346 assert_eq!(key, "develop");
347 }
348
349 #[test]
350 fn normalize_next_version_pads_integer() {
351 assert_eq!(normalize_next_version("1"), "1.0");
353 assert_eq!(normalize_next_version("2"), "2.0");
354 assert_eq!(normalize_next_version("1.0"), "1.0");
355 assert_eq!(normalize_next_version("1.2.3"), "1.2.3");
356 assert_eq!(normalize_next_version("1.0.0-beta"), "1.0.0-beta");
357 }
358
359 #[test]
360 fn resolve_label_branch_name_capture() {
361 let cfg = defaults::gitflow();
362 let eff = EffectiveConfiguration::resolve(&cfg, "feature/my-feat");
364 assert_eq!(eff.label, "my-feat");
365 }
366
367 #[test]
368 fn resolve_label_unmatched_token_stays_literal() {
369 let r = resolve_label("{BranchName}", &Some("^custom/".into()), "custom/x");
372 assert_eq!(r, "{BranchName}");
373 let r = resolve_label(
375 "{BranchName}",
376 &Some(r"^features?[/-](?<BranchName>.+)".into()),
377 "feature/a_b",
378 );
379 assert_eq!(r, "a-b");
380 }
381
382 #[test]
383 fn resolve_label_slash_dot_sanitized() {
384 let cfg = defaults::gitflow();
385 let eff = EffectiveConfiguration::resolve(&cfg, "feature/my.feature");
387 assert_eq!(eff.label, "my-feature");
388 }
389
390 #[test]
391 fn resolve_increment_inherit_falls_back_to_patch() {
392 let cfg = defaults::gitflow();
393 let eff = EffectiveConfiguration::resolve(&cfg, "develop");
395 assert_eq!(eff.increment, crate::config::IncrementStrategy::Minor);
396 }
397
398 #[test]
399 fn resolve_sets_is_main_branch_for_main() {
400 let cfg = defaults::gitflow();
401 let eff = EffectiveConfiguration::resolve(&cfg, "main");
402 assert!(eff.is_main_branch);
403 }
404
405 #[test]
406 fn resolve_sets_is_release_branch_for_release() {
407 let cfg = defaults::gitflow();
408 let eff = EffectiveConfiguration::resolve(&cfg, "release/1.0.0");
409 assert!(eff.is_release_branch);
410 }
411
412 #[test]
413 fn resolve_hotfix_inherits_patch() {
414 let cfg = defaults::gitflow();
415 let eff = EffectiveConfiguration::resolve(&cfg, "hotfix/1.0.1");
416 assert_eq!(eff.increment, crate::config::IncrementStrategy::Patch);
417 }
418}