1*90c8c64dSAndroid Build Coastguard Worker#!/usr/bin/env python3 2*90c8c64dSAndroid Build Coastguard Worker 3*90c8c64dSAndroid Build Coastguard Worker# 4*90c8c64dSAndroid Build Coastguard Worker# Copyright (C) 2018 The Android Open Source Project 5*90c8c64dSAndroid Build Coastguard Worker# 6*90c8c64dSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License"); 7*90c8c64dSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License. 8*90c8c64dSAndroid Build Coastguard Worker# You may obtain a copy of the License at 9*90c8c64dSAndroid Build Coastguard Worker# 10*90c8c64dSAndroid Build Coastguard Worker# http://www.apache.org/licenses/LICENSE-2.0 11*90c8c64dSAndroid Build Coastguard Worker# 12*90c8c64dSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software 13*90c8c64dSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS, 14*90c8c64dSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15*90c8c64dSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and 16*90c8c64dSAndroid Build Coastguard Worker# limitations under the License. 17*90c8c64dSAndroid Build Coastguard Worker# 18*90c8c64dSAndroid Build Coastguard Worker 19*90c8c64dSAndroid Build Coastguard Worker"""A command line utility to post review votes to multiple CLs on Gerrit.""" 20*90c8c64dSAndroid Build Coastguard Worker 21*90c8c64dSAndroid Build Coastguard Workerfrom __future__ import print_function 22*90c8c64dSAndroid Build Coastguard Worker 23*90c8c64dSAndroid Build Coastguard Workerimport argparse 24*90c8c64dSAndroid Build Coastguard Workerimport json 25*90c8c64dSAndroid Build Coastguard Workerimport os 26*90c8c64dSAndroid Build Coastguard Workerimport sys 27*90c8c64dSAndroid Build Coastguard Worker 28*90c8c64dSAndroid Build Coastguard Workertry: 29*90c8c64dSAndroid Build Coastguard Worker from urllib.error import HTTPError # PY3 30*90c8c64dSAndroid Build Coastguard Workerexcept ImportError: 31*90c8c64dSAndroid Build Coastguard Worker from urllib2 import HTTPError # PY2 32*90c8c64dSAndroid Build Coastguard Worker 33*90c8c64dSAndroid Build Coastguard Workerfrom gerrit import ( 34*90c8c64dSAndroid Build Coastguard Worker abandon, add_common_parse_args, add_reviewers, create_url_opener_from_args, 35*90c8c64dSAndroid Build Coastguard Worker delete, delete_reviewer, delete_topic, find_gerrit_name, normalize_gerrit_name, 36*90c8c64dSAndroid Build Coastguard Worker query_change_lists, restore, set_hashtags, set_review, set_topic, submit 37*90c8c64dSAndroid Build Coastguard Worker) 38*90c8c64dSAndroid Build Coastguard Worker 39*90c8c64dSAndroid Build Coastguard Worker 40*90c8c64dSAndroid Build Coastguard Workerdef _get_labels_from_args(args): 41*90c8c64dSAndroid Build Coastguard Worker """Collect and check labels from args.""" 42*90c8c64dSAndroid Build Coastguard Worker if not args.label: 43*90c8c64dSAndroid Build Coastguard Worker return None 44*90c8c64dSAndroid Build Coastguard Worker labels = {} 45*90c8c64dSAndroid Build Coastguard Worker for (name, value) in args.label: 46*90c8c64dSAndroid Build Coastguard Worker try: 47*90c8c64dSAndroid Build Coastguard Worker labels[name] = int(value) 48*90c8c64dSAndroid Build Coastguard Worker except ValueError: 49*90c8c64dSAndroid Build Coastguard Worker print('error: Label {} takes integer, but {} is specified' 50*90c8c64dSAndroid Build Coastguard Worker .format(name, value), file=sys.stderr) 51*90c8c64dSAndroid Build Coastguard Worker return labels 52*90c8c64dSAndroid Build Coastguard Worker 53*90c8c64dSAndroid Build Coastguard Worker 54*90c8c64dSAndroid Build Coastguard Worker# pylint: disable=redefined-builtin 55*90c8c64dSAndroid Build Coastguard Workerdef _print_change_lists(change_lists, file=sys.stdout): 56*90c8c64dSAndroid Build Coastguard Worker """Print matching change lists for each projects.""" 57*90c8c64dSAndroid Build Coastguard Worker change_lists = sorted( 58*90c8c64dSAndroid Build Coastguard Worker change_lists, key=lambda change: (change['project'], change['_number'])) 59*90c8c64dSAndroid Build Coastguard Worker 60*90c8c64dSAndroid Build Coastguard Worker prev_project = None 61*90c8c64dSAndroid Build Coastguard Worker print('Change Lists:', file=file) 62*90c8c64dSAndroid Build Coastguard Worker for change in change_lists: 63*90c8c64dSAndroid Build Coastguard Worker project = change['project'] 64*90c8c64dSAndroid Build Coastguard Worker if project != prev_project: 65*90c8c64dSAndroid Build Coastguard Worker print(' ', project, file=file) 66*90c8c64dSAndroid Build Coastguard Worker prev_project = project 67*90c8c64dSAndroid Build Coastguard Worker 68*90c8c64dSAndroid Build Coastguard Worker change_id = change['change_id'] 69*90c8c64dSAndroid Build Coastguard Worker revision_sha1 = change['current_revision'] 70*90c8c64dSAndroid Build Coastguard Worker revision = change['revisions'][revision_sha1] 71*90c8c64dSAndroid Build Coastguard Worker subject = revision['commit']['subject'] 72*90c8c64dSAndroid Build Coastguard Worker print(' ', change_id, '--', subject, file=file) 73*90c8c64dSAndroid Build Coastguard Worker 74*90c8c64dSAndroid Build Coastguard Worker 75*90c8c64dSAndroid Build Coastguard Workerdef _confirm(question): 76*90c8c64dSAndroid Build Coastguard Worker """Confirm before proceeding.""" 77*90c8c64dSAndroid Build Coastguard Worker try: 78*90c8c64dSAndroid Build Coastguard Worker if input(question + ' [yn] ').lower() not in {'y', 'yes'}: 79*90c8c64dSAndroid Build Coastguard Worker print('Cancelled', file=sys.stderr) 80*90c8c64dSAndroid Build Coastguard Worker sys.exit(1) 81*90c8c64dSAndroid Build Coastguard Worker except KeyboardInterrupt: 82*90c8c64dSAndroid Build Coastguard Worker print('Cancelled', file=sys.stderr) 83*90c8c64dSAndroid Build Coastguard Worker sys.exit(1) 84*90c8c64dSAndroid Build Coastguard Worker 85*90c8c64dSAndroid Build Coastguard Worker 86*90c8c64dSAndroid Build Coastguard Workerdef _parse_args(): 87*90c8c64dSAndroid Build Coastguard Worker """Parse command line options.""" 88*90c8c64dSAndroid Build Coastguard Worker parser = argparse.ArgumentParser() 89*90c8c64dSAndroid Build Coastguard Worker add_common_parse_args(parser) 90*90c8c64dSAndroid Build Coastguard Worker 91*90c8c64dSAndroid Build Coastguard Worker parser.add_argument('-l', '--label', nargs=2, action='append', 92*90c8c64dSAndroid Build Coastguard Worker help='Labels to be added') 93*90c8c64dSAndroid Build Coastguard Worker parser.add_argument('-m', '--message', help='Review message') 94*90c8c64dSAndroid Build Coastguard Worker 95*90c8c64dSAndroid Build Coastguard Worker parser.add_argument('--submit', action='store_true', help='Submit a CL') 96*90c8c64dSAndroid Build Coastguard Worker 97*90c8c64dSAndroid Build Coastguard Worker parser.add_argument('--abandon', help='Abandon a CL with a message') 98*90c8c64dSAndroid Build Coastguard Worker parser.add_argument('--restore', action='store_true', help='Restore a CL') 99*90c8c64dSAndroid Build Coastguard Worker parser.add_argument('--delete', action='store_true', help='Delete a CL') 100*90c8c64dSAndroid Build Coastguard Worker 101*90c8c64dSAndroid Build Coastguard Worker parser.add_argument('--add-hashtag', action='append', help='Add hashtag') 102*90c8c64dSAndroid Build Coastguard Worker parser.add_argument('--remove-hashtag', action='append', 103*90c8c64dSAndroid Build Coastguard Worker help='Remove hashtag') 104*90c8c64dSAndroid Build Coastguard Worker parser.add_argument('--delete-hashtag', action='append', 105*90c8c64dSAndroid Build Coastguard Worker help='Remove hashtag', dest='remove_hashtag') 106*90c8c64dSAndroid Build Coastguard Worker 107*90c8c64dSAndroid Build Coastguard Worker parser.add_argument('--set-topic', help='Set topic name') 108*90c8c64dSAndroid Build Coastguard Worker parser.add_argument('--delete-topic', action='store_true', 109*90c8c64dSAndroid Build Coastguard Worker help='Delete topic name') 110*90c8c64dSAndroid Build Coastguard Worker parser.add_argument('--remove-topic', action='store_true', 111*90c8c64dSAndroid Build Coastguard Worker help='Delete topic name', dest='delete_topic') 112*90c8c64dSAndroid Build Coastguard Worker parser.add_argument('--add-reviewer', action='append', default=[], 113*90c8c64dSAndroid Build Coastguard Worker help='Add reviewer') 114*90c8c64dSAndroid Build Coastguard Worker parser.add_argument('--delete-reviewer', action='append', default=[], 115*90c8c64dSAndroid Build Coastguard Worker help='Delete reviewer') 116*90c8c64dSAndroid Build Coastguard Worker 117*90c8c64dSAndroid Build Coastguard Worker return parser.parse_args() 118*90c8c64dSAndroid Build Coastguard Worker 119*90c8c64dSAndroid Build Coastguard Worker 120*90c8c64dSAndroid Build Coastguard Workerdef _has_task(args): 121*90c8c64dSAndroid Build Coastguard Worker """Determine whether a task has been specified in the arguments.""" 122*90c8c64dSAndroid Build Coastguard Worker if args.label is not None or args.message is not None: 123*90c8c64dSAndroid Build Coastguard Worker return True 124*90c8c64dSAndroid Build Coastguard Worker if args.submit: 125*90c8c64dSAndroid Build Coastguard Worker return True 126*90c8c64dSAndroid Build Coastguard Worker if args.abandon is not None: 127*90c8c64dSAndroid Build Coastguard Worker return True 128*90c8c64dSAndroid Build Coastguard Worker if args.restore: 129*90c8c64dSAndroid Build Coastguard Worker return True 130*90c8c64dSAndroid Build Coastguard Worker if args.delete: 131*90c8c64dSAndroid Build Coastguard Worker return True 132*90c8c64dSAndroid Build Coastguard Worker if args.add_hashtag or args.remove_hashtag: 133*90c8c64dSAndroid Build Coastguard Worker return True 134*90c8c64dSAndroid Build Coastguard Worker if args.set_topic or args.delete_topic: 135*90c8c64dSAndroid Build Coastguard Worker return True 136*90c8c64dSAndroid Build Coastguard Worker if args.add_reviewer or args.delete_reviewer: 137*90c8c64dSAndroid Build Coastguard Worker return True 138*90c8c64dSAndroid Build Coastguard Worker return False 139*90c8c64dSAndroid Build Coastguard Worker 140*90c8c64dSAndroid Build Coastguard Worker 141*90c8c64dSAndroid Build Coastguard Worker_SEP_SPLIT = '=' * 79 142*90c8c64dSAndroid Build Coastguard Worker_SEP = '-' * 79 143*90c8c64dSAndroid Build Coastguard Worker 144*90c8c64dSAndroid Build Coastguard Worker 145*90c8c64dSAndroid Build Coastguard Workerdef _print_error(change, res_code, res_body, res_json): 146*90c8c64dSAndroid Build Coastguard Worker """Print the error message""" 147*90c8c64dSAndroid Build Coastguard Worker 148*90c8c64dSAndroid Build Coastguard Worker change_id = change['change_id'] 149*90c8c64dSAndroid Build Coastguard Worker project = change['project'] 150*90c8c64dSAndroid Build Coastguard Worker revision_sha1 = change['current_revision'] 151*90c8c64dSAndroid Build Coastguard Worker revision = change['revisions'][revision_sha1] 152*90c8c64dSAndroid Build Coastguard Worker subject = revision['commit']['subject'] 153*90c8c64dSAndroid Build Coastguard Worker 154*90c8c64dSAndroid Build Coastguard Worker print(_SEP_SPLIT, file=sys.stderr) 155*90c8c64dSAndroid Build Coastguard Worker print('Project:', project, file=sys.stderr) 156*90c8c64dSAndroid Build Coastguard Worker print('Change-Id:', change_id, file=sys.stderr) 157*90c8c64dSAndroid Build Coastguard Worker print('Subject:', subject, file=sys.stderr) 158*90c8c64dSAndroid Build Coastguard Worker print('HTTP status code:', res_code, file=sys.stderr) 159*90c8c64dSAndroid Build Coastguard Worker if res_json: 160*90c8c64dSAndroid Build Coastguard Worker print(_SEP, file=sys.stderr) 161*90c8c64dSAndroid Build Coastguard Worker json.dump(res_json, sys.stderr, indent=4, 162*90c8c64dSAndroid Build Coastguard Worker separators=(', ', ': ')) 163*90c8c64dSAndroid Build Coastguard Worker print(file=sys.stderr) 164*90c8c64dSAndroid Build Coastguard Worker elif res_body: 165*90c8c64dSAndroid Build Coastguard Worker print(_SEP, file=sys.stderr) 166*90c8c64dSAndroid Build Coastguard Worker print(res_body.decode('utf-8'), file=sys.stderr) 167*90c8c64dSAndroid Build Coastguard Worker print(_SEP_SPLIT, file=sys.stderr) 168*90c8c64dSAndroid Build Coastguard Worker 169*90c8c64dSAndroid Build Coastguard Worker 170*90c8c64dSAndroid Build Coastguard Workerdef _do_task(change, func, *args, **kwargs): 171*90c8c64dSAndroid Build Coastguard Worker """Process a task and report errors when necessary.""" 172*90c8c64dSAndroid Build Coastguard Worker 173*90c8c64dSAndroid Build Coastguard Worker res_code, res_body, res_json = func(*args) 174*90c8c64dSAndroid Build Coastguard Worker 175*90c8c64dSAndroid Build Coastguard Worker if res_code != kwargs.get('expected_http_code', 200): 176*90c8c64dSAndroid Build Coastguard Worker _print_error(change, res_code, res_body, res_json) 177*90c8c64dSAndroid Build Coastguard Worker 178*90c8c64dSAndroid Build Coastguard Worker errors = kwargs.get('errors') 179*90c8c64dSAndroid Build Coastguard Worker if errors is not None: 180*90c8c64dSAndroid Build Coastguard Worker errors['num_errors'] += 1 181*90c8c64dSAndroid Build Coastguard Worker 182*90c8c64dSAndroid Build Coastguard Worker 183*90c8c64dSAndroid Build Coastguard Workerdef main(): 184*90c8c64dSAndroid Build Coastguard Worker """Set review labels to selected change lists""" 185*90c8c64dSAndroid Build Coastguard Worker 186*90c8c64dSAndroid Build Coastguard Worker # Parse and check the command line options 187*90c8c64dSAndroid Build Coastguard Worker args = _parse_args() 188*90c8c64dSAndroid Build Coastguard Worker 189*90c8c64dSAndroid Build Coastguard Worker if args.gerrit: 190*90c8c64dSAndroid Build Coastguard Worker args.gerrit = normalize_gerrit_name(args.gerrit) 191*90c8c64dSAndroid Build Coastguard Worker else: 192*90c8c64dSAndroid Build Coastguard Worker try: 193*90c8c64dSAndroid Build Coastguard Worker args.gerrit = find_gerrit_name() 194*90c8c64dSAndroid Build Coastguard Worker # pylint: disable=bare-except 195*90c8c64dSAndroid Build Coastguard Worker except: 196*90c8c64dSAndroid Build Coastguard Worker print('gerrit instance not found, use [-g GERRIT]') 197*90c8c64dSAndroid Build Coastguard Worker sys.exit(1) 198*90c8c64dSAndroid Build Coastguard Worker 199*90c8c64dSAndroid Build Coastguard Worker if not _has_task(args): 200*90c8c64dSAndroid Build Coastguard Worker print('error: Either --label, --message, --submit, --abandon, --restore, ' 201*90c8c64dSAndroid Build Coastguard Worker '--add-hashtag, --remove-hashtag, --set-topic, --delete-topic, ' 202*90c8c64dSAndroid Build Coastguard Worker '--add-reviewer, --delete-reviewer or --delete must be specified', 203*90c8c64dSAndroid Build Coastguard Worker file=sys.stderr) 204*90c8c64dSAndroid Build Coastguard Worker sys.exit(1) 205*90c8c64dSAndroid Build Coastguard Worker 206*90c8c64dSAndroid Build Coastguard Worker # Convert label arguments 207*90c8c64dSAndroid Build Coastguard Worker labels = _get_labels_from_args(args) 208*90c8c64dSAndroid Build Coastguard Worker 209*90c8c64dSAndroid Build Coastguard Worker # Convert reviewer arguments 210*90c8c64dSAndroid Build Coastguard Worker new_reviewers = [{'reviewer': name} for name in args.add_reviewer] 211*90c8c64dSAndroid Build Coastguard Worker 212*90c8c64dSAndroid Build Coastguard Worker # Load authentication credentials 213*90c8c64dSAndroid Build Coastguard Worker url_opener = create_url_opener_from_args(args) 214*90c8c64dSAndroid Build Coastguard Worker 215*90c8c64dSAndroid Build Coastguard Worker # Retrieve change lists 216*90c8c64dSAndroid Build Coastguard Worker change_lists = query_change_lists( 217*90c8c64dSAndroid Build Coastguard Worker url_opener, args.gerrit, args.query, args.start, args.limits) 218*90c8c64dSAndroid Build Coastguard Worker if not change_lists: 219*90c8c64dSAndroid Build Coastguard Worker print('error: No matching change lists.', file=sys.stderr) 220*90c8c64dSAndroid Build Coastguard Worker sys.exit(1) 221*90c8c64dSAndroid Build Coastguard Worker 222*90c8c64dSAndroid Build Coastguard Worker # Print matching lists 223*90c8c64dSAndroid Build Coastguard Worker _print_change_lists(change_lists, file=sys.stdout) 224*90c8c64dSAndroid Build Coastguard Worker 225*90c8c64dSAndroid Build Coastguard Worker # Confirm 226*90c8c64dSAndroid Build Coastguard Worker _confirm('Do you want to continue?') 227*90c8c64dSAndroid Build Coastguard Worker 228*90c8c64dSAndroid Build Coastguard Worker # Post review votes 229*90c8c64dSAndroid Build Coastguard Worker errors = {'num_errors': 0} 230*90c8c64dSAndroid Build Coastguard Worker for change in change_lists: 231*90c8c64dSAndroid Build Coastguard Worker if args.label or args.message: 232*90c8c64dSAndroid Build Coastguard Worker _do_task(change, set_review, url_opener, args.gerrit, change['id'], 233*90c8c64dSAndroid Build Coastguard Worker labels, args.message, errors=errors) 234*90c8c64dSAndroid Build Coastguard Worker if args.add_hashtag or args.remove_hashtag: 235*90c8c64dSAndroid Build Coastguard Worker _do_task(change, set_hashtags, url_opener, args.gerrit, 236*90c8c64dSAndroid Build Coastguard Worker change['id'], args.add_hashtag, args.remove_hashtag, 237*90c8c64dSAndroid Build Coastguard Worker errors=errors) 238*90c8c64dSAndroid Build Coastguard Worker if args.set_topic: 239*90c8c64dSAndroid Build Coastguard Worker _do_task(change, set_topic, url_opener, args.gerrit, change['id'], 240*90c8c64dSAndroid Build Coastguard Worker args.set_topic, errors=errors) 241*90c8c64dSAndroid Build Coastguard Worker if args.delete_topic: 242*90c8c64dSAndroid Build Coastguard Worker _do_task(change, delete_topic, url_opener, args.gerrit, 243*90c8c64dSAndroid Build Coastguard Worker change['id'], expected_http_code=204, errors=errors) 244*90c8c64dSAndroid Build Coastguard Worker if args.submit: 245*90c8c64dSAndroid Build Coastguard Worker _do_task(change, submit, url_opener, args.gerrit, change['id'], 246*90c8c64dSAndroid Build Coastguard Worker errors=errors) 247*90c8c64dSAndroid Build Coastguard Worker if args.abandon: 248*90c8c64dSAndroid Build Coastguard Worker _do_task(change, abandon, url_opener, args.gerrit, change['id'], 249*90c8c64dSAndroid Build Coastguard Worker args.abandon, errors=errors) 250*90c8c64dSAndroid Build Coastguard Worker if args.restore: 251*90c8c64dSAndroid Build Coastguard Worker _do_task(change, restore, url_opener, args.gerrit, change['id'], 252*90c8c64dSAndroid Build Coastguard Worker errors=errors) 253*90c8c64dSAndroid Build Coastguard Worker if args.delete: 254*90c8c64dSAndroid Build Coastguard Worker _do_task(change, delete, url_opener, args.gerrit, change['id'], 255*90c8c64dSAndroid Build Coastguard Worker errors=errors) 256*90c8c64dSAndroid Build Coastguard Worker if args.add_reviewer: 257*90c8c64dSAndroid Build Coastguard Worker _do_task(change, add_reviewers, url_opener, args.gerrit, 258*90c8c64dSAndroid Build Coastguard Worker change['id'], new_reviewers, errors=errors) 259*90c8c64dSAndroid Build Coastguard Worker for name in args.delete_reviewer: 260*90c8c64dSAndroid Build Coastguard Worker _do_task(change, delete_reviewer, url_opener, args.gerrit, 261*90c8c64dSAndroid Build Coastguard Worker change['id'], name, expected_http_code=204, errors=errors) 262*90c8c64dSAndroid Build Coastguard Worker 263*90c8c64dSAndroid Build Coastguard Worker 264*90c8c64dSAndroid Build Coastguard Worker if errors['num_errors']: 265*90c8c64dSAndroid Build Coastguard Worker sys.exit(1) 266*90c8c64dSAndroid Build Coastguard Worker 267*90c8c64dSAndroid Build Coastguard Worker 268*90c8c64dSAndroid Build Coastguard Workerif __name__ == '__main__': 269*90c8c64dSAndroid Build Coastguard Worker main() 270