crates_index_diff/index/diff/
github.rs

1use reqwest::{StatusCode, Url};
2
3#[derive(Debug)]
4pub(crate) enum FastPath {
5    UpToDate,
6    NeedsFetch,
7    Indeterminate,
8}
9
10/// extract username & repository from a fetch URL, only if it's on Github.
11fn user_and_repo_from_url_if_github(fetch_url: &gix::Url) -> Option<(String, String)> {
12    let url = Url::parse(&fetch_url.to_string()).ok()?;
13    if !(url.host_str() == Some("github.com")) {
14        return None;
15    }
16
17    // This expects GitHub urls in the form `github.com/user/repo` and nothing
18    // else
19    let mut pieces = url.path_segments()?;
20    let username = pieces.next()?;
21    let repository = pieces.next()?;
22    let repository = repository.strip_suffix(".git").unwrap_or(repository);
23    if pieces.next().is_some() {
24        return None;
25    }
26    Some((username.to_string(), repository.to_string()))
27}
28
29/// use github fast-path to check if the repository has any changes
30/// since the last seen reference.
31///
32/// To save server side resources on github side, we can use an API
33/// to check if there are any changes in the repository before we
34/// actually run `git fetch`.
35///
36/// On non-github fetch URLs we don't do anything and always run the fetch.
37///
38/// Code gotten and adapted from
39/// https://github.com/rust-lang/cargo/blob/edd36eba5e0d6e0cfcb84bd0cc651ba8bf5e7f83/src/cargo/sources/git/utils.rs#L1396
40///
41/// GitHub documentation:
42/// https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#get-a-commit
43/// specifically using `application/vnd.github.sha`
44pub(crate) fn has_changes(
45    fetch_url: &gix::Url,
46    last_seen_reference: &gix::ObjectId,
47    branch_name: &str,
48) -> Result<FastPath, reqwest::Error> {
49    let (username, repository) = match user_and_repo_from_url_if_github(fetch_url) {
50        Some(url) => url,
51        None => return Ok(FastPath::Indeterminate),
52    };
53
54    let url = format!(
55        "https://api.github.com/repos/{}/{}/commits/{}",
56        username, repository, branch_name,
57    );
58
59    let client = reqwest::blocking::Client::builder()
60        .user_agent("crates-index-diff")
61        .build()?;
62    let response = client
63        .get(&url)
64        .header("Accept", "application/vnd.github.sha")
65        .header("If-None-Match", format!("\"{}\"", last_seen_reference))
66        .send()?;
67
68    let status = response.status();
69    if status == StatusCode::NOT_MODIFIED {
70        Ok(FastPath::UpToDate)
71    } else if status.is_success() {
72        Ok(FastPath::NeedsFetch)
73    } else {
74        // Usually response_code == 404 if the repository does not exist, and
75        // response_code == 422 if exists but GitHub is unable to resolve the
76        // requested rev.
77        Ok(FastPath::Indeterminate)
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use std::convert::TryFrom;
85
86    #[test]
87    fn test_github_http_url() {
88        let (user, repo) = user_and_repo_from_url_if_github(
89            &gix::Url::try_from("https://github.com/some_user/some_repo.git").unwrap(),
90        )
91        .unwrap();
92        assert_eq!(user, "some_user");
93        assert_eq!(repo, "some_repo");
94    }
95
96    #[test]
97    fn test_github_ssh_url() {
98        let (user, repo) = user_and_repo_from_url_if_github(
99            &gix::Url::try_from("ssh://git@github.com/some_user/some_repo.git").unwrap(),
100        )
101        .unwrap();
102        assert_eq!(user, "some_user");
103        assert_eq!(repo, "some_repo");
104    }
105
106    #[test]
107    fn test_non_github_url() {
108        assert!(user_and_repo_from_url_if_github(
109            &gix::Url::try_from("https://not_github.com/some_user/some_repo.git").unwrap(),
110        )
111        .is_none());
112    }
113}