Skip to content

Commit b1725c7

Browse files
authored
fix(uv): correct GitHub archive URLs for git ref sources (#1238)
### Changes are visible to end-users: no ### Test plan - Covered by existing test cases - New test cases added
1 parent 1834c16 commit b1725c7

9 files changed

Lines changed: 299 additions & 5 deletions

File tree

e2e/cases/MODULE.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ include("//uv-deps-650:setup.MODULE.bazel")
123123
include("//uv-data-purelib:setup.MODULE.bazel")
124124
include("//uv-dup-lock-records:setup.MODULE.bazel")
125125
include("//uv-duplicate-whl-labels:setup.MODULE.bazel")
126+
include("//uv-git-source:setup.MODULE.bazel")
126127
include("//uv-include-group:setup.MODULE.bazel")
127128
include("//uv-incompatible-wheel-source:setup.MODULE.bazel")
128129
include("//uv-invalid-build-overrides:setup.MODULE.bazel")
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
load("@aspect_rules_py//py:defs.bzl", "py_test")
2+
3+
# Runtime coverage for git-sourced lockfile packages. six is locked with
4+
# `source = { git = "https://github.com/benjaminp/six?tag=1.17.0#<commit>" }`,
5+
# exercising parse_git_url on a real uv-generated URL, the GitHub
6+
# git-to-http_archive conversion in collect_sdists, and the source build of
7+
# the resulting archive.
8+
py_test(
9+
name = "test",
10+
srcs = ["__test__.py"],
11+
dep_group = "uv_git_source",
12+
main = "__test__.py",
13+
python_version = "3.11",
14+
deps = [
15+
"@pypi_git_source//six",
16+
],
17+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import six
2+
3+
assert six.__version__ == "1.17.0", six.__version__
4+
assert six.ensure_str(b"git-source") == "git-source"
5+
6+
print("six", six.__version__, "imported from git source")
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[project]
2+
name = "uv_git_source"
3+
version = "0.0.0"
4+
requires-python = ">=3.11"
5+
dependencies = [
6+
# `build` and `setuptools` surface in the project repo as deps of the
7+
# pep517_whl build_tool that builds six from its git source tree.
8+
"build",
9+
"setuptools",
10+
"six",
11+
]
12+
13+
# six is resolved from git rather than a registry, exercising the
14+
# `source = { git = ... }` lockfile pathway: parse_git_url on the
15+
# uv-generated URL (query params + pinned commit fragment) and the
16+
# git-to-http_archive conversion for GitHub remotes.
17+
[tool.uv.sources]
18+
six = { git = "https://github.com/benjaminp/six", tag = "1.17.0" }
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""Coverage for git-sourced lockfile packages: six resolves from a
2+
`source = { git = ... }` entry (uv-generated URL with query params and a
3+
pinned commit fragment), which is converted to an http_archive of the
4+
GitHub tarball and built from source. Uses its own hub to keep the git
5+
source isolated from the shared `pypi` hub.
6+
"""
7+
8+
uv = use_extension("@aspect_rules_py//uv:extensions.bzl", "uv")
9+
uv.declare_hub(hub_name = "pypi_git_source")
10+
uv.project(
11+
default_build_dependencies = [
12+
"build",
13+
"setuptools",
14+
],
15+
hub_name = "pypi_git_source",
16+
lock = "//uv-git-source:uv.lock",
17+
pyproject = "//uv-git-source:pyproject.toml",
18+
)
19+
use_repo(
20+
uv,
21+
"pypi_git_source",
22+
"sdist_build__uv_git_source__six__1_17_0",
23+
)

e2e/cases/uv-git-source/uv.lock

Lines changed: 75 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

uv/private/extension/BUILD.bazel

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
load("@bazel_lib//:bzl_library.bzl", "bzl_library")
22
load(":test_defs.bzl", "defs_test_suite")
3+
load(":test_git_utils.bzl", "git_utils_test_suite")
34
load(":test_graph_utils.bzl", "graph_utils_test_suite")
45
load(":test_projectfile.bzl", "projectfile_test_suite")
56

67
defs_test_suite()
78

9+
git_utils_test_suite()
10+
811
graph_utils_test_suite()
912

1013
projectfile_test_suite()
@@ -84,6 +87,16 @@ bzl_library(
8487
],
8588
)
8689

90+
bzl_library(
91+
name = "test_git_utils",
92+
srcs = ["test_git_utils.bzl"],
93+
visibility = ["//uv:__subpackages__"],
94+
deps = [
95+
":git_utils",
96+
"@bazel_skylib//lib:unittest",
97+
],
98+
)
99+
87100
bzl_library(
88101
name = "test_graph_utils",
89102
srcs = ["test_graph_utils.bzl"],

uv/private/extension/git_utils.bzl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@ Utilities for parsing git URLs and converting them to http_archive.
33
"""
44

55
def ensure_ref(maybe_ref):
6-
"""Ensures a git ref starts with "ref/".
6+
"""Ensures a git ref starts with "refs/".
77
88
Args:
99
maybe_ref: The git ref string.
1010
1111
Returns:
12-
The git ref string, prefixed with "ref/" if it is not already.
12+
The git ref string, prefixed with "refs/" if it is not already.
1313
"""
1414
if maybe_ref == None:
1515
return None
1616

17-
if not maybe_ref.startswith("ref/"):
18-
return "ref/" + maybe_ref
17+
if not maybe_ref.startswith("refs/"):
18+
return "refs/" + maybe_ref
1919

2020
return maybe_ref
2121

@@ -98,7 +98,7 @@ def try_git_to_http_archive(git_cfg):
9898
"url": url,
9999
}
100100
elif "ref" in git_cfg:
101-
url = "{}/archive/{}.tar.gz".format(url, git_cfg["tag"])
101+
url = "{}/archive/{}.tar.gz".format(url, git_cfg["ref"])
102102
return {
103103
"url": url,
104104
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest")
2+
load(":git_utils.bzl", "ensure_ref", "parse_git_url", "try_git_to_http_archive")
3+
4+
def _ensure_ref_test_impl(ctx):
5+
env = unittest.begin(ctx)
6+
asserts.equals(env, None, ensure_ref(None))
7+
asserts.equals(env, "refs/heads/main", ensure_ref("heads/main"))
8+
asserts.equals(env, "refs/pull/64/head", ensure_ref("pull/64/head"))
9+
10+
# An already fully-qualified ref must not be prefixed again.
11+
asserts.equals(env, "refs/pull/64/head", ensure_ref("refs/pull/64/head"))
12+
asserts.equals(env, "refs/tags/v1.0.0", ensure_ref("refs/tags/v1.0.0"))
13+
return unittest.end(env)
14+
15+
ensure_ref_test = unittest.make(_ensure_ref_test_impl)
16+
17+
def _parse_git_url_fragment_commit_test_impl(ctx):
18+
env = unittest.begin(ctx)
19+
result = parse_git_url("https://github.com/user/repo.git#c7076a0c6e34d7b2fa4e0ecd7ba4b8e9d3d9e0f1")
20+
asserts.equals(env, {
21+
"remote": "https://github.com/user/repo.git",
22+
"commit": "c7076a0c6e34d7b2fa4e0ecd7ba4b8e9d3d9e0f1",
23+
}, result)
24+
return unittest.end(env)
25+
26+
parse_git_url_fragment_commit_test = unittest.make(_parse_git_url_fragment_commit_test_impl)
27+
28+
def _parse_git_url_query_commit_test_impl(ctx):
29+
env = unittest.begin(ctx)
30+
result = parse_git_url("https://github.com/user/repo.git?commit=c7076a0c6e34d7b2fa4e0ecd7ba4b8e9d3d9e0f1")
31+
asserts.equals(env, {
32+
"remote": "https://github.com/user/repo.git",
33+
"commit": "c7076a0c6e34d7b2fa4e0ecd7ba4b8e9d3d9e0f1",
34+
}, result)
35+
return unittest.end(env)
36+
37+
parse_git_url_query_commit_test = unittest.make(_parse_git_url_query_commit_test_impl)
38+
39+
def _parse_git_url_query_ref_test_impl(ctx):
40+
env = unittest.begin(ctx)
41+
result = parse_git_url("https://github.com/user/repo.git?ref=refs%2Fpull%2F64%2Fhead")
42+
asserts.equals(env, {
43+
"remote": "https://github.com/user/repo.git",
44+
"ref": "refs/pull/64/head",
45+
}, result)
46+
return unittest.end(env)
47+
48+
parse_git_url_query_ref_test = unittest.make(_parse_git_url_query_ref_test_impl)
49+
50+
def _parse_git_url_fragment_wins_over_query_test_impl(ctx):
51+
env = unittest.begin(ctx)
52+
result = parse_git_url("https://github.com/user/repo.git?ref=refs%2Fheads%2Fmain#c7076a0c6e34d7b2fa4e0ecd7ba4b8e9d3d9e0f1")
53+
asserts.equals(env, {
54+
"remote": "https://github.com/user/repo.git",
55+
"commit": "c7076a0c6e34d7b2fa4e0ecd7ba4b8e9d3d9e0f1",
56+
}, result)
57+
return unittest.end(env)
58+
59+
parse_git_url_fragment_wins_over_query_test = unittest.make(_parse_git_url_fragment_wins_over_query_test_impl)
60+
61+
def _parse_git_url_bare_remote_test_impl(ctx):
62+
env = unittest.begin(ctx)
63+
result = parse_git_url("https://github.com/user/repo.git")
64+
asserts.equals(env, {"remote": "https://github.com/user/repo.git"}, result)
65+
return unittest.end(env)
66+
67+
parse_git_url_bare_remote_test = unittest.make(_parse_git_url_bare_remote_test_impl)
68+
69+
def _git_to_http_archive_commit_test_impl(ctx):
70+
env = unittest.begin(ctx)
71+
result = try_git_to_http_archive({
72+
"remote": "git+https://github.com/user/repo.git",
73+
"commit": "c7076a0c6e34d7b2fa4e0ecd7ba4b8e9d3d9e0f1",
74+
})
75+
asserts.equals(env, {
76+
"url": "https://github.com/user/repo/archive/c7076a0c6e34d7b2fa4e0ecd7ba4b8e9d3d9e0f1.tar.gz",
77+
}, result)
78+
return unittest.end(env)
79+
80+
git_to_http_archive_commit_test = unittest.make(_git_to_http_archive_commit_test_impl)
81+
82+
def _git_to_http_archive_ref_test_impl(ctx):
83+
env = unittest.begin(ctx)
84+
85+
# Regression test: this branch used to read git_cfg["tag"], a key
86+
# parse_git_url never produces.
87+
result = try_git_to_http_archive({
88+
"remote": "https://github.com/user/repo",
89+
"ref": "refs/pull/64/head",
90+
})
91+
asserts.equals(env, {
92+
"url": "https://github.com/user/repo/archive/refs/pull/64/head.tar.gz",
93+
}, result)
94+
return unittest.end(env)
95+
96+
git_to_http_archive_ref_test = unittest.make(_git_to_http_archive_ref_test_impl)
97+
98+
def _git_to_http_archive_parsed_ref_url_test_impl(ctx):
99+
env = unittest.begin(ctx)
100+
result = try_git_to_http_archive(parse_git_url("git+https://github.com/user/repo.git?ref=refs%2Ftags%2Fv1.0.0"))
101+
asserts.equals(env, {
102+
"url": "https://github.com/user/repo/archive/refs/tags/v1.0.0.tar.gz",
103+
}, result)
104+
return unittest.end(env)
105+
106+
git_to_http_archive_parsed_ref_url_test = unittest.make(_git_to_http_archive_parsed_ref_url_test_impl)
107+
108+
def _git_to_http_archive_non_github_test_impl(ctx):
109+
env = unittest.begin(ctx)
110+
result = try_git_to_http_archive({
111+
"remote": "https://gitlab.com/user/repo.git",
112+
"commit": "c7076a0c6e34d7b2fa4e0ecd7ba4b8e9d3d9e0f1",
113+
})
114+
asserts.equals(env, None, result)
115+
return unittest.end(env)
116+
117+
git_to_http_archive_non_github_test = unittest.make(_git_to_http_archive_non_github_test_impl)
118+
119+
def _git_to_http_archive_no_rev_test_impl(ctx):
120+
env = unittest.begin(ctx)
121+
result = try_git_to_http_archive({"remote": "https://github.com/user/repo.git"})
122+
asserts.equals(env, None, result)
123+
return unittest.end(env)
124+
125+
git_to_http_archive_no_rev_test = unittest.make(_git_to_http_archive_no_rev_test_impl)
126+
127+
def git_utils_test_suite():
128+
unittest.suite(
129+
"git_utils_tests",
130+
ensure_ref_test,
131+
parse_git_url_fragment_commit_test,
132+
parse_git_url_query_commit_test,
133+
parse_git_url_query_ref_test,
134+
parse_git_url_fragment_wins_over_query_test,
135+
parse_git_url_bare_remote_test,
136+
git_to_http_archive_commit_test,
137+
git_to_http_archive_ref_test,
138+
git_to_http_archive_parsed_ref_url_test,
139+
git_to_http_archive_non_github_test,
140+
git_to_http_archive_no_rev_test,
141+
)

0 commit comments

Comments
 (0)