Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mask: support auto filing of last-rite bug & PMASKED bugs #187

Merged
merged 3 commits into from
May 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
experimental: true
- os: macos-latest
python-version: '3.11'
experimental: false
experimental: true
fail-fast: false

steps:
Expand Down
4 changes: 3 additions & 1 deletion data/share/bash-completion/completions/pkgdev
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,12 @@ _pkgdev() {
-r --rites
-b --bug
--email
--api-key
--file-bug
"

case "${prev}" in
-[rb] | --rites | --bugs)
-[rb] | --rites | --bugs | --api-key)
COMPREPLY=()
;;
*)
Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[build-system]
requires = [
"flit_core >=3.8,<4",
"snakeoil ~=0.10.5",
"snakeoil ~=0.10.8",
]
build-backend = "py_build"
backend-path = ["."]
Expand All @@ -27,9 +27,9 @@ classifiers = [
dynamic = ["version"]

dependencies = [
"snakeoil~=0.10.5",
"snakeoil~=0.10.8",
"pkgcore~=0.12.23",
"pkgcheck~=0.10.25",
"pkgcheck~=0.10.30",
]

[project.optional-dependencies]
Expand Down
151 changes: 116 additions & 35 deletions src/pkgdev/scripts/pkgdev_mask.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import json
import os
import re
import shlex
import subprocess
import tempfile
import textwrap
import urllib.request as urllib
from collections import deque
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
Expand All @@ -20,13 +22,14 @@
from snakeoil.strings import pluralism

from .. import git
from .argparsers import cwd_repo_argparser, git_repo_argparser
from .argparsers import cwd_repo_argparser, git_repo_argparser, BugzillaApiKey

mask = arghparse.ArgumentParser(
prog="pkgdev mask",
description="mask packages",
parents=(cwd_repo_argparser, git_repo_argparser),
)
BugzillaApiKey.mangle_argparser(mask)
mask.add_argument(
"targets",
metavar="TARGET",
Expand Down Expand Up @@ -81,11 +84,26 @@
``x11-misc/xdg-utils`` package.
""",
)
mask_opts.add_argument(
"--file-bug",
action="store_true",
help="file a last-rite bug",
docs="""
Files a last-rite bug for the masked package, which blocks listed
reference bugs. ``PMASKED`` keyword is added all all referenced bugs.
""",
)


@mask.bind_final_check
def _mask_validate(parser, namespace):
atoms = []
atoms = set()
maintainers = set()

if not namespace.rites and namespace.file_bug:
mask.error("bug filing requires last rites")

Check warning on line 104 in src/pkgdev/scripts/pkgdev_mask.py

View check run for this annotation

Codecov / codecov/patch

src/pkgdev/scripts/pkgdev_mask.py#L104

Added line #L104 was not covered by tests
if namespace.file_bug and not namespace.api_key:
mask.error("bug filing requires a Bugzilla API key")

Check warning on line 106 in src/pkgdev/scripts/pkgdev_mask.py

View check run for this annotation

Codecov / codecov/patch

src/pkgdev/scripts/pkgdev_mask.py#L106

Added line #L106 was not covered by tests

if namespace.email and not namespace.rites:
mask.error("last rites required for email support")
Expand All @@ -96,23 +114,30 @@
restrict = namespace.repo.path_restrict(x)
pkg = next(namespace.repo.itermatch(restrict))
atom = pkg.versioned_atom
maintainers.update(maintainer.email for maintainer in pkg.maintainers)
else:
try:
atom = atom_cls(x)
except MalformedAtom:
mask.error(f"invalid atom: {x!r}")
if not namespace.repo.match(atom):
if pkgs := namespace.repo.match(atom):
maintainers.update(
maintainer.email for pkg in pkgs for maintainer in pkg.maintainers
)
else:
mask.error(f"no repo matches: {x!r}")
atoms.append(atom)
atoms.add(atom)
else:
restrict = namespace.repo.path_restrict(os.getcwd())
# repo, category, and package level restricts
if len(restrict) != 3:
mask.error("not in a package directory")
pkg = next(namespace.repo.itermatch(restrict))
atoms.append(pkg.unversioned_atom)
atoms.add(pkg.unversioned_atom)
maintainers.update(maintainer.email for maintainer in pkg.maintainers)

namespace.atoms = sorted(atoms)
namespace.maintainers = sorted(maintainers) or ["[email protected]"]


@dataclass(frozen=True)
Expand Down Expand Up @@ -208,38 +233,24 @@
return "".join(self.header) + "\n\n".join(map(str, self.masks))


def get_comment(bugs, rites: int):
def get_comment():
"""Spawn editor to get mask comment."""
tmp = tempfile.NamedTemporaryFile(mode="w")
summary = []
if rites:
summary.append(f"Removal on {datetime.now(timezone.utc) + timedelta(days=rites):%Y-%m-%d}.")
if bugs:
# Bug(s) #A, #B, #C
bug_list = ", ".join(f"#{b}" for b in bugs)
s = pluralism(bugs)
summary.append(f"Bug{s} {bug_list}.")
if summary := " ".join(summary):
tmp.write(f"\n{summary}")
tmp.write(
textwrap.dedent(
"""

# Please enter the mask message. Lines starting with '#' will be ignored.
#
# - Best last rites (removal) practices -
# If last-rite was requested, it would be added automatically.
#
# Include the following info:
# a) reason for masking
# b) bug # for the removal (and yes you should have one)
# c) date of removal (either the date or "in x days")
# For rules on writing mask messages, see GLEP-84:
# https://glep.gentoo.org/glep-0084.html
#
# Example:
#
# Masked for removal in 30 days. Doesn't work
# with new libfoo. Upstream dead, gtk-1, smells
# Doesn't work with new libfoo. Upstream dead, gtk-1, smells
# funny.
# Bug #987654
"""
)
)
Expand All @@ -262,10 +273,71 @@
comment = "\n".join(comment).strip().splitlines()
if not comment:
mask.error("empty mask comment")

return comment


def message_removal_notice(bugs: list[int], rites: int):
summary = []
if rites:
summary.append(f"Removal on {datetime.now(timezone.utc) + timedelta(days=rites):%Y-%m-%d}.")
if bugs:
# Bug(s) #A, #B, #C
bug_list = ", ".join(f"#{b}" for b in bugs)
s = pluralism(bugs)
summary.append(f"Bug{s} {bug_list}.")
return " ".join(summary)


def file_last_rites_bug(options, message: str) -> int:
summary = f"{', '.join(map(str, options.atoms))}: removal"

Check warning on line 292 in src/pkgdev/scripts/pkgdev_mask.py

View check run for this annotation

Codecov / codecov/patch

src/pkgdev/scripts/pkgdev_mask.py#L292

Added line #L292 was not covered by tests
if len(summary) > 90 and len(options.atoms) > 1:
summary = f"{options.atoms[0]} and friends: removal"
request_data = dict(

Check warning on line 295 in src/pkgdev/scripts/pkgdev_mask.py

View check run for this annotation

Codecov / codecov/patch

src/pkgdev/scripts/pkgdev_mask.py#L294-L295

Added lines #L294 - L295 were not covered by tests
Bugzilla_api_key=options.api_key,
product="Gentoo Linux",
component="Current packages",
version="unspecified",
summary=summary,
description="\n".join([*message, "", "package list:", *map(str, options.atoms)]).strip(),
keywords=["PMASKED"],
assigned_to=options.maintainers[0],
cc=options.maintainers[1:] + ["[email protected]"],
deadline=(datetime.now(timezone.utc) + timedelta(days=options.rites)).strftime("%Y-%m-%d"),
blocks=list(options.bugs),
)
request = urllib.Request(

Check warning on line 308 in src/pkgdev/scripts/pkgdev_mask.py

View check run for this annotation

Codecov / codecov/patch

src/pkgdev/scripts/pkgdev_mask.py#L308

Added line #L308 was not covered by tests
url="https://bugs.gentoo.org/rest/bug",
data=json.dumps(request_data).encode("utf-8"),
method="POST",
headers={
"Content-Type": "application/json",
"Accept": "application/json",
},
)
with urllib.urlopen(request, timeout=30) as response:
reply = json.loads(response.read().decode("utf-8"))
return int(reply["id"])

Check warning on line 319 in src/pkgdev/scripts/pkgdev_mask.py

View check run for this annotation

Codecov / codecov/patch

src/pkgdev/scripts/pkgdev_mask.py#L318-L319

Added lines #L318 - L319 were not covered by tests


def update_bugs_pmasked(api_key: str, bugs: list[int]):
request_data = dict(

Check warning on line 323 in src/pkgdev/scripts/pkgdev_mask.py

View check run for this annotation

Codecov / codecov/patch

src/pkgdev/scripts/pkgdev_mask.py#L323

Added line #L323 was not covered by tests
Bugzilla_api_key=api_key,
ids=bugs,
keywords=dict(add=["PMASKED"]),
)
request = urllib.Request(

Check warning on line 328 in src/pkgdev/scripts/pkgdev_mask.py

View check run for this annotation

Codecov / codecov/patch

src/pkgdev/scripts/pkgdev_mask.py#L328

Added line #L328 was not covered by tests
url=f"https://bugs.gentoo.org/rest/bug/{bugs[0]}",
data=json.dumps(request_data).encode("utf-8"),
method="PUT",
headers={
"Content-Type": "application/json",
"Accept": "application/json",
},
)
with urllib.urlopen(request, timeout=30) as response:
return response.status == 200

Check warning on line 338 in src/pkgdev/scripts/pkgdev_mask.py

View check run for this annotation

Codecov / codecov/patch

src/pkgdev/scripts/pkgdev_mask.py#L338

Added line #L338 was not covered by tests


def send_last_rites_email(m: Mask, subject_prefix: str):
try:
atoms = ", ".join(map(str, m.atoms))
Expand Down Expand Up @@ -298,16 +370,25 @@
p = git.run("config", "user.email", stdout=subprocess.PIPE)
email = p.stdout.strip()

# initial args for Mask obj
mask_args = {
"author": author,
"email": email,
"date": today.strftime("%Y-%m-%d"),
"comment": get_comment(options.bugs, options.rites),
"atoms": options.atoms,
}

m = Mask(**mask_args)
message = get_comment()
if options.file_bug:
if bug_no := file_last_rites_bug(options, message):
out.write(out.fg("green"), f"filed bug https://bugs.gentoo.org/{bug_no}", out.reset)
out.flush()

Check warning on line 377 in src/pkgdev/scripts/pkgdev_mask.py

View check run for this annotation

Codecov / codecov/patch

src/pkgdev/scripts/pkgdev_mask.py#L376-L377

Added lines #L376 - L377 were not covered by tests
if not update_bugs_pmasked(options.api_key, options.bugs):
err.write(err.fg("red"), "failed to update referenced bugs", err.reset)
err.flush()
options.bugs.insert(0, bug_no)

Check warning on line 381 in src/pkgdev/scripts/pkgdev_mask.py

View check run for this annotation

Codecov / codecov/patch

src/pkgdev/scripts/pkgdev_mask.py#L379-L381

Added lines #L379 - L381 were not covered by tests
if removal := message_removal_notice(options.bugs, options.rites):
message.append(removal)

m = Mask(
author=author,
email=email,
date=today.strftime("%Y-%m-%d"),
comment=message,
atoms=options.atoms,
)
mask_file.add(m)
mask_file.write()

Expand Down
Loading