pub fn slugify(branch: &str) -> String {
let mut out = String::with_capacity(branch.len());
let mut prev_dash = false;
for ch in branch.chars() {
if ch.is_ascii_alphanumeric() || ch == '.' {
out.push(ch);
prev_dash = false;
} else if !prev_dash {
out.push('-');
prev_dash = true;
}
}
out.trim_matches('-').to_string()
}
pub fn slugify_with_fallback(branch: &str, fallback: &str) -> String {
let slug = slugify(branch);
if slug.is_empty() {
fallback.to_string()
} else {
slug
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn replaces_slashes_and_backslashes() {
assert_eq!(slugify("feature/login"), "feature-login");
assert_eq!(slugify("a\\b"), "a-b");
assert_eq!(slugify("a/b/c"), "a-b-c");
}
#[test]
fn replaces_disallowed_runs_with_single_dash() {
assert_eq!(slugify("feat@#login"), "feat-login");
assert_eq!(slugify("hello world"), "hello-world");
assert_eq!(slugify("a b"), "a-b");
}
#[test]
fn keeps_dots_and_digits_and_case() {
assert_eq!(slugify("v1.2.3"), "v1.2.3");
assert_eq!(slugify("Feature-XYZ"), "Feature-XYZ");
}
#[test]
fn collapses_consecutive_dashes() {
assert_eq!(slugify("a--b"), "a-b");
assert_eq!(slugify("a//b"), "a-b");
assert_eq!(slugify("a-/-b"), "a-b");
}
#[test]
fn strips_leading_and_trailing_dashes() {
assert_eq!(slugify("/feature/"), "feature");
assert_eq!(slugify("---x---"), "x");
assert_eq!(slugify("@@@edge@@@"), "edge");
}
#[test]
fn non_ascii_becomes_dashes() {
assert_eq!(slugify("café"), "caf");
assert_eq!(slugify("中文branch"), "branch");
}
#[test]
fn empty_result_uses_fallback() {
assert_eq!(slugify(""), "");
assert_eq!(slugify("///"), "");
assert_eq!(slugify("@@@"), "");
assert_eq!(slugify_with_fallback("///", "abc1234"), "abc1234");
assert_eq!(slugify_with_fallback("feature/x", "abc1234"), "feature-x");
}
}