pub enum UsesRef {
Local,
DockerImage,
Repository {
owner_repo: String,
git_ref: Option<RefKind>,
},
}
pub enum RefKind {
CommitSha(String),
Mutable(String),
}
pub fn parse(value: &str) -> UsesRef {
if value.starts_with("./") || value.starts_with(".\\") {
return UsesRef::Local;
}
if value.starts_with("docker://") {
return UsesRef::DockerImage;
}
match value.split_once('@') {
None => UsesRef::Repository {
owner_repo: value.to_string(),
git_ref: None,
},
Some((path, r)) => {
let kind = if is_commit_sha(r) {
RefKind::CommitSha(r.to_string())
} else {
RefKind::Mutable(r.to_string())
};
UsesRef::Repository {
owner_repo: path.to_string(),
git_ref: Some(kind),
}
}
}
}
fn is_commit_sha(s: &str) -> bool {
s.len() == 40 && s.chars().all(|c| c.is_ascii_hexdigit())
}
pub fn repo_root(owner_repo: &str) -> &str {
match owner_repo.match_indices('/').nth(1) {
Some((idx, _)) => &owner_repo[..idx],
None => owner_repo,
}
}
#[cfg(test)]
mod tests {
use super::{RefKind, UsesRef, parse};
#[test]
fn classifies_local_and_docker() {
assert!(matches!(parse("./.github/actions/x"), UsesRef::Local));
assert!(matches!(
parse("docker://alpine:3.19"),
UsesRef::DockerImage
));
}
#[test]
fn full_sha_is_immutable() {
let r = parse("owner/repo@0123456789abcdef0123456789abcdef01234567");
assert!(matches!(
r,
UsesRef::Repository {
git_ref: Some(RefKind::CommitSha(_)),
..
}
));
}
#[test]
fn tag_branch_and_short_sha_are_mutable() {
for v in ["owner/repo@v4", "owner/repo@main", "owner/repo@abc1234"] {
assert!(matches!(
parse(v),
UsesRef::Repository {
git_ref: Some(RefKind::Mutable(_)),
..
}
));
}
}
#[test]
fn missing_ref_is_detected() {
assert!(matches!(
parse("owner/repo"),
UsesRef::Repository { git_ref: None, .. }
));
}
}