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