You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1078 lines
37 KiB

  1. # Text user interface for reportbug
  2. # Written by Chris Lawrence <lawrencc@debian.org>
  3. # (C) 2001-08 Chris Lawrence
  4. # Copyright (C) 2008-2017 Sandro Tosi <morph@debian.org>
  5. #
  6. # This program is freely distributable per the following license:
  7. #
  8. # Permission to use, copy, modify, and distribute this software and its
  9. # documentation for any purpose and without fee is hereby granted,
  10. # provided that the above copyright notice appears in all copies and that
  11. # both that copyright notice and this permission notice appear in
  12. # supporting documentation.
  13. #
  14. # I DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL
  15. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL I
  16. # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
  17. # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
  18. # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
  19. # ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
  20. # SOFTWARE.
  21. import sys
  22. import os
  23. import subprocess
  24. import re
  25. import math
  26. import string
  27. import errno
  28. import glob
  29. import getpass
  30. import textwrap
  31. import locale
  32. from functools import reduce
  33. try:
  34. import readline
  35. except ImportError:
  36. readline = None
  37. from reportbug import debbugs, hiermatch
  38. from reportbug.exceptions import (
  39. NoReport, NoPackage, NoBugs, NoNetwork,
  40. InvalidRegex,
  41. )
  42. from reportbug.urlutils import launch_browser
  43. import reportbug.utils
  44. ISATTY = sys.stdin.isatty()
  45. charset = 'us-ascii'
  46. try:
  47. r, c = subprocess.getoutput('stty size').split()
  48. rows, columns = int(r) or 24, int(c) or 79
  49. except:
  50. rows, columns = 24, 79
  51. def ewrite(message, *args):
  52. if not ISATTY:
  53. return
  54. if args:
  55. message = message % args
  56. sys.stderr.write(message)
  57. sys.stderr.flush()
  58. log_message = ewrite
  59. display_failure = ewrite
  60. def system(cmdline):
  61. try:
  62. x = os.getcwd()
  63. except OSError:
  64. os.chdir('/')
  65. return os.system(cmdline)
  66. def indent_wrap_text(text, starttext='', indent=0, linelen=None):
  67. """Wrapper for textwrap.fill to the existing API."""
  68. if not linelen:
  69. linelen = columns - 1
  70. if indent:
  71. si = ' ' * indent
  72. else:
  73. si = ''
  74. text = ' '.join(text.split())
  75. if not text:
  76. return starttext + '\n'
  77. output = textwrap.fill(text, width=linelen, initial_indent=starttext,
  78. subsequent_indent=si)
  79. if output.endswith('\n'):
  80. return output
  81. return output + '\n'
  82. # Readline support, if available
  83. if readline is not None:
  84. readline.parse_and_bind("tab: complete")
  85. try:
  86. # minimize the word delimeter list if possible
  87. readline.set_completer_delims(' ')
  88. except:
  89. pass
  90. def _launch_mbox_reader(mbox_reader_cmd, bts, bugs, number, mirrors, archived,
  91. mbox, http_proxy, timeout):
  92. try:
  93. number = int(number)
  94. if number not in bugs and 1 <= number <= len(bugs):
  95. number = bugs[number - 1]
  96. reportbug.utils.launch_mbox_reader(mbox_reader_cmd,
  97. debbugs.get_report_url(bts, number, mirrors, archived, mbox),
  98. http_proxy, timeout)
  99. except ValueError:
  100. ewrite('Invalid report number: %s\n',
  101. number)
  102. class our_completer(object):
  103. def __init__(self, completions=None):
  104. self.completions = None
  105. if completions:
  106. self.completions = tuple(map(str, completions))
  107. def complete(self, text, i):
  108. if not self.completions:
  109. return None
  110. matching = [x for x in self.completions if x.startswith(text)]
  111. if i < len(matching):
  112. return matching[i]
  113. else:
  114. return None
  115. def our_raw_input(prompt=None, completions=None, completer=None):
  116. istty = sys.stdout.isatty()
  117. if not istty:
  118. sys.stderr.write(prompt)
  119. sys.stderr.flush()
  120. if readline:
  121. if completions and not completer:
  122. completer = our_completer(completions).complete
  123. if completer:
  124. readline.set_completer(completer)
  125. try:
  126. if istty:
  127. ret = input(prompt)
  128. else:
  129. ret = input()
  130. except EOFError:
  131. ewrite('\nUser interrupt (^D).\n')
  132. raise SystemExit
  133. if readline:
  134. readline.set_completer(None)
  135. return ret.strip()
  136. def select_options(msg, ok, help, allow_numbers=None, nowrap=False):
  137. err_message = ''
  138. for option in ok:
  139. if option in string.ascii_uppercase:
  140. default = option
  141. break
  142. if not help:
  143. help = {}
  144. if '?' not in ok:
  145. ok = ok + '?'
  146. if nowrap:
  147. longmsg = msg + ' [' + '|'.join(ok) + ']?' + ' '
  148. else:
  149. longmsg = indent_wrap_text(msg + ' [' + '|'.join(ok) + ']?').strip() + ' '
  150. ch = our_raw_input(longmsg, allow_numbers)
  151. # Allow entry of a bug number here
  152. if allow_numbers:
  153. while ch and ch[0] == '#':
  154. ch = ch[1:]
  155. if isinstance(allow_numbers, int):
  156. try:
  157. return str(int(ch))
  158. except ValueError:
  159. pass
  160. else:
  161. try:
  162. number = int(ch)
  163. if number in allow_numbers:
  164. return str(number)
  165. else:
  166. nums = list(allow_numbers)
  167. nums.sort()
  168. err_message = 'Only the following entries are allowed: ' + \
  169. ', '.join(map(str, nums))
  170. except (ValueError, TypeError):
  171. pass
  172. if not ch:
  173. ch = default
  174. ch = ch[0]
  175. if ch == '?':
  176. help['?'] = 'Display this help.'
  177. for ch in ok:
  178. if ch in string.ascii_uppercase:
  179. desc = '(default) '
  180. else:
  181. desc = ''
  182. desc += help.get(ch, help.get(ch.lower(),
  183. 'No help for this option.'))
  184. ewrite(indent_wrap_text(desc + '\n', '%s - ' % ch, 4))
  185. return select_options(msg, ok, help, allow_numbers, nowrap)
  186. elif (ch.lower() in ok) or (ch.upper() in ok):
  187. return ch.lower()
  188. elif err_message:
  189. ewrite(indent_wrap_text(err_message))
  190. else:
  191. ewrite('Invalid selection.\n')
  192. return select_options(msg, ok, help, allow_numbers, nowrap)
  193. def yes_no(msg, yeshelp, nohelp, default=True, nowrap=False):
  194. "Return True for yes, False for no."
  195. if default:
  196. ok = 'Ynq'
  197. else:
  198. ok = 'yNq'
  199. res = select_options(msg, ok, {'y': yeshelp, 'n': nohelp, 'q': 'Quit.'},
  200. nowrap=nowrap)
  201. if res == 'q':
  202. raise SystemExit
  203. return (res == 'y')
  204. def long_message(text, *args):
  205. if args:
  206. ewrite(indent_wrap_text(text % args))
  207. else:
  208. ewrite(indent_wrap_text(text))
  209. final_message = long_message
  210. def get_string(prompt, options=None, title=None, empty_ok=False, force_prompt=False,
  211. default='', completer=None):
  212. if prompt and (len(prompt) < 2 * columns / 3) and not force_prompt:
  213. if default:
  214. prompt = '%s [%s]: ' % (prompt, default)
  215. response = our_raw_input(prompt, options, completer) or default
  216. else:
  217. response = our_raw_input(prompt, options, completer)
  218. else:
  219. if prompt:
  220. ewrite(indent_wrap_text(prompt))
  221. if default:
  222. response = our_raw_input('[%s]> ' % default, options, completer) or default
  223. else:
  224. response = our_raw_input('> ', options, completer)
  225. return response
  226. def get_multiline(prompt):
  227. ewrite('\n')
  228. ewrite(indent_wrap_text(prompt + " Press ENTER on a blank line to continue.\n"))
  229. l = []
  230. while 1:
  231. entry = get_string('', force_prompt=True).strip()
  232. if not entry:
  233. break
  234. l.append(entry)
  235. ewrite('\n')
  236. return l
  237. def get_password(prompt=None):
  238. return getpass.getpass(prompt)
  239. def FilenameCompleter(text, i):
  240. text = os.path.expanduser(text)
  241. text = os.path.expandvars(text)
  242. paths = glob.glob(text + '*')
  243. if not paths:
  244. return None
  245. if i < len(paths):
  246. entry = paths[i]
  247. if os.path.isdir(entry):
  248. return entry + '/'
  249. return entry
  250. else:
  251. return None
  252. def get_filename(prompt, title=None, force_prompt=False, default=''):
  253. return get_string(prompt, title=title, force_prompt=force_prompt,
  254. default=default, completer=FilenameCompleter)
  255. def select_multiple(par, options, prompt, title=None, order=None, extras=None):
  256. return menu(par, options, prompt, title=title, order=order, extras=extras,
  257. multiple=True, empty_ok=False)
  258. def menu(par, options, prompt, default=None, title=None, any_ok=False,
  259. order=None, extras=None, multiple=False, empty_ok=False):
  260. selected = {}
  261. if not extras:
  262. extras = []
  263. else:
  264. extras = list(extras)
  265. if title:
  266. ewrite(title + '\n\n')
  267. ewrite(indent_wrap_text(par, linelen=columns) + '\n')
  268. if isinstance(options, dict):
  269. options = options.copy()
  270. # Convert to a list
  271. if order:
  272. olist = []
  273. for key in order:
  274. if key in options:
  275. olist.append((key, options[key]))
  276. del options[key]
  277. # Append anything out of order
  278. options = list(options.items())
  279. options.sort()
  280. for option in options:
  281. olist.append(option)
  282. options = olist
  283. else:
  284. options = list(options.items())
  285. options.sort()
  286. if multiple:
  287. options.append(('none', ''))
  288. default = 'none'
  289. extras += ['done']
  290. allowed = [x[0] for x in options]
  291. allowed = allowed + extras
  292. maxlen_name = min(max(list(map(len, allowed))), columns // 3)
  293. digits = int(math.ceil(math.log10(len(options) + 1)))
  294. i = 1
  295. for name, desc in options:
  296. text = indent_wrap_text(desc, indent=(maxlen_name + digits + 3),
  297. starttext=('%*d %-*.*s ' % (digits, i, maxlen_name, maxlen_name, name)))
  298. ewrite(text)
  299. if len(options) < 5:
  300. ewrite('\n')
  301. i += 1
  302. if len(options) >= 5:
  303. ewrite('\n')
  304. if multiple:
  305. prompt += '(one at a time) '
  306. while 1:
  307. if default:
  308. aprompt = prompt + '[%s] ' % default
  309. else:
  310. aprompt = prompt
  311. response = our_raw_input(aprompt, allowed)
  312. if not response:
  313. response = default
  314. try:
  315. num = int(response)
  316. if 1 <= num <= len(options):
  317. response = options[num - 1][0]
  318. except (ValueError, TypeError):
  319. pass
  320. if response in allowed or (response == default and response):
  321. if multiple:
  322. if response == 'done':
  323. return list(selected.keys())
  324. elif response == 'none':
  325. return []
  326. elif selected.get(response):
  327. del selected[response]
  328. else:
  329. selected[response] = 1
  330. ewrite('- selected: %s\n' % ', '.join(list(selected.keys())))
  331. if len(selected):
  332. default = 'done'
  333. else:
  334. default = 'none'
  335. continue
  336. else:
  337. return response
  338. if any_ok and response:
  339. return response
  340. elif empty_ok and not response:
  341. return
  342. ewrite('Invalid entry.\n')
  343. return
  344. # Things that are very UI dependent go here
  345. def show_report(number, system, mirrors,
  346. http_proxy, timeout, screen=None, queryonly=False, title='',
  347. archived='no', mbox_reader_cmd=None):
  348. sysinfo = debbugs.SYSTEMS[system]
  349. ewrite('Retrieving report #%d from %s bug tracking system...\n',
  350. number, sysinfo['name'])
  351. try:
  352. info = debbugs.get_report(number, timeout, system, mirrors=mirrors,
  353. followups=1, http_proxy=http_proxy, archived=archived)
  354. except:
  355. info = None
  356. if not info:
  357. ewrite('No report available: #%s\n', number)
  358. raise NoBugs
  359. buginfo, messages = info
  360. # XXX: could this be ever possible?
  361. if not (buginfo.package or not buginfo.source):
  362. ewrite('Cannot retrieve bug\'s package, exiting...\n')
  363. sys.exit(-1)
  364. current_message = 0
  365. skip_pager = False
  366. while 1:
  367. if current_message:
  368. text = 'Followup %d - %s\n\n%s' % (current_message, buginfo.subject,
  369. messages[current_message])
  370. else:
  371. text = 'Original report - %s\n\n%s' % (buginfo.subject, messages[0])
  372. if not skip_pager:
  373. fd = os.popen('sensible-pager', 'w')
  374. try:
  375. fd.write(text)
  376. fd.close()
  377. except IOError as x:
  378. if x.errno == errno.EPIPE:
  379. pass
  380. else:
  381. raise
  382. skip_pager = False
  383. options = 'xOrbeq'
  384. if (current_message + 1) < len(messages):
  385. options = 'N' + options.lower()
  386. if (current_message):
  387. options = 'p' + options
  388. x = select_options("What do you want to do now?", options,
  389. {'x': 'Provide extra information.',
  390. 'o': 'Show other bug reports (return to bug listing).',
  391. 'n': 'Show next message (followup).',
  392. 'p': 'Show previous message (followup).',
  393. 'r': 'Redisplay this message.',
  394. 'e': 'Launch e-mail client to read full log.',
  395. 'b': 'Launch web browser to read full log.',
  396. 'q': "I'm bored; quit please."},
  397. allow_numbers=list(range(1, len(messages) + 1)))
  398. if x == 'x':
  399. return buginfo
  400. elif x == 'q':
  401. raise NoReport
  402. elif x == 'b':
  403. launch_browser(debbugs.get_report_url(
  404. system, number, mirrors, archived))
  405. skip_pager = True
  406. elif x == 'e':
  407. reportbug.utils.launch_mbox_reader(mbox_reader_cmd,
  408. debbugs.get_report_url(system, number, mirrors, archived, True),
  409. http_proxy, timeout)
  410. skip_pager = True
  411. elif x == 'o':
  412. break
  413. elif x == 'n':
  414. current_message += 1
  415. elif x == 'p':
  416. current_message -= 1
  417. return
  418. def handle_bts_query(package, bts, timeout, mirrors=None, http_proxy="",
  419. queryonly=False, title="", screen=None, archived='no',
  420. source=False, version=None, mbox=False, buglist=None,
  421. mbox_reader_cmd=None, latest_first=False):
  422. root = debbugs.SYSTEMS[bts].get('btsroot')
  423. if not root:
  424. ewrite('%s bug tracking system has no web URL; bypassing query\n',
  425. debbugs.SYSTEMS[bts]['name'])
  426. return
  427. srcstr = ""
  428. if source:
  429. srcstr = " (source)"
  430. if isinstance(package, str):
  431. long_message('Querying %s BTS for reports on %s%s...\n',
  432. debbugs.SYSTEMS[bts]['name'], package, srcstr)
  433. else:
  434. long_message('Querying %s BTS for reports on %s%s...\n',
  435. debbugs.SYSTEMS[bts]['name'],
  436. ' '.join([str(x) for x in package]), srcstr)
  437. bugs = []
  438. try:
  439. (count, title, hierarchy) = debbugs.get_reports(
  440. package, timeout, bts, mirrors=mirrors, version=version,
  441. source=source, http_proxy=http_proxy, archived=archived)
  442. except Exception as e:
  443. ewrite('Unable to connect to %s BTS (error: "%s"); ' % (debbugs.SYSTEMS[bts]['name'], repr(e)))
  444. res = select_options('continue', 'yN',
  445. {'y': 'Keep going.',
  446. 'n': 'Abort.'})
  447. if res == 'n':
  448. raise NoNetwork
  449. else:
  450. raise NoBugs
  451. try:
  452. # If there's no report, then skip all the rest
  453. if not count:
  454. if hierarchy is None:
  455. raise NoPackage
  456. else:
  457. raise NoBugs
  458. hierarchy_new = []
  459. if mbox:
  460. mboxbuglist = []
  461. for entry in hierarchy:
  462. for bug in entry[1]:
  463. mboxbuglist.append(bug.bug_num)
  464. return mboxbuglist
  465. if buglist:
  466. for entry in hierarchy:
  467. # second item is a list of bugs report
  468. for bug in entry[1]:
  469. msg = "#%d %s" % (bug.bug_num, bug.subject)
  470. print(msg)
  471. sys.exit(0)
  472. for entry in hierarchy:
  473. # first item is the title of the section
  474. entry_new = entry[0]
  475. bugs_new = {}
  476. bugs_numbers = []
  477. # XXX: we can probably simplify this code, with some lists
  478. # generations and map()
  479. # second item is a list of bugs report
  480. for bug in entry[1]:
  481. # show if the bugs is already resolved
  482. done = ''
  483. if bug.pending == 'done':
  484. done = ' [RESOLVED]'
  485. # we take the info we need (bug number and subject)
  486. # we use a dict so it's easy to sort the keys and then values
  487. bugs_new[bug.bug_num] = "%s%s" % (bug.subject, done)
  488. # and at the same time create a list of bugs numbers
  489. bugs_numbers.append(bug.bug_num)
  490. # then we sort both the lists
  491. hierarchy_new.append((entry_new, ["#%d %s" % (k, bugs_new[k]) for k in sorted(list(bugs_new.keys()), reverse=latest_first)]))
  492. bugs.extend(sorted(bugs_numbers, reverse=latest_first))
  493. # replace old hierarchy with hierarchy_new
  494. hierarchy = hierarchy_new
  495. if not count:
  496. if hierarchy is None:
  497. raise NoPackage
  498. else:
  499. raise NoBugs
  500. elif count == 1:
  501. ewrite('%d bug report found:\n\n', count)
  502. else:
  503. ewrite('%d bug reports found:\n\n', count)
  504. return browse_bugs(hierarchy, count, bugs, bts, queryonly,
  505. mirrors, http_proxy, timeout, screen, title, package,
  506. mbox_reader_cmd)
  507. except NoPackage:
  508. long_message('No record of this package found.')
  509. raise NoPackage
  510. def browse_bugs(hierarchy, count, bugs, bts, queryonly, mirrors,
  511. http_proxy, timeout, screen, title, package, mbox_reader_cmd):
  512. try:
  513. output_encoding = locale.getpreferredencoding()
  514. except locale.Error as msg:
  515. print(msg)
  516. sys.exit(1)
  517. endcount = catcount = 0
  518. scount = startcount = 1
  519. category = hierarchy[0]
  520. lastpage = []
  521. digits = len(str(len(bugs)))
  522. linefmt = ' %' + str(digits) + 'd) %s\n'
  523. while category:
  524. scount += 1
  525. catname, reports = category[0:2]
  526. while catname.endswith(':'):
  527. catname = catname[:-1]
  528. total = len(reports)
  529. while len(reports):
  530. these = reports[:rows - 2]
  531. reports = reports[rows - 2:]
  532. remain = len(reports)
  533. tplural = rplural = 's'
  534. if total == 1:
  535. tplural = ''
  536. if remain != 1:
  537. rplural = ''
  538. if remain:
  539. lastpage.append(' %s: %d remain%s\n' %
  540. (catname, remain, rplural))
  541. else:
  542. lastpage.append(catname + '\n')
  543. oldscount, oldecount = scount, endcount
  544. for report in these:
  545. scount = scount + 1
  546. endcount = endcount + 1
  547. lastpage.append(linefmt % (endcount, report[:columns - digits - 5]))
  548. if category == hierarchy[-1] or \
  549. (scount >= (rows - len(hierarchy[catcount + 1][1]) - 1)):
  550. skipmsg = ' (s to skip rest)'
  551. if endcount == count:
  552. skipmsg = ''
  553. options = 'yNbmrqsfe'
  554. if queryonly:
  555. options = 'Nbmrqfe'
  556. rstr = "(%d-%d/%d) " % (startcount, endcount, count)
  557. pstr = rstr + "Is the bug you found listed above"
  558. if queryonly:
  559. pstr = rstr + "What would you like to do next"
  560. allowed = bugs + list(range(1, count + 1))
  561. helptext = {
  562. 'y': 'Problem already reported; optionally add extra information.',
  563. 'n': 'Problem not listed above; possibly check more.',
  564. 'b': 'Open the complete bugs list in a web browser.',
  565. 'm': 'Get more information about a bug (you can also enter a number\n'
  566. ' without selecting "m" first).',
  567. 'r': 'Redisplay the last bugs shown.',
  568. 'q': "I'm bored; quit please.",
  569. 's': 'Skip remaining problems; file a new report immediately.',
  570. 'e': 'Open the report using an e-mail client.',
  571. 'f': 'Filter bug list using a pattern.'}
  572. if skipmsg:
  573. helptext['n'] = helptext['n'][:-1] + ' (skip to Next page).'
  574. while 1:
  575. for line in lastpage:
  576. sys.stderr.write(line)
  577. x = select_options(pstr, options, helptext,
  578. allow_numbers=allowed)
  579. if x == 'n':
  580. lastpage = []
  581. break
  582. elif x == 'b':
  583. launch_browser('https://bugs.debian.org/%s' % package)
  584. continue
  585. elif x == 'r':
  586. continue
  587. elif x == 'q':
  588. raise NoReport
  589. elif x == 's':
  590. return
  591. elif x == 'y':
  592. if queryonly:
  593. return
  594. if len(bugs) == 1:
  595. number = '1'
  596. else:
  597. number = our_raw_input(
  598. 'Enter the number of the bug report '
  599. 'you want to give more info on,\n'
  600. 'or press ENTER to exit: #', allowed)
  601. while number and number[0] == '#':
  602. number = number[1:]
  603. if number:
  604. try:
  605. number = int(number)
  606. if number not in bugs and 1 <= number <= len(bugs):
  607. number = bugs[number - 1]
  608. return debbugs.get_report(number, timeout)[0]
  609. except ValueError:
  610. ewrite('Invalid report number: %s\n',
  611. number)
  612. else:
  613. raise NoReport
  614. elif x == 'f':
  615. # Do filter. Recursive done.
  616. retval = search_bugs(hierarchy, bts, queryonly, mirrors, http_proxy, timeout, screen, title,
  617. package, mbox_reader_cmd)
  618. if isinstance(retval, str) and retval in ["FilterEnd", "Top"]:
  619. continue
  620. else:
  621. return retval
  622. elif x == 'e':
  623. number = our_raw_input('Please enter the number of the bug you would like to view: #', allowed)
  624. _launch_mbox_reader(mbox_reader_cmd, bts, bugs, number,
  625. mirrors, 'no', True, http_proxy,
  626. timeout)
  627. else:
  628. if x == 'm' or x == 'i':
  629. if len(bugs) == 1:
  630. number = '1'
  631. else:
  632. number = our_raw_input('Please enter the number of the bug '
  633. 'you would like more info on: #', allowed)
  634. else:
  635. number = x
  636. while number and number[0] == '#':
  637. number = number[1:]
  638. if number:
  639. try:
  640. number = int(number)
  641. if number not in bugs and 1 <= number <= len(bugs):
  642. number = bugs[number - 1]
  643. res = show_report(number, bts, mirrors,
  644. http_proxy, timeout,
  645. queryonly=queryonly,
  646. screen=screen,
  647. title=title,
  648. mbox_reader_cmd=mbox_reader_cmd)
  649. if res:
  650. return res
  651. except ValueError:
  652. ewrite('Invalid report number: %s\n',
  653. number)
  654. startcount = endcount + 1
  655. scount = 0
  656. # these now empty
  657. if category == hierarchy[-1]:
  658. break
  659. catcount = catcount + 1
  660. category = hierarchy[catcount]
  661. if scount:
  662. lastpage.append('\n')
  663. scount = scount + 1
  664. def proc_hierarchy(hierarchy):
  665. """Find out bug count and bug # in the hierarchy."""
  666. lenlist = [len(i[1]) for i in hierarchy]
  667. if lenlist:
  668. count = reduce(lambda x, y: x + y, lenlist)
  669. else:
  670. return 0, 0
  671. # Copy & paste from handle_bts_query()
  672. bugs = []
  673. exp = re.compile(r'\#(\d+)[ :]')
  674. for entry in hierarchy or []:
  675. for bug in entry[1]:
  676. match = exp.match(bug)
  677. if match:
  678. bugs.append(int(match.group(1)))
  679. return count, bugs
  680. def search_bugs(hierarchyfull, bts, queryonly, mirrors,
  681. http_proxy, timeout, screen, title, package, mbox_reader_cmd):
  682. """Search for the bug list using a pattern."""
  683. """Return string "FilterEnd" when we are done with search."""
  684. try:
  685. output_encoding = locale.getpreferredencoding()
  686. except locale.Error as msg:
  687. print(msg)
  688. sys.exit(1)
  689. pattern = our_raw_input('Enter the search pattern (a Perl-compatible regular expression)\n'
  690. 'or press ENTER to exit: ')
  691. if not pattern:
  692. return "FilterEnd"
  693. # Create new hierarchy match the pattern.
  694. try:
  695. hierarchy = hiermatch.matched_hierarchy(hierarchyfull, pattern)
  696. except InvalidRegex:
  697. our_raw_input('Invalid regular expression, press ENTER to continue.')
  698. return "FilterEnd"
  699. count, bugs = proc_hierarchy(hierarchy)
  700. exp = re.compile(r'\#(\d+):')
  701. if not count:
  702. our_raw_input('No match found, press ENTER to continue.')
  703. return "FilterEnd"
  704. endcount = catcount = 0
  705. scount = startcount = 1
  706. category = hierarchy[0]
  707. lastpage = []
  708. digits = len(str(len(bugs)))
  709. linefmt = ' %' + str(digits) + 'd) %s\n'
  710. # XXX: it's kinda non-sense to replicate all this code here!!! it's the same
  711. # as of browse_report!
  712. while category:
  713. scount = scount + 1
  714. catname, reports = category[0:2]
  715. while catname.endswith(':'):
  716. catname = catname[:-1]
  717. total = len(reports)
  718. while len(reports):
  719. these = reports[:rows - 2]
  720. reports = reports[rows - 2:]
  721. remain = len(reports)
  722. tplural = rplural = 's'
  723. if total == 1:
  724. tplural = ''
  725. if remain != 1:
  726. rplural = ''
  727. if remain:
  728. lastpage.append(' %s: %d report%s (%d remain%s)\n' %
  729. (catname, total, tplural, remain, rplural))
  730. else:
  731. lastpage.append(' %s: %d report%s\n' %
  732. (catname, total, tplural))
  733. oldscount, oldecount = scount, endcount
  734. for report in these:
  735. scount = scount + 1
  736. endcount = endcount + 1
  737. lastpage.append(linefmt % (endcount, report[:columns - digits - 5]))
  738. if category == hierarchy[-1] or \
  739. (scount >= (rows - len(hierarchy[catcount + 1][1]) - 1)):
  740. skipmsg = ' (s to skip rest)'
  741. if endcount == count:
  742. skipmsg = ''
  743. options = 'yNbmrqsfute'
  744. if queryonly:
  745. options = 'Nmbrqfute'
  746. rstr = "(%d-%d/%d) " % (startcount, endcount, count)
  747. pstr = rstr + "Is the bug you found listed above"
  748. if queryonly:
  749. pstr = rstr + "What would you like to do next"
  750. allowed = bugs + list(range(1, count + 1))
  751. helptext = {
  752. 'y': 'Problem already reported; optionally add extra information.',
  753. 'n': 'Problem not listed above; possibly check more.',
  754. 'b': 'Open the complete bugs list in a web browser.',
  755. 'm': 'Get more information about a bug (you can also enter a number\n'
  756. ' without selecting "m" first).',
  757. 'r': 'Redisplay the last bugs shown.',
  758. 'q': "I'm bored; quit please.",
  759. 's': 'Skip remaining problems; file a new report immediately.',
  760. 'f': 'Filter (search) bug list using a pattern.',
  761. 'u': 'Up one level of filter.',
  762. 'e': 'Open the report using an e-mail client.',
  763. 't': 'Top of the bug list (remove all filters).'}
  764. if skipmsg:
  765. helptext['n'] = helptext['n'][:-1] + ' (skip to Next page).'
  766. while 1:
  767. for line in lastpage:
  768. sys.stderr.write(line)
  769. x = select_options(pstr, options, helptext,
  770. allow_numbers=allowed)
  771. if x == 'n':
  772. lastpage = []
  773. break
  774. elif x == 'b':
  775. launch_browser('https://bugs.debian.org/%s' % package)
  776. elif x == 'r':
  777. continue
  778. elif x == 'q':
  779. raise NoReport
  780. elif x == 's':
  781. return
  782. elif x == 'y':
  783. if queryonly:
  784. return
  785. if len(bugs) == 1:
  786. number = '1'
  787. else:
  788. number = our_raw_input(
  789. 'Enter the number of the bug report '
  790. 'you want to give more info on,\n'
  791. 'or press ENTER to exit: #', allowed)
  792. while number and number[0] == '#':
  793. number = number[1:]
  794. if number:
  795. try:
  796. number = int(number)
  797. if number not in bugs and 1 <= number <= len(bugs):
  798. number = bugs[number - 1]
  799. return debbugs.get_report(number, timeout)[0]
  800. except ValueError:
  801. ewrite('Invalid report number: %s\n',
  802. number)
  803. else:
  804. raise NoReport
  805. elif x == 'f':
  806. # Do filter. Recursive done.
  807. retval = search_bugs(hierarchy, bts, queryonly, mirrors, http_proxy, timeout, screen,
  808. title, package, mbox_reader_cmd)
  809. if isinstance(retval, str) and retval in ["FilterEnd", "Top"]:
  810. continue
  811. else:
  812. return retval
  813. elif x == 'u':
  814. # Up a level
  815. return "FilterEnd"
  816. elif x == 't':
  817. # go back to the Top level.
  818. return "Top"
  819. elif x == 'e':
  820. number = our_raw_input('Please enter the number of the '
  821. 'bug you would like to view: #', allowed)
  822. _launch_mbox_reader(mbox_reader_cmd, bts, bugs, number,
  823. mirrors, 'no', True, http_proxy, timeout)
  824. else:
  825. if x == 'm' or x == 'i':
  826. number = our_raw_input(
  827. 'Please enter the number of the bug '
  828. 'you would like more info on: #',
  829. allowed)
  830. else:
  831. number = x
  832. while number and number[0] == '#':
  833. number = number[1:]
  834. if number:
  835. try:
  836. number = int(number)
  837. if number not in bugs and 1 <= number <= len(bugs):
  838. number = bugs[number - 1]
  839. res = show_report(number, bts, mirrors,
  840. http_proxy, timeout,
  841. queryonly=queryonly,
  842. screen=screen,
  843. title=title)
  844. if res:
  845. return res
  846. except ValueError:
  847. ewrite('Invalid report number: %s\n', number)
  848. startcount = endcount + 1
  849. scount = 0
  850. # these now empty
  851. if category == hierarchy[-1]:
  852. break
  853. catcount += 1
  854. category = hierarchy[catcount]
  855. if scount:
  856. lastpage.append('\n')
  857. scount += 1
  858. return "FilterEnd"
  859. def display_report(text, use_pager=True, presubj=False):
  860. if not use_pager:
  861. ewrite(text)
  862. return
  863. elif presubj:
  864. text += "\n(You may need to press 'q' to exit your pager and continue using\nreportbug at this point.)"
  865. pager = os.environ.get('PAGER', 'sensible-pager')
  866. try:
  867. os.popen(pager, 'w').write(text)
  868. except IOError:
  869. pass
  870. def spawn_editor(message, filename, editor, charset='utf-8'):
  871. if not editor:
  872. ewrite('No editor found!\n')
  873. return (message, 0)
  874. edname = os.path.basename(editor.split()[0])
  875. # Move the cursor for lazy buggers like me; add your editor here...
  876. ourline = 0
  877. for (lineno, line) in enumerate(open(filename)):
  878. if line == '\n' and not ourline:
  879. ourline = lineno + 2
  880. opts = ''
  881. if 'vim' in edname:
  882. # Force *vim to edit the file in the foreground, instead of forking
  883. opts = '-f '
  884. if ourline:
  885. if edname in ('vi', 'nvi', 'vim', 'elvis', 'gvim', 'kvim'):
  886. opts = '-c :%d' % ourline
  887. elif (edname in ('elvis-tiny', 'gnuclient', 'ee', 'pico', 'nano', 'zile') or
  888. 'emacs' in edname):
  889. opts = '+%d' % ourline
  890. elif edname in ('jed', 'xjed'):
  891. opts = '-g %d' % ourline
  892. elif edname == 'kate':
  893. opts = '--line %d' % ourline
  894. if '&' in editor or edname == 'kate':
  895. ewrite("Spawning %s in background; please press Enter when done "
  896. "editing.\n", edname)
  897. else:
  898. ewrite("Spawning %s...\n", edname)
  899. result = os.system("%s %s '%s'" % (editor, opts, filename))
  900. if result:
  901. ewrite('Warning: possible error exit from %s: %d\n', edname, result)
  902. if not os.path.exists(filename):
  903. ewrite('Bug report file %s removed!', filename)
  904. sys.exit(1)
  905. if '&' in editor:
  906. return (None, 1)
  907. newmessage = open(filename).read()
  908. if newmessage == message:
  909. ewrite('No changes were made in the editor.\n')
  910. return (newmessage, newmessage != message)
  911. def initialize():
  912. return True
  913. def can_input():
  914. return sys.stdin.isatty()