From cca841a44299006dd932213d63187a721018072f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 2 Dec 2024 22:39:32 -0500 Subject: [PATCH] Respect path dependencies within Git dependencies --- .../uv-distribution/src/metadata/lowering.rs | 27 ++++++ crates/uv/tests/it/sync.rs | 89 +++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/crates/uv-distribution/src/metadata/lowering.rs b/crates/uv-distribution/src/metadata/lowering.rs index 8fd4e9dd87ba..41f4df47e948 100644 --- a/crates/uv-distribution/src/metadata/lowering.rs +++ b/crates/uv-distribution/src/metadata/lowering.rs @@ -220,6 +220,7 @@ impl LoweredRequirement { } let source = path_source( PathBuf::from(path), + git_member, origin, project_dir, workspace.install_path(), @@ -465,6 +466,7 @@ impl LoweredRequirement { } let source = path_source( PathBuf::from(path), + None, RequirementOrigin::Project, dir, dir, @@ -553,6 +555,8 @@ pub enum LoweringError { WorkspaceFalse, #[error("Editable must refer to a local directory, not a file: `{0}`")] EditableFile(String), + #[error("Git repository references local file source, but only directories are supported as transitive Git dependencies: `{0}`")] + GitFile(String), #[error(transparent)] ParsedUrl(#[from] ParsedUrlError), #[error("Path must be UTF-8: `{0}`")] @@ -678,6 +682,7 @@ fn registry_source( /// Convert a path string to a file or directory source. fn path_source( path: impl AsRef, + git_member: Option<&GitWorkspaceMember>, origin: RequirementOrigin, project_dir: &Path, workspace_root: &Path, @@ -702,6 +707,24 @@ fn path_source( install_path.extension().is_none() }; if is_dir { + if let Some(git_member) = git_member { + let subdirectory = uv_fs::normalize_path( + &uv_fs::relative_to(install_path, git_member.fetch_root) + .expect("Workspace member must be relative"), + ); + return Ok(RequirementSource::Git { + repository: git_member.git_source.git.repository().clone(), + reference: git_member.git_source.git.reference().clone(), + precise: git_member.git_source.git.precise(), + subdirectory: if subdirectory == PathBuf::new() { + None + } else { + Some(subdirectory) + }, + url, + }); + } + if editable { Ok(RequirementSource::Directory { install_path, @@ -731,6 +754,10 @@ fn path_source( }) } } else { + // TODO(charlie): If a Git repo contains a source that points to a file, what should we do? + if git_member.is_some() { + return Err(LoweringError::GitFile(url.to_string())); + } if editable { return Err(LoweringError::EditableFile(url.to_string())); } diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 09821d006d8f..154ba1e35ea4 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -5200,3 +5200,92 @@ fn mismatched_name_cached_wheel() -> Result<()> { Ok(()) } + +/// Sync a Git repository that depends on a package within the same repository via a `path` source. +/// +/// See: +#[test] +fn sync_git_path_dependency() -> Result<()> { + let context = TestContext::new("3.13"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "foo" + version = "0.1.0" + requires-python = ">=3.13" + dependencies = ["package2"] + + [tool.uv.sources] + package2 = { git = "https://git@github.com/astral-sh/uv-path-dependency-test.git", subdirectory = "package2" } + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "###); + + let lock = context.read("uv.lock"); + + insta::with_settings!( + { + filters => context.filters(), + }, + { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.13" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "foo" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "package2" }, + ] + + [package.metadata] + requires-dist = [{ name = "package2", git = "https://github.com/astral-sh/uv-path-dependency-test.git?subdirectory=package2" }] + + [[package]] + name = "package1" + version = "0.1.0" + source = { git = "https://github.com/astral-sh/uv-path-dependency-test.git?subdirectory=package1#28781b32cf1f260cdb2c8040628079eb265202bd" } + + [[package]] + name = "package2" + version = "0.1.0" + source = { git = "https://github.com/astral-sh/uv-path-dependency-test.git?subdirectory=package2#28781b32cf1f260cdb2c8040628079eb265202bd" } + dependencies = [ + { name = "package1" }, + ] + "### + ); + } + ); + + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + package1==0.1.0 (from git+https://github.com/astral-sh/uv-path-dependency-test.git@28781b32cf1f260cdb2c8040628079eb265202bd#subdirectory=package1) + + package2==0.1.0 (from git+https://github.com/astral-sh/uv-path-dependency-test.git@28781b32cf1f260cdb2c8040628079eb265202bd#subdirectory=package2) + "###); + + Ok(()) +}