Merge "repo-review: Add command to post review votes"
This commit is contained in:
260
tools/repo_pull/repo_review.py
Executable file
260
tools/repo_pull/repo_review.py
Executable file
@@ -0,0 +1,260 @@
|
|||||||
|
#!/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 collections
|
||||||
|
import itertools
|
||||||
|
import json
|
||||||
|
import multiprocessing
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import xml.dom.minidom
|
||||||
|
|
||||||
|
try:
|
||||||
|
from urllib.parse import urlencode # PY3
|
||||||
|
except ImportError:
|
||||||
|
from urllib import urlencode # PY2
|
||||||
|
|
||||||
|
try:
|
||||||
|
from urllib.request import (
|
||||||
|
HTTPBasicAuthHandler, Request, build_opener) # PY3
|
||||||
|
except ImportError:
|
||||||
|
from urllib2 import HTTPBasicAuthHandler, Request, build_opener # PY2
|
||||||
|
|
||||||
|
|
||||||
|
def load_auth(cookie_file_path):
|
||||||
|
"""Load username and password from .gitcookies and return an
|
||||||
|
HTTPBasicAuthHandler."""
|
||||||
|
auth_handler = HTTPBasicAuthHandler()
|
||||||
|
with open(cookie_file_path, 'r') as cookie_file:
|
||||||
|
for lineno, line in enumerate(cookie_file, start=1):
|
||||||
|
if line.startswith('#HttpOnly_'):
|
||||||
|
line = line[len('#HttpOnly_'):]
|
||||||
|
if not line or line[0] == '#':
|
||||||
|
continue
|
||||||
|
row = line.split('\t')
|
||||||
|
if len(row) != 7:
|
||||||
|
continue
|
||||||
|
domain = row[0]
|
||||||
|
cookie = row[6]
|
||||||
|
sep = cookie.find('=')
|
||||||
|
if sep == -1:
|
||||||
|
continue
|
||||||
|
username = cookie[0:sep]
|
||||||
|
password = cookie[sep + 1:]
|
||||||
|
auth_handler.add_password(domain, domain, username, password)
|
||||||
|
return auth_handler
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_xssi_json(data):
|
||||||
|
"""Trim XSSI protector and decode JSON objects."""
|
||||||
|
# Trim cross site script inclusion (XSSI) protector
|
||||||
|
data = data.decode('utf-8')[4:]
|
||||||
|
# Parse JSON objects
|
||||||
|
return json.loads(data)
|
||||||
|
|
||||||
|
|
||||||
|
def query_change_lists(gerrit, query_string, gitcookies, limits):
|
||||||
|
"""Query change lists."""
|
||||||
|
data = [
|
||||||
|
('q', query_string),
|
||||||
|
('o', 'CURRENT_REVISION'),
|
||||||
|
('o', 'CURRENT_COMMIT'),
|
||||||
|
('n', str(limits)),
|
||||||
|
]
|
||||||
|
url = gerrit + '/a/changes/?' + urlencode(data)
|
||||||
|
|
||||||
|
auth_handler = load_auth(gitcookies)
|
||||||
|
opener = build_opener(auth_handler)
|
||||||
|
|
||||||
|
response_file = opener.open(url)
|
||||||
|
try:
|
||||||
|
return _decode_xssi_json(response_file.read())
|
||||||
|
finally:
|
||||||
|
response_file.close()
|
||||||
|
|
||||||
|
|
||||||
|
def set_review(gerrit, gitcookies, change_id, labels, message):
|
||||||
|
"""Set review votes to a change list."""
|
||||||
|
|
||||||
|
url = '{}/a/changes/{}/revisions/current/review'.format(gerrit, change_id)
|
||||||
|
|
||||||
|
auth_handler = load_auth(gitcookies)
|
||||||
|
opener = build_opener(auth_handler)
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
if labels:
|
||||||
|
data['labels'] = labels
|
||||||
|
if message:
|
||||||
|
data['message'] = message
|
||||||
|
data = json.dumps(data).encode('utf-8')
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json; charset=UTF-8',
|
||||||
|
}
|
||||||
|
|
||||||
|
request = Request(url, data, headers)
|
||||||
|
response_file = opener.open(request)
|
||||||
|
try:
|
||||||
|
res_code = response_file.getcode()
|
||||||
|
res_json = _decode_xssi_json(response_file.read())
|
||||||
|
return (res_code, res_json)
|
||||||
|
finally:
|
||||||
|
response_file.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_change_lists_from_args(args):
|
||||||
|
"""Query the change lists by args."""
|
||||||
|
return query_change_lists(args.gerrit, args.query, args.gitcookies,
|
||||||
|
args.limits)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_labels_from_args(args):
|
||||||
|
"""Collect and check labels from args."""
|
||||||
|
if not args.label:
|
||||||
|
print('error: --label must be specified', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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', required=True,
|
||||||
|
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')
|
||||||
|
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
_SEP_SPLIT = '=' * 79
|
||||||
|
_SEP = '-' * 79
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Set review labels to selected change lists"""
|
||||||
|
|
||||||
|
args = _parse_args()
|
||||||
|
|
||||||
|
# Convert label arguments
|
||||||
|
labels = _get_labels_from_args(args)
|
||||||
|
|
||||||
|
# Retrieve change lists
|
||||||
|
change_lists = _get_change_lists_from_args(args)
|
||||||
|
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
|
||||||
|
has_error = False
|
||||||
|
for change in change_lists:
|
||||||
|
try:
|
||||||
|
res_code, res_json = set_review(
|
||||||
|
args.gerrit, args.gitcookies, change['id'], labels,
|
||||||
|
args.message)
|
||||||
|
except HTTPError as e:
|
||||||
|
res_code = e.code
|
||||||
|
res_json = None
|
||||||
|
|
||||||
|
if res_code != 200:
|
||||||
|
has_error = True
|
||||||
|
|
||||||
|
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)
|
||||||
|
print(_SEP_SPLIT, file=sys.stderr)
|
||||||
|
|
||||||
|
if has_error:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user