cargo_unmaintained/github/
mod.rs1use super::{RepoStatus, Url};
2use anyhow::Result;
3use regex::Regex;
4use std::{sync::LazyLock, time::SystemTime};
5
6#[allow(clippy::unwrap_used)]
7static GITHUB_RE: LazyLock<Regex> = LazyLock::new(|| {
8 Regex::new(r"^https://github\.com/([^/?#]+)/([^/?#]+?)(?:\.git)?(?:[/#?]|$)").unwrap()
9});
10
11pub(crate) fn parse_github_url(url: &str) -> Option<(&str, &str)> {
14 let captures = GITHUB_RE.captures(url)?;
15 #[allow(clippy::unwrap_used)]
16 let owner = captures.get(1).unwrap().as_str();
17 #[allow(clippy::unwrap_used)]
18 let repo = captures.get(2).unwrap().as_str();
19 Some((owner, repo))
20}
21
22pub(crate) struct GithubRepo<'a> {
24 pub url: Url<'a>,
25 pub owner: &'a str,
26 pub repo: &'a str,
27}
28
29impl<'a> GithubRepo<'a> {
30 pub(crate) fn from_url(url: Url<'a>) -> Option<Self> {
31 let (owner, repo) = parse_github_url(url.as_str())?;
32 Some(Self {
33 url: canonical_github_url(url),
34 owner,
35 repo,
36 })
37 }
38}
39
40pub(crate) fn canonical_github_url(url: Url<'_>) -> Url<'_> {
41 let shortened = url.shorten().unwrap_or(url);
42 shortened
43 .as_str()
44 .strip_suffix(".git")
45 .map_or(shortened, Url::from)
46}
47
48pub(crate) trait Github {
49 fn load_token(f: impl FnOnce(&str) -> Result<()>) -> Result<bool>;
50 fn save_token() -> Result<()>;
51 fn archival_status(url: Url) -> Result<RepoStatus<()>>;
52 fn prefetch<'a>(repos: &'a [GithubRepo<'a>]) -> Result<Vec<RepoStatus<'a, SystemTime>>>;
53}
54
55#[cfg(all(feature = "__mock_github", not(feature = "__real_github")))]
59mod mock;
60#[cfg(all(feature = "__mock_github", not(feature = "__real_github")))]
61pub use mock::Impl;
62
63#[cfg(any(not(feature = "__mock_github"), feature = "__real_github"))]
64mod real;
65#[cfg(any(not(feature = "__mock_github"), feature = "__real_github"))]
66pub use real::Impl;
67#[cfg(any(not(feature = "__mock_github"), feature = "__real_github"))]
68pub use real::util;
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 #[test]
75 fn parse_github_url_accepts_common_forms() {
76 assert_eq!(
77 parse_github_url("https://github.com/owner/repo"),
78 Some(("owner", "repo"))
79 );
80 assert_eq!(
81 parse_github_url("https://github.com/owner/repo.git"),
82 Some(("owner", "repo"))
83 );
84 assert_eq!(
85 parse_github_url("https://github.com/owner/repo/"),
86 Some(("owner", "repo"))
87 );
88 }
89
90 #[test]
91 fn parse_github_url_stops_at_query() {
92 assert_eq!(
93 parse_github_url("https://github.com/owner/repo?tab=readme"),
94 Some(("owner", "repo"))
95 );
96 }
97
98 #[test]
99 fn parse_github_url_stops_at_fragment() {
100 assert_eq!(
101 parse_github_url("https://github.com/owner/repo#readme"),
102 Some(("owner", "repo"))
103 );
104 }
105
106 #[test]
107 fn parse_github_url_rejects_incomplete_or_non_github_urls() {
108 assert_eq!(parse_github_url("https://github.com/"), None);
109 assert_eq!(parse_github_url("https://github.com/owner"), None);
110 assert_eq!(parse_github_url("https://example.com/owner/repo"), None);
111 }
112
113 #[test]
114 fn canonical_github_url_normalizes_aliases() {
115 assert_eq!(
116 canonical_github_url("https://github.com/owner/repo.git".into()).as_str(),
117 "https://github.com/owner/repo"
118 );
119 assert_eq!(
120 canonical_github_url("https://github.com/owner/repo/tree/main".into()).as_str(),
121 "https://github.com/owner/repo"
122 );
123 assert_eq!(
124 canonical_github_url("https://github.com/owner/repo.git/tree/main".into()).as_str(),
125 "https://github.com/owner/repo"
126 );
127 }
128
129 #[test]
130 fn github_repo_from_url_returns_parts_and_canonical_url() {
131 let github_repo =
132 GithubRepo::from_url("https://github.com/owner/repo.git/tree/main".into());
133 assert!(github_repo.is_some());
134 let Some(github_repo) = github_repo else {
135 return;
136 };
137
138 assert_eq!(github_repo.url.as_str(), "https://github.com/owner/repo");
139 assert_eq!(github_repo.owner, "owner");
140 assert_eq!(github_repo.repo, "repo");
141 }
142}