X-Git-Url: http://git.ozlabs.org/?a=blobdiff_plain;f=apps%2Fpatchwork%2Fbin%2Fpwclient;h=8d1f4766c0bfe7b594e1b118c7348ba4f06af9d5;hb=f0ad2c6a249c0ee3a4b356e10033ea0041ecbea4;hp=dba68fb036b1b28c9351548f6cf142b770238ccb;hpb=ad4715cef72910b1310d4b93a2294ace878b0b4a;p=patchwork diff --git a/apps/patchwork/bin/pwclient b/apps/patchwork/bin/pwclient index dba68fb..8d1f476 100755 --- a/apps/patchwork/bin/pwclient +++ b/apps/patchwork/bin/pwclient @@ -22,19 +22,21 @@ import os import sys import xmlrpclib -import getopt +import argparse import string import tempfile import subprocess import base64 import ConfigParser +import shutil +import re # Default Patchwork remote XML-RPC server URL # This script will check the PW_XMLRPC_URL environment variable # for the URL to access. If that is unspecified, it will fallback to # the hardcoded default value specified here. DEFAULT_URL = "http://patchwork/xmlrpc/" -CONFIG_FILES = [os.path.expanduser('~/.pwclientrc')] +CONFIG_FILE = os.path.expanduser('~/.pwclientrc') class Filter: """Filter for selecting patches.""" @@ -67,7 +69,7 @@ class Filter: else: self.d['state_id'] = id - if self.project != "": + if self.project != None: id = project_id_by_name(rpc, self.project) if id == 0: sys.stderr.write("Note: No Project found matching %s, " \ @@ -105,33 +107,6 @@ class BasicHTTPAuthTransport(xmlrpclib.SafeTransport): fn = xmlrpclib.Transport.make_connection return fn(self, host) -def usage(): - sys.stderr.write("Usage: %s [options]\n\n" % \ - (os.path.basename(sys.argv[0]))) - sys.stderr.write("Where is one of:\n") - sys.stderr.write( -""" apply : Apply a patch (in the current dir, using -p1) - get : Download a patch and save it locally - projects : List all projects - states : Show list of potential patch states - list [str] : List patches, using the optional filters specified - below and an optional substring to search for patches - by name - search [str] : Same as 'list' - view : View a patch - update [-s state] [-c commit-ref] - : Update patch\n""") - sys.stderr.write("""\nFilter options for 'list' and 'search': - -s : Filter by patch state (e.g., 'New', 'Accepted', etc.) - -p : Filter by project name (see 'projects' for list) - -w : Filter by submitter (name, e-mail substring search) - -d : Filter by delegate (name, e-mail substring search) - -n : Restrict number of results\n""") - sys.stderr.write("""\nActions that take an ID argument can also be \ -invoked with: - -h : Lookup by patch hash\n""") - sys.exit(1) - def project_id_by_name(rpc, linkname): """Given a project short name, look up the Project ID.""" if len(linkname) == 0: @@ -160,20 +135,37 @@ def person_ids_by_name(rpc, name): people = rpc.person_list(name, 0) return map(lambda x: x['id'], people) -def list_patches(patches): +def list_patches(patches, format_str=None): """Dump a list of patches to stdout.""" - print("%-5s %-12s %s" % ("ID", "State", "Name")) - print("%-5s %-12s %s" % ("--", "-----", "----")) - for patch in patches: - print("%-5d %-12s %s" % (patch['id'], patch['state'], patch['name'])) + if format_str: + format_field_re = re.compile("%{([a-z0-9_]+)}") + + def patch_field(matchobj): + fieldname = matchobj.group(1) + + if fieldname == "_msgid_": + # naive way to strip < and > from message-id + val = string.strip(str(patch["msgid"]), "<>") + else: + val = str(patch[fieldname]) + + return val -def action_list(rpc, filter, submitter_str, delegate_str): + for patch in patches: + print(format_field_re.sub(patch_field, format_str)) + else: + print("%-7s %-12s %s" % ("ID", "State", "Name")) + print("%-7s %-12s %s" % ("--", "-----", "----")) + for patch in patches: + print("%-7d %-12s %s" % (patch['id'], patch['state'], patch['name'])) + +def action_list(rpc, filter, submitter_str, delegate_str, format_str=None): filter.resolve_ids(rpc) - if submitter_str != "": + if submitter_str != None: ids = person_ids_by_name(rpc, submitter_str) if len(ids) == 0: - sys.stderr.write("Note: Nobody found matching *%s*\n", \ + sys.stderr.write("Note: Nobody found matching *%s*\n" % \ submitter_str) else: for id in ids: @@ -184,13 +176,13 @@ def action_list(rpc, filter, submitter_str, delegate_str): f = filter f.add("submitter_id", id) patches = rpc.patch_list(f.d) - list_patches(patches) + list_patches(patches, format_str) return - if delegate_str != "": + if delegate_str != None: ids = person_ids_by_name(rpc, delegate_str) if len(ids) == 0: - sys.stderr.write("Note: Nobody found matching *%s*\n", \ + sys.stderr.write("Note: Nobody found matching *%s*\n" % \ delegate_str) else: for id in ids: @@ -200,11 +192,11 @@ def action_list(rpc, filter, submitter_str, delegate_str): f = filter f.add("delegate_id", id) patches = rpc.patch_list(f.d) - list_patches(patches) + list_patches(patches, format_str) return patches = rpc.patch_list(filter.d) - list_patches(patches) + list_patches(patches, format_str) def action_projects(rpc): projects = rpc.project_list("", 0) @@ -222,6 +214,14 @@ def action_states(rpc): for state in states: print("%-5d %s" % (state['id'], state['name'])) +def action_info(rpc, patch_id): + patch = rpc.patch_get(patch_id) + s = "Information for patch id %d" % (patch_id) + print(s) + print('-' * len(s)) + for key, value in sorted(patch.iteritems()): + print("- %- 14s: %s" % (key, unicode(value).encode("utf-8"))) + def action_get(rpc, patch_id): patch = rpc.patch_get(patch_id) s = rpc.patch_get_mbox(patch_id) @@ -250,23 +250,31 @@ def action_get(rpc, patch_id): sys.stderr.write("Failed to write to %s\n" % fname) sys.exit(1) -def action_apply(rpc, patch_id): +def action_apply(rpc, patch_id, apply_cmd=None): patch = rpc.patch_get(patch_id) if patch == {}: sys.stderr.write("Error getting information on patch ID %d\n" % \ patch_id) sys.exit(1) - print "Applying patch #%d to current directory" % patch_id + + if apply_cmd is None: + print "Applying patch #%d to current directory" % patch_id + apply_cmd = ['patch', '-p1'] + else: + print "Applying patch #%d using %s" % ( + patch_id, repr(' '.join(apply_cmd))) + print "Description: %s" % patch['name'] s = rpc.patch_get_mbox(patch_id) if len(s) > 0: - proc = subprocess.Popen(['patch', '-p1'], stdin = subprocess.PIPE) - proc.communicate(s) + proc = subprocess.Popen(apply_cmd, stdin = subprocess.PIPE) + proc.communicate(unicode(s).encode('utf-8')) + return proc.returncode else: sys.stderr.write("Error: No patch content found\n") sys.exit(1) -def action_update_patch(rpc, patch_id, state = None, commit = None): +def action_update_patch(rpc, patch_id, state = None, archived = None, commit = None): patch = rpc.patch_get(patch_id) if patch == {}: sys.stderr.write("Error getting information on patch ID %d\n" % \ @@ -285,6 +293,9 @@ def action_update_patch(rpc, patch_id, state = None, commit = None): if commit: params['commit_ref'] = commit + if archived: + params['archived'] = archived == 'yes' + success = False try: success = rpc.patch_set(patch_id, params) @@ -303,82 +314,321 @@ def patch_id_from_hash(rpc, project, hash): patch = rpc.patch_get_by_hash(hash) if patch == {}: - return None + sys.stderr.write("No patch has the hash provided\n") + sys.exit(1) - return patch['id'] + patch_id = patch['id'] + # be super paranoid + try: + patch_id = int(patch_id) + except: + sys.stderr.write("Invalid patch ID obtained from server\n") + sys.exit(1) + return patch_id auth_actions = ['update'] -def main(): - try: - opts, args = getopt.getopt(sys.argv[2:], 's:p:w:d:n:c:h:') - except getopt.GetoptError, err: - print str(err) - usage() +# unfortunately we currently have to revert to this ugly hack.. +class _RecursiveHelpAction(argparse._HelpAction): + + def __call__(self, parser, namespace, values, option_string=None): + parser.print_help() + print + + subparsers_actions = [ + action for action in parser._actions + if isinstance(action, argparse._SubParsersAction) + ] + hash_n_id_actions = set(['hash', 'id', 'help']) + for subparsers_action in subparsers_actions: + for choice, subparser in subparsers_action.choices.items(): + # gross but the whole thing is.. + if (len(subparser._actions) == 3 \ + and set([a.dest for a in subparser._actions]) \ + == hash_n_id_actions) \ + or len(subparser._actions) == 0: + continue + print("command '{}'".format(choice)) + print(subparser.format_help()) + + parser.exit() +def main(): + hash_parser = argparse.ArgumentParser(add_help=False, version=False) + hash_parser.add_argument( + '-h', metavar='HASH', dest='hash', action='store', + help='''Lookup by patch hash''' + ) + hash_parser.add_argument( + 'id', metavar='ID', nargs='*', action='store', type=int, + help='Patch ID', + ) + hash_parser.add_argument( + '-p', metavar='PROJECT', + help='''Lookup patch in project''' + ) + + filter_parser = argparse.ArgumentParser(add_help=False, version=False) + filter_parser.add_argument( + '-s', metavar='STATE', + help='''Filter by patch state (e.g., 'New', 'Accepted', etc.)''' + ) + filter_parser.add_argument( + '-a', choices=['yes','no'], + help='''Filter by patch archived state''' + ) + filter_parser.add_argument( + '-p', metavar='PROJECT', + help='''Filter by project name (see 'projects' for list)''' + ) + filter_parser.add_argument( + '-w', metavar='WHO', + help='''Filter by submitter (name, e-mail substring search)''' + ) + filter_parser.add_argument( + '-d', metavar='WHO', + help='''Filter by delegate (name, e-mail substring search)''' + ) + filter_parser.add_argument( + '-n', metavar='MAX#', + type=int, + help='''Restrict number of results''' + ) + filter_parser.add_argument( + '-m', metavar='MESSAGEID', + help='''Filter by Message-Id''' + ) + filter_parser.add_argument( + '-f', metavar='FORMAT', + help='''Print output in the given format. You can use tags matching ''' + '''fields, e.g. %%{id}, %%{state}, or %%{msgid}.''' + ) + filter_parser.add_argument( + 'patch_name', metavar='STR', nargs='?', + help='substring to search for patches by name', + ) + help_parser = argparse.ArgumentParser(add_help=False, version=False) + help_parser.add_argument( + '--help', action='help', help=argparse.SUPPRESS, + #help='''show this help message and exit''' + ) + + action_parser = argparse.ArgumentParser( + prog='pwclient', + add_help=False, + version=False, + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog='''(apply | get | info | view | update) (-h HASH | ID [ID ...])''', + ) + action_parser.add_argument( + '--help', + #action='help', + action=_RecursiveHelpAction, + help='''Print this help text''' + ) + + subparsers = action_parser.add_subparsers( + title='Commands', + metavar='' + ) + apply_parser = subparsers.add_parser( + 'apply', parents=[hash_parser, help_parser], + add_help=False, + help='''Apply a patch (in the current dir, using -p1)''' + ) + apply_parser.set_defaults(subcmd='apply') + git_am_parser = subparsers.add_parser( + 'git-am', parents=[hash_parser, help_parser], + add_help=False, + help='''Apply a patch to current git branch using "git am".''' + ) + git_am_parser.set_defaults(subcmd='git_am') + git_am_parser.add_argument( + '-s', '--signoff', + action='store_true', + help='''pass --signoff to git-am''' + ) + get_parser = subparsers.add_parser( + 'get', parents=[hash_parser, help_parser], + add_help=False, + help='''Download a patch and save it locally''' + ) + get_parser.set_defaults(subcmd='get') + info_parser = subparsers.add_parser( + 'info', parents=[hash_parser, help_parser], + add_help=False, + help='''Display patchwork info about a given patch ID''' + ) + info_parser.set_defaults(subcmd='info') + projects_parser = subparsers.add_parser( + 'projects', + add_help=False, + help='''List all projects''' + ) + projects_parser.set_defaults(subcmd='projects') + states_parser = subparsers.add_parser( + 'states', + add_help=False, + help='''Show list of potential patch states''' + ) + states_parser.set_defaults(subcmd='states') + view_parser = subparsers.add_parser( + 'view', parents=[hash_parser, help_parser], + add_help=False, + help='''View a patch''' + ) + view_parser.set_defaults(subcmd='view') + update_parser = subparsers.add_parser( + 'update', parents=[hash_parser, help_parser], + add_help=False, + help='''Update patch''', + epilog='''Using a COMMIT-REF allows for only one ID to be specified''', + ) + update_parser.add_argument( + '-c', metavar='COMMIT-REF', + help='''commit reference hash''' + ) + update_parser.add_argument( + '-s', metavar='STATE', + required=True, + help='''Set patch state (e.g., 'Accepted', 'Superseded' etc.)''' + ) + update_parser.add_argument( + '-a', choices=['yes', 'no'], + help='''Set patch archived state''' + ) + update_parser.set_defaults(subcmd='update') + list_parser = subparsers.add_parser("list", + add_help=False, + #aliases=['search'], + parents=[filter_parser, help_parser], + help='''List patches, using the optional filters specified + below and an optional substring to search for patches + by name''' + ) + list_parser.set_defaults(subcmd='list') + search_parser = subparsers.add_parser("search", + add_help=False, + parents=[filter_parser, help_parser], + help='''Alias for "list"''' + ) + # Poor man's argparse aliases: + # We register the "search" parser but effectively use "list" for the + # help-text. + search_parser.set_defaults(subcmd='list') if len(sys.argv) < 2: - usage() - - action = sys.argv[1].lower() + action_parser.print_help() + sys.exit(0) + + args = action_parser.parse_args() + args = dict(vars(args)) + action = args.get('subcmd') + + if args.get('hash') and len(args.get('id')): + # mimic mutual exclusive group + sys.stderr.write("Error: [-h HASH] and [ID [ID ...]] " + + "are mutually exlusive\n") + locals()[action + '_parser'].print_help() + sys.exit(1) # set defaults filt = Filter() - submitter_str = "" - delegate_str = "" - project_str = "" - commit_str = "" - state_str = "" - hash_str = "" + commit_str = None url = DEFAULT_URL - config = ConfigParser.ConfigParser() - config.read(CONFIG_FILES) + archived_str = args.get('a') + state_str = args.get('s') + project_str = args.get('p') + submitter_str = args.get('w') + delegate_str = args.get('d') + format_str = args.get('f') + hash_str = args.get('hash') + patch_ids = args.get('id') + msgid_str = args.get('m') + if args.get('c'): + # update multiple IDs with a single commit-hash does not make sense + if action == 'update' and patch_ids and len(patch_ids) > 1: + sys.stderr.write( + "Declining update with COMMIT-REF on multiple IDs\n" + ) + update_parser.print_help() + sys.exit(1) + commit_str = args.get('c') + + if args.get('n') != None: + try: + filt.add("max_count", args.get('n')) + except: + sys.stderr.write("Invalid maximum count '%s'\n" % args.get('n')) + action_parser.print_help() + sys.exit(1) + + do_signoff = args.get('signoff') # grab settings from config files - if config.has_option('base', 'url'): - url = config.get('base', 'url') - - if config.has_option('base', 'project'): - project_str = config.get('base', 'project') - - for name, value in opts: - if name == '-s': - state_str = value - elif name == '-p': - project_str = value - elif name == '-w': - submitter_str = value - elif name == '-d': - delegate_str = value - elif name == '-c': - commit_str = value - elif name == '-h': - hash_str = value - elif name == '-n': - try: - filt.add("max_count", int(value)) - except: - sys.stderr.write("Invalid maximum count '%s'\n" % value) - usage() - else: - sys.stderr.write("Unknown option '%s'\n" % name) - usage() + config = ConfigParser.ConfigParser() + config.read([CONFIG_FILE]) + + if not config.has_section('options'): + sys.stderr.write('~/.pwclientrc is in the old format. Migrating it...') + + old_project = config.get('base','project') + + new_config = ConfigParser.ConfigParser() + new_config.add_section('options') - if len(args) > 1: - sys.stderr.write("Too many arguments specified\n") - usage() + new_config.set('options','default',old_project) + new_config.add_section(old_project) + + new_config.set(old_project,'url',config.get('base','url')) + if config.has_option('auth', 'username'): + new_config.set(old_project,'username',config.get('auth','username')) + if config.has_option('auth', 'password'): + new_config.set(old_project,'password',config.get('auth','password')) + + old_config_file = CONFIG_FILE + '.orig' + shutil.copy2(CONFIG_FILE,old_config_file) + + with open(CONFIG_FILE, 'wb') as fd: + new_config.write(fd) + + sys.stderr.write(' Done.\n') + sys.stderr.write('Your old ~/.pwclientrc was saved to %s\n' % old_config_file) + sys.stderr.write('and was converted to the new format. You may want to\n') + sys.stderr.write('inspect it before continuing.\n') + sys.exit(1) + + if not project_str: + try: + project_str = config.get('options', 'default') + except: + sys.stderr.write("No default project configured in ~/.pwclientrc\n") + action_parser.print_help() + sys.exit(1) + + if not config.has_section(project_str): + sys.stderr.write("No section for project %s\n" % project_str) + sys.exit(1) + if not config.has_option(project_str, 'url'): + sys.stderr.write("No URL for project %s\n" % project_str) + sys.exit(1) + if not do_signoff and config.has_option('options', 'signoff'): + do_signoff = config.getboolean('options', 'signoff') + if not do_signoff and config.has_option(project_str, 'signoff'): + do_signoff = config.getboolean(project_str, 'signoff') + + url = config.get(project_str, 'url') - (username, password) = (None, None) transport = None if action in auth_actions: - if config.has_option('auth', 'username') and \ - config.has_option('auth', 'password'): + if config.has_option(project_str, 'username') and \ + config.has_option(project_str, 'password'): use_https = url.startswith('https') transport = BasicHTTPAuthTransport( \ - config.get('auth', 'username'), - config.get('auth', 'password'), + config.get(project_str, 'username'), + config.get(project_str, 'password'), use_https) else: @@ -392,24 +642,40 @@ def main(): if state_str: filt.add("state", state_str) + if archived_str: + filt.add("archived", archived_str == 'yes') + + if msgid_str: + filt.add("msgid", msgid_str) + try: rpc = xmlrpclib.Server(url, transport = transport) except: sys.stderr.write("Unable to connect to %s\n" % url) sys.exit(1) - patch_id = None - if hash_str: - patch_id = patch_id_from_hash(rpc, project_str, hash_str) - if patch_id is None: - sys.stderr.write("No patch has the hash provided\n") + # It should be safe to assume hash_str is not zero, but who knows.. + if hash_str != None: + patch_ids = [patch_id_from_hash(rpc, project_str, hash_str)] + + # helper for non_empty() to print correct helptext + h = locals()[action + '_parser'] + + # Require either hash_str or IDs for + def non_empty(h, patch_ids): + """Error out if no patch IDs were specified""" + if patch_ids == None or len(patch_ids) < 1: + sys.stderr.write("Error: Missing Argument! " + + "Either [-h HASH] or [ID [ID ...]] are required\n") + if h: + h.print_help() sys.exit(1) - + return patch_ids if action == 'list' or action == 'search': - if len(args) > 0: - filt.add("name__icontains", args[0]) - action_list(rpc, filt, submitter_str, delegate_str) + if args.get('patch_name') != None: + filt.add("name__icontains", args.get('patch_name')) + action_list(rpc, filt, submitter_str, delegate_str, format_str) elif action.startswith('project'): action_projects(rpc) @@ -418,47 +684,61 @@ def main(): action_states(rpc) elif action == 'view': - try: - patch_id = patch_id or int(args[0]) - except: - sys.stderr.write("Invalid patch ID given\n") - sys.exit(1) - - s = rpc.patch_get_mbox(patch_id) - if len(s) > 0: - print unicode(s).encode("utf-8") + pager = os.environ.get('PAGER') + if pager: + pager = subprocess.Popen( + pager.split(), stdin=subprocess.PIPE + ) + if pager: + i = list() + for patch_id in non_empty(h, patch_ids): + s = rpc.patch_get_mbox(patch_id) + if len(s) > 0: + i.append(unicode(s).encode("utf-8")) + if len(i) > 0: + pager.communicate(input="\n".join(i)) + pager.stdin.close() + else: + for patch_id in non_empty(h, patch_ids): + s = rpc.patch_get_mbox(patch_id) + if len(s) > 0: + print unicode(s).encode("utf-8") - elif action == 'get' or action == 'save': - try: - patch_id = patch_id or int(args[0]) - except: - sys.stderr.write("Invalid patch ID given\n") - sys.exit(1) + elif action == 'info': + for patch_id in non_empty(h, patch_ids): + action_info(rpc, patch_id) - action_get(rpc, patch_id) + elif action == 'get': + for patch_id in non_empty(h, patch_ids): + action_get(rpc, patch_id) elif action == 'apply': - try: - patch_id = patch_id or int(args[0]) - except: - sys.stderr.write("Invalid patch ID given\n") - sys.exit(1) - - action_apply(rpc, patch_id) + for patch_id in non_empty(h, patch_ids): + ret = action_apply(rpc, patch_id) + if ret: + sys.stderr.write("Apply failed with exit status %d\n" % ret) + sys.exit(1) + + elif action == 'git_am': + cmd = ['git', 'am'] + if do_signoff: + cmd.append('-s') + for patch_id in non_empty(h, patch_ids): + ret = action_apply(rpc, patch_id, cmd) + if ret: + sys.stderr.write("'git am' failed with exit status %d\n" % ret) + sys.exit(1) elif action == 'update': - try: - patch_id = patch_id or int(args[0]) - except: - sys.stderr.write("Invalid patch ID given\n") - sys.exit(1) - - action_update_patch(rpc, patch_id, state = state_str, - commit = commit_str) + for patch_id in non_empty(h, patch_ids): + action_update_patch(rpc, patch_id, state = state_str, + archived = archived_str, commit = commit_str + ) else: sys.stderr.write("Unknown action '%s'\n" % action) - usage() + action_parser.print_help() + sys.exit(1) if __name__ == "__main__": main()