1pub fn slugify(branch: &str) -> String {
15 let mut out = String::with_capacity(branch.len());
16 let mut prev_dash = false;
17 for ch in branch.chars() {
18 if ch.is_ascii_alphanumeric() || ch == '.' {
19 out.push(ch);
21 prev_dash = false;
22 } else if !prev_dash {
23 out.push('-');
26 prev_dash = true;
27 }
28 }
29 out.trim_matches('-').to_string()
31}
32
33pub fn slugify_with_fallback(branch: &str, fallback: &str) -> String {
36 let slug = slugify(branch);
37 if slug.is_empty() {
38 fallback.to_string()
39 } else {
40 slug
41 }
42}
43
44#[cfg(test)]
45mod tests {
46 use super::*;
47
48 #[test]
49 fn replaces_slashes_and_backslashes() {
50 assert_eq!(slugify("feature/login"), "feature-login");
51 assert_eq!(slugify("a\\b"), "a-b");
52 assert_eq!(slugify("a/b/c"), "a-b-c");
53 }
54
55 #[test]
56 fn replaces_disallowed_runs_with_single_dash() {
57 assert_eq!(slugify("feat@#login"), "feat-login");
58 assert_eq!(slugify("hello world"), "hello-world");
59 assert_eq!(slugify("a b"), "a-b");
60 }
61
62 #[test]
63 fn keeps_dots_and_digits_and_case() {
64 assert_eq!(slugify("v1.2.3"), "v1.2.3");
65 assert_eq!(slugify("Feature-XYZ"), "Feature-XYZ");
66 }
67
68 #[test]
69 fn collapses_consecutive_dashes() {
70 assert_eq!(slugify("a--b"), "a-b");
71 assert_eq!(slugify("a//b"), "a-b");
72 assert_eq!(slugify("a-/-b"), "a-b");
73 }
74
75 #[test]
76 fn strips_leading_and_trailing_dashes() {
77 assert_eq!(slugify("/feature/"), "feature");
78 assert_eq!(slugify("---x---"), "x");
79 assert_eq!(slugify("@@@edge@@@"), "edge");
80 }
81
82 #[test]
83 fn non_ascii_becomes_dashes() {
84 assert_eq!(slugify("café"), "caf");
85 assert_eq!(slugify("中文branch"), "branch");
86 }
87
88 #[test]
89 fn empty_result_uses_fallback() {
90 assert_eq!(slugify(""), "");
91 assert_eq!(slugify("///"), "");
92 assert_eq!(slugify("@@@"), "");
93 assert_eq!(slugify_with_fallback("///", "abc1234"), "abc1234");
94 assert_eq!(slugify_with_fallback("feature/x", "abc1234"), "feature-x");
95 }
96}