1 # Copyright (C) 2021 Free Software Foundation, Inc.
3 # This program is free software: you can redistribute it and/or modify it under
4 # the terms of the GNU Lesser General Public License as published by the Free
5 # Software Foundation, either version 3 of the License, or at your option) any
8 # This program is distributed in the hope that it will be useful, but WITHOUT
9 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
13 # You should have received a copy of the GNU Lesser General Public License
14 # along with this program. If not, see <https://www.gnu.org/licenses/>.
16 """CLI for debbugs.gnu.org (and other debbugs trackers)."""
21 from pathlib import Path
23 from typing import List, Optional, Tuple
25 from .control import Control
28 from . import ui as ui_mod
32 "gnu": ("https://debbugs.gnu.org/cgi/soap.cgi", "control@debbugs.gnu.org"),
35 # Servers reject attempts to query more than this many bugs at once.
36 GET_STATUS_MAX_COUNT = 1000
39 def add_raw_subparser(parser: argparse.ArgumentParser) -> None:
40 group = parser.add_argument_group("output format control")
44 help="Format the output in a human friendly form",
46 # Have to specify this for each one that uses dest=format and not just the
47 # canonical --format as argparse will use None by default, and clobber the
49 default_format = "json"
52 default=default_format,
56 help="Output the entire raw SOAP (XML) document",
60 default=default_format,
64 help="Output the raw data in JSON (default)",
68 default=default_format,
69 choices=("full-xml", "xml", "json"),
70 help="Select the output format",
77 help="Automatically break up large requests to avoid server limits",
84 help="Do not automatically break up large requests",
87 subparsers = parser.add_subparsers(title="API", dest="api", required=True)
89 subparser = subparsers.add_parser("get_bug_log")
90 subparser.add_argument("bug", type=int, help="The bug to inspect")
92 subparser = subparsers.add_parser("get_bugs")
93 subparser.add_argument("args", nargs="+", help="Search terms")
95 subparser = subparsers.add_parser("get_status")
96 subparser.add_argument("bugs", nargs="+", type=int, help="The bugs to inspect")
98 subparser = subparsers.add_parser("get_usertag")
99 subparser.add_argument("user", help="The user whose tags to lookup")
100 subparser.add_argument("tags", nargs="+", help="Tags to filter by")
102 subparser = subparsers.add_parser("newest_bugs")
103 subparser.add_argument("count", type=int, help="How many recent bugs to return")
106 def get_parser() -> argparse.ArgumentParser:
107 """Get CLI parser."""
108 parser = argparse.ArgumentParser(description=__doc__)
111 "-B", "--bts", default="gnu", metavar="SYSTEM", help="The bug tracker to use"
118 choices=("cli", "urwid"),
119 help="The UI to use (default: %(default)s)",
125 help="Enable mouse support (%(default)s)",
131 action="store_false",
132 help="Do not enable mouse support",
135 # TODO: Add these to subparsers too.
136 parser.add_argument("-v", "--verbose", action="count", help="More verbose output")
137 parser.add_argument("-q", "--quiet", action="count", help="Only display errors")
139 "-n", "--dry-run", dest="dryrun", action="store_true", help="Don't make changes"
142 group = parser.add_argument_group("e-mail options")
143 group.add_argument("--sendmail", default="sendmail", help="Sendmail command to use")
145 subparsers = parser.add_subparsers(
146 title="Subcommands", dest="command", required=True
150 subparser = subparsers.add_parser(
153 description="Make direct calls to the server & return raw results",
155 add_raw_subparser(subparser)
157 # Subcommand: interactive
158 subparser = subparsers.add_parser(
161 description="Make direct calls to the server & return raw results",
163 add_raw_subparser(subparser)
166 subparser = subparsers.add_parser("query")
167 subparser.add_argument(
170 help="Package or bug numbers to query",
171 metavar="package|bugnum",
174 # Subcommand: control
175 subparser = subparsers.add_parser(
176 "control", description="Send control messages to change bug state"
178 subparser.add_argument("commands", nargs="+", help="Send control messages")
184 argv: Optional[List[str]] = None,
185 ) -> Tuple[argparse.ArgumentParser, argparse.Namespace]:
186 """Parse the command line."""
190 parser = get_parser()
191 opts = parser.parse_args(argv)
193 if opts.bts not in SYSTEMS:
194 parser.error(f"Unknown system: {opts.bts}")
196 return (parser, opts)
203 "unreproducible": "R",
214 def subcmd_query(opts: argparse.Namespace, client: soap.Client) -> Optional[int]:
216 ui = ui_mod.get_backend(opts.ui)(mouse=opts.mouse)
217 except ui_mod.Error as e:
223 # Server doesn't maintain order, so we don't either (wrt the user args).
224 ui.status("Searching for bugs ...")
226 for arg in opts.args:
231 found_bugs = client.get_bugs(package=arg)
233 bugs.update(found_bugs)
235 sys.exit('\nNo bugs found!')
236 bug_width = math.floor(math.log10(max(bugs))) + 1
237 bugs_list = list(bugs)
239 # Servers restrict how many bugs you can query at once.
241 for i in range(0, len(bugs), GET_STATUS_MAX_COUNT):
242 slice = bugs_list[i : i + GET_STATUS_MAX_COUNT]
243 ui.status(f"Loading {i + len(slice)}/{len(bugs)} bugs ...")
244 found_bugs = client.get_status(slice)
246 statuses.update(found_bugs)
248 sys.exit('\nNo bugs found!')
254 for bug, status in statuses.items():
255 bug_tags = set(status["tags"].split())
256 summary_bug_tags = "".join(TAG_SUMMARY[x] for x in bug_tags)
257 tags_width = max(tags_width, len(summary_bug_tags))
258 summary_tags[bug] = summary_bug_tags
260 # TODO: Figure out precedence order.
262 if status["archived"]:
264 elif status["mergedwith"]:
266 elif status["blockedby"]:
268 elif status["fixed_versions"]:
270 states_width = max(states_width, len(state))
271 summary_states[bug] = state
274 ui.interface_query(client, opts.to, statuses, bug_width, summary_tags, tags_width, summary_states, states_width)
275 except KeyboardInterrupt:
276 # We ignore it here because interface_query already took care of quitting.
282 def subcmd_control(opts: argparse.Namespace, client: soap.Client) -> Optional[int]:
283 # TODO: Make this configurable.
284 control = Control(opts.to, "Mike Frysinger <vapier@gentoo.org>")
285 for message in opts.commands:
286 control.queue._queue(message)
287 control.send(dryrun=opts.dryrun)
290 def subcmd_raw(opts: argparse.Namespace, client: soap.Client) -> Optional[int]:
291 # Is this more terrible than hand maintaining a CLI<->enum mapping?
292 sformat = opts.format.upper().replace("-", "_")
293 for prop in dir(soap.Format):
295 format = getattr(soap.Format, prop)
298 raise RuntimeError(f'Unable to locate "{sformat}" in {dir(soap.Format)}')
300 if opts.api == "get_bug_log":
301 ret = client.get_bug_log(opts.bug, format=format)
302 elif opts.api == "get_bugs":
304 for arg in opts.args:
305 key, value = arg.split("=", 1)
307 ret = client.get_bugs(format=format, **kwargs)
308 elif opts.api == "get_status":
309 bugs = [int(x) for x in opts.bugs]
311 # TODO: Implement XML chunking.
312 assert format == soap.Format.JSON, 'Chunking only supported for JSON currently'
314 for i in range(0, len(bugs), GET_STATUS_MAX_COUNT):
316 client.get_status(bugs[i : i + GET_STATUS_MAX_COUNT], format=format)
319 ret = client.get_status(bugs, format=format)
320 elif opts.api == "get_usertag":
321 ret = client.get_usertag(opts.user, opts.tags, format=format)
322 elif opts.api == "newest_bugs":
323 ret = client.newest_bugs(opts.count, format=format)
325 raise RuntimeError(f'Unhandled API "{opts.api}"')
328 if format == soap.Format.JSON:
329 ret = pretty.json(ret)
334 def main(argv: Optional[List[str]] = None) -> Optional[int]:
335 """The main entry point for scripts."""
336 _, opts = parse_args(argv)
338 tracker, to = SYSTEMS[opts.bts]
339 client = soap.Client(tracker)
342 return {"query": subcmd_query, "control": subcmd_control, "raw": subcmd_raw}[