X-Git-Url: http://git.ozlabs.org/?a=blobdiff_plain;f=apps%2Fpatchwork%2Fbin%2Fpwclient;h=a31099da099e6671297659c36cd638302d212ccb;hb=84e00517d15bfd536d64bd15ed1669116e2ca7fa;hp=dba68fb036b1b28c9351548f6cf142b770238ccb;hpb=ad4715cef72910b1310d4b93a2294ace878b0b4a;p=patchwork diff --git a/apps/patchwork/bin/pwclient b/apps/patchwork/bin/pwclient index dba68fb..a31099d 100755 --- a/apps/patchwork/bin/pwclient +++ b/apps/patchwork/bin/pwclient @@ -22,19 +22,20 @@ import os import sys import xmlrpclib -import getopt +import argparse import string import tempfile import subprocess import base64 import ConfigParser +import shutil # 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.""" @@ -111,7 +112,9 @@ def usage(): sys.stderr.write("Where is one of:\n") sys.stderr.write( """ apply : Apply a patch (in the current dir, using -p1) + git-am : Apply a patch to current git branch using "git am" get : Download a patch and save it locally + info : Display patchwork info about a given patch ID projects : List all projects states : Show list of potential patch states list [str] : List patches, using the optional filters specified @@ -126,7 +129,8 @@ def usage(): -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""") + -n : Restrict number of results + -m : Filter by Message-Id\n""") sys.stderr.write("""\nActions that take an ID argument can also be \ invoked with: -h : Lookup by patch hash\n""") @@ -162,10 +166,10 @@ def person_ids_by_name(rpc, name): def list_patches(patches): """Dump a list of patches to stdout.""" - print("%-5s %-12s %s" % ("ID", "State", "Name")) - print("%-5s %-12s %s" % ("--", "-----", "----")) + print("%-7s %-12s %s" % ("ID", "State", "Name")) + print("%-7s %-12s %s" % ("--", "-----", "----")) for patch in patches: - print("%-5d %-12s %s" % (patch['id'], patch['state'], patch['name'])) + print("%-7d %-12s %s" % (patch['id'], patch['state'], patch['name'])) def action_list(rpc, filter, submitter_str, delegate_str): filter.resolve_ids(rpc) @@ -173,7 +177,7 @@ def action_list(rpc, filter, submitter_str, delegate_str): if submitter_str != "": 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: @@ -190,7 +194,7 @@ def action_list(rpc, filter, submitter_str, delegate_str): if delegate_str != "": 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: @@ -222,6 +226,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,18 +262,25 @@ 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')) else: sys.stderr.write("Error: No patch content found\n") sys.exit(1) @@ -303,23 +322,188 @@ 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) + ] + for subparsers_action in subparsers_actions: + for choice, subparser in subparsers_action.choices.items(): + # gross but the whole thing is.. + if (len(subparser._actions) == 2 \ + and ['hash', 'id'] == [a.dest for a in subparser._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_x = hash_parser.add_mutually_exclusive_group(required=True) + hash_parser_x.add_argument( + '-h', metavar='HASH', dest='hash', action='store', required=False, + help='''Lookup by patch hash''' + ) + hash_parser_x.add_argument( + 'id', metavar='ID', nargs='?', action='store', type=int, + help='Patch ID', + ) + + 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( + '-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( + 'patch_name', metavar='STR', nargs='?', + help='substring to search for patches by name', + ) + + action_parser = argparse.ArgumentParser( + prog='pwclient', + add_help=False, + version=False, + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog='''(apply | get | info | view | update) (-h HASH | 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], + 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], + 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], + 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], + 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], + add_help=False, + help='''View a patch''' + ) + view_parser.set_defaults(subcmd='view') + update_parser = subparsers.add_parser( + 'update', parents=[hash_parser], + add_help=False, + help='''Update patch''' + ) + update_parser.set_defaults(subcmd='update') + 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.)''' + ) + + list_parser = subparsers.add_parser("list", + add_help=False, + #aliases=['search'], + parents=[filter_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='''Alias for "list"''' + ) + search_parser.set_defaults(subcmd='list') if len(sys.argv) < 2: - usage() + action_parser.print_help() + sys.exit(0) - action = sys.argv[1].lower() + args = action_parser.parse_args() + args=dict(vars(args)) # set defaults filt = Filter() @@ -328,57 +512,98 @@ def main(): project_str = "" commit_str = "" state_str = "" - hash_str = "" + hash_str = None + msgid_str = "" + id_str = None url = DEFAULT_URL - config = ConfigParser.ConfigParser() - config.read(CONFIG_FILES) + action = args.get('subcmd') + + if args.get('s'): + state_str = args.get('s') + if args.get('p'): + project_str = args.get('p') + if args.get('w'): + submitter_str = args.get('w') + if args.get('d'): + delegate_str = args.get('d') + if args.get('c'): + commit_str = args.get('c') + if args.get('hash'): + hash_str = args.get('hash') + if args.get('id'): + id_str = args.get('id') + if args.get('m'): + msgid_str = args.get('m') + 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) # 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) + 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') + + 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") usage() - if len(args) > 1: - sys.stderr.write("Too many arguments specified\n") - usage() + 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) + + 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,6 +617,9 @@ def main(): if state_str: filt.add("state", state_str) + if msgid_str: + filt.add("msgid", msgid_str) + try: rpc = xmlrpclib.Server(url, transport = transport) except: @@ -399,16 +627,16 @@ def main(): sys.exit(1) patch_id = None + # hash_str and id_str are mutually exclusive 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") - sys.exit(1) - + else: + # id_str from argparse is an int + patch_id = id_str if action == 'list' or action == 'search': - if len(args) > 0: - filt.add("name__icontains", args[0]) + if args.get('patch_name') != None: + filt.add("name__icontains", args.get('patch_name')) action_list(rpc, filt, submitter_str, delegate_str) elif action.startswith('project'): @@ -418,41 +646,26 @@ 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") - 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) - - action_get(rpc, patch_id) + elif action in ('get', 'save', 'info'): + if action == 'info': + action_info(rpc, patch_id) + else: + 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) - elif action == 'update': - try: - patch_id = patch_id or int(args[0]) - except: - sys.stderr.write("Invalid patch ID given\n") - sys.exit(1) + elif action == 'git-am': + cmd = ['git', 'am'] + if args.get('signoff'): + cmd.append('-s') + action_apply(rpc, patch_id, cmd) + elif action == 'update': action_update_patch(rpc, patch_id, state = state_str, commit = commit_str)