use std::borrow::Cow;
pub const SHA_PLACEHOLDER: &str = "<sha?>";
const MIN_SHA_LEN: usize = 7;
const MAX_SHA_LEN: usize = 40;
fn is_word_byte(c: u8) -> bool {
c.is_ascii_alphanumeric() || c == b'_' || c == b'-'
}
pub fn is_sha_like(word: &str) -> bool {
let bytes = word.as_bytes();
(MIN_SHA_LEN..=MAX_SHA_LEN).contains(&bytes.len())
&& bytes.iter().all(u8::is_ascii_hexdigit)
&& bytes.iter().any(u8::is_ascii_digit)
&& bytes.iter().any(u8::is_ascii_alphabetic)
}
pub fn scrub_sha_like(text: &str) -> Cow<'_, str> {
let bytes = text.as_bytes();
let mut out: Option<String> = None;
let mut copied = 0;
let mut i = 0;
while i < bytes.len() {
if !is_word_byte(bytes[i]) {
i += 1;
continue;
}
let start = i;
while i < bytes.len() && is_word_byte(bytes[i]) {
i += 1;
}
if is_sha_like(&text[start..i]) {
let buf = out.get_or_insert_with(|| String::with_capacity(text.len()));
buf.push_str(&text[copied..start]);
buf.push_str(SHA_PLACEHOLDER);
copied = i;
}
}
match out {
Some(mut buf) => {
buf.push_str(&text[copied..]);
Cow::Owned(buf)
}
None => Cow::Borrowed(text),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_token_rule_flags_shas_and_keeps_words_dates_and_ids() {
let cases = [
("PR #12 head d3963848ab.", "PR #12 head <sha?>."),
("compare abc1234..def5678", "compare <sha?>..<sha?>"),
("`0123456789abcdef0123456789abcdef01234567`", "`<sha?>`"),
("see commit/ABC1234F", "see commit/<sha?>"),
("added the facade, deadbeef", "added the facade, deadbeef"),
("xdeadbeef1x and 0x1a2b3c4d5", "xdeadbeef1x and 0x1a2b3c4d5"),
("2026-10-10 21:46 20261010", "2026-10-10 21:46 20261010"),
(
"feat/9390-x #9390 feat/fix-1a2b3c4d",
"feat/9390-x #9390 feat/fix-1a2b3c4d",
),
(
"session 7bd5c27a-475b-41df-9e9f-a6f630801717",
"session 7bd5c27a-475b-41df-9e9f-a6f630801717",
),
("abc123 is six", "abc123 is six"),
("é d3963848ab é", "é <sha?> é"),
];
for (input, want) in cases {
assert_eq!(scrub_sha_like(input), want, "input {input:?}");
}
let long = "a1".repeat(20) + "a";
assert_eq!(scrub_sha_like(&long), long);
assert!(matches!(scrub_sha_like("no ids here"), Cow::Borrowed(_)));
}
}