uv_git_types/
github.rs

1use tracing::debug;
2use url::Url;
3
4/// A reference to a repository on GitHub.
5#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6pub struct GitHubRepository<'a> {
7    /// The `owner` field for the repository, i.e., the user or organization that owns the
8    /// repository, like `astral-sh`.
9    pub owner: &'a str,
10    /// The `repo` field for the repository, i.e., the name of the repository, like `uv`.
11    pub repo: &'a str,
12}
13
14impl<'a> GitHubRepository<'a> {
15    /// Parse a GitHub repository from a URL.
16    ///
17    /// Expects to receive a URL of the form: `https://github.com/{user}/{repo}[.git]`, e.g.,
18    /// `https://github.com/astral-sh/uv`. Otherwise, returns `None`.
19    pub fn parse(url: &'a Url) -> Option<Self> {
20        // The fast path is only available for GitHub repositories.
21        if url.host_str() != Some("github.com") {
22            return None;
23        }
24
25        // The GitHub URL must take the form: `https://github.com/{user}/{repo}`, e.g.,
26        // `https://github.com/astral-sh/uv`.
27        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        // Trim off the `.git` from the repository, if present.
45        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}