mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-01 12:24:15 +00:00
Add a GitHub repository struct to uv-git (#10768)
## Summary This is useful for https://github.com/astral-sh/uv/pull/10765, but we already have one usage today, so carving it out into a standalone PR.
This commit is contained in:
parent
b40fa912e0
commit
9e6e1e56fd
3 changed files with 106 additions and 37 deletions
|
|
@ -7,18 +7,19 @@ use std::path::{Path, PathBuf};
|
||||||
use std::str::{self, FromStr};
|
use std::str::{self, FromStr};
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use crate::sha::GitOid;
|
use anyhow::{Context, Result};
|
||||||
use crate::GitSha;
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
|
||||||
use cargo_util::{paths, ProcessBuilder};
|
use cargo_util::{paths, ProcessBuilder};
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use reqwest_middleware::ClientWithMiddleware;
|
use reqwest_middleware::ClientWithMiddleware;
|
||||||
|
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_static::EnvVars;
|
use uv_static::EnvVars;
|
||||||
|
|
||||||
|
use crate::sha::GitOid;
|
||||||
|
use crate::{GitHubRepository, GitSha};
|
||||||
|
|
||||||
/// A file indicates that if present, `git reset` has been done and a repo
|
/// A file indicates that if present, `git reset` has been done and a repo
|
||||||
/// checkout is ready to go. See [`GitCheckout::reset`] for why we need this.
|
/// checkout is ready to go. See [`GitCheckout::reset`] for why we need this.
|
||||||
const CHECKOUT_READY_LOCK: &str = ".ok";
|
const CHECKOUT_READY_LOCK: &str = ".ok";
|
||||||
|
|
@ -720,16 +721,17 @@ enum FastPathRev {
|
||||||
///
|
///
|
||||||
/// [^1]: <https://developer.github.com/v3/repos/commits/#get-the-sha-1-of-a-commit-reference>
|
/// [^1]: <https://developer.github.com/v3/repos/commits/#get-the-sha-1-of-a-commit-reference>
|
||||||
fn github_fast_path(
|
fn github_fast_path(
|
||||||
repo: &mut GitRepository,
|
git: &mut GitRepository,
|
||||||
url: &Url,
|
url: &Url,
|
||||||
reference: &GitReference,
|
reference: &GitReference,
|
||||||
client: &ClientWithMiddleware,
|
client: &ClientWithMiddleware,
|
||||||
) -> Result<FastPathRev> {
|
) -> Result<FastPathRev> {
|
||||||
if !is_github(url) {
|
let Some(GitHubRepository { owner, repo }) = GitHubRepository::parse(url) else {
|
||||||
return Ok(FastPathRev::Indeterminate);
|
return Ok(FastPathRev::Indeterminate);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
let local_object = reference.resolve(git).ok();
|
||||||
|
|
||||||
let local_object = reference.resolve(repo).ok();
|
|
||||||
let github_branch_name = match reference {
|
let github_branch_name = match reference {
|
||||||
GitReference::Branch(branch) => branch,
|
GitReference::Branch(branch) => branch,
|
||||||
GitReference::Tag(tag) => tag,
|
GitReference::Tag(tag) => tag,
|
||||||
|
|
@ -765,28 +767,12 @@ fn github_fast_path(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// This expects GitHub urls in the form `github.com/user/repo` and nothing
|
// TODO(charlie): If we _know_ that we have a full commit SHA, there's no need to perform this
|
||||||
// else
|
// request. We can just return `FastPathRev::NeedsFetch`. However, we need to audit all uses of
|
||||||
let mut pieces = url
|
// `GitReference::FullCommit` to ensure that we _know_ it's a SHA, as opposed to (e.g.) a Git
|
||||||
.path_segments()
|
// tag that just "looks like" a commit (i.e., a tag composed of 40 hex characters).
|
||||||
.ok_or_else(|| anyhow!("no path segments on url"))?;
|
|
||||||
let username = pieces
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| anyhow!("couldn't find username"))?;
|
|
||||||
let repository = pieces
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| anyhow!("couldn't find repository name"))?;
|
|
||||||
if pieces.next().is_some() {
|
|
||||||
anyhow::bail!("too many segments on URL");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trim off the `.git` from the repository, if present, since that's
|
let url = format!("https://api.github.com/repos/{owner}/{repo}/commits/{github_branch_name}");
|
||||||
// optional for GitHub and won't work when we try to use the API as well.
|
|
||||||
let repository = repository.strip_suffix(".git").unwrap_or(repository);
|
|
||||||
|
|
||||||
let url = format!(
|
|
||||||
"https://api.github.com/repos/{username}/{repository}/commits/{github_branch_name}"
|
|
||||||
);
|
|
||||||
|
|
||||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
|
|
@ -802,7 +788,11 @@ fn github_fast_path(
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = request.send().await?;
|
let response = request.send().await?;
|
||||||
|
|
||||||
|
// GitHub returns a 404 if the repository does not exist, and a 422 if it exists but GitHub
|
||||||
|
// is unable to resolve the requested revision.
|
||||||
response.error_for_status_ref()?;
|
response.error_for_status_ref()?;
|
||||||
|
|
||||||
let response_code = response.status();
|
let response_code = response.status();
|
||||||
if response_code == StatusCode::NOT_MODIFIED {
|
if response_code == StatusCode::NOT_MODIFIED {
|
||||||
Ok(FastPathRev::UpToDate)
|
Ok(FastPathRev::UpToDate)
|
||||||
|
|
@ -810,19 +800,11 @@ fn github_fast_path(
|
||||||
let oid_to_fetch = response.text().await?.parse()?;
|
let oid_to_fetch = response.text().await?.parse()?;
|
||||||
Ok(FastPathRev::NeedsFetch(oid_to_fetch))
|
Ok(FastPathRev::NeedsFetch(oid_to_fetch))
|
||||||
} else {
|
} else {
|
||||||
// Usually response_code == 404 if the repository does not exist, and
|
|
||||||
// response_code == 422 if exists but GitHub is unable to resolve the
|
|
||||||
// requested rev.
|
|
||||||
Ok(FastPathRev::Indeterminate)
|
Ok(FastPathRev::Indeterminate)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether a `url` is one from GitHub.
|
|
||||||
fn is_github(url: &Url) -> bool {
|
|
||||||
url.host_str() == Some("github.com")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether a `rev` looks like a commit hash (ASCII hex digits).
|
/// Whether a `rev` looks like a commit hash (ASCII hex digits).
|
||||||
fn looks_like_commit_hash(rev: &str) -> bool {
|
fn looks_like_commit_hash(rev: &str) -> bool {
|
||||||
rev.len() >= 7 && rev.chars().all(|ch| ch.is_ascii_hexdigit())
|
rev.len() >= 7 && rev.chars().all(|ch| ch.is_ascii_hexdigit())
|
||||||
|
|
|
||||||
85
crates/uv-git/src/github.rs
Normal file
85
crates/uv-git/src/github.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
use tracing::debug;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
/// A reference to a repository on GitHub.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct GitHubRepository<'a> {
|
||||||
|
/// The `owner` field for the repository, i.e., the user or organization that owns the
|
||||||
|
/// repository, like `astral-sh`.
|
||||||
|
pub owner: &'a str,
|
||||||
|
/// The `repo` field for the repository, i.e., the name of the repository, like `uv`.
|
||||||
|
pub repo: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GitHubRepository<'a> {
|
||||||
|
/// Parse a GitHub repository from a URL.
|
||||||
|
///
|
||||||
|
/// Expects to receive a URL of the form: `https://github.com/{user}/{repo}[.git]`, e.g.,
|
||||||
|
/// `https://github.com/astral-sh/uv`. Otherwise, returns `None`.
|
||||||
|
pub fn parse(url: &'a Url) -> Option<Self> {
|
||||||
|
// The fast path is only available for GitHub repositories.
|
||||||
|
if url.host_str() != Some("github.com") {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The GitHub URL must take the form: `https://github.com/{user}/{repo}`, e.g.,
|
||||||
|
// `https://github.com/astral-sh/uv`.
|
||||||
|
let Some(mut segments) = url.path_segments() else {
|
||||||
|
debug!("GitHub URL is missing path segments: {url}");
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let Some(owner) = segments.next() else {
|
||||||
|
debug!("GitHub URL is missing owner: {url}");
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let Some(repo) = segments.next() else {
|
||||||
|
debug!("GitHub URL is missing repo: {url}");
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
if segments.next().is_some() {
|
||||||
|
debug!("GitHub URL has too many path segments: {url}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim off the `.git` from the repository, if present.
|
||||||
|
let repo = repo.strip_suffix(".git").unwrap_or(repo);
|
||||||
|
|
||||||
|
Some(Self { owner, repo })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_valid_url() {
|
||||||
|
let url = Url::parse("https://github.com/astral-sh/uv").unwrap();
|
||||||
|
let repo = GitHubRepository::parse(&url).unwrap();
|
||||||
|
assert_eq!(repo.owner, "astral-sh");
|
||||||
|
assert_eq!(repo.repo, "uv");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_with_git_suffix() {
|
||||||
|
let url = Url::parse("https://github.com/astral-sh/uv.git").unwrap();
|
||||||
|
let repo = GitHubRepository::parse(&url).unwrap();
|
||||||
|
assert_eq!(repo.owner, "astral-sh");
|
||||||
|
assert_eq!(repo.repo, "uv");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_invalid_host() {
|
||||||
|
let url = Url::parse("https://gitlab.com/astral-sh/uv").unwrap();
|
||||||
|
assert!(GitHubRepository::parse(&url).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_invalid_path() {
|
||||||
|
let url = Url::parse("https://github.com/astral-sh").unwrap();
|
||||||
|
assert!(GitHubRepository::parse(&url).is_none());
|
||||||
|
|
||||||
|
let url = Url::parse("https://github.com/astral-sh/uv/extra").unwrap();
|
||||||
|
assert!(GitHubRepository::parse(&url).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ use url::Url;
|
||||||
|
|
||||||
pub use crate::credentials::{store_credentials_from_url, GIT_STORE};
|
pub use crate::credentials::{store_credentials_from_url, GIT_STORE};
|
||||||
pub use crate::git::{GitReference, GIT};
|
pub use crate::git::{GitReference, GIT};
|
||||||
|
pub use crate::github::GitHubRepository;
|
||||||
pub use crate::resolver::{
|
pub use crate::resolver::{
|
||||||
GitResolver, GitResolverError, RepositoryReference, ResolvedRepositoryReference,
|
GitResolver, GitResolverError, RepositoryReference, ResolvedRepositoryReference,
|
||||||
};
|
};
|
||||||
|
|
@ -10,6 +11,7 @@ pub use crate::source::{Fetch, GitSource, Reporter};
|
||||||
|
|
||||||
mod credentials;
|
mod credentials;
|
||||||
mod git;
|
mod git;
|
||||||
|
mod github;
|
||||||
mod resolver;
|
mod resolver;
|
||||||
mod sha;
|
mod sha;
|
||||||
mod source;
|
mod source;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue