Skip to content

Commit 8105fee

Browse files
committed
bugs: add --dry-run
Signed-off-by: Arthur Zamarin <arthurzam@gentoo.org>
1 parent 2ac25e9 commit 8105fee

5 files changed

Lines changed: 141 additions & 9 deletions

File tree

‎NEWS.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ pkgdev 0.2.19 (unreleased)
1414
- bugs: fix a package being filed as its own dependency, when an unsolvable
1515
``|| ( ... )`` is answered by the package's own slot (Arthur Zamarin, #208)
1616

17+
- bugs: add ``--dry-run``, printing the bugs which would be filed and the
18+
updates to existing bugs instead of sending them. It needs no API key
19+
(Arthur Zamarin)
20+
1721
**pkgdev commit:**
1822

1923
- commit: a path argument sharing its name with a ref, e.g. ``pkgdev commit

‎data/share/bash-completion/completions/pkgdev‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ _pkgdev() {
282282
bugs)
283283
subcmd_options="
284284
--api-key
285+
--dry-run
285286
--edit-graph
286287
--auto-cc-arches
287288
--find-by-maintainer

‎data/share/zsh/site-functions/_pkgdev‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ case $state in
144144
$base_options \
145145
'--api-key[Bugzilla API key]:api key' \
146146
'--dot[path file where to save the graph in dot format]:file:_files' \
147+
'--dry-run[print the bugs instead of filing them]' \
147148
'--edit-graph[open editor to modify the graph before filing bugs]' \
148149
'--auto-cc-arches[automatically add CC-ARCHES for listed email addresses]:emails' \
149150
'--find-by-maintainer[collect all packages maintained by listed email addresses]:emails' \

‎src/pkgdev/scripts/pkgdev_bugs.py‎

Lines changed: 92 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Automatic bugs filer"""
22

33
import contextlib
4+
import dataclasses
45
import os
56
import shlex
67
import subprocess
@@ -11,7 +12,7 @@
1112
from collections import defaultdict
1213
from datetime import UTC, datetime
1314
from functools import partial
14-
from itertools import chain
15+
from itertools import chain, count
1516
from os.path import join as pjoin
1617
from typing import Literal
1718

@@ -158,6 +159,17 @@ def __call__(self, parser, namespace, values, option_string=None):
158159
"--dot",
159160
help="path file where to save the graph in dot format",
160161
)
162+
bugs.add_argument(
163+
"--dry-run",
164+
action="store_true",
165+
help="print the bugs instead of filing them",
166+
docs="""
167+
Do everything up to filing, including searching for open bugs, but
168+
print the bugs which would be filed and the updates to existing bugs,
169+
instead of sending them to Bugzilla. Bugs not yet filed are numbered
170+
``new-1``, ``new-2``, ... in the output. No API key is required.
171+
""",
172+
)
161173
bugs.add_argument(
162174
"--edit-graph",
163175
action="store_true",
@@ -304,6 +316,69 @@ def _naming(what: str):
304316
raise BugzillaError(f"{what}: {exc}") from exc
305317

306318

319+
class DryRunBugzilla:
320+
"""Bugzilla client which searches as usual, but prints any change instead."""
321+
322+
def __init__(self, bugzilla: Bugzilla, out: Formatter):
323+
self.bugzilla = bugzilla
324+
self.out = out
325+
self._new_ids = (f"new-{i}" for i in count(1))
326+
327+
def search(self, query: BugQuery):
328+
return self.bugzilla.search(query)
329+
330+
def _field(self, name: str, value):
331+
if value:
332+
self.out.write(f" {name + ':':<12}{value}")
333+
334+
def create(self, bug: NewBug) -> str:
335+
bugno = next(self._new_ids)
336+
out = self.out
337+
out.write(out.fg("green"), f"Would file {bugno}: ", out.reset, bug.summary)
338+
self._field("component", f"{bug.product} / {bug.component}")
339+
self._field("assignee", bug.assigned_to)
340+
self._field("CC", ", ".join(bug.cc))
341+
self._field("keywords", ", ".join(bug.keywords))
342+
self._field("depends on", ", ".join(sorted(map(str, bug.depends_on))))
343+
self._field("blocks", ", ".join(sorted(map(str, bug.blocks))))
344+
out.write(" packages:")
345+
for line in str(bug.package_list).splitlines():
346+
out.write(f" {line}")
347+
out.write(" description:")
348+
for line in bug.description.splitlines():
349+
out.write(f" {line}".rstrip())
350+
out.write()
351+
out.flush()
352+
return bugno
353+
354+
def update(self, bugs, update: BugUpdate):
355+
ids = [bugs] if isinstance(bugs, int) else list(bugs)
356+
out = self.out
357+
out.write(
358+
out.fg("yellow"),
359+
"Would update bugs " if len(ids) > 1 else "Would update bug ",
360+
out.reset,
361+
", ".join(map(str, ids)),
362+
)
363+
for field in dataclasses.fields(update):
364+
value = getattr(update, field.name)
365+
if isinstance(value, ListChange):
366+
value = " ".join(
367+
chain(
368+
(f"+{x}" for x in value.add),
369+
(f"-{x}" for x in value.remove),
370+
(f"={x}" for x in value.replace or ()),
371+
)
372+
)
373+
elif isinstance(value, tuple):
374+
value = ", ".join(map(str, value))
375+
elif value is not None:
376+
value = str(value)
377+
self._field(field.name.replace("_", " "), value)
378+
out.write()
379+
out.flush()
380+
381+
307382
class GraphNode:
308383
__slots__ = ("bugno", "category", "cc_arches", "edges", "obsoletes", "pkgs", "summary")
309384

@@ -1184,8 +1259,9 @@ def observe(node: GraphNode):
11841259
)
11851260
self.out.flush()
11861261

1262+
observer = None if isinstance(bugzilla, DryRunBugzilla) else observe
11871263
for node in self.starting_nodes:
1188-
node.file_bug(bugzilla, auto_cc_arches, block_bugs, self.modified_repo, observe)
1264+
node.file_bug(bugzilla, auto_cc_arches, block_bugs, self.modified_repo, observer)
11891265
self.obsolete_bugs(bugzilla)
11901266

11911267
def obsolete_bugs(self, bugzilla: Bugzilla):
@@ -1210,7 +1286,9 @@ def _load_from_stdin(out: Formatter):
12101286

12111287
@bugs.bind_main_func
12121288
def main(options, out: Formatter, err: Formatter):
1213-
if options.api_key is None:
1289+
if options.dry_run:
1290+
options.bugzilla = DryRunBugzilla(options.bugzilla, out)
1291+
elif options.api_key is None:
12141292
err.write(out.fg("red"), "No API key provided, exiting", out.reset)
12151293
return 1
12161294

@@ -1286,11 +1364,14 @@ def main(options, out: Formatter, err: Formatter):
12861364
pending = [node for node in d.nodes if node.bugno is None]
12871365
if not pending:
12881366
# no bugs to file, but obsoletions of matched bugs may still be pending
1289-
if (obsoletes := {bugno for node in d.nodes for bugno in node.obsoletes}) and userquery(
1290-
f"No bugs to file, still obsolete {len(obsoletes)} bugs?",
1291-
out,
1292-
err,
1293-
default_answer=False,
1367+
if (obsoletes := {bugno for node in d.nodes for bugno in node.obsoletes}) and (
1368+
options.dry_run
1369+
or userquery(
1370+
f"No bugs to file, still obsolete {len(obsoletes)} bugs?",
1371+
out,
1372+
err,
1373+
default_answer=False,
1374+
)
12941375
):
12951376
d.obsolete_bugs(options.bugzilla)
12961377
return 0
@@ -1302,7 +1383,9 @@ def main(options, out: Formatter, err: Formatter):
13021383
}
13031384
summary = ", ".join(f"{count} {suffix}" for suffix, count in counts.items() if count)
13041385

1305-
if not userquery(
1386+
if options.dry_run:
1387+
out.write(f"Dry run, would create {len(pending)} bugs ({summary}):\n")
1388+
elif not userquery(
13061389
f"Continue and create {len(pending)} bugs ({summary})?", out, err, default_answer=False
13071390
):
13081391
return 1

‎tests/scripts/test_pkgdev_bugs.py‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,3 +1218,46 @@ def test_bugs_are_filed(self, repo, bugs_options, bugzilla_cassette, monkeypatch
12181218
create = bugzilla_cassette.calls[-1].body
12191219
assert create["summary"] == "cat/a-2: stablereq"
12201220
assert create["cf_stabilisation_atoms"] == "=cat/a-2 *"
1221+
1222+
def test_dry_run_files_nothing(self, repo, bugs_options, bugzilla_cassette, monkeypatch):
1223+
self.mk_target(repo)
1224+
set_userquery(monkeypatch, yesno=True)
1225+
bugzilla_cassette.expect_bugs()
1226+
out = mk_out()
1227+
# no api key needed, searching is read only
1228+
bugs.main(bugs_options("--dry-run", "=cat/a-2"), out, out)
1229+
1230+
assert [call.method for call in bugzilla_cassette.calls] == ["GET"]
1231+
assert "Would file new-1: cat/a-2: stablereq" in out.lines
1232+
assert " component: Gentoo Linux / Stabilization" in out.lines
1233+
assert " =cat/a-2 *" in out.lines
1234+
1235+
1236+
def test_dry_run_prints_new_bugs_and_updates(repo, bugzilla_cassette):
1237+
mk_repo(repo)
1238+
pkgU = max(repo.itermatch(atom("=cat/u-0")))
1239+
pkgX = max(repo.itermatch(atom("=cat/x-0")))
1240+
pkgY = max(repo.itermatch(atom("=cat/y-0")))
1241+
node = bugs.GraphNode((), bugno=200)
1242+
dep = bugs.GraphNode(((pkgX, {"*"}), (pkgY, {"*"})))
1243+
dep.edges.add(bugs.GraphNode(((pkgU, {"*"}),)))
1244+
dep.obsoletes.add(100)
1245+
node.edges.add(dep)
1246+
1247+
out = mk_out()
1248+
dry_run = bugs.DryRunBugzilla(bugzilla_cassette.client(), out)
1249+
assert node.file_bug(dry_run, frozenset(), (), None) == 200
1250+
assert bugzilla_cassette.calls == []
1251+
assert dep.bugno == "new-2"
1252+
1253+
lines = out.lines
1254+
assert "Would file new-1: cat/u-0: stablereq" in lines
1255+
assert "Would file new-2: cat/x-0, cat/y-0: stablereq" in lines
1256+
assert " assignee: dev3@gentoo.org" in lines
1257+
assert " CC: dev1@gentoo.org" in lines
1258+
assert " depends on: new-1" in lines
1259+
assert "Would update bug 100" in lines
1260+
assert " resolution: OBSOLETE" in lines
1261+
assert " see also: +https://bugs.gentoo.org/new-2" in lines
1262+
assert "Would update bug 200" in lines
1263+
assert " depends on: +new-2" in lines

0 commit comments

Comments
 (0)