1use tracing::debug;
2use url::Url;
3
4#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6pub struct GitHubRepository<'a> {
7 pub owner: &'a str,
10 pub repo: &'a str,
12}
13
14impl<'a> GitHubRepository<'a> {
15 pub fn parse(url: &'a Url) -> Option<Self> {
20 if url.host_str() != Some("github.com") {
22 return None;
23 }
24
25 let Some(mut segments) = url.path_segments() else {
28 debug!("GitHub URL is missing path segments: {url}");
29 return None;
30 };
31 let Some(owner) = segments.next() else {
32 debug!("GitHub URL is missing owner: {url}");
33 return None;
34 };
35 let Some(repo) = segments.next() else {
36 debug!("GitHub URL is missing repo: {url}");
37 return None;
38 };
39 if segments.next().is_some() {
40 debug!("GitHub URL has too many path segments: {url}");
41 return None;
42 }
43
44 let repo = repo.strip_suffix(".git").unwrap_or(repo);
46
47 Some(Self { owner, repo })
48 }
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54
55 #[test]
56 fn test_parse_valid_url() {
57 let url = Url::parse("https://github.com/astral-sh/uv").unwrap();
58 let repo = GitHubRepository::parse(&url).unwrap();
59 assert_eq!(repo.owner, "astral-sh");
60 assert_eq!(repo.repo, "uv");
61 }
62
63 #[test]
64 fn test_parse_with_git_suffix() {
65 let url = Url::parse("https://github.com/astral-sh/uv.git").unwrap();
66 let repo = GitHubRepository::parse(&url).unwrap();
67 assert_eq!(repo.owner, "astral-sh");
68 assert_eq!(repo.repo, "uv");
69 }
70
71 #[test]
72 fn test_parse_invalid_host() {
73 let url = Url::parse("https://gitlab.com/astral-sh/uv").unwrap();
74 assert!(GitHubRepository::parse(&url).is_none());
75 }
76
77 #[test]
78 fn test_parse_invalid_path() {
79 let url = Url::parse("https://github.com/astral-sh").unwrap();
80 assert!(GitHubRepository::parse(&url).is_none());
81
82 let url = Url::parse("https://github.com/astral-sh/uv/extra").unwrap();
83 assert!(GitHubRepository::parse(&url).is_none());
84 }
85}