mirror of https://github.com/astral-sh/uv
Enable `uv run` with a GitHub Gist (#15058)
## Summary You can now run `uv run https://gist.github.com/charliermarsh/ea9eab7f56b1b3d41e51960001cae31d` to execute a single-file Gist without having to go in and copy the raw URL.
This commit is contained in:
parent
a28c3fb7d9
commit
3b15da3c5d
|
|
@ -42,6 +42,17 @@ use uv_warnings::warn_user;
|
||||||
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceCache, WorkspaceError};
|
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceCache, WorkspaceError};
|
||||||
|
|
||||||
use crate::child::run_to_completion;
|
use crate::child::run_to_completion;
|
||||||
|
|
||||||
|
/// GitHub Gist API response structure
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct GistResponse {
|
||||||
|
files: std::collections::HashMap<String, GistFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct GistFile {
|
||||||
|
raw_url: String,
|
||||||
|
}
|
||||||
use crate::commands::pip::loggers::{
|
use crate::commands::pip::loggers::{
|
||||||
DefaultInstallLogger, DefaultResolveLogger, SummaryInstallLogger, SummaryResolveLogger,
|
DefaultInstallLogger, DefaultResolveLogger, SummaryInstallLogger, SummaryResolveLogger,
|
||||||
};
|
};
|
||||||
|
|
@ -1596,6 +1607,66 @@ impl std::fmt::Display for RunCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve a GitHub Gist URL to its raw file URL using the GitHub API.
|
||||||
|
async fn resolve_gist_url(
|
||||||
|
url: &DisplaySafeUrl,
|
||||||
|
network_settings: &NetworkSettings,
|
||||||
|
) -> anyhow::Result<DisplaySafeUrl> {
|
||||||
|
// Extract the Gist ID from the URL.
|
||||||
|
let gist_id = url
|
||||||
|
.path_segments()
|
||||||
|
.and_then(|mut segments| segments.nth(1))
|
||||||
|
.ok_or_else(|| anyhow!("Invalid Gist URL format"))?;
|
||||||
|
|
||||||
|
// Build the API URL.
|
||||||
|
let api_url = format!("https://api.github.com/gists/{gist_id}");
|
||||||
|
|
||||||
|
let client = BaseClientBuilder::new()
|
||||||
|
.retries_from_env()?
|
||||||
|
.connectivity(network_settings.connectivity)
|
||||||
|
.native_tls(network_settings.native_tls)
|
||||||
|
.allow_insecure_host(network_settings.allow_insecure_host.clone())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Build the request with appropriate headers.
|
||||||
|
let api_url_parsed = DisplaySafeUrl::parse(&api_url)?;
|
||||||
|
let mut request = client
|
||||||
|
.for_host(&api_url_parsed)
|
||||||
|
.get(Url::from(api_url_parsed));
|
||||||
|
request = request.header("Accept", "application/vnd.github.v3+json");
|
||||||
|
|
||||||
|
// Add GitHub token, if available.
|
||||||
|
if let Ok(token) = std::env::var(EnvVars::UV_GITHUB_TOKEN) {
|
||||||
|
request = request.header("Authorization", format!("Bearer {token}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the API request.
|
||||||
|
let response = request.send().await?;
|
||||||
|
response.error_for_status_ref()?;
|
||||||
|
|
||||||
|
// Parse the response
|
||||||
|
let gist_data: GistResponse = response.json().await?;
|
||||||
|
|
||||||
|
// Get the raw URL of the first `.py` file (or just the first file).
|
||||||
|
let raw_url = gist_data
|
||||||
|
.files
|
||||||
|
.iter()
|
||||||
|
.filter(|(name, _)| {
|
||||||
|
Path::new(name)
|
||||||
|
.extension()
|
||||||
|
.is_some_and(|ext| ext.eq_ignore_ascii_case("py"))
|
||||||
|
})
|
||||||
|
.map(|(_, file)| &file.raw_url)
|
||||||
|
.next()
|
||||||
|
// If no `.py` file is found, use the first file.
|
||||||
|
.or_else(|| gist_data.files.values().next().map(|file| &file.raw_url))
|
||||||
|
.ok_or_else(|| anyhow!("No files found in the Gist"))?;
|
||||||
|
|
||||||
|
let url = DisplaySafeUrl::parse(raw_url)?;
|
||||||
|
|
||||||
|
Ok(url)
|
||||||
|
}
|
||||||
|
|
||||||
impl RunCommand {
|
impl RunCommand {
|
||||||
/// Determine the [`RunCommand`] for a given set of arguments.
|
/// Determine the [`RunCommand`] for a given set of arguments.
|
||||||
#[allow(clippy::fn_params_excessive_bools)]
|
#[allow(clippy::fn_params_excessive_bools)]
|
||||||
|
|
@ -1633,7 +1704,12 @@ impl RunCommand {
|
||||||
// We don't do this check on Windows since the file path would
|
// We don't do this check on Windows since the file path would
|
||||||
// be invalid anyway, and thus couldn't refer to a local file.
|
// be invalid anyway, and thus couldn't refer to a local file.
|
||||||
if !cfg!(unix) || matches!(target_path.try_exists(), Ok(false)) {
|
if !cfg!(unix) || matches!(target_path.try_exists(), Ok(false)) {
|
||||||
let url = DisplaySafeUrl::parse(&target.to_string_lossy())?;
|
let mut url = DisplaySafeUrl::parse(&target.to_string_lossy())?;
|
||||||
|
|
||||||
|
// If it's a Gist URL, use the GitHub API to get the raw URL.
|
||||||
|
if url.host_str() == Some("gist.github.com") {
|
||||||
|
url = resolve_gist_url(&url, &network_settings).await?;
|
||||||
|
}
|
||||||
|
|
||||||
let file_stem = url
|
let file_stem = url
|
||||||
.path_segments()
|
.path_segments()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue