1pub enum UsesRef {
8 Local,
10 DockerImage,
12 Repository {
14 owner_repo: String,
15 git_ref: Option<RefKind>,
16 },
17}
18
19pub enum RefKind {
21 CommitSha(String),
23 Mutable(String),
25}
26
27pub fn parse(value: &str) -> UsesRef {
29 if value.starts_with("./") || value.starts_with(".\\") {
30 return UsesRef::Local;
31 }
32 if value.starts_with("docker://") {
33 return UsesRef::DockerImage;
34 }
35 match value.split_once('@') {
36 None => UsesRef::Repository {
37 owner_repo: value.to_string(),
38 git_ref: None,
39 },
40 Some((path, r)) => {
41 let kind = if is_commit_sha(r) {
42 RefKind::CommitSha(r.to_string())
43 } else {
44 RefKind::Mutable(r.to_string())
45 };
46 UsesRef::Repository {
47 owner_repo: path.to_string(),
48 git_ref: Some(kind),
49 }
50 }
51 }
52}
53
54fn is_commit_sha(s: &str) -> bool {
55 s.len() == 40 && s.chars().all(|c| c.is_ascii_hexdigit())
56}
57
58pub fn repo_root(owner_repo: &str) -> &str {
60 match owner_repo.match_indices('/').nth(1) {
61 Some((idx, _)) => &owner_repo[..idx],
62 None => owner_repo,
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::{RefKind, UsesRef, parse};
69
70 #[test]
71 fn classifies_local_and_docker() {
72 assert!(matches!(parse("./.github/actions/x"), UsesRef::Local));
73 assert!(matches!(
74 parse("docker://alpine:3.19"),
75 UsesRef::DockerImage
76 ));
77 }
78
79 #[test]
80 fn full_sha_is_immutable() {
81 let r = parse("owner/repo@0123456789abcdef0123456789abcdef01234567");
82 assert!(matches!(
83 r,
84 UsesRef::Repository {
85 git_ref: Some(RefKind::CommitSha(_)),
86 ..
87 }
88 ));
89 }
90
91 #[test]
92 fn tag_branch_and_short_sha_are_mutable() {
93 for v in ["owner/repo@v4", "owner/repo@main", "owner/repo@abc1234"] {
94 assert!(matches!(
95 parse(v),
96 UsesRef::Repository {
97 git_ref: Some(RefKind::Mutable(_)),
98 ..
99 }
100 ));
101 }
102 }
103
104 #[test]
105 fn missing_ref_is_detected() {
106 assert!(matches!(
107 parse("owner/repo"),
108 UsesRef::Repository { git_ref: None, .. }
109 ));
110 }
111}