pub const MAX_LABELS: usize = 64;
pub const MAX_LEN: usize = 63;
pub fn parse(kv: &str) -> Result<(String, String), String> {
let (k, v) = kv
.split_once('=')
.ok_or_else(|| format!("--label {kv}: give KEY=VALUE"))?;
if !key_ok(k) {
return Err(format!(
"--label {kv}: `{k}` is lower-case letters, digits, `_`, `-`, at most {MAX_LEN} characters"
));
}
if !value_ok(v) {
return Err(format!(
"--label {kv}: `{v}` is lower-case letters, digits, `_`, `-`, at most {MAX_LEN} characters"
));
}
Ok((k.to_string(), v.to_string()))
}
pub fn collect(
pairs: impl IntoIterator<Item = (String, String)>,
) -> Result<Vec<(String, String)>, String> {
let mut out: Vec<(String, String)> = Vec::new();
for (k, v) in pairs {
if k == "purpose" {
return Err("--label purpose: the platform sets this label on a build machine".into());
}
if out.iter().any(|(seen, _)| *seen == k) {
return Err(format!("--label {k}=...: given more than once"));
}
out.push((k, v));
}
if out.len() > MAX_LABELS - 1 {
return Err(format!("--label: at most {} labels", MAX_LABELS - 1));
}
out.sort();
Ok(out)
}
fn key_ok(s: &str) -> bool {
!s.is_empty() && chars_ok(s)
}
fn value_ok(s: &str) -> bool {
chars_ok(s)
}
fn chars_ok(s: &str) -> bool {
s.len() <= MAX_LEN
&& s.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_' || b == b'-')
}
pub fn slug(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
let c = c.to_ascii_lowercase();
if c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'_' as char || c == b'-' as char {
out.push(c);
} else {
out.push('-');
}
}
let out = out.trim_matches('-');
out.chars()
.take(MAX_LEN)
.collect::<String>()
.trim_end_matches('-')
.to_string()
}
pub fn repo(root: &std::path::Path) -> Option<String> {
let out = std::process::Command::new("git")
.arg("-C")
.arg(root)
.args(["remote", "get-url", "origin"])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let url = String::from_utf8(out.stdout).ok()?.trim().to_string();
let tail = url.rsplit(['/', ':']).next()?.trim_end_matches(".git");
let name = slug(tail);
(!name.is_empty()).then_some(name)
}
pub fn defaults(root: &std::path::Path) -> Vec<(String, String)> {
let mut out = Vec::new();
if let Some(name) = root.file_name().and_then(|n| n.to_str()) {
let name = slug(name);
if !name.is_empty() {
out.push(("worktree".to_string(), name));
}
}
if let Some(repo) = repo(root) {
out.push(("source".to_string(), repo));
}
out
}
pub fn merge(
defaults: Vec<(String, String)>,
caller: Vec<(String, String)>,
) -> Vec<(String, String)> {
let mut out = defaults;
for (k, v) in caller {
match out.iter_mut().find(|(seen, _)| *seen == k) {
Some(slot) => slot.1 = v,
None => out.push((k, v)),
}
}
out.retain(|(_, v)| !v.is_empty());
out.sort();
out.truncate(MAX_LABELS - 1);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_label_is_lower_case_and_within_the_lease_rules() {
assert_eq!(
parse("worktree=w-22292").unwrap(),
("worktree".into(), "w-22292".into())
);
assert_eq!(parse("fresh=").unwrap(), ("fresh".into(), String::new()));
for bad in [
"novalue",
"=v",
"Key=v",
"k=V",
"k=a b",
&format!("{}=v", "k".repeat(64)),
&format!("k={}", "v".repeat(64)),
] {
assert!(parse(bad).is_err(), "{bad:?}");
}
}
#[test]
fn labels_are_sorted_and_unique_and_never_rebind_purpose() {
let v = collect([
("worktree".to_string(), "w".to_string()),
("source".to_string(), "s".to_string()),
])
.unwrap();
assert_eq!(v[0].0, "source");
assert_eq!(v[1].0, "worktree");
assert!(collect([
("worktree".to_string(), "a".to_string()),
("worktree".to_string(), "b".to_string()),
])
.is_err());
assert!(collect([("purpose".to_string(), "other".to_string())]).is_err());
let many: Vec<_> = (0..MAX_LABELS)
.map(|i| (format!("k{i}"), "v".to_string()))
.collect();
assert!(collect(many).is_err());
}
#[test]
fn a_slug_is_a_valid_label_value() {
assert_eq!(slug("Build-Reservation-Demand"), "build-reservation-demand");
assert_eq!(slug("SylphxAI/cloud"), "sylphxai-cloud");
let long = slug(&"a".repeat(80));
assert_eq!(long.len(), MAX_LEN);
assert!(!long.ends_with('-'));
assert_eq!(slug("☃☃"), "");
}
#[test]
fn defaults_name_the_worktree_without_a_caller_opt_in() {
let root = std::path::Path::new("/scratch/wt/build-reservation-demand");
let d = defaults(root);
assert!(d.contains(&(
"worktree".to_string(),
"build-reservation-demand".to_string()
)));
}
#[test]
fn the_callers_label_wins_over_a_default_of_the_same_name() {
let m = merge(
vec![("worktree".to_string(), "from-git".to_string())],
vec![("worktree".to_string(), "from-caller".to_string())],
);
assert_eq!(m, vec![("worktree".to_string(), "from-caller".to_string())]);
let m = merge(vec![], vec![("fresh".to_string(), String::new())]);
assert!(m.is_empty());
}
}