mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 10:58:28 +00:00
Retain passwords in Git URLs (#1717)
Fixes handling of GitHub PATs in HTTPS URLs, which were otherwise dropped. We now supporting the following authentication schemes: ``` git+https://<user>:<token>/... git+https://<token>/... ``` On Windows, the username is required. We can consider adding a special-case for this in the future, but this just matches libgit2's behavior. I tested with fine-grained tokens, OAuth tokens, and "classic" tokens. There's test coverage for fine-grained tokens in CI where we use a real private repository and PAT. Yes, the PAT is committed to make this test usable by anyone. It has read-only permissions to the single repository, expires Feb 1 2025, and is in an isolated organization and GitHub account. Does not yet address SSH authentication. Related: - https://github.com/astral-sh/uv/issues/1514 - https://github.com/astral-sh/uv/issues/1452
This commit is contained in:
parent
2e60c1d734
commit
d07b587f3f
6 changed files with 161 additions and 5 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -4142,6 +4142,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"assert_cmd",
|
||||
"assert_fs",
|
||||
"base64 0.21.7",
|
||||
"chrono",
|
||||
"clap",
|
||||
"clap_complete_command",
|
||||
|
|
|
@ -109,8 +109,12 @@ impl RepositoryUrl {
|
|||
|
||||
// If a Git URL ends in a reference (like a branch, tag, or commit), remove it.
|
||||
if url.scheme().starts_with("git+") {
|
||||
if let Some((prefix, _)) = url.as_str().rsplit_once('@') {
|
||||
url = prefix.parse().unwrap();
|
||||
if let Some(prefix) = url
|
||||
.path()
|
||||
.rsplit_once('@')
|
||||
.map(|(prefix, _suffix)| prefix.to_string())
|
||||
{
|
||||
url.set_path(&prefix);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -65,10 +65,15 @@ impl TryFrom<Url> for GitUrl {
|
|||
// If the URL ends with a reference, like `https://git.example.com/MyProject.git@v1.0`,
|
||||
// extract it.
|
||||
let mut reference = GitReference::DefaultBranch;
|
||||
if let Some((prefix, rev)) = url.as_str().rsplit_once('@') {
|
||||
reference = GitReference::from_rev(rev);
|
||||
url = Url::parse(prefix)?;
|
||||
if let Some((prefix, suffix)) = url
|
||||
.path()
|
||||
.rsplit_once('@')
|
||||
.map(|(prefix, suffix)| (prefix.to_string(), suffix.to_string()))
|
||||
{
|
||||
reference = GitReference::from_rev(&suffix);
|
||||
url.set_path(&prefix);
|
||||
}
|
||||
|
||||
let precise = if let GitReference::FullCommit(rev) = &reference {
|
||||
Some(GitSha::from_str(rev)?)
|
||||
} else {
|
||||
|
|
|
@ -39,6 +39,7 @@ requirements-txt = { path = "../requirements-txt" }
|
|||
|
||||
anstream = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
clap_complete_command = { workspace = true }
|
||||
|
|
|
@ -87,6 +87,15 @@ impl TestContext {
|
|||
.current_dir(&self.temp_dir)
|
||||
.assert()
|
||||
}
|
||||
|
||||
/// Assert a package is installed with the given version.
|
||||
pub fn assert_installed(&self, package: &'static str, version: &'static str) {
|
||||
self.assert_command(
|
||||
format!("import {package} as package; print(package.__version__, end='')").as_str(),
|
||||
)
|
||||
.success()
|
||||
.stdout(version);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn venv_to_interpreter(venv: &Path) -> PathBuf {
|
||||
|
|
|
@ -5,7 +5,9 @@ use std::process::Command;
|
|||
use anyhow::Result;
|
||||
use assert_cmd::prelude::*;
|
||||
use assert_fs::prelude::*;
|
||||
use base64::{prelude::BASE64_STANDARD as base64, Engine};
|
||||
use indoc::indoc;
|
||||
use itertools::Itertools;
|
||||
use url::Url;
|
||||
|
||||
use common::{uv_snapshot, TestContext, EXCLUDE_NEWER, INSTA_FILTERS};
|
||||
|
@ -14,6 +16,24 @@ use crate::common::get_bin;
|
|||
|
||||
mod common;
|
||||
|
||||
// This is a fine-grained token that only has read-only access to the `uv-private-pypackage` repository
|
||||
const READ_ONLY_GITHUB_TOKEN: &[&str] = &[
|
||||
"Z2l0aHViX3BhdA==",
|
||||
"MTFCR0laQTdRMGdXeGsweHV6ekR2Mg==",
|
||||
"NVZMaExzZmtFMHZ1ZEVNd0pPZXZkV040WUdTcmk2WXREeFB4TFlybGlwRTZONEpHV01FMnFZQWJVUm4=",
|
||||
];
|
||||
|
||||
/// Decode a split, base64 encoded authentication token.
|
||||
/// We split and encode the token to bypass revoke by GitHub's secret scanning
|
||||
fn decode_token(content: &[&str]) -> String {
|
||||
let token = content
|
||||
.iter()
|
||||
.map(|part| base64.decode(part).unwrap())
|
||||
.map(|decoded| std::str::from_utf8(decoded.as_slice()).unwrap().to_string())
|
||||
.join("_");
|
||||
token
|
||||
}
|
||||
|
||||
/// Create a `pip install` command with options shared across scenarios.
|
||||
fn command(context: &TestContext) -> Command {
|
||||
let mut command = Command::new(get_bin());
|
||||
|
@ -769,6 +789,122 @@ fn install_no_index_version() {
|
|||
context.assert_command("import flask").failure();
|
||||
}
|
||||
|
||||
/// Install a package from a public GitHub repository
|
||||
#[test]
|
||||
#[cfg(feature = "git")]
|
||||
fn install_git_public_https() {
|
||||
let context = TestContext::new("3.8");
|
||||
|
||||
uv_snapshot!(command(&context)
|
||||
.arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage")
|
||||
, @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
Downloaded 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage@0dacfd662c64cb4ceb16e6cf65a157a8b715b979)
|
||||
"###);
|
||||
|
||||
context.assert_installed("uv_public_pypackage", "0.1.0");
|
||||
}
|
||||
|
||||
/// Install a package from a private GitHub repository using a PAT
|
||||
#[test]
|
||||
#[cfg(all(not(windows), feature = "git"))]
|
||||
fn install_git_private_https_pat() {
|
||||
let context = TestContext::new("3.8");
|
||||
let token = decode_token(READ_ONLY_GITHUB_TOKEN);
|
||||
|
||||
let mut filters = INSTA_FILTERS.to_vec();
|
||||
filters.insert(0, (&token, "***"));
|
||||
|
||||
uv_snapshot!(filters, command(&context)
|
||||
.arg(format!("uv-private-pypackage @ git+https://{token}@github.com/astral-test/uv-private-pypackage"))
|
||||
, @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
Downloaded 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ uv-private-pypackage==0.1.0 (from git+https://***@github.com/astral-test/uv-private-pypackage@6c09ce9ae81f50670a60abd7d95f30dd416d00ac)
|
||||
"###);
|
||||
|
||||
context.assert_installed("uv_private_pypackage", "0.1.0");
|
||||
}
|
||||
|
||||
/// Install a package from a private GitHub repository at a specific commit using a PAT
|
||||
#[test]
|
||||
#[cfg(feature = "git")]
|
||||
fn install_git_private_https_pat_at_ref() {
|
||||
let context = TestContext::new("3.8");
|
||||
let token = decode_token(READ_ONLY_GITHUB_TOKEN);
|
||||
|
||||
let mut filters = INSTA_FILTERS.to_vec();
|
||||
filters.insert(0, (&token, "***"));
|
||||
filters.push((r"git\+https://", ""));
|
||||
|
||||
// A user is _required_ on Windows
|
||||
let user = if cfg!(windows) {
|
||||
filters.push((r"git:", ""));
|
||||
"git:"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
uv_snapshot!(filters, command(&context)
|
||||
.arg(format!("uv-private-pypackage @ git+https://{user}{token}@github.com/astral-test/uv-private-pypackage@6c09ce9ae81f50670a60abd7d95f30dd416d00ac"))
|
||||
, @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
Downloaded 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ uv-private-pypackage==0.1.0 (from ***@github.com/astral-test/uv-private-pypackage@6c09ce9ae81f50670a60abd7d95f30dd416d00ac)
|
||||
"###);
|
||||
|
||||
context.assert_installed("uv_private_pypackage", "0.1.0");
|
||||
}
|
||||
|
||||
/// Install a package from a private GitHub repository using a PAT and username
|
||||
/// An arbitrary username is supported when using a PAT.
|
||||
#[test]
|
||||
#[cfg(feature = "git")]
|
||||
fn install_git_private_https_pat_and_username() {
|
||||
let context = TestContext::new("3.8");
|
||||
let token = decode_token(READ_ONLY_GITHUB_TOKEN);
|
||||
let user = "astral-test-bot";
|
||||
|
||||
let mut filters = INSTA_FILTERS.to_vec();
|
||||
filters.insert(0, (&token, "***"));
|
||||
filters.push(("failed to clone into: .*", "failed to clone into: [PATH]"));
|
||||
|
||||
uv_snapshot!(filters, command(&context)
|
||||
.arg(format!("uv-private-pypackage @ git+https://{user}:{token}@github.com/astral-test/uv-private-pypackage"))
|
||||
, @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
Downloaded 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ uv-private-pypackage==0.1.0 (from git+https://astral-test-bot:***@github.com/astral-test/uv-private-pypackage@6c09ce9ae81f50670a60abd7d95f30dd416d00ac)
|
||||
"###);
|
||||
|
||||
context.assert_installed("uv_private_pypackage", "0.1.0");
|
||||
}
|
||||
|
||||
/// Install a package without using pre-built wheels.
|
||||
#[test]
|
||||
fn reinstall_no_binary() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue