def resolve(ui, db, prefix, resolution=None, *args, **opts): '''Marks an issue resolved If the issue is not simply "resolved", for instance it is concluded it will not be fixed, or it lacks information, it may be considered resolved nevertheless. Therefore you can specify a custom resolved status. ''' try: if resolution and db.meta_prefix['resolution']: resolution = db.meta_prefix['resolution'][resolution] except error.UnknownPrefix as err: raise error.Abort("%s is not a valid option for resolution" % err.prefix) except error.AmbiguousPrefix as err: raise error.Abort( "%s is an ambiguous option for resolution, choices: %s" % (err.prefix, util.list2str(err.choices))) iss = db.get_issue(prefix) if iss.resolution: raise error.Abort( "Cannot resolve issue %s, it is already resolved with resolution %s." "Use open to reopen a resolved issue." % (db.iss_prefix.pref_str(iss.id, True), iss.resolution)) iss.status = ui.config('metadata', 'status.resolved') iss.resolution = resolution or ui.config('metadata', 'resolution.default') iss.to_JSON(db.issues) ui.write("Resolved issue %s with resolution %s" % (db.iss_prefix.pref_str(iss.id, True), iss.resolution))
def update(ui, db, prefix, *args, **opts): '''Updates the information associated with an issue''' # metadata try: metas = ['issue', 'severity', 'status', 'resolution', 'category'] for meta in metas: try: if opts[meta]: if db.meta_prefix[meta]: opts[meta] = db.meta_prefix[meta][opts[meta]] except Exception as err: err.cause = meta raise err except error.UnknownPrefix as err: raise error.Abort("%s is not a valid option for %s" % (err.prefix, err.cause)) except error.AmbiguousPrefix as err: raise error.Abort("%s is an ambiguous option for %s, choices: %s" % (err.prefix, err.cause, util.list2str(err.choices))) iss = db.get_issue(prefix) origiss = db.get_issue(prefix) if len(opts) == 0: raise error.Abort("Did not specify any updates to make to issue %s" % db.iss_prefix.pref_str(iss.id, True)) if opts['assign_to']: iss.assigned_to = db.get_user(opts['assign_to']) if opts['listener']: iss.listeners.extend([db.get_user(i) for i in opts['listener']]) if opts['rl']: for l in [db.get_user(i) for i in opts['rl']]: iss.listeners.remove(l) if opts['issue']: iss.issue = opts['issue'] if opts['target']: iss.target = opts['target'] if opts['severity']: iss.severity = opts['severity'] if opts['status']: iss.status = opts['status'] if opts['category']: iss.category = opts['category'] iss.to_JSON(db.issues) ui.write("Updated issue %s" % db.iss_prefix.pref_str(iss.id, True)) ui.write(iss.descChanges(origiss, ui))
def get_user(self, prefix): try: ret = self.usr_prefix[prefix] if ret == 'me' or ret == 'nobody': return None return ret except error.AmbiguousPrefix as err: raise error.Abort( "User prefix %s is ambiguous\n Suggestions: %s" % (err.prefix, err.choices)) except error.UnknownPrefix as err: raise error.Abort( "User prefix %s does not correspond to any known user" % err.prefix)
def child(ui, db, child_pref, parent_pref, *args, **opts): '''Mark an issue as a child of another issue This simply affects the relationship between the issues, and does not, for instance, prevent a user from resolving the parent issue if they so desire. ''' child = db.get_issue(child_pref) parent = db.get_issue(parent_pref) if child.parent: if not ui.confirm( "Issue %s is already a child of issue %s, do you really want to change it's parent to issue %s?" % (child_pref, db.iss_prefix.prefix(child.parent), parent_pref), True): raise error.Abort("Did not change issue %s's parent." % child_pref) orig = db.get_issue(child.parent) orig.children.remove(child.id) orig.to_JSON(db.issues) child.parent = parent.id parent.children.append(child.id) child.to_JSON(db.issues) parent.to_JSON(db.issues) ui.write("Marked issue %s as a child of issue %s" % (child_pref, parent_pref)) return 0
def comment(ui, db, pref, *args, **opts): '''Add a comment to an issue Appends a time and potentially user-stamped comment to the specified issue. If -m,--message is passed, the specified message is used as the comment text. Otherwise, an editor is launched and the user is prompted to construct a comment. ''' iss = db.get_issue(pref) if opts['message']: message = opts['message'].strip() else: lines = util.ab_strip( ui.edit("AB: Commenting on Issue %s: %s\n" "AB: Lines starting with 'AB:' are ignored.\n\n" % (db.iss_prefix.prefix(iss.id), iss.title))) message = ''.join(lines).strip() if message == '': raise error.Abort("Must provide a comment for the specified issue.") comment = [ui.config('ui', 'username'), time.time(), message] iss.comments.append(comment) iss.to_JSON(db.issues) ui.write("Added Comment to Issue %s:" % db.iss_prefix.prefix(iss.id)) ui.write(issue.comment_to_str(comment, ui)) return 0
def list(ui, db, *args, **opts): '''Get a list of open issues Called without arguments, list will display all open issues. The -r,--resolved flag will instead display resolved issues. Use the other parameters, detailed below, to further filter the issues you wish to see. ''' if opts['assigned_to'] != '*': user = db.get_user( opts['assigned_to']) if opts['assigned_to'] else None if opts['listener']: lstset = set([db.get_user(i) for i in opts['listener']]) # Metadata metas = ['issue', 'severity', 'status', 'category', 'resolution'] for meta in metas: try: if opts[meta] and db.meta_prefix[meta]: opts[meta] = db.meta_prefix[meta][opts[meta]] except error.AmbiguousPrefix as err: raise error.Abort("%s is an ambiguous option for %s, choices: %s" % (err.prefix, meta, util.list2str(err.choices))) except Exception as err: pass # do nothing, it's not a valid prefix iss_iter = ( i for i in db.get_issues() if (bool(i.resolution) == bool(opts['resolved'])) and ( opts['assigned_to'] == '*' or i.assigned_to == user) and (not opts['listener'] or not set(i.listeners).isdisjoint(lstset)) and ( not opts['issue'] or i.issue == opts['issue']) and (not opts['target'] or i.target == opts['target']) and ( not opts['severity'] or i.severity == opts['severity']) and ( not opts['status'] or i.status == opts['status']) and ( not opts['category'] or i.category == opts['category']) and (not opts['resolution'] or i.resolution == opts['resolution']) and ( not opts['creator'] or i.creator == db.get_user(opts['creator'])) and (not opts['grep'] or opts['grep'].lower() in i.title.lower())) count = 0 # for now, if the user wants to slow down output, they must pipe output through less/more # we ought to be able to do this for them in certain cases for i in iss_iter: ui.quiet(db.iss_prefix.prefix(i.id), ln=False) ui.write(":\t%s" % i.title, ln=False) ui.quiet() count += 1 ui.write("Found %s matching issue%s" % (count if count > 0 else "no", "" if count == 1 else "s")) return 0 if count > 0 else 1
def get_issue_id(self, pref): try: return self.iss_prefix[pref] except error.AmbiguousPrefix as err: def choices(issLs): ls = (self.get_issue(i) for i in (issLs[:2] if len(issLs) > 3 else issLs[:])) return ', '.join((self.iss_prefix.prefix(i.id) + (':' + i.title if i.title else '') for i in ls)) raise error.Abort( "Issue prefix %s is ambiguous\n Suggestions: %s" % (err.prefix, choices(err.choices))) except error.UnknownPrefix as err: raise error.Abort( "Issue prefix %s does not correspond to any issues" % err.prefix)
def open_iss(ui, db, prefix, status=None, *args, **opts): '''Opens a previously resolved issue This command reopens the issue, and optionally sets its status to the passed status.''' iss = db.get_issue(prefix) if not iss.resolution: raise error.Abort("Cannot open issue %s, it is already open.\n" "Use resolve to close an open issue." % db.iss_prefix.pref_str(iss.id, True)) iss.status = status or ui.config('metadata', 'status.opened') iss.resolution = None iss.to_JSON(db.issues) ui.write("Reopened issue %s, set status to %s" % (db.iss_prefix.pref_str(iss.id, True), iss.status))
def init(ui, dir='.', *args, **opts): '''Initialize an Abundant database Creates an '.ab' directory in the specified directory, or the cwd if not otherwise set. ''' db = database.DB(dir, False) if db.exists(): raise error.Abort("Abundant database already exists.") # don't need to make db.db because makedirs handles that os.makedirs(db.issues) os.mkdir(db.cache) with open(db.conf, 'w'): # as conf: # write any initial configuration to config file pass with open(db.local_conf, 'w'): # as lconf: # write any initial configuration to local config file pass with open(db.users, 'w'): # as usr pass ui.write("Created Abundant issue database in %s" % db.path)
def exec(cmds, cwd): exec_timer = util.Timer("Full command execution") try: ui_load_timer = util.Timer("UI load") ui = usrint.UI() ui_load_timer.stop( ) # since we haven't parsed --debug yet, we can't use ui.debug() except: sys.stderr.write("FAILED TO CREATE UI OBJECT.\n" "This should not have been possible.\n" "Please report this issue immediately.\n\n") raise try: parse_timer = util.Timer("Command parsing") if len(cmds) < 1 or (len(cmds[0]) > 0 and cmds[0][0] == '-'): prefix = commands.fallback_cmd args = cmds else: prefix = cmds[0] args = cmds[1:] # load config settings try: task = cmdPfx[prefix] except error.UnknownPrefix as err: ui.alert("Unknown Command: %s" % err.prefix) task = commands.fallback_cmd args = [] except error.AmbiguousPrefix as err: ui.alert("Ambiguous Command: %s" % err.prefix) ui.alert("Did you mean: %s" % util.list2str(err.choices)) task = commands.fallback_cmd args = [] func, options, args_left = _parse(task, args) #set volume ui.set_volume(options['volume']) ui.debug(ui_load_timer) ui.debug(parse_timer) #check for -h,--help if options['help']: new_args = [task] + args task = commands.fallback_cmd func, options, args_left = _parse(task, new_args) command_timer = None if task not in commands.no_db: db_load_timer = util.Timer("Database load") path = os.path.join( cwd, options['database']) if options['database'] else cwd db = database.DB(path, ui=ui) if not db.exists(): raise error.Abort("No Abundant database found.") ui.db_conf(db) ui.debug(db_load_timer) command_timer = util.Timer("Command '%s'" % task) ret = func(ui, db, *args_left, **options) else: command_timer = util.Timer("Command %s" % task) ret = func(ui, *args_left, **options) ui.debug(command_timer) ui.debug(exec_timer) if ret is None: return 0 else: return ret # Global error handling starts here except error.Abort as err: ui.alert("Abort: ", err) return 2 except error.CommandError as err: ui.alert("Invalid Command:\n", err) try: ui.flush() # ensure error displays first exec([commands.fallback_cmd, err.task], cwd) except: # if there is no err.task then don't bother outputting help on it pass return 3 except Exception as err: '''Exceptions we were not expecting.''' exc_type, exc_value, exc_traceback = sys.exc_info() sys.stderr.write( "Unexpected exception was raised. This should not happen.\n") sys.stderr.write("Please report the entire output to Michael\n") sys.stderr.write("\nCommand line arguments:\n %s\n" % ' '.join(sys.argv)) traceback.print_exception(exc_type, exc_value, exc_traceback) return 10
def new(ui, db, *args, **opts): '''Create a new issue Creates a new open issue and, if set, marks the current user as the creator. Options can be used to set additional information about the issue. See the update command to change/add/remove this information from an existing issue. ''' if not opts['user']: opts['user'] = ui.config('ui', 'username') if not opts['assign_to']: opts['assign_to'] = opts['user'] # metadata try: metas = ['issue', 'severity', 'category'] for meta in metas: try: if opts[meta]: if db.meta_prefix[meta]: opts[meta] = db.meta_prefix[meta][opts[meta]] else: opts[meta] = ui.config('metadata', meta + '.default') except Exception as err: err.cause = meta raise err except error.UnknownPrefix as err: raise error.Abort("%s is not a valid option for %s" % (err.prefix, err.cause)) except error.AmbiguousPrefix as err: raise error.Abort("%s is an ambiguous option for %s, choices: %s" % (err.prefix, err.cause, util.list2str(err.choices))) opts['status'] = ui.config('metadata', 'status.default') #construct issue iss = issue.Issue( title=(' '.join(args)).strip(), assigned_to=db.get_user(opts['assign_to']) if opts['assign_to'] else None, listeners=[db.get_user(i) for i in opts['listener']] if opts['listener'] else None, issue=opts['issue'], severity=opts['severity'], category=opts['category'], target=opts['target'], parent=db.get_issue_id(opts['parent']) if opts['parent'] else None, creator=db.get_user(opts['user']) if opts['user'] else None) if opts['parent']: parent = db.get_issue(opts['parent']) parent.children.append(iss.id) parent.to_JSON(db.issues) db.iss_prefix.add(iss.id) iss.to_JSON(db.issues) if ui.volume == useri.quiet: ui.quiet(iss.id) ui.write("Created new issue with ID %s" % db.iss_prefix.pref_str(iss.id, True)) skip = ['id', 'creation_date' ] + (['creator', 'assigned_to'] if db.single_user() and ui.volume < useri.verbose else []) ui.write(iss.descChanges(issue.base, ui, skip=skip))
def edit(ui, db, pref, *args, **opts): '''Edit the content of the issue Notably the fields Paths, Description, Reproduction Steps, Expected Result, and Stack Trace. An editor is launched prompting the user to update this data, unless any of these are provided at the command line, in which case the provided fields are overwritten without launching an editor. ''' iss = db.get_issue(pref) origiss = db.get_issue(pref) if opts['paths'] or opts['description'] or opts['reproduction'] or opts[ 'expected'] or opts['trace']: iss.paths = opts['paths'] if opts['paths'] else iss.paths iss.description = opts['description'] if opts[ 'description'] else iss.description iss.reproduction = opts['reproduction'] if opts[ 'reproduction'] else iss.reproduction iss.expected = opts['expected'] if opts['expected'] else iss.expected iss.trace = opts['trace'] if opts['trace'] else iss.trace else: formatting = ( ("Editing Issue %s: %s\n\n" "[Paths]\n%s\n\n" "[Description]\n%s\n\n" "[Reproduction Steps]\n%s\n\n" "[Expected Result]\n%s\n\n" "[Stack Trace]\n%s") % (db.iss_prefix.prefix(iss.id), iss.title, util.list2str(iss.paths, True, ''), iss.description if iss.description else '', iss.reproduction if iss.reproduction else '', iss.expected if iss.expected else '', iss.trace if iss.trace else '')) details = util.bracket_strip(util.ab_strip(ui.edit(formatting))) danger = False count = len(details) if count != 6: if count < 6: details += [''] * (6 - count) danger = True _, paths, description, reproduction, expected, trace = details[0:6] if danger: ui.alert("Warning: The details could not be parsed cleanly.\n" "This is usually due to inserting or removing sections.") ui.alert(( "The issue's details will be set to the following:\n" "Paths:\n%s\n\nDescription:\n%s\n\nReproduction Steps:\n%s\n\n" "Expected Results:\n%s\n\nStack Trace:\n%s") % (paths, description, reproduction, expected, trace)) safe = ui.confirm("Do you want to continue?", False, err=True) if not safe: raise error.Abort("Failed to parse the details file.") iss.paths = paths.splitlines() iss.description = description if description else None iss.reproduction = reproduction if reproduction else None iss.expected = expected if expected else None iss.trace = trace if trace else None iss.to_JSON(db.issues) ui.write("Updated issue %s" % db.iss_prefix.pref_str(iss.id, True)) ui.write(iss.descChanges(origiss, ui)) return 0