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, create_url_opener_from_args, delete_topic, query_change_lists, 35 set_hashtags, set_review, set_topic) 36 37 38def _get_labels_from_args(args): 39 """Collect and check labels from args.""" 40 if not args.label: 41 return None 42 labels = {} 43 for (name, value) in args.label: 44 try: 45 labels[name] = int(value) 46 except ValueError: 47 print('error: Label {} takes integer, but {} is specified' 48 .format(name, value), file=sys.stderr) 49 return labels 50 51 52# pylint: disable=redefined-builtin 53def _print_change_lists(change_lists, file=sys.stdout): 54 """Print matching change lists for each projects.""" 55 change_lists = sorted( 56 change_lists, key=lambda change: (change['project'], change['_number'])) 57 58 prev_project = None 59 print('Change Lists:', file=file) 60 for change in change_lists: 61 project = change['project'] 62 if project != prev_project: 63 print(' ', project, file=file) 64 prev_project = project 65 66 change_id = change['change_id'] 67 revision_sha1 = change['current_revision'] 68 revision = change['revisions'][revision_sha1] 69 subject = revision['commit']['subject'] 70 print(' ', change_id, '--', subject, file=file) 71 72 73def _confirm(question): 74 """Confirm before proceeding.""" 75 try: 76 if input(question + ' [yn] ').lower() not in {'y', 'yes'}: 77 print('Cancelled', file=sys.stderr) 78 sys.exit(1) 79 except KeyboardInterrupt: 80 print('Cancelled', file=sys.stderr) 81 sys.exit(1) 82 83 84def _parse_args(): 85 """Parse command line options.""" 86 parser = argparse.ArgumentParser() 87 88 parser.add_argument('query', help='Change list query string') 89 parser.add_argument('-g', '--gerrit', required=True, 90 help='Gerrit review URL') 91 92 parser.add_argument('--gitcookies', 93 default=os.path.expanduser('~/.gitcookies'), 94 help='Gerrit cookie file') 95 parser.add_argument('--limits', default=1000, 96 help='Max number of change lists') 97 98 parser.add_argument('-l', '--label', nargs=2, action='append', 99 help='Labels to be added') 100 parser.add_argument('-m', '--message', help='Review message') 101 102 parser.add_argument('--abandon', help='Abandon a CL with a message') 103 104 parser.add_argument('--add-hashtag', action='append', help='Add hashtag') 105 parser.add_argument('--remove-hashtag', action='append', 106 help='Remove hashtag') 107 parser.add_argument('--delete-hashtag', action='append', 108 help='Remove hashtag', dest='remove_hashtag') 109 110 parser.add_argument('--set-topic', help='Set topic name') 111 parser.add_argument('--delete-topic', action='store_true', 112 help='Delete topic name') 113 parser.add_argument('--remove-topic', action='store_true', 114 help='Delete topic name', dest='delete_topic') 115 116 return parser.parse_args() 117 118 119def _has_task(args): 120 """Determine whether a task has been specified in the arguments.""" 121 if args.label is not None or args.message is not None: 122 return True 123 if args.abandon is not None: 124 return True 125 if args.add_hashtag or args.remove_hashtag: 126 return True 127 if args.set_topic or args.delete_topic: 128 return True 129 return False 130 131 132_SEP_SPLIT = '=' * 79 133_SEP = '-' * 79 134 135 136def _print_error(change, res_code, res_json): 137 """Print the error message""" 138 139 change_id = change['change_id'] 140 project = change['project'] 141 revision_sha1 = change['current_revision'] 142 revision = change['revisions'][revision_sha1] 143 subject = revision['commit']['subject'] 144 145 print(_SEP_SPLIT, file=sys.stderr) 146 print('Project:', project, file=sys.stderr) 147 print('Change-Id:', change_id, file=sys.stderr) 148 print('Subject:', subject, file=sys.stderr) 149 print('HTTP status code:', res_code, file=sys.stderr) 150 if res_json: 151 print(_SEP, file=sys.stderr) 152 json.dump(res_json, sys.stderr, indent=4, 153 separators=(', ', ': ')) 154 print(file=sys.stderr) 155 print(_SEP_SPLIT, file=sys.stderr) 156 157 158def _do_task(change, func, *args, **kwargs): 159 """Process a task and report errors when necessary.""" 160 try: 161 res_code, res_json = func(*args) 162 except HTTPError as error: 163 res_code = error.code 164 res_json = None 165 166 if res_code != kwargs.get('expected_http_code', 200): 167 _print_error(change, res_code, res_json) 168 169 errors = kwargs.get('errors') 170 if errors is not None: 171 errors['num_errors'] += 1 172 173 174def main(): 175 """Set review labels to selected change lists""" 176 177 # Parse and check the command line options 178 args = _parse_args() 179 if not _has_task(args): 180 print('error: Either --label, --message, --abandon, --add-hashtag, ' 181 '--remove-hashtag, --set-topic, or --delete-topic must be ', 182 'specified', file=sys.stderr) 183 sys.exit(1) 184 185 # Convert label arguments 186 labels = _get_labels_from_args(args) 187 188 # Load authentication credentials 189 url_opener = create_url_opener_from_args(args) 190 191 # Retrieve change lists 192 change_lists = query_change_lists( 193 url_opener, args.gerrit, args.query, args.limits) 194 if not change_lists: 195 print('error: No matching change lists.', file=sys.stderr) 196 sys.exit(1) 197 198 # Print matching lists 199 _print_change_lists(change_lists, file=sys.stdout) 200 201 # Confirm 202 _confirm('Do you want to continue?') 203 204 # Post review votes 205 errors = {'num_errors': 0} 206 for change in change_lists: 207 if args.label or args.message: 208 _do_task(change, set_review, url_opener, args.gerrit, change['id'], 209 labels, args.message, errors=errors) 210 if args.add_hashtag or args.remove_hashtag: 211 _do_task(change, set_hashtags, url_opener, args.gerrit, 212 change['id'], args.add_hashtag, args.remove_hashtag, 213 errors=errors) 214 if args.set_topic: 215 _do_task(change, set_topic, url_opener, args.gerrit, change['id'], 216 args.set_topic, errors=errors) 217 if args.delete_topic: 218 _do_task(change, delete_topic, url_opener, args.gerrit, 219 change['id'], expected_http_code=204, errors=errors) 220 if args.abandon: 221 _do_task(change, abandon, url_opener, args.gerrit, change['id'], 222 args.abandon, errors=errors) 223 224 if errors['num_errors']: 225 sys.exit(1) 226 227 228if __name__ == '__main__': 229 main() 230