]>
git.wh0rd.org - gnudebbugs.git/blob - src/gnudebbugs/__main__.py
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
}[