Skip to content

Commit f8355c3

Browse files
committed
commit: build the historical repo over an already filled directory
The repo of pre-change packages was created empty, then had the old files extracted into it, then told about them through notify_add_package. That is pkgcore's backchannel for repositories mutated in place, an internal never meant for an immutable UnconfiguredTree, and it only ever worked because nothing happened to read the repo between the two steps. Extract the packages before the repo is built instead, so there is nothing to invalidate and no internal to reach for. HistoricalRepo is left holding its tmpdir, and the extraction becomes a plain helper the summary calls for every package it is about to look at. Resolves: #172 Signed-off-by: Arthur Zamarin <arthurzam@gentoo.org>
1 parent 5e40695 commit f8355c3

3 files changed

Lines changed: 63 additions & 38 deletions

File tree

‎NEWS.rst‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ pkgdev 0.2.18 (unreleased)
2323
version the stablereq check considers due for stabilization if possible
2424
(Arthur Zamarin)
2525

26+
**pkgdev commit:**
27+
28+
- commit: the repository of pre-change packages is now built over an already
29+
filled directory, instead of being populated underneath a live repository
30+
and having its caches invalidated through a pkgcore internal meant for
31+
mutable repositories (Arthur Zamarin, #172)
32+
2633
**pkgdev showkw:**
2734

2835
- showkw: drop the vendored copy of tabulate in favor of a dependency on

‎src/pkgdev/scripts/pkgdev_commit.py‎

Lines changed: 23 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import tempfile
1010
import textwrap
1111
from collections import UserDict, defaultdict, deque
12+
from collections.abc import Iterable
1213
from dataclasses import dataclass
1314
from enum import Enum
1415
from functools import partial
@@ -292,35 +293,26 @@ def __call__(self, parser, namespace, values, option_string=None):
292293
class HistoricalRepo(UnconfiguredTree):
293294
"""Repository of historical packages stored in a temporary directory."""
294295

295-
def __init__(self, parent_repo, tmpdir, *args, **kwargs):
296-
self.__parent_repo = parent_repo
296+
def __init__(self, tmpdir, *args, **kwargs):
297297
# cache tmpdir to delay cleanup until exit
298298
self.__tmpdir = tmpdir
299299
super().__init__(*args, **kwargs)
300300

301-
def add_pkgs(self, pkgs):
302-
"""Update the repo with a given sequence of packages."""
303-
self._populate(pkgs)
304-
# notify the repo object that new pkgs were added
305-
for pkg in pkgs:
306-
self.notify_add_package(pkg)
307-
308-
def _populate(self, pkgs):
309-
"""Populate the repo with a given sequence of historical packages."""
310-
paths = {pkg.key for pkg in pkgs}
311-
old_files = subprocess.Popen(
312-
["git", "archive", "HEAD"] + list(paths),
313-
stdout=subprocess.PIPE,
314-
stderr=subprocess.PIPE,
315-
cwd=self.__parent_repo.location,
316-
)
317-
if old_files.poll(): # pragma: no cover
318-
error = old_files.stderr.read().decode().strip()
319-
raise Exception(f"failed populating archive repo: {error}")
320-
with tarfile.open(mode="r|", fileobj=old_files.stdout) as tar:
321-
# see filter in https://docs.python.org/3/library/tarfile.html#tarfile.TarFile.extractall
322-
# Whilst we trust git archive, we still leave the basic protections on.
323-
tar.extractall(path=self.location, filter="data")
301+
302+
def _extract_historical(repo, path: str, keys: Iterable[str]):
303+
"""Extract the packages as they are in HEAD into a directory."""
304+
old_files = subprocess.Popen(
305+
["git", "archive", "HEAD", *keys],
306+
stdout=subprocess.PIPE,
307+
stderr=subprocess.PIPE,
308+
cwd=repo.location,
309+
)
310+
if old_files.poll(): # pragma: no cover
311+
error = old_files.stderr.read().decode().strip()
312+
raise RuntimeError(f"failed populating archive repo: {error}")
313+
with tarfile.open(mode="r|", fileobj=old_files.stdout) as tar:
314+
# Whilst we trust git archive, we still leave the basic protections on.
315+
tar.extractall(path=path, filter="data")
324316

325317

326318
# GLEP 81 categories, mapped to what their packages allocate
@@ -349,13 +341,14 @@ class ChangeSummary:
349341
# mapping of handled statuses to functions
350342
status_funcs = None
351343

352-
def __init__(self, options):
344+
def __init__(self, options, changes: "Iterable[PkgChange]"):
353345
self.options = options
354346
self.repo = options.repo
347+
self.changes = {x.atom: x for x in changes}
355348

356349
@jit_attr
357350
def old_repo(self):
358-
"""Create a repository of historical packages removed from git."""
351+
"""Create a repository of the changed packages as they are in HEAD."""
359352
tmpdir = tempfile.TemporaryDirectory()
360353
repo_dir = tmpdir.name
361354

@@ -367,8 +360,10 @@ def old_repo(self):
367360
with open(pjoin(repo_dir, "profiles", "repo_name"), "w") as f:
368361
f.write(f"{self.repo.repo_id}-old\n")
369362

363+
_extract_historical(self.repo, repo_dir, {atom.key for atom in self.changes})
364+
370365
repo_config = RepoConfig(repo_dir)
371-
tree_cls = partial(HistoricalRepo, self.repo, tmpdir)
366+
tree_cls = partial(HistoricalRepo, tmpdir)
372367
return tree(self.options.config, repo_config, tree_cls=tree_cls)
373368

374369
def __str__(self):
@@ -387,16 +382,11 @@ class MetadataSummary(ChangeSummary):
387382

388383
status_funcs: ClassVar[dict[str, callable]] = {}
389384

390-
def __init__(self, options, changes):
391-
super().__init__(options)
392-
self.changes = {x.atom: x for x in changes}
393-
394385
@change("M")
395386
def modify(self):
396387
"""Generate summaries for modify actions."""
397388
atom = next(iter(self.changes))
398389
pkgs = self.repo.match(atom)
399-
self.old_repo.add_pkgs(pkgs)
400390
try:
401391
old_pkg = self.old_repo.match(atom)[0]
402392
new_pkg = pkgs[0]
@@ -443,10 +433,6 @@ class PkgSummary(ChangeSummary):
443433

444434
status_funcs: ClassVar[dict[str, callable]] = {}
445435

446-
def __init__(self, options, changes):
447-
super().__init__(options)
448-
self.changes = {x.atom: x for x in changes}
449-
450436
@jit_attr
451437
def versions(self):
452438
"""Tuple of package versions that were changed."""
@@ -535,7 +521,6 @@ def modify(self):
535521
summaries = set()
536522
for atom in self.changes:
537523
pkgs = self.repo.match(atom)
538-
self.old_repo.add_pkgs(pkgs)
539524
try:
540525
old_pkg = self.old_repo.match(atom)[0]
541526
new_pkg = pkgs[0]

‎tests/scripts/test_pkgdev_commit.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@
66
from functools import partial
77
from io import StringIO
88
from os.path import join as pjoin
9+
from types import SimpleNamespace
910
from unittest.mock import patch
1011

1112
import pytest
13+
from pkgcore.ebuild.atom import atom as atom_cls
1214
from snakeoil.contexts import os_environ
1315

1416
from pkgdev.mangle import copyright_regex, keywords_regex
17+
from pkgdev.scripts import pkgdev_commit as commit
1518
from pkgdev.scripts import run
1619

1720

@@ -1194,3 +1197,33 @@ def test_failed_manifest(self, capsys, repo, make_git_repo):
11941197
out, err = capsys.readouterr()
11951198
assert not err
11961199
assert out == " * cat/pkg-1: invalid EAPI '-1'\n"
1200+
1201+
1202+
class TestHistoricalRepo:
1203+
def mk(self, repo, git_repo, tool, pkgs):
1204+
for cpvstr in pkgs:
1205+
repo.create_ebuild(cpvstr, description="old")
1206+
git_repo.add_all("initial")
1207+
for cpvstr in pkgs:
1208+
repo.create_ebuild(cpvstr, description="new")
1209+
with chdir(repo.location):
1210+
options, _ = tool.parse_args(["commit", "-a"])
1211+
changes = [SimpleNamespace(atom=atom_cls(f"={cpvstr}"), status="M") for cpvstr in pkgs]
1212+
return commit.MetadataSummary(options, changes)
1213+
1214+
def test_populated_without_being_told(self, repo, make_git_repo, tool):
1215+
git_repo = make_git_repo(repo.location)
1216+
summary = self.mk(repo, git_repo, tool, ["cat/pkg-1"])
1217+
# no add_pkgs call: the directory is filled before the repo is built
1218+
old_pkg = summary.old_repo.match(atom_cls("=cat/pkg-1"))[0]
1219+
assert old_pkg.description == "old"
1220+
1221+
def test_covers_every_changed_package(self, repo, make_git_repo, tool):
1222+
git_repo = make_git_repo(repo.location)
1223+
summary = self.mk(repo, git_repo, tool, ["cat/a-1", "cat/b-1"])
1224+
# a second package must not need the repo's caches invalidated
1225+
for cpvstr in ("cat/a-1", "cat/b-1"):
1226+
assert summary.old_repo.match(atom_cls(f"={cpvstr}"))[0].description == "old"
1227+
1228+
def test_no_reliance_on_the_mutable_repo_backchannel(self):
1229+
assert not hasattr(commit.HistoricalRepo, "add_pkgs")

0 commit comments

Comments
 (0)