From 27bb3876451d2412f152a39333f301dfa5d66d06 Mon Sep 17 00:00:00 2001 From: "maruel@chromium.org" Date: Mon, 30 May 2011 20:33:19 +0000 Subject: [PATCH] Add commit_queue.py tool to toggle the bit of the commit queue from command line Add "git cl set_commit" command to set the flag more easily. Add --commit to "git cl upload" to streamline workflow even more. Continue conversion to Rietveld object. R=dpranke@chromium.org BUG= TEST= Review URL: http://codereview.chromium.org/7084037 git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@87253 0039d316-1c4b-4281-b951-d872f2087c98 --- commit_queue.py | 187 ++++++++++++++++++++++++++++++++++++++++++++++++ git_cl.py | 62 ++++++++++------ 2 files changed, 228 insertions(+), 21 deletions(-) create mode 100755 commit_queue.py diff --git a/commit_queue.py b/commit_queue.py new file mode 100755 index 0000000000..3f4e7b68de --- /dev/null +++ b/commit_queue.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python +# Copyright (c) 2011 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Access the commit queue from the command line. +""" + +__version__ = '0.1' + +import functools +import logging +import optparse +import os +import sys +import urllib2 + +import breakpad # pylint: disable=W0611 + +import fix_encoding +import rietveld + + +def usage(more): + def hook(fn): + fn.func_usage_more = more + return fn + return hook + + +def need_issue(fn): + """Post-parse args to create a Rietveld object.""" + @functools.wraps(fn) + def hook(parser, args, *extra_args, **kwargs): + old_parse_args = parser.parse_args + + def new_parse_args(args): + options, args = old_parse_args(args) + if not options.issue: + parser.error('Require --issue') + obj = rietveld.Rietveld(options.server, options.user, None) + return options, args, obj + + parser.parse_args = new_parse_args + + parser.add_option( + '-u', '--user', + metavar='U', + default=os.environ.get('EMAIL_ADDRESS', None), + help='Email address, default: %default') + parser.add_option( + '-i', '--issue', + metavar='I', + type='int', + help='Rietveld issue number') + parser.add_option( + '-s', + '--server', + metavar='S', + default='http://codereview.chromium.org', + help='Rietveld server, default: %default') + + # Call the original function with the modified parser. + return fn(parser, args, *extra_args, **kwargs) + + hook.func_usage_more = '[options]' + return hook + + +def set_commit(obj, issue, flag): + """Sets the commit bit flag on an issue.""" + try: + patchset = obj.get_issue_properties(issue, False)['patchsets'][-1] + print obj.set_flag(issue, patchset, 'commit', flag) + except urllib2.HTTPError, e: + if e.code == 404: + print >> sys.stderr, 'Issue %d doesn\'t exist.' % issue + elif e.code == 403: + print >> sys.stderr, 'Access denied to issue %d.' % issue + else: + raise + return 1 + +@need_issue +def CMDset(parser, args): + """Sets the commit bit.""" + options, args, obj = parser.parse_args(args) + if args: + parser.error('Unrecognized args: %s' % ' '.join(args)) + return set_commit(obj, options.issue, '1') + + +@need_issue +def CMDclear(parser, args): + """Clears the commit bit.""" + options, args, obj = parser.parse_args(args) + if args: + parser.error('Unrecognized args: %s' % ' '.join(args)) + return set_commit(obj, options.issue, '0') + + +############################################################################### +## Boilerplate code + + +def gen_parser(): + """Returns an OptionParser instance with default options. + + It should be then processed with gen_usage() before being used. + """ + parser = optparse.OptionParser(version=__version__) + # Remove description formatting + parser.format_description = lambda x: parser.description + # Add common parsing. + old_parser_args = parser.parse_args + + def Parse(*args, **kwargs): + options, args = old_parser_args(*args, **kwargs) + logging.basicConfig( + level=[logging.WARNING, logging.INFO, logging.DEBUG][ + min(2, options.verbose)], + format='%(levelname)s %(filename)s(%(lineno)d): %(message)s') + return options, args + + parser.parse_args = Parse + + parser.add_option( + '-v', '--verbose', action='count', default=0, + help='Use multiple times to increase logging level') + return parser + + +def Command(name): + return getattr(sys.modules[__name__], 'CMD' + name, None) + + +@usage('') +def CMDhelp(parser, args): + """Print list of commands or use 'help '.""" + # Strip out the help command description and replace it with the module + # docstring. + parser.description = sys.modules[__name__].__doc__ + parser.description += '\nCommands are:\n' + '\n'.join( + ' %-12s %s' % ( + fn[3:], Command(fn[3:]).__doc__.split('\n', 1)[0].rstrip('.')) + for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')) + + _, args = parser.parse_args(args) + if len(args) == 1 and args[0] != 'help': + return main(args + ['--help']) + parser.print_help() + return 0 + + +def gen_usage(parser, command): + """Modifies an OptionParser object with the command's documentation. + + The documentation is taken from the function's docstring. + """ + obj = Command(command) + more = getattr(obj, 'func_usage_more') + # OptParser.description prefer nicely non-formatted strings. + parser.description = obj.__doc__ + '\n' + parser.set_usage('usage: %%prog %s %s' % (command, more)) + + +def main(args=None): + # Do it late so all commands are listed. + # pylint: disable=E1101 + parser = gen_parser() + if args is None: + args = sys.argv[1:] + if args: + command = Command(args[0]) + if command: + # "fix" the usage and the description now that we know the subcommand. + gen_usage(parser, args[0]) + return command(parser, args[1:]) + + # Not a known command. Default to help. + gen_usage(parser, 'help') + return CMDhelp(parser, args) + + +if __name__ == "__main__": + fix_encoding.fix_encoding() + sys.exit(main()) diff --git a/git_cl.py b/git_cl.py index 5cf2580e4c..1cd591d7f3 100755 --- a/git_cl.py +++ b/git_cl.py @@ -463,9 +463,8 @@ or verify this branch is set up to track another (via the --track argument to def GetDescription(self, pretty=False): if not self.has_description: if self.GetIssue(): - path = '/' + self.GetIssue() + '/description' - rpc_server = self.RpcServer() - self.description = rpc_server.Send(path).strip() + self.description = self.RpcServer().get_description( + int(self.GetIssue())).strip() self.has_description = True if pretty: wrapper = textwrap.TextWrapper() @@ -494,10 +493,9 @@ or verify this branch is set up to track another (via the --track argument to self.has_patchset = False def GetPatchSetDiff(self, issue): - # Grab the last patchset of the issue first. - data = json.loads(self.RpcServer().Send('/api/%s' % issue)) - patchset = data['patchsets'][-1] - return self.RpcServer().Send( + patchset = self.RpcServer().get_issue_properties( + int(issue), False)['patchsets'][-1] + return self.RpcServer().get( '/download/issue%s_%s.diff' % (issue, patchset)) def SetIssue(self, issue): @@ -564,20 +562,23 @@ or verify this branch is set up to track another (via the --track argument to return output def CloseIssue(self): - rpc_server = self.RpcServer() - # Newer versions of Rietveld require us to pass an XSRF token to POST, so - # we fetch it from the server. (The version used by Chromium has been - # modified so the token isn't required when closing an issue.) - xsrf_token = rpc_server.Send('/xsrf_token', - extra_headers={'X-Requesting-XSRF-Token': '1'}) - - # You cannot close an issue with a GET. - # We pass an empty string for the data so it is a POST rather than a GET. - data = [("description", self.description), - ("xsrf_token", xsrf_token)] - ctype, body = upload.EncodeMultipartFormData(data, []) - rpc_server.Send( - '/' + self.GetIssue() + '/close', payload=body, content_type=ctype) + return self.RpcServer().close_issue(int(self.GetIssue())) + + def SetFlag(self, flag, value): + """Patchset must match.""" + if not self.GetPatchset(): + DieWithError('The patchset needs to match. Send another patchset.') + try: + return self.RpcServer().set_flag( + int(self.GetIssue()), int(self.GetPatchset()), flag, value) + except urllib2.HTTPError, e: + if e.code == 404: + DieWithError('The issue %s doesn\'t exist.' % self.GetIssue()) + if e.code == 403: + DieWithError( + ('Access denied to issue %s. Maybe the patchset %s doesn\'t ' + 'match?') % (self.GetIssue(), self.GetPatchset())) + raise def RpcServer(self): """Returns an upload.RpcServer() to access this review's rietveld instance. @@ -913,6 +914,8 @@ def CMDupload(parser, args): dest="from_logs", help="""Squashes git commit logs into change description and uses message as subject""") + parser.add_option('-c', '--use-commit-queue', action='store_true', + help='tell the commit queue to commit this patchset') (options, args) = parser.parse_args(args) # Make sure index is up-to-date before running diff-index. @@ -1021,6 +1024,9 @@ def CMDupload(parser, args): if not cl.GetIssue(): cl.SetIssue(issue) cl.SetPatchset(patchset) + + if options.use_commit_queue: + cl.SetFlag('commit', '1') return 0 @@ -1265,6 +1271,8 @@ def CMDpatch(parser, args): return 1 issue_arg = args[0] + # TODO(maruel): Use apply_issue.py + if re.match(r'\d+', issue_arg): # Input is an issue id. Figure out the URL. issue = issue_arg @@ -1378,11 +1386,23 @@ def CMDtree(parser, args): def CMDupstream(parser, args): """print the name of the upstream branch, if any""" _, args = parser.parse_args(args) + if args: + parser.error('Unrecognized args: %s' % ' '.join(args)) cl = Changelist() print cl.GetUpstreamBranch() return 0 +def CMDset_commit(parser, args): + """set the commit bit""" + _, args = parser.parse_args(args) + if args: + parser.error('Unrecognized args: %s' % ' '.join(args)) + cl = Changelist() + cl.SetFlag('commit', '1') + return 0 + + def Command(name): return getattr(sys.modules[__name__], 'CMD' + name, None)