EVOLUTION-MANAGER
Edit File: fedora-hosted
#!/usr/bin/python # fedora-hosted - a commandline frontend for the Fedora Hosted Projects Trac # # Copyright (C) 2008 Red Hat Inc. # Author: Jesse Keating <jkeating@redhat.com> # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 2 of the License, or (at your # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. # TODO: this file should probably go away and be far more generic # And we should load specific things like url structure from a config # store of some sort. import getpass import optparse import offtrac import sys import time import os import os.path import subprocess import textwrap import six from six.moves import input # Define some constants BASEURL = 'fedorahosted.org/' WRAP_LEN = 60 LINE_LEN = 75 # List currently available commands cmdlist = ('list', 'list-milestones', 'ticket-info', 'milestone-info', 'new-ticket', 'new-milestone', 'show', 'update-ticket', 'close') def getOtherConfiguration(opts): """ Try to search for configuration variables in other locations, than just on the command line. """ searched_keys = ('user', 'password', 'project', ) other_conf = { 'user': opts.user, 'password': opts.password, 'project': opts.project } # Look for configuration in the configuration file config_file = os.path.expanduser("~/.fedora/tracrc") if os.path.exists(config_file): from six.moves.configparser import SafeConfigParser conf_rc = SafeConfigParser() conf_rc.read(config_file) if conf_rc.has_section('trac'): for var in searched_keys: if (not other_conf[var]) and conf_rc.has_option('trac', var): other_conf[var] = conf_rc.get('trac', var) # Look for configuration in the local git configuration proc = subprocess.Popen('git rev-parse --git-dir', stdout=file("/dev/null", 'w'), shell=True) ret = os.WEXITSTATUS(os.waitpid(proc.pid, 0)[1]) if ret == 0: raw_value = subprocess.Popen(['git', 'config', '--get-regexp', r'trac\..*'], stdout=subprocess.PIPE).communicate()[0].strip() values = dict([line.strip().split()[:2] for line in raw_value.split('\n') if line]) for var in searched_keys: key = "trac.%s" % var if (not other_conf[var]) and key in values: other_conf[var] = values[key] return other_conf def wrap_text(text, width=WRAP_LEN): """ Wrap text without joining all lines together first. See this note in the textwrap documentation: If replace_whitespace is false, newlines may appear in the middle of a line and cause strange output. For this reason, text should be split into paragraphs (using str.splitlines() or similar) which are wrapped separately. """ out = [] raw_lines = text.splitlines() for rawl in raw_lines: out += textwrap.wrap(rawl, width) return out def edit_description(): """ Open editor on temporary file and return the result. """ import tempfile if 'EDITOR' in os.environ: editprog = os.environ['EDITOR'] else: editprog = "/bin/vi" descfile = tempfile.NamedTemporaryFile() editproc = subprocess.Popen(editprog + " " + descfile.name, shell=True) os.waitpid(editproc.pid, 0)[1] outstr = descfile.read().strip() descfile.close() return outstr def list_tickets(opts): """ List all tickets with some details. """ # setup the query string query = "status%s" % opts.status if opts.owner: query += "&owner=%s" % opts.owner if opts.component: query += "&component=%s" % opts.component if opts.type: query += "&type=%s" % opts.type results = trac.query_tickets(query) tickets_info = get_ticket_info(results, comments=False) for ticket in tickets_info: meta = ticket[3] wrapped_summary = wrap_text(meta['summary']) print("%4s (%5s): %s" % ("#" + str(ticket[0]), meta['status'], wrapped_summary[0])) for line in wrapped_summary[1:]: print("%13s %s" % ('', line)) def collect_comments(ticketNos): idx_keys = ('time', 'who', 'what', 'oldValue', 'newValue', 'permanent',) trac.setup_multicall() for ticket in ticketNos: trac.server.ticket.changeLog(ticket) ret = trac.do_multicall()[0] changes = [] for log_item in ret: change_item = {} for key in idx_keys: change_item[key] = log_item[idx_keys.index(key)] changes.append(change_item) return changes def show_tickets(tickets): for ticket in tickets: meta = ticket[3] wrapped_summary = wrap_text(meta['summary']) wrapped_desc = wrap_text(meta['description']) if not wrapped_desc: # yes, I have managed to have empty description wrapped_desc = [""] if 'milestone' not in meta: meta['milestone'] = "<none>" print("%4s (%5s): %s" % ('#' + str(ticket[0]), meta['status'], wrapped_summary[0])) for line in wrapped_summary[1:]: print("%14s %s" % ('', line)) print("%s" % "-" * LINE_LEN) print("component: %s, priority: %s, milestone: %s" % \ (meta['component'], meta['priority'], meta['milestone'])) print("%s" % "=" * LINE_LEN) print("Description: %s" % wrapped_desc[0]) for line in wrapped_desc[1:]: print("%12s %s" % ('', line)) for item in meta['changelog']: if item['what'] == 'comment' and item['newValue']: wrapped_comment = wrap_text(six.u(item['newValue']), WRAP_LEN + 9) print("%s\n%2s: %s" % ('-' * LINE_LEN, item['oldValue'], wrapped_comment[0])) for line in wrapped_comment[1:]: print("%3s %s" % ('', line)) if len(tickets) > 1: print("\n") def get_ticket_info(args, comments=True): out = [] # Setup the trac object for multicall trac.setup_multicall() for number in args: trac.get_ticket(number) # Do the multicall and print out the results for result in trac.do_multicall(): if comments: result[3]['changelog'] = collect_comments([result[0]]) out.append(result) return out # Define some functions def setup_action_parser(action): """Setup parsers for the various action types""" usage = "usage: %%prog %s [options]" % action p = optparse.OptionParser(usage=usage) if action == "list": p.add_option("--owner", "-o") p.add_option("--status", "-s", default="!=closed", help="Query string for status, default is '!=closed'") p.add_option("--component", "-c") p.add_option("--type", "-t") # elif action == "list-milestones": # p.add_option("--name", "-n", # help="Show information about a particular milestone") # p.add_option("--all", "-a", action="store_true", # help="Show all milestones, otherwise only show active.") elif action == "ticket-info": p.set_usage("usage: %%prog %s [ticket numbers]" % action) elif action == "show": p.set_usage("usage: %%prog %s [ticket numbers]" % action) elif action == "milestone_info": p.set_usage("usage: %%prog %s [milestones]" % action) elif action == "new-ticket": p.add_option("--summary", "-s", help="REQUIRED!") p.add_option("--description", "-d", default=None) p.add_option("--descedit", "-D", action="store_true", dest="descedit") p.add_option("--type", "-t", default=None) p.add_option("--priority", "-p", default=None) p.add_option("--milestone", "-m", default=None) p.add_option("--component", "-C", default=None) p.add_option("--version", "-v", default=None) p.add_option("--keyword", "-k", action="append", help="Keyword to add, can be used multiple times.") p.add_option("--assignee", "-a", default=None) p.add_option("--cc", action="append", help="Carbon Copy address, can be used multiple times.") # This one is a little backwards. The rpc call is actually notify, # and defaults to false, but we want to default to true. p.add_option("--stealth", action="store_false", default=True, help="Suppress initial notification of this ticket.") elif action == "update-ticket": p.add_option("--ticket", "-n", help="Ticket number. REQUIRED!") p.add_option("--comment", "-c", default='', help="Value - make program to open $EDITOR for editing it.") p.add_option("--summary", "-s", default=None) p.add_option("--description", "-d", default=None) p.add_option("--type", "-t", default=None) p.add_option("--priority", "-p", default=None) p.add_option("--milestone", "-m", default=None) p.add_option("--component", "-C", default=None) p.add_option("--version", "-v", default=None) p.add_option("--keyword", "-k", action="append", help="Keyword to add, can be used multiple times.") p.add_option("--assignee", "-a", default=None) p.add_option("--cc", action="append", help="Carbon Copy address, can be used multiple times.") p.add_option("--status", "-S", default=None) p.add_option("--resolution", "-r", default=None) # This one is a little backwards. The rpc call is actually notify, # and defaults to false, but we want to default to true. p.add_option("--stealth", action="store_false", default=True, help="Suppress notification of this update.") elif action == "close": p.add_option("--comment", "-c", default='', help="Comment; - to open $EDITOR.") p.add_option("--resolution", "-r", default=None) p.add_option("--ticket", "-n", default=None, help="Ticket number") p.add_option("--stealth", action="store_false", default=True, help="Suppress notification of this update.") elif action == "new-milestone": p.add_option("--name", "-n", help="REQUIRED!") p.add_option("--description", "-d", default=None) p.add_option("--due", "-D", default=None, help="Due date in MM-DD-YY format.") return p # get command line options usage = "usage: %prog [global options] COMMAND [options]" usage += "\nCommands: %s" % ', '.join(cmdlist) parser = optparse.OptionParser(usage=usage) parser.disable_interspersed_args() parser.add_option("--user", "-u") parser.add_option("--password", "-p") parser.add_option("--project", "-P") # Parse our global options (opts, args) = parser.parse_args() # See if we got a command if len(args) and args[0] in cmdlist: action = args.pop(0) else: parser.print_help() sys.exit(1) # Parse the command action_parser = setup_action_parser(action) (actopt, actargs) = action_parser.parse_args(args) # Check other locations of configuration for required variables oopts = getOtherConfiguration(opts) if not oopts['user']: oopts['user'] = input('Username: ') if not oopts['password']: oopts['password'] = getpass.getpass('Password for %s: ' % oopts['user']) if not oopts['project']: oopts['project'] = input('Project space: ') # Create the TracServ object uri = 'https://%s:%s@%s/%s/login/xmlrpc' % (oopts['user'], oopts['password'], BASEURL, oopts['project']) trac = offtrac.TracServer(uri) # Try to do something if action == "list": list_tickets(actopt) elif action == "list-milestones": results = trac.list_milestones() print(results) elif action == "ticket-info": if not actargs: # FIXME, this isn't working action_parser.print_help() sys.exit(1) print("\n".join([six.u(ticket) for ticket in get_ticket_info(actargs)])) elif action == "show": if not actargs: # FIXME, this isn't working action_parser.print_help() sys.exit(1) show_tickets(get_ticket_info(actargs)) elif action == "milestone-info": if not actargs: # FIXME, this isn't working action_parser.print_help() sys.exit(1) trac.setup_multicall() for milestone in actargs: trac.get_milestone(milestone) for result in trac.do_multicall(): print(result) elif action == "new-ticket": # Check to make sure we got all we need if actopt.summary and (actopt.description or actopt.descedit): pass else: action_parser.print_help() sys.exit(1) # Wrap up our keywords and cc into one string, if any keywords = None ccs = None if actopt.keyword: keywords = ' '.join(actopt.keyword) if actopt.cc: ccs = ' '.join(actopt.cc) if actopt.descedit: actopt.description = edit_description() result = trac.create_ticket(actopt.summary, actopt.description, actopt.type, actopt.priority, actopt.milestone, actopt.component, actopt.version, keywords, actopt.assignee, ccs, actopt.stealth) print(result) elif action == "update-ticket": # Check to make sure we got all we need if actopt.ticket: pass else: action_parser.print_help() sys.exit(1) if actopt.comment == "-": actopt.comment = edit_description() # Wrap up our keywords and cc into one string, if any keywords = None ccs = None if actopt.keyword: keywords = ' '.join(actopt.keyword) if actopt.cc: ccs = ' '.join(actopt.cc) result = trac.update_ticket(actopt.ticket, actopt.comment, actopt.summary, actopt.type, actopt.description, actopt.priority, actopt.milestone, actopt.component, actopt.version, keywords, ccs, actopt.status, actopt.resolution, actopt.assignee, actopt.stealth) if result[0].isdigit(): print("Something's wrong; result:\n%s" % result) sys.exit(2) elif action == "close": if (len(actargs) > 0) and actargs[-1].isdigit(): actopt.ticket = int(actargs[-1]) if (not actopt.ticket): action_parser.print_help() sys.exit(1) else: actopt.status = "closed" if not actopt.resolution: if (len(actargs) > 1): actopt.resolution = actargs[-2] else: actopt.resolution = "fixed" if actopt.comment == "-": actopt.comment = edit_description() result = trac.update_ticket(actopt.ticket, actopt.comment, status=actopt.status, resolution=actopt.resolution, notify=actopt.stealth) if result[3]['status'] == 'closed': print("OK") else: print("Something is wrong; result:\n%s" % result) sys.exit(2) elif action == "new-milestone": if not actopt.name: action_parser.print_help() sys.exit(1) # Convert due date to seconds if needed due = None if actopt.due: due = int(time.mktime(time.strptime(actopt.due, "%m-%d-%y"))) result = trac.create_milestone(actopt.name, actopt.description, due) print(result) # The result here is "0" if successful, printing isn't fun