123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597 |
- #!/usr/bin/env vpython3
- # Copyright 2017 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.
- """Simple client for the Gerrit REST API.
- Example usage:
- ./gerrit_client.py [command] [args]
- """
- import json
- import logging
- import optparse
- import subcommand
- import sys
- import urllib.parse
- import gerrit_util
- import setup_color
- __version__ = '0.1'
- def write_result(result, opt):
- if opt.json_file:
- with open(opt.json_file, 'w') as json_file:
- json_file.write(json.dumps(result))
- @subcommand.usage('[args ...]')
- def CMDmovechanges(parser, args):
- """Move changes to a different destination branch."""
- parser.add_option('-p',
- '--param',
- dest='params',
- action='append',
- help='repeatable query parameter, format: -p key=value')
- parser.add_option('--destination_branch',
- dest='destination_branch',
- help='where to move changes to')
- (opt, args) = parser.parse_args(args)
- if not opt.destination_branch:
- parser.error('--destination_branch is required')
- for p in opt.params:
- if '=' not in p:
- parser.error('--param is key=value, not "%s"' % p)
- host = urllib.parse.urlparse(opt.host).netloc
- limit = 100
- while True:
- result = gerrit_util.QueryChanges(
- host,
- list(tuple(p.split('=', 1)) for p in opt.params),
- limit=limit,
- )
- for change in result:
- gerrit_util.MoveChange(host, change['id'], opt.destination_branch)
- if len(result) < limit:
- break
- logging.info("Done")
- @subcommand.usage('[args ...]')
- def CMDbranchinfo(parser, args):
- """Get information on a gerrit branch."""
- parser.add_option('--branch', dest='branch', help='branch name')
- (opt, args) = parser.parse_args(args)
- host = urllib.parse.urlparse(opt.host).netloc
- project = urllib.parse.quote_plus(opt.project)
- branch = urllib.parse.quote_plus(opt.branch)
- result = gerrit_util.GetGerritBranch(host, project, branch)
- logging.info(result)
- write_result(result, opt)
- @subcommand.usage('[args ...]')
- def CMDrawapi(parser, args):
- """Call an arbitrary Gerrit REST API endpoint."""
- parser.add_option('--path',
- dest='path',
- help='HTTP path of the API endpoint')
- parser.add_option('--method',
- dest='method',
- help='HTTP method for the API (default: GET)')
- parser.add_option('--body', dest='body', help='API JSON body contents')
- parser.add_option('--accept_status',
- dest='accept_status',
- help='Comma-delimited list of status codes for success.')
- (opt, args) = parser.parse_args(args)
- if not opt.path:
- parser.error('--path is required')
- host = urllib.parse.urlparse(opt.host).netloc
- kwargs = {}
- if opt.method:
- kwargs['reqtype'] = opt.method.upper()
- if opt.body:
- kwargs['body'] = json.loads(opt.body)
- if opt.accept_status:
- kwargs['accept_statuses'] = [
- int(x) for x in opt.accept_status.split(',')
- ]
- result = gerrit_util.CallGerritApi(host, opt.path, **kwargs)
- logging.info(result)
- write_result(result, opt)
- @subcommand.usage('[args ...]')
- def CMDbranch(parser, args):
- """Create a branch in a gerrit project."""
- parser.add_option('--branch', dest='branch', help='branch name')
- parser.add_option('--commit', dest='commit', help='commit hash')
- parser.add_option(
- '--allow-existent-branch',
- action='store_true',
- help=('Accept that the branch alread exists as long as the'
- ' branch head points the given commit'))
- (opt, args) = parser.parse_args(args)
- if not opt.project:
- parser.error('--project is required')
- if not opt.branch:
- parser.error('--branch is required')
- if not opt.commit:
- parser.error('--commit is required')
- project = urllib.parse.quote_plus(opt.project)
- host = urllib.parse.urlparse(opt.host).netloc
- branch = urllib.parse.quote_plus(opt.branch)
- result = gerrit_util.GetGerritBranch(host, project, branch)
- if result:
- if not opt.allow_existent_branch:
- raise gerrit_util.GerritError(200, 'Branch already exists')
- if result.get('revision') != opt.commit:
- raise gerrit_util.GerritError(
- 200, ('Branch already exists but '
- 'the branch head is not at the given commit'))
- else:
- try:
- result = gerrit_util.CreateGerritBranch(host, project, branch,
- opt.commit)
- except gerrit_util.GerritError as e:
- result = gerrit_util.GetGerritBranch(host, project, branch)
- if not result:
- raise e
- # If reached here, we hit a real conflict error, because the
- # branch just created is pointing a different commit.
- if result.get('revision') != opt.commit:
- raise gerrit_util.GerritError(
- 200, ('Conflict: branch was created but '
- 'the branch head is not at the given commit'))
- logging.info(result)
- write_result(result, opt)
- @subcommand.usage('[args ...]')
- def CMDtag(parser, args):
- """Create a tag in a gerrit project."""
- parser.add_option('--tag', dest='tag', help='tag name')
- parser.add_option('--commit', dest='commit', help='commit hash')
- (opt, args) = parser.parse_args(args)
- if not opt.project:
- parser.error('--project is required')
- if not opt.tag:
- parser.error('--tag is required')
- if not opt.commit:
- parser.error('--commit is required')
- project = urllib.parse.quote_plus(opt.project)
- host = urllib.parse.urlparse(opt.host).netloc
- tag = urllib.parse.quote_plus(opt.tag)
- result = gerrit_util.CreateGerritTag(host, project, tag, opt.commit)
- logging.info(result)
- write_result(result, opt)
- @subcommand.usage('[args ...]')
- def CMDhead(parser, args):
- """Update which branch the project HEAD points to."""
- parser.add_option('--branch', dest='branch', help='branch name')
- (opt, args) = parser.parse_args(args)
- if not opt.project:
- parser.error('--project is required')
- if not opt.branch:
- parser.error('--branch is required')
- project = urllib.parse.quote_plus(opt.project)
- host = urllib.parse.urlparse(opt.host).netloc
- branch = urllib.parse.quote_plus(opt.branch)
- result = gerrit_util.UpdateHead(host, project, branch)
- logging.info(result)
- write_result(result, opt)
- @subcommand.usage('[args ...]')
- def CMDheadinfo(parser, args):
- """Retrieves the current HEAD of the project."""
- (opt, args) = parser.parse_args(args)
- if not opt.project:
- parser.error('--project is required')
- project = urllib.parse.quote_plus(opt.project)
- host = urllib.parse.urlparse(opt.host).netloc
- result = gerrit_util.GetHead(host, project)
- logging.info(result)
- write_result(result, opt)
- @subcommand.usage('[args ...]')
- def CMDchanges(parser, args):
- """Queries gerrit for matching changes."""
- parser.add_option('-p',
- '--param',
- dest='params',
- action='append',
- default=[],
- help='repeatable query parameter, format: -p key=value')
- parser.add_option('--query', help='raw gerrit search query string')
- parser.add_option('-o',
- '--o-param',
- dest='o_params',
- action='append',
- help='gerrit output parameters, e.g. ALL_REVISIONS')
- parser.add_option('--limit',
- dest='limit',
- type=int,
- help='maximum number of results to return')
- parser.add_option('--start',
- dest='start',
- type=int,
- help='how many changes to skip '
- '(starting with the most recent)')
- (opt, args) = parser.parse_args(args)
- if not (opt.params or opt.query):
- parser.error('--param or --query required')
- for p in opt.params:
- if '=' not in p:
- parser.error('--param is key=value, not "%s"' % p)
- result = gerrit_util.QueryChanges(
- urllib.parse.urlparse(opt.host).netloc,
- list(tuple(p.split('=', 1)) for p in opt.params),
- first_param=opt.query,
- start=opt.start, # Default: None
- limit=opt.limit, # Default: None
- o_params=opt.o_params, # Default: None
- )
- logging.info('Change query returned %d changes.', len(result))
- write_result(result, opt)
- @subcommand.usage('[args ...]')
- def CMDrelatedchanges(parser, args):
- """Gets related changes for a given change and revision."""
- parser.add_option('-c', '--change', type=str, help='change id')
- parser.add_option('-r', '--revision', type=str, help='revision id')
- (opt, args) = parser.parse_args(args)
- result = gerrit_util.GetRelatedChanges(
- urllib.parse.urlparse(opt.host).netloc,
- change=opt.change,
- revision=opt.revision,
- )
- logging.info(result)
- write_result(result, opt)
- @subcommand.usage('[args ...]')
- def CMDcreatechange(parser, args):
- """Create a new change in gerrit."""
- parser.add_option('-s', '--subject', help='subject for change')
- parser.add_option('-b',
- '--branch',
- default='main',
- help='target branch for change')
- parser.add_option(
- '-p',
- '--param',
- dest='params',
- action='append',
- help='repeatable field value parameter, format: -p key=value')
- parser.add_option('--cc',
- dest='cc_list',
- action='append',
- help='CC address to notify, format: --cc foo@example.com')
- (opt, args) = parser.parse_args(args)
- for p in opt.params:
- if '=' not in p:
- parser.error('--param is key=value, not "%s"' % p)
- params = list(tuple(p.split('=', 1)) for p in opt.params)
- if opt.cc_list:
- params.append(('notify_details', {'CC': {'accounts': opt.cc_list}}))
- result = gerrit_util.CreateChange(
- urllib.parse.urlparse(opt.host).netloc,
- opt.project,
- branch=opt.branch,
- subject=opt.subject,
- params=params,
- )
- logging.info(result)
- write_result(result, opt)
- @subcommand.usage('[args ...]')
- def CMDchangeedit(parser, args):
- """Puts content of a file into a change edit."""
- parser.add_option('-c', '--change', type=int, help='change number')
- parser.add_option('--path', help='path for file')
- parser.add_option('--file', help='file to place at |path|')
- (opt, args) = parser.parse_args(args)
- with open(opt.file) as f:
- data = f.read()
- result = gerrit_util.ChangeEdit(
- urllib.parse.urlparse(opt.host).netloc, opt.change, opt.path, data)
- logging.info(result)
- write_result(result, opt)
- @subcommand.usage('[args ...]')
- def CMDpublishchangeedit(parser, args):
- """Publish a Gerrit change edit."""
- parser.add_option('-c', '--change', type=int, help='change number')
- parser.add_option('--notify', help='whether to notify')
- (opt, args) = parser.parse_args(args)
- result = gerrit_util.PublishChangeEdit(
- urllib.parse.urlparse(opt.host).netloc, opt.change, opt.notify)
- logging.info(result)
- write_result(result, opt)
- @subcommand.usage('[args ...]')
- def CMDsubmitchange(parser, args):
- """Submit a Gerrit change."""
- parser.add_option('-c', '--change', type=int, help='change number')
- (opt, args) = parser.parse_args(args)
- result = gerrit_util.SubmitChange(
- urllib.parse.urlparse(opt.host).netloc, opt.change)
- logging.info(result)
- write_result(result, opt)
- @subcommand.usage('[args ...]')
- def CMDchangesubmittedtogether(parser, args):
- """Get all changes submitted with the given one."""
- parser.add_option('-c', '--change', type=int, help='change number')
- (opt, args) = parser.parse_args(args)
- result = gerrit_util.GetChangesSubmittedTogether(
- urllib.parse.urlparse(opt.host).netloc, opt.change)
- logging.info(result)
- write_result(result, opt)
- @subcommand.usage('[args ...]')
- def CMDgetcommitincludedin(parser, args):
- """Retrieves the branches and tags for a given commit."""
- parser.add_option('--commit', dest='commit', help='commit hash')
- (opt, args) = parser.parse_args(args)
- result = gerrit_util.GetCommitIncludedIn(
- urllib.parse.urlparse(opt.host).netloc, opt.project, opt.commit)
- logging.info(result)
- write_result(result, opt)
- @subcommand.usage('[args ...]')
- def CMDsetbotcommit(parser, args):
- """Sets bot-commit+1 to a bot generated change."""
- parser.add_option('-c', '--change', type=int, help='change number')
- (opt, args) = parser.parse_args(args)
- result = gerrit_util.SetReview(urllib.parse.urlparse(opt.host).netloc,
- opt.change,
- labels={'Bot-Commit': 1},
- ready=True)
- logging.info(result)
- write_result(result, opt)
- @subcommand.usage('[args ...]')
- def CMDsetlabel(parser, args):
- """Sets a label to a specific value on a given change."""
- parser.add_option('-c', '--change', type=int, help='change number')
- parser.add_option('-l',
- '--label',
- nargs=2,
- metavar=('label_name', 'label_value'))
- (opt, args) = parser.parse_args(args)
- result = gerrit_util.SetReview(urllib.parse.urlparse(opt.host).netloc,
- opt.change,
- labels={opt.label[0]: opt.label[1]})
- logging.info(result)
- write_result(result, opt)
- @subcommand.usage('[args ...]')
- def CMDaddMessage(parser, args):
- """Adds a message to a given change at given revision."""
- parser.add_option('-c', '--change', type=int, help='change number')
- parser.add_option(
- '-r',
- '--revision',
- type=str,
- default='current',
- help='revision ID. See '
- 'https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#revision-id ' # pylint: disable=line-too-long
- 'for acceptable format')
- parser.add_option('-m', '--message', type=str, help='message to add')
- # This default matches the Gerrit REST API & web UI defaults.
- parser.add_option('--automatic-attention-set-update',
- action='store_true',
- help='Update the attention set (default)')
- parser.add_option('--no-automatic-attention-set-update',
- dest='automatic_attention_set_update',
- action='store_false',
- help='Do not update the attention set')
- (opt, args) = parser.parse_args(args)
- if not opt.change:
- parser.error('--change is required')
- if not opt.message:
- parser.error('--message is required')
- result = gerrit_util.SetReview(
- urllib.parse.urlparse(opt.host).netloc,
- opt.change,
- revision=opt.revision,
- msg=opt.message,
- automatic_attention_set_update=opt.automatic_attention_set_update)
- logging.info(result)
- write_result(result, opt)
- @subcommand.usage('')
- def CMDrestore(parser, args):
- """Restores a Gerrit change."""
- parser.add_option('-c', '--change', type=str, help='change number')
- parser.add_option('-m',
- '--message',
- default='',
- help='reason for restoring')
- (opt, args) = parser.parse_args(args)
- if not opt.change:
- parser.error('--change is required')
- result = gerrit_util.RestoreChange(
- urllib.parse.urlparse(opt.host).netloc, opt.change, opt.message)
- logging.info(result)
- write_result(result, opt)
- @subcommand.usage('')
- def CMDabandon(parser, args):
- """Abandons a Gerrit change."""
- parser.add_option('-c', '--change', type=int, help='change number')
- parser.add_option('-m',
- '--message',
- default='',
- help='reason for abandoning')
- (opt, args) = parser.parse_args(args)
- if not opt.change:
- parser.error('--change is required')
- result = gerrit_util.AbandonChange(
- urllib.parse.urlparse(opt.host).netloc, opt.change, opt.message)
- logging.info(result)
- write_result(result, opt)
- @subcommand.usage('')
- def CMDmass_abandon(parser, args):
- """Mass abandon changes
- Abandons CLs that match search criteria provided by user. Before any change
- is actually abandoned, user is presented with a list of CLs that will be
- affected if user confirms. User can skip confirmation by passing --force
- parameter.
- The script can abandon up to 100 CLs per invocation.
- Examples:
- gerrit_client.py mass-abandon --host https://HOST -p 'project=repo2'
- gerrit_client.py mass-abandon --host https://HOST -p 'message=testing'
- gerrit_client.py mass-abandon --host https://HOST -p 'is=wip' -p 'age=1y'
- """
- parser.add_option('-p',
- '--param',
- dest='params',
- action='append',
- default=[],
- help='repeatable query parameter, format: -p key=value')
- parser.add_option('-m',
- '--message',
- default='',
- help='reason for abandoning')
- parser.add_option('-f',
- '--force',
- action='store_true',
- help='Don\'t prompt for confirmation')
- opt, args = parser.parse_args(args)
- for p in opt.params:
- if '=' not in p:
- parser.error('--param is key=value, not "%s"' % p)
- search_query = list(tuple(p.split('=', 1)) for p in opt.params)
- if not any(t for t in search_query if t[0] == 'owner'):
- # owner should always be present when abandoning changes
- search_query.append(('owner', 'me'))
- search_query.append(('status', 'open'))
- logging.info("Searching for: %s" % search_query)
- host = urllib.parse.urlparse(opt.host).netloc
- result = gerrit_util.QueryChanges(
- host,
- search_query,
- # abandon at most 100 changes as not all Gerrit instances support
- # unlimited results.
- limit=100,
- )
- if len(result) == 0:
- logging.warning("Nothing to abandon")
- return
- logging.warning("%s CLs match search query: " % len(result))
- for change in result:
- logging.warning("[ID: %d] %s" % (change['_number'], change['subject']))
- if not opt.force:
- q = input('Do you want to move forward with abandoning? [y to confirm] '
- ).strip()
- if q not in ['y', 'Y']:
- logging.warning("Aborting...")
- return
- for change in result:
- logging.warning("Abandoning: %s" % change['subject'])
- gerrit_util.AbandonChange(host, change['id'], opt.message)
- logging.warning("Done")
- class OptionParser(optparse.OptionParser):
- """Creates the option parse and add --verbose support."""
- def __init__(self, *args, **kwargs):
- optparse.OptionParser.__init__(self,
- *args,
- version=__version__,
- **kwargs)
- self.add_option('--verbose',
- action='count',
- default=0,
- help='Use 2 times for more debugging info')
- self.add_option('--host', dest='host', help='Url of host.')
- self.add_option('--project', dest='project', help='project name')
- self.add_option('--json_file',
- dest='json_file',
- help='output json filepath')
- def parse_args(self, args=None, values=None):
- options, args = optparse.OptionParser.parse_args(self, args, values)
- # Host is always required
- if not options.host:
- self.error('--host is required')
- levels = [logging.WARNING, logging.INFO, logging.DEBUG]
- logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
- return options, args
- def main(argv):
- dispatcher = subcommand.CommandDispatcher(__name__)
- return dispatcher.execute(OptionParser(), argv)
- if __name__ == '__main__':
- # These affect sys.stdout so do it outside of main() to simplify mocks in
- # unit testing.
- setup_color.init()
- try:
- sys.exit(main(sys.argv[1:]))
- except KeyboardInterrupt:
- sys.stderr.write('interrupted\n')
- sys.exit(1)
|