mirror of
https://github.com/astral-sh/uv.git
synced 2025-09-26 20:19:08 +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",
|
"anyhow",
|
||||||
"assert_cmd",
|
"assert_cmd",
|
||||||
"assert_fs",
|
"assert_fs",
|
||||||
|
"base64 0.21.7",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"clap_complete_command",
|
"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 a Git URL ends in a reference (like a branch, tag, or commit), remove it.
|
||||||
if url.scheme().starts_with("git+") {
|
if url.scheme().starts_with("git+") {
|
||||||
if let Some((prefix, _)) = url.as_str().rsplit_once('@') {
|
if let Some(prefix) = url
|
||||||
url = prefix.parse().unwrap();
|
.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`,
|
// If the URL ends with a reference, like `https://git.example.com/MyProject.git@v1.0`,
|
||||||
// extract it.
|
// extract it.
|
||||||
let mut reference = GitReference::DefaultBranch;
|
let mut reference = GitReference::DefaultBranch;
|
||||||
if let Some((prefix, rev)) = url.as_str().rsplit_once('@') {
|
if let Some((prefix, suffix)) = url
|
||||||
reference = GitReference::from_rev(rev);
|
.path()
|
||||||
url = Url::parse(prefix)?;
|
.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 {
|
let precise = if let GitReference::FullCommit(rev) = &reference {
|
||||||
Some(GitSha::from_str(rev)?)
|
Some(GitSha::from_str(rev)?)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -39,6 +39,7 @@ requirements-txt = { path = "../requirements-txt" }
|
||||||
|
|
||||||
anstream = { workspace = true }
|
anstream = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
base64 = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
clap = { workspace = true, features = ["derive"] }
|
clap = { workspace = true, features = ["derive"] }
|
||||||
clap_complete_command = { workspace = true }
|
clap_complete_command = { workspace = true }
|
||||||
|
|
|
@ -87,6 +87,15 @@ impl TestContext {
|
||||||
.current_dir(&self.temp_dir)
|
.current_dir(&self.temp_dir)
|
||||||
.assert()
|
.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 {
|
pub fn venv_to_interpreter(venv: &Path) -> PathBuf {
|
||||||
|
|
|
@ -5,7 +5,9 @@ use std::process::Command;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use assert_cmd::prelude::*;
|
use assert_cmd::prelude::*;
|
||||||
use assert_fs::prelude::*;
|
use assert_fs::prelude::*;
|
||||||
|
use base64::{prelude::BASE64_STANDARD as base64, Engine};
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
|
use itertools::Itertools;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use common::{uv_snapshot, TestContext, EXCLUDE_NEWER, INSTA_FILTERS};
|
use common::{uv_snapshot, TestContext, EXCLUDE_NEWER, INSTA_FILTERS};
|
||||||
|
@ -14,6 +16,24 @@ use crate::common::get_bin;
|
||||||
|
|
||||||
mod common;
|
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.
|
/// Create a `pip install` command with options shared across scenarios.
|
||||||
fn command(context: &TestContext) -> Command {
|
fn command(context: &TestContext) -> Command {
|
||||||
let mut command = Command::new(get_bin());
|
let mut command = Command::new(get_bin());
|
||||||
|
@ -769,6 +789,122 @@ fn install_no_index_version() {
|
||||||
context.assert_command("import flask").failure();
|
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.
|
/// Install a package without using pre-built wheels.
|
||||||
#[test]
|
#[test]
|
||||||
fn reinstall_no_binary() {
|
fn reinstall_no_binary() {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue