| #!/usr/bin/env python3 |
| |
| # |
| # Copyright (C) 2018 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # |
| |
| """A command line utility to post review votes to multiple CLs on Gerrit.""" |
| |
| from __future__ import print_function |
| |
| import argparse |
| import json |
| import os |
| import sys |
| |
| try: |
| from urllib.error import HTTPError # PY3 |
| except ImportError: |
| from urllib2 import HTTPError # PY2 |
| |
| from gerrit import ( |
| abandon, add_reviewers, create_url_opener_from_args, delete_reviewer, |
| delete_topic, find_gerrit_name, query_change_lists, restore, set_hashtags, |
| set_review, set_topic, submit |
| ) |
| |
| |
| def _get_labels_from_args(args): |
| """Collect and check labels from args.""" |
| if not args.label: |
| return None |
| labels = {} |
| for (name, value) in args.label: |
| try: |
| labels[name] = int(value) |
| except ValueError: |
| print('error: Label {} takes integer, but {} is specified' |
| .format(name, value), file=sys.stderr) |
| return labels |
| |
| |
| # pylint: disable=redefined-builtin |
| def _print_change_lists(change_lists, file=sys.stdout): |
| """Print matching change lists for each projects.""" |
| change_lists = sorted( |
| change_lists, key=lambda change: (change['project'], change['_number'])) |
| |
| prev_project = None |
| print('Change Lists:', file=file) |
| for change in change_lists: |
| project = change['project'] |
| if project != prev_project: |
| print(' ', project, file=file) |
| prev_project = project |
| |
| change_id = change['change_id'] |
| revision_sha1 = change['current_revision'] |
| revision = change['revisions'][revision_sha1] |
| subject = revision['commit']['subject'] |
| print(' ', change_id, '--', subject, file=file) |
| |
| |
| def _confirm(question): |
| """Confirm before proceeding.""" |
| try: |
| if input(question + ' [yn] ').lower() not in {'y', 'yes'}: |
| print('Cancelled', file=sys.stderr) |
| sys.exit(1) |
| except KeyboardInterrupt: |
| print('Cancelled', file=sys.stderr) |
| sys.exit(1) |
| |
| |
| def _parse_args(): |
| """Parse command line options.""" |
| parser = argparse.ArgumentParser() |
| |
| parser.add_argument('query', help='Change list query string') |
| parser.add_argument('-g', '--gerrit', help='Gerrit review URL') |
| |
| parser.add_argument('--gitcookies', |
| default=os.path.expanduser('~/.gitcookies'), |
| help='Gerrit cookie file') |
| parser.add_argument('--limits', default=1000, |
| help='Max number of change lists') |
| |
| parser.add_argument('-l', '--label', nargs=2, action='append', |
| help='Labels to be added') |
| parser.add_argument('-m', '--message', help='Review message') |
| |
| parser.add_argument('--submit', action='store_true', help='Submit a CL') |
| |
| parser.add_argument('--abandon', help='Abandon a CL with a message') |
| parser.add_argument('--restore', action='store_true', help='Restore a CL') |
| |
| parser.add_argument('--add-hashtag', action='append', help='Add hashtag') |
| parser.add_argument('--remove-hashtag', action='append', |
| help='Remove hashtag') |
| parser.add_argument('--delete-hashtag', action='append', |
| help='Remove hashtag', dest='remove_hashtag') |
| |
| parser.add_argument('--set-topic', help='Set topic name') |
| parser.add_argument('--delete-topic', action='store_true', |
| help='Delete topic name') |
| parser.add_argument('--remove-topic', action='store_true', |
| help='Delete topic name', dest='delete_topic') |
| parser.add_argument('--add-reviewer', action='append', default=[], |
| help='Add reviewer') |
| parser.add_argument('--delete-reviewer', action='append', default=[], |
| help='Delete reviewer') |
| |
| return parser.parse_args() |
| |
| |
| def _has_task(args): |
| """Determine whether a task has been specified in the arguments.""" |
| if args.label is not None or args.message is not None: |
| return True |
| if args.submit: |
| return True |
| if args.abandon is not None: |
| return True |
| if args.restore: |
| return True |
| if args.add_hashtag or args.remove_hashtag: |
| return True |
| if args.set_topic or args.delete_topic: |
| return True |
| if args.add_reviewer or args.delete_reviewer: |
| return True |
| return False |
| |
| |
| _SEP_SPLIT = '=' * 79 |
| _SEP = '-' * 79 |
| |
| |
| def _print_error(change, res_code, res_body, res_json): |
| """Print the error message""" |
| |
| change_id = change['change_id'] |
| project = change['project'] |
| revision_sha1 = change['current_revision'] |
| revision = change['revisions'][revision_sha1] |
| subject = revision['commit']['subject'] |
| |
| print(_SEP_SPLIT, file=sys.stderr) |
| print('Project:', project, file=sys.stderr) |
| print('Change-Id:', change_id, file=sys.stderr) |
| print('Subject:', subject, file=sys.stderr) |
| print('HTTP status code:', res_code, file=sys.stderr) |
| if res_json: |
| print(_SEP, file=sys.stderr) |
| json.dump(res_json, sys.stderr, indent=4, |
| separators=(', ', ': ')) |
| print(file=sys.stderr) |
| elif res_body: |
| print(_SEP, file=sys.stderr) |
| print(res_body.decode('utf-8'), file=sys.stderr) |
| print(_SEP_SPLIT, file=sys.stderr) |
| |
| |
| def _do_task(change, func, *args, **kwargs): |
| """Process a task and report errors when necessary.""" |
| |
| res_code, res_body, res_json = func(*args) |
| |
| if res_code != kwargs.get('expected_http_code', 200): |
| _print_error(change, res_code, res_body, res_json) |
| |
| errors = kwargs.get('errors') |
| if errors is not None: |
| errors['num_errors'] += 1 |
| |
| |
| def main(): |
| """Set review labels to selected change lists""" |
| |
| # Parse and check the command line options |
| args = _parse_args() |
| |
| if not args.gerrit: |
| try: |
| args.gerrit = find_gerrit_name() |
| # pylint: disable=bare-except |
| except: |
| print('gerrit instance not found, use [-g GERRIT]') |
| sys.exit(1) |
| |
| if not _has_task(args): |
| print('error: Either --label, --message, --submit, --abandon, --restore, ' |
| '--add-hashtag, --remove-hashtag, --set-topic, --delete-topic, ' |
| '--add-reviewer or --delete-reviewer must be specified', |
| file=sys.stderr) |
| sys.exit(1) |
| |
| # Convert label arguments |
| labels = _get_labels_from_args(args) |
| |
| # Convert reviewer arguments |
| new_reviewers = [{'reviewer': name} for name in args.add_reviewer] |
| |
| # Load authentication credentials |
| url_opener = create_url_opener_from_args(args) |
| |
| # Retrieve change lists |
| change_lists = query_change_lists( |
| url_opener, args.gerrit, args.query, args.limits) |
| if not change_lists: |
| print('error: No matching change lists.', file=sys.stderr) |
| sys.exit(1) |
| |
| # Print matching lists |
| _print_change_lists(change_lists, file=sys.stdout) |
| |
| # Confirm |
| _confirm('Do you want to continue?') |
| |
| # Post review votes |
| errors = {'num_errors': 0} |
| for change in change_lists: |
| if args.label or args.message: |
| _do_task(change, set_review, url_opener, args.gerrit, change['id'], |
| labels, args.message, errors=errors) |
| if args.add_hashtag or args.remove_hashtag: |
| _do_task(change, set_hashtags, url_opener, args.gerrit, |
| change['id'], args.add_hashtag, args.remove_hashtag, |
| errors=errors) |
| if args.set_topic: |
| _do_task(change, set_topic, url_opener, args.gerrit, change['id'], |
| args.set_topic, errors=errors) |
| if args.delete_topic: |
| _do_task(change, delete_topic, url_opener, args.gerrit, |
| change['id'], expected_http_code=204, errors=errors) |
| if args.submit: |
| _do_task(change, submit, url_opener, args.gerrit, change['id'], |
| errors=errors) |
| if args.abandon: |
| _do_task(change, abandon, url_opener, args.gerrit, change['id'], |
| args.abandon, errors=errors) |
| if args.restore: |
| _do_task(change, restore, url_opener, args.gerrit, change['id'], |
| errors=errors) |
| if args.add_reviewer: |
| _do_task(change, add_reviewers, url_opener, args.gerrit, |
| change['id'], new_reviewers, errors=errors) |
| for name in args.delete_reviewer: |
| _do_task(change, delete_reviewer, url_opener, args.gerrit, |
| change['id'], name, expected_http_code=204, errors=errors) |
| |
| |
| if errors['num_errors']: |
| sys.exit(1) |
| |
| |
| if __name__ == '__main__': |
| main() |