Skip to content
Snippets Groups Projects
classes.py 8.54 KiB
import dataclasses
import enum
import typing

import debian.debian_support
import gitlab.v4.objects

# FIXME: handle renamed projects without having to hardcode them here
RENAMED = {
    "dbus-cxx": "dbus-c++",
    "gnome-settings-daemon-data": "gnome-settings-daemon",
    "gtk-2.0": "gtk+2.0",
    "gtk-3.0": "gtk+3.0",
    "libsigcxx-2.0": "libsigc++-2.0",
    "libxmlxx2.6": "libxml++2.6",
}


class Report(enum.Enum):
    def _generate_next_value_(name, start, count, last_values):
        return name.lower().replace("_", "-")

    APT_PACKAGE_BINARIES_AMBIGUOUS = enum.auto()
    APT_PACKAGE_BINARIES_MISSING = enum.auto()
    APT_PACKAGE_MISSING = enum.auto()
    APT_PACKAGE_MISSING_BUT_ON_OBS = enum.auto()
    APT_PACKAGE_SOURCE_AMBIGUOUS = enum.auto()
    APT_PACKAGE_SOURCE_MISSING = enum.auto()
    APT_PACKAGE_SOURCE_VERSION_MISMATCH_BINARIES = enum.auto()
    APT_PACKAGE_VERSION_MISMATCH_OBS = enum.auto()
    GIT_BRANCH_COMPONENT_MISSING = enum.auto()
    GIT_BRANCH_FOLDED_BUT_NOT_REMOVED = enum.auto()
    GIT_BRANCH_HAS_AMBIGUOUS_TAGS = enum.auto()
    GIT_BRANCH_LICENSING_REPORT_MISSING = enum.auto()
    GIT_BRANCH_MISSING_BUT_ON_OBS = enum.auto()
    GIT_BRANCH_NOT_POINTING_TO_TAGGED_COMMIT = enum.auto()
    GIT_BRANCH_PIPELINE_FAILED = enum.auto()
    GIT_CHANNEL_LAGGING = enum.auto()
    GIT_PROJECT_MISSING = enum.auto()
    GIT_UPSTREAM_BRANCH_DROPPED = enum.auto()
    GIT_UPSTREAM_BRANCH_NOT_MERGED = enum.auto()
    OBS_PACKAGE_AMBIGUOUS = enum.auto()
    OBS_PACKAGE_BUILD_FAILED = enum.auto()
    OBS_PACKAGE_MISSING_BUT_IN_GIT = enum.auto()
    OBS_PACKAGE_MISSING_BUT_ON_APT = enum.auto()
    OBS_PACKAGE_MISSING_BUT_PUBLISHED = enum.auto()
    OBS_PACKAGE_VERSION_MISMATCH = enum.auto()
    UPDATE_AVAILABLE = enum.auto()
    UPDATE_AVAILABLE_MAINLINE = enum.auto()


@dataclasses.dataclass
class UpstreamPackage:
    name: str
    version: debian.debian_support.Version
    source: str
    component: str

    @classmethod
    def to_yaml(cls, dumper, data):
        d = {
            "name": data.name,
            "version": data.version,
            "source": data.source,
            "component": data.component,
        }
        return dumper.represent_dict(d)


@dataclasses.dataclass
class UpstreamBinaryPackage:
    name: str
    version: debian.debian_support.Version
    pkg_source: str
    repo_source: str
    component: str
    architectures: typing.List[str]

    @classmethod
    def to_yaml(cls, dumper, data):
        d = {
            data.name: {
                data.version: data.architectures,
            },
        }
        return dumper.represent_dict(d)

    def append_arch(self, arch):
        if arch not in self.architectures:
            self.architectures.append(arch)


@dataclasses.dataclass
class UpstreamSource:
    destination: str
    distribution: str
    release: str
    suite: str
    base: str
    url_template: str
    components: typing.List[str]

    @staticmethod
    def load_source_definitions(definitions):
        sources = {}
        keys = set(UpstreamSource.__dataclass_fields__.keys()) - {"destination"}
        for destination, definition in definitions.items():
            subset = {k: definition[k] for k in keys}
            sources[destination] = UpstreamSource(destination=destination, **subset)
        return sources

    @property
    def url(self):
        data = dataclasses.asdict(self)
        return self.url_template.format(**data)

    @property
    def component_urls(self):
        base_url = self.url
        urls = {component: f"{base_url}/{component}" for component in self.components}
        return urls

    @classmethod
    def to_yaml(cls, dumper, data):
        d = {
            "destination": data.destination,
            "distribution": data.distribution,
            "release": data.release,
            "suite": data.suite,
            "base": data.base,
            "url_template": data.url_template,
            "components": data.components,
        }
        return dumper.represent_dict(d)


@dataclasses.dataclass
class GitTag:
    name: str
    commit_id: str
    descendant_branches: typing.List[str] = None

    @classmethod
    def to_yaml(cls, dumper, data):
        d = dataclasses.asdict(data)
        return dumper.represent_dict(d)

    @staticmethod
    def version(name):
        v = name.split("/", 1)[1]
        # see https://dep-team.pages.debian.net/deps/dep14/
        v = v.replace("%", ":")
        v = v.replace("_", "~")
        v = v.replace("#", "")
        return debian.debian_support.Version(v)


@dataclasses.dataclass
class GitBranch:
    name: str
    commit_id: str
    component: str = ""
    tags: typing.List[str] = None
    descendant_branches: typing.List[str] = None
    pipeline: typing.Dict[str, str] = None
    license_report: bool = False

    @classmethod
    def to_yaml(cls, dumper, data):
        d = {
            "name": data.name,
            "version": data.version,
            "commit_id": data.commit_id,
            "component": data.component,
            "license_report": data.license_report,
            "tags": data.tags,
            "descendant_branches": data.descendant_branches,
        }
        if data.pipeline:
            d["pipeline"] = data.pipeline
        return dumper.represent_dict(d)

    @property
    def version(self):
        versions = (GitTag.version(t) for t in self.tags if self.is_version_tag(t))
        version = max(versions, default=None)
        return version

    def is_version_tag(self, tagname):
        prefix = self.name.split("/", 1)[0] + "/"
        return tagname.startswith(prefix)


@dataclasses.dataclass
class GitProject:
    project: gitlab.v4.objects.Project
    branches: typing.Dict[str, GitBranch] = None
    tags: typing.Dict[str, GitTag] = None

    @classmethod
    def to_yaml(cls, dumper, data):
        d = {
            "id": data.project.id,
            "web_url": data.project.web_url,
            "path_with_namespace": data.path_with_namespace,
            "path": data.path,
            "branches": data.branches,
            "tags": data.tags,
        }
        return dumper.represent_dict(d)

    @property
    def path_with_namespace(self):
        path = self.project.path_with_namespace
        return path

    @property
    def path(self):
        path = self.project.path
        return path

    @property
    def packagename(self):
        name = self.path
        pkgname = RENAMED.get(name, name)
        return pkgname


@dataclasses.dataclass
class OBSEntry:
    api_url: str
    project: str
    name: str
    results: typing.Dict[str, typing.Dict[str, typing.Dict[str, str]]]
    files: typing.List[str] = None

    @classmethod
    def to_yaml(cls, dumper, data):
        d = {
            "project": data.project,
            "web_url": data.web_url,
            "version_without_epoch": data.version_without_epoch,
            "files": data.files,
        }
        if data.results:
            d["results"] = data.results
        return dumper.represent_dict(d)

    @property
    def id(self):
        return f"{self.project}/{self.name}"

    @property
    def version_without_epoch(self):
        suffix = ".dsc"
        dsc = max((f for f in self.files if f.endswith(suffix)), default=None)
        if not dsc:
            return None
        v = dsc[: -len(suffix)].split("_")[1] if dsc and "_" in dsc else None
        return debian.debian_support.Version(v)

    @property
    def web_url(self):
        return f"{self.api_url}/package/show/{self.project}/{self.name}"


@dataclasses.dataclass
class OBSProject:
    api_url: str
    name: str

    @classmethod
    def to_yaml(cls, dumper, data):
        d = dict(
            name=data.name,
            release=data.release,
            component=data.component,
            section=data.section,
            web_url=data.web_url,
        )
        return dumper.represent_dict(d)

    @property
    def release(self):
        s = self.name.split(":")[:2]
        return "/".join(s)

    @property
    def component(self):
        # for instance, `target` in `apertis:v2020:security:target:snapshots`
        s = self.name.split(":")
        if s[-1] == "snapshots":
            return s[-2]
        return s[-1]

    @property
    def section(self):
        # for instance, `security` in `apertis:v2020:security:target:snapshots`
        s = self.name.split(":")
        parts = len(s)
        if s[-1] == "snapshots":
            parts -= 1
        if parts == 3:
            # for instance `apertis:v2020:target`
            return "main"
        return s[2]

    @property
    def web_url(self):
        return f"{self.api_url}/project/show/{self.name}"

    def __repr__(self):
        return self.name