#[must_use]
pub fn extract_coauthors(message: &str) -> Vec<String> {
extract_by_key(message, "co-authored-by:")
}
#[must_use]
pub fn extract_reviewers(message: &str) -> Vec<String> {
extract_by_key(message, "reviewed-by:")
}
fn extract_by_key(message: &str, key_lower: &str) -> Vec<String> {
let mut out = Vec::new();
for line in message.lines() {
let trimmed = line.trim();
let lower = trimmed.to_lowercase();
if let Some(rest) = lower.strip_prefix(key_lower) {
let body = rest.trim();
if let (Some(lt), Some(gt)) = (body.find('<'), body.find('>'))
&& lt < gt
{
let email = body[(lt + 1)..gt].trim();
if !email.is_empty() {
out.push(email.to_string());
continue;
}
}
if !body.is_empty() {
out.push(body.to_string());
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extracts_single_coauthor() {
let msg = "feat: thing\n\nCo-authored-by: Bob <bob@example.com>";
assert_eq!(extract_coauthors(msg), vec!["bob@example.com"]);
}
#[test]
fn extracts_multiple_coauthors() {
let msg = "feat: thing\n\nCo-Authored-By: Alice <alice@example.com>\nCo-authored-by: Carol <carol@example.com>";
assert_eq!(
extract_coauthors(msg),
vec!["alice@example.com", "carol@example.com"]
);
}
#[test]
fn no_coauthors_returns_empty() {
assert!(extract_coauthors("feat: just one author").is_empty());
}
#[test]
fn malformed_trailer_falls_through_to_body() {
let msg = "feat: x\n\nCo-authored-by: no-email-here";
assert_eq!(extract_coauthors(msg), vec!["no-email-here"]);
}
#[test]
fn extracts_reviewed_by() {
let msg = "fix: bug\n\nReviewed-By: Dave <dave@example.com>";
assert_eq!(extract_reviewers(msg), vec!["dave@example.com"]);
}
#[test]
fn reviewed_by_case_insensitive() {
let msg = "fix: bug\n\nreviewed-by: Eve <eve@example.com>";
assert_eq!(extract_reviewers(msg), vec!["eve@example.com"]);
}
}