Skip to content

Commit e9860ea

Browse files
committed
Respect creds on sources
1 parent 778da33 commit e9860ea

File tree

6 files changed

+234
-26
lines changed

6 files changed

+234
-26
lines changed

crates/uv-git/src/credentials.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
use std::collections::HashMap;
2-
use std::sync::{Arc, RwLock};
3-
41
use cache_key::RepositoryUrl;
2+
use std::collections::HashMap;
3+
use std::sync::{Arc, LazyLock, RwLock};
4+
use tracing::trace;
5+
use url::Url;
56
use uv_auth::Credentials;
67

8+
/// Global authentication cache for a uv invocation.
9+
///
10+
/// This is used to share Git credentials within a single process.
11+
pub static GIT_STORE: LazyLock<GitStore> = LazyLock::new(GitStore::default);
12+
713
/// A store for Git credentials.
814
#[derive(Debug, Default)]
915
pub struct GitStore(RwLock<HashMap<RepositoryUrl, Arc<Credentials>>>);
@@ -19,3 +25,16 @@ impl GitStore {
1925
self.0.read().unwrap().get(url).cloned()
2026
}
2127
}
28+
29+
/// Populate the global authentication store with credentials on a Git URL, if there are any.
30+
///
31+
/// Returns `true` if the store was updated.
32+
pub fn store_credentials_from_url(url: &Url) -> bool {
33+
if let Some(credentials) = Credentials::from_url(url) {
34+
trace!("Caching credentials for {url}");
35+
GIT_STORE.insert(RepositoryUrl::new(url), credentials);
36+
true
37+
} else {
38+
false
39+
}
40+
}

crates/uv-git/src/lib.rs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
use std::sync::LazyLock;
2-
31
use url::Url;
42

5-
use crate::credentials::GitStore;
3+
pub use crate::credentials::{store_credentials_from_url, GIT_STORE};
64
pub use crate::git::GitReference;
75
pub use crate::resolver::{
86
GitResolver, GitResolverError, RepositoryReference, ResolvedRepositoryReference,
@@ -16,11 +14,6 @@ mod resolver;
1614
mod sha;
1715
mod source;
1816

19-
/// Global authentication cache for a uv invocation.
20-
///
21-
/// This is used to share Git credentials within a single process.
22-
pub static GIT_STORE: LazyLock<GitStore> = LazyLock::new(GitStore::default);
23-
2417
/// A URL reference to a Git repository.
2518
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Hash, Ord)]
2619
pub struct GitUrl {

crates/uv-workspace/src/pyproject.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ pub struct Project {
120120
pub version: Option<Version>,
121121
/// The Python versions this project is compatible with.
122122
pub requires_python: Option<VersionSpecifiers>,
123+
/// The dependencies of the project.
124+
pub dependencies: Option<Vec<String>>,
123125
/// The optional dependencies of the project.
124126
pub optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
125127

crates/uv-workspace/src/workspace.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,22 @@ impl Workspace {
531531
&self.sources
532532
}
533533

534+
/// Returns an iterator over all sources in the workspace.
535+
pub fn iter_sources(&self) -> impl Iterator<Item = &Source> {
536+
self.packages
537+
.values()
538+
.filter_map(|member| {
539+
member.pyproject_toml().tool.as_ref().and_then(|tool| {
540+
tool.uv
541+
.as_ref()
542+
.and_then(|uv| uv.sources.as_ref())
543+
.map(ToolUvSources::inner)
544+
.map(|sources| sources.values())
545+
})
546+
})
547+
.flatten()
548+
}
549+
534550
/// The `pyproject.toml` of the workspace.
535551
pub fn pyproject_toml(&self) -> &PyProjectToml {
536552
&self.pyproject_toml

crates/uv/src/commands/project/sync.rs

Lines changed: 94 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1+
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger};
2+
use crate::commands::pip::operations::Modifications;
3+
use crate::commands::project::lock::do_safe_lock;
4+
use crate::commands::project::{ProjectError, SharedState};
5+
use crate::commands::{pip, project, ExitStatus};
6+
use crate::printer::Printer;
7+
use crate::settings::{InstallerSettingsRef, ResolverInstallerSettings};
18
use anyhow::{Context, Result};
2-
use itertools::Itertools;
3-
49
use distribution_types::{DirectorySourceDist, Dist, ResolvedDist, SourceDist};
5-
use pep508_rs::MarkerTree;
6-
use uv_auth::store_credentials_from_url;
10+
use itertools::Itertools;
11+
use pep508_rs::{MarkerTree, Requirement, VersionOrUrl};
12+
use pypi_types::{
13+
LenientRequirement, ParsedArchiveUrl, ParsedGitUrl, ParsedUrl, VerbatimParsedUrl,
14+
};
15+
use std::borrow::Cow;
16+
use std::str::FromStr;
717
use uv_cache::Cache;
818
use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder};
919
use uv_configuration::{
@@ -18,16 +28,9 @@ use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequ
1828
use uv_resolver::{FlatIndex, Lock};
1929
use uv_types::{BuildIsolation, HashStrategy};
2030
use uv_warnings::warn_user;
31+
use uv_workspace::pyproject::{Source, ToolUvSources};
2132
use uv_workspace::{DiscoveryOptions, InstallTarget, MemberDiscovery, VirtualProject, Workspace};
2233

23-
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger};
24-
use crate::commands::pip::operations::Modifications;
25-
use crate::commands::project::lock::do_safe_lock;
26-
use crate::commands::project::{ProjectError, SharedState};
27-
use crate::commands::{pip, project, ExitStatus};
28-
use crate::printer::Printer;
29-
use crate::settings::{InstallerSettingsRef, ResolverInstallerSettings};
30-
3134
/// Sync the project environment.
3235
#[allow(clippy::fn_params_excessive_bools)]
3336
pub(crate) async fn sync(
@@ -250,9 +253,12 @@ pub(super) async fn do_sync(
250253

251254
// Add all authenticated sources to the cache.
252255
for url in index_locations.urls() {
253-
store_credentials_from_url(url);
256+
uv_auth::store_credentials_from_url(url);
254257
}
255258

259+
// Populate credentials from the workspace.
260+
store_credentials_from_workspace(target.workspace());
261+
256262
// Initialize the registry client.
257263
let client = RegistryClientBuilder::new(cache.clone())
258264
.native_tls(native_tls)
@@ -399,3 +405,78 @@ fn apply_editable_mode(
399405
}),
400406
}
401407
}
408+
409+
fn store_credentials_from_workspace(workspace: &Workspace) {
410+
for member in workspace.packages().values() {
411+
// Iterate over the `tool.uv.sources`.
412+
for source in member
413+
.pyproject_toml()
414+
.tool
415+
.as_ref()
416+
.and_then(|tool| tool.uv.as_ref())
417+
.and_then(|uv| uv.sources.as_ref())
418+
.map(ToolUvSources::inner)
419+
.iter()
420+
.flat_map(|sources| sources.values())
421+
{
422+
match source {
423+
Source::Git { git, .. } => {
424+
uv_git::store_credentials_from_url(git);
425+
}
426+
Source::Url { url, .. } => {
427+
uv_auth::store_credentials_from_url(url);
428+
}
429+
_ => {}
430+
}
431+
}
432+
433+
// Iterate over all dependencies.
434+
let dependencies = member
435+
.pyproject_toml()
436+
.project
437+
.as_ref()
438+
.and_then(|project| project.dependencies.as_ref())
439+
.into_iter()
440+
.flatten();
441+
let optional_dependencies = member
442+
.pyproject_toml()
443+
.project
444+
.as_ref()
445+
.and_then(|project| project.optional_dependencies.as_ref())
446+
.into_iter()
447+
.flat_map(|optional| optional.values())
448+
.flatten();
449+
let dev_dependencies = member
450+
.pyproject_toml()
451+
.tool
452+
.as_ref()
453+
.and_then(|tool| tool.uv.as_ref())
454+
.and_then(|uv| uv.dev_dependencies.as_ref())
455+
.into_iter()
456+
.flatten();
457+
458+
for requirement in dependencies
459+
.chain(optional_dependencies)
460+
.filter_map(|requires_dist| {
461+
LenientRequirement::<VerbatimParsedUrl>::from_str(requires_dist)
462+
.map(Requirement::from)
463+
.map(Cow::Owned)
464+
.ok()
465+
})
466+
.chain(dev_dependencies.map(Cow::Borrowed))
467+
{
468+
let Some(VersionOrUrl::Url(url)) = &requirement.version_or_url else {
469+
continue;
470+
};
471+
match &url.parsed_url {
472+
ParsedUrl::Git(ParsedGitUrl { url, .. }) => {
473+
uv_git::store_credentials_from_url(url.repository());
474+
}
475+
ParsedUrl::Archive(ParsedArchiveUrl { url, .. }) => {
476+
uv_auth::store_credentials_from_url(url);
477+
}
478+
_ => {}
479+
}
480+
}
481+
}
482+
}

crates/uv/tests/lock.rs

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6034,7 +6034,7 @@ fn lock_redact_https() -> Result<()> {
60346034

60356035
/// However, we don't currently avoid persisting Git credentials in `uv.lock`.
60366036
#[test]
6037-
fn lock_redact_git() -> Result<()> {
6037+
fn lock_redact_git_pep508() -> Result<()> {
60386038
let context = TestContext::new("3.12");
60396039
let token = decode_token(common::READ_ONLY_GITHUB_TOKEN);
60406040

@@ -6111,7 +6111,104 @@ fn lock_redact_git() -> Result<()> {
61116111
"###);
61126112

61136113
// Install from the lockfile.
6114-
uv_snapshot!(&filters, context.sync().arg("--frozen"), @r###"
6114+
uv_snapshot!(&filters, context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r###"
6115+
success: true
6116+
exit_code: 0
6117+
----- stdout -----
6118+
6119+
----- stderr -----
6120+
Prepared 2 packages in [TIME]
6121+
Installed 2 packages in [TIME]
6122+
+ foo==0.1.0 (from file://[TEMP_DIR]/)
6123+
+ uv-private-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-private-pypackage@d780faf0ac91257d4d5a4f0c5a0e4509608c0071)
6124+
"###);
6125+
6126+
Ok(())
6127+
}
6128+
6129+
/// However, we don't currently avoid persisting Git credentials in `uv.lock`.
6130+
#[test]
6131+
fn lock_redact_git_sources() -> Result<()> {
6132+
let context = TestContext::new("3.12");
6133+
let token = decode_token(common::READ_ONLY_GITHUB_TOKEN);
6134+
6135+
let filters: Vec<_> = [(token.as_str(), "***")]
6136+
.into_iter()
6137+
.chain(context.filters())
6138+
.collect();
6139+
6140+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
6141+
pyproject_toml.write_str(&formatdoc! {
6142+
r#"
6143+
[project]
6144+
name = "foo"
6145+
version = "0.1.0"
6146+
requires-python = ">=3.12"
6147+
dependencies = ["uv-private-pypackage"]
6148+
6149+
[build-system]
6150+
requires = ["setuptools>=42"]
6151+
build-backend = "setuptools.build_meta"
6152+
6153+
[tool.uv.sources]
6154+
uv-private-pypackage = {{ git = "https://{token}@github.com/astral-test/uv-private-pypackage" }}
6155+
"#,
6156+
token = token,
6157+
})?;
6158+
6159+
uv_snapshot!(&filters, context.lock(), @r###"
6160+
success: true
6161+
exit_code: 0
6162+
----- stdout -----
6163+
6164+
----- stderr -----
6165+
Resolved 2 packages in [TIME]
6166+
"###);
6167+
6168+
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
6169+
6170+
insta::with_settings!({
6171+
filters => filters.clone(),
6172+
}, {
6173+
assert_snapshot!(
6174+
lock, @r###"
6175+
version = 1
6176+
requires-python = ">=3.12"
6177+
6178+
[options]
6179+
exclude-newer = "2024-03-25T00:00:00Z"
6180+
6181+
[[package]]
6182+
name = "foo"
6183+
version = "0.1.0"
6184+
source = { editable = "." }
6185+
dependencies = [
6186+
{ name = "uv-private-pypackage" },
6187+
]
6188+
6189+
[package.metadata]
6190+
requires-dist = [{ name = "uv-private-pypackage", git = "https://github.com/astral-test/uv-private-pypackage" }]
6191+
6192+
[[package]]
6193+
name = "uv-private-pypackage"
6194+
version = "0.1.0"
6195+
source = { git = "https://github.com/astral-test/uv-private-pypackage#d780faf0ac91257d4d5a4f0c5a0e4509608c0071" }
6196+
"###
6197+
);
6198+
});
6199+
6200+
// Re-run with `--locked`.
6201+
uv_snapshot!(&filters, context.lock().arg("--locked"), @r###"
6202+
success: true
6203+
exit_code: 0
6204+
----- stdout -----
6205+
6206+
----- stderr -----
6207+
Resolved 2 packages in [TIME]
6208+
"###);
6209+
6210+
// Install from the lockfile.
6211+
uv_snapshot!(&filters, context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r###"
61156212
success: true
61166213
exit_code: 0
61176214
----- stdout -----

0 commit comments

Comments
 (0)