Skip to content

Commit e4cd3fb

Browse files
committed
Add support for yank/unyank
Assisted-By: claude-opus-4.6
1 parent 3bb452d commit e4cd3fb

13 files changed

Lines changed: 551 additions & 58 deletions

File tree

.ci/scripts/check_requirements.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import warnings
1010
from packaging.requirements import Requirement
1111

12-
1312
CHECK_MATRIX = [
1413
("pyproject.toml", True, True, True),
1514
("requirements.txt", True, True, True),

.ci/scripts/collect_changes.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from git import GitCommandError, Repo
1818
from packaging.version import parse as parse_version
1919

20-
2120
PYPI_PROJECT = "pulp_rust"
2221

2322
# Read Towncrier settings

.github/workflows/scripts/stage-changelog-for-default-branch.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from git import Repo
1313
from git.exc import GitCommandError
1414

15-
1615
helper = textwrap.dedent(
1716
"""\
1817
Stage the changelog for a release on main branch.

pulp_rust/app/migrations/0001_initial.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,8 @@ class Migration(migrations.Migration):
1919
fields=[
2020
('content_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.content')),
2121
('name', models.CharField(db_index=True, max_length=255)),
22-
('vers', models.CharField(max_length=64)),
23-
('cksum', models.CharField(max_length=64)),
24-
('yanked', models.BooleanField(default=False)),
22+
('vers', models.CharField(db_index=True, max_length=64)),
23+
('cksum', models.CharField(db_index=True, max_length=64)),
2524
('features', models.JSONField(blank=True, default=dict)),
2625
('features2', models.JSONField(blank=True, default=dict, null=True)),
2726
('links', models.CharField(blank=True, max_length=255, null=True)),
@@ -87,4 +86,18 @@ class Migration(migrations.Migration):
8786
'indexes': [models.Index(fields=['content', 'kind'], name='rust_rustde_content_a46e30_idx'), models.Index(fields=['name'], name='rust_rustde_name_6a2db4_idx')],
8887
},
8988
),
89+
migrations.CreateModel(
90+
name='RustPackageYank',
91+
fields=[
92+
('content_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.content')),
93+
('name', models.CharField(db_index=True, max_length=255)),
94+
('vers', models.CharField(db_index=True, max_length=64)),
95+
('_pulp_domain', models.ForeignKey(default=pulpcore.app.util.get_domain_pk, on_delete=django.db.models.deletion.PROTECT, to='core.domain')),
96+
],
97+
options={
98+
'default_related_name': '%(app_label)s_%(model_name)s',
99+
'unique_together': {('name', 'vers', '_pulp_domain')},
100+
},
101+
bases=('core.content',),
102+
),
90103
]

pulp_rust/app/migrations/0002_alter_rustcontent_cksum_alter_rustcontent_vers.py

Lines changed: 0 additions & 23 deletions
This file was deleted.

pulp_rust/app/models.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ class RustContent(Content):
5252
name: The package name (crate name)
5353
vers: The semantic version string (SemVer 2.0.0)
5454
cksum: SHA256 checksum of the .crate file (tarball)
55-
yanked: Whether this version has been yanked (removed from normal use)
5655
features: JSON object mapping feature names to their dependencies
5756
features2: JSON object with extended feature syntax support
5857
links: Value from Cargo.toml manifest 'links' field (for native library linking)
@@ -72,11 +71,6 @@ class RustContent(Content):
7271
# SHA256 checksum (hex-encoded) of the .crate tarball file for verification
7372
cksum = models.CharField(max_length=64, blank=False, null=False, db_index=True)
7473

75-
# Indicates if this version has been yanked (deprecated/removed from use)
76-
# Yanked versions can still be used by existing Cargo.lock files but won't be selected
77-
# for new builds
78-
yanked = models.BooleanField(default=False)
79-
8074
# Feature flags and compatibility
8175
# Maps feature names to lists of features/dependencies they enable
8276
# Example: {"default": ["std"], "std": [], "serde": ["dep:serde"]}
@@ -244,14 +238,36 @@ class Meta:
244238
default_related_name = "%(app_label)s_%(model_name)s"
245239

246240

241+
class RustPackageYank(Content):
242+
"""
243+
A marker content type indicating a crate version is yanked in a repository.
244+
245+
This is a per-repository marker: its presence in a repository version means
246+
the (name, vers) pair is yanked in that repository. Its absence means it is
247+
not yanked. This allows yanked status to vary across repositories without
248+
mutating the global RustContent object.
249+
"""
250+
251+
TYPE = "rust_yank"
252+
repo_key_fields = ("name", "vers")
253+
254+
name = models.CharField(max_length=255, db_index=True)
255+
vers = models.CharField(max_length=64, db_index=True)
256+
_pulp_domain = models.ForeignKey("core.Domain", default=get_domain_pk, on_delete=models.PROTECT)
257+
258+
class Meta:
259+
default_related_name = "%(app_label)s_%(model_name)s"
260+
unique_together = (("name", "vers", "_pulp_domain"),)
261+
262+
247263
class RustRepository(Repository):
248264
"""
249265
A Repository for RustContent.
250266
"""
251267

252268
TYPE = "rust"
253269

254-
CONTENT_TYPES = [RustContent]
270+
CONTENT_TYPES = [RustContent, RustPackageYank]
255271
REMOTE_TYPES = [RustRemote]
256272
PULL_THROUGH_SUPPORTED = True
257273

pulp_rust/app/serializers.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,6 @@ class RustContentSerializer(core_serializers.SingleArtifactContentSerializer):
135135
help_text=_("Extended feature syntax support (newer registry format)"),
136136
)
137137

138-
yanked = serializers.BooleanField(
139-
default=False,
140-
required=False,
141-
help_text=_("Whether this version has been yanked (removed from normal use)"),
142-
)
143-
144138
links = serializers.CharField(
145139
allow_null=True,
146140
required=False,
@@ -189,7 +183,6 @@ class Meta:
189183
"cksum",
190184
"features",
191185
"features2",
192-
"yanked",
193186
"links",
194187
"v",
195188
"rust_version",
@@ -265,6 +258,19 @@ class Meta:
265258
model = models.RustDistribution
266259

267260

261+
class YankSerializer(serializers.Serializer):
262+
"""Serializer for yank/unyank operations on a repository."""
263+
264+
name = serializers.CharField(
265+
required=True,
266+
help_text=_("The crate name to yank or unyank."),
267+
)
268+
vers = serializers.CharField(
269+
required=True,
270+
help_text=_("The crate version to yank or unyank."),
271+
)
272+
273+
268274
class RepositoryAddCachedContentSerializer(
269275
core_serializers.ValidateFieldsMixin, serializers.Serializer
270276
):

pulp_rust/app/tasks/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
from .synchronizing import synchronize # noqa
22
from .streaming import add_cached_content_to_repository # noqa
3+
from .yanking import ayank_package, aunyank_package # noqa

pulp_rust/app/tasks/streaming.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
11
import datetime
22

3-
from asgiref.sync import sync_to_async
4-
53
from pulpcore.plugin.models import Content, ContentArtifact, RemoteArtifact
6-
from pulpcore.plugin.tasking import add_and_remove
74

85
from pulp_rust.app.models import RustRemote, RustRepository
96

107

11-
async def aadd_and_remove(*args, **kwargs):
12-
return await sync_to_async(add_and_remove)(*args, **kwargs)
13-
14-
15-
# TODO: look at the version in models/repository.py
8+
# Note: pulpcore's Repository.pull_through_add_content() is a different pattern — it adds a
9+
# single content unit immediately during streaming. This task instead does a batch "catch up",
10+
# finding all content cached since the last repo version and adding them in one new version.
1611
def add_cached_content_to_repository(repository_pk=None, remote_pk=None):
1712
"""
1813
Create a new repository version by adding content that was cached by pulpcore-content when

pulp_rust/app/tasks/yanking.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from pulpcore.plugin.tasking import aadd_and_remove
2+
3+
from pulp_rust.app.models import RustContent, RustPackageYank, RustRepository
4+
5+
6+
async def ayank_package(repository_pk, name, vers):
7+
"""
8+
Yank a package version in a repository by adding a RustPackageYank marker.
9+
10+
Creates a new repository version with the yank marker added.
11+
"""
12+
repository = await RustRepository.objects.aget(pk=repository_pk)
13+
latest = await repository.alatest_version()
14+
15+
# Verify the package version exists in this repository
16+
exists = await RustContent.objects.filter(pk__in=latest.content, name=name, vers=vers).aexists()
17+
if not exists:
18+
raise ValueError(f"Package {name}=={vers} not found in repository")
19+
20+
# Check if already yanked
21+
already_yanked = await RustPackageYank.objects.filter(
22+
pk__in=latest.content, name=name, vers=vers
23+
).aexists()
24+
if already_yanked:
25+
return # Already yanked, no-op
26+
27+
yank_marker, _ = await RustPackageYank.objects.aget_or_create(
28+
name=name, vers=vers, _pulp_domain_id=repository.pulp_domain_id
29+
)
30+
31+
await aadd_and_remove(
32+
repository_pk=repository.pk,
33+
add_content_units=[yank_marker.pk],
34+
remove_content_units=[],
35+
)
36+
37+
38+
async def aunyank_package(repository_pk, name, vers):
39+
"""
40+
Unyank a package version by removing its RustPackageYank marker.
41+
42+
Creates a new repository version with the yank marker removed.
43+
"""
44+
repository = await RustRepository.objects.aget(pk=repository_pk)
45+
latest = await repository.alatest_version()
46+
47+
yank_marker = await RustPackageYank.objects.filter(
48+
pk__in=latest.content, name=name, vers=vers
49+
).afirst()
50+
51+
if yank_marker is None:
52+
return # Not yanked, no-op
53+
54+
await aadd_and_remove(
55+
repository_pk=repository.pk,
56+
add_content_units=[],
57+
remove_content_units=[yank_marker.pk],
58+
)

0 commit comments

Comments
 (0)