blob: 063b35105c1576522301380417ce88a10933572c [file] [log] [blame]
Logan Chien3f67c282018-06-06 18:16:31 +08001#!/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
Logan Chienbfb414f2018-06-15 16:06:56 +080019"""Gerrit Restful API client library."""
20
Logan Chien3f67c282018-06-06 18:16:31 +080021from __future__ import print_function
22
23import argparse
Logan Chien4721aa12018-06-07 10:50:15 +080024import base64
Logan Chien3f67c282018-06-06 18:16:31 +080025import json
26import os
27import sys
28
29try:
Bowgo Tsai33b91c12018-06-08 17:00:46 +080030 from urllib.request import (
31 HTTPBasicAuthHandler, Request, build_opener) # PY3
Logan Chien3f67c282018-06-06 18:16:31 +080032except ImportError:
Bowgo Tsai33b91c12018-06-08 17:00:46 +080033 from urllib2 import (
34 HTTPBasicAuthHandler, Request, build_opener) # PY2
Logan Chien3f67c282018-06-06 18:16:31 +080035
36try:
Logan Chienbfb414f2018-06-15 16:06:56 +080037 # pylint: disable=ungrouped-imports
Logan Chien3f67c282018-06-06 18:16:31 +080038 from urllib.parse import urlencode, urlparse # PY3
39except ImportError:
Logan Chienbfb414f2018-06-15 16:06:56 +080040 # pylint: disable=ungrouped-imports
Logan Chien3f67c282018-06-06 18:16:31 +080041 from urllib import urlencode # PY2
42 from urlparse import urlparse # PY2
43
44
45def load_auth_credentials_from_file(cookie_file):
46 """Load credentials from an opened .gitcookies file."""
47 credentials = {}
Logan Chienbfb414f2018-06-15 16:06:56 +080048 for line in cookie_file:
Logan Chien3f67c282018-06-06 18:16:31 +080049 if line.startswith('#HttpOnly_'):
50 line = line[len('#HttpOnly_'):]
51
52 if not line or line[0] == '#':
53 continue
54
55 row = line.split('\t')
56 if len(row) != 7:
57 continue
58
59 domain = row[0]
60 cookie = row[6]
61
62 sep = cookie.find('=')
63 if sep == -1:
64 continue
65 username = cookie[0:sep]
66 password = cookie[sep + 1:]
67
68 credentials[domain] = (username, password)
69 return credentials
70
71
72def load_auth_credentials(cookie_file_path):
73 """Load credentials from a .gitcookies file path."""
74 with open(cookie_file_path, 'r') as cookie_file:
75 return load_auth_credentials_from_file(cookie_file)
76
77
78def create_url_opener(cookie_file_path, domain):
79 """Load username and password from .gitcookies and return a URL opener with
80 an authentication handler."""
81
82 # Load authentication credentials
83 credentials = load_auth_credentials(cookie_file_path)
84 username, password = credentials[domain]
85
86 # Create URL opener with authentication handler
87 auth_handler = HTTPBasicAuthHandler()
88 auth_handler.add_password(domain, domain, username, password)
89 return build_opener(auth_handler)
90
91
92def create_url_opener_from_args(args):
93 """Create URL opener from command line arguments."""
94
95 domain = urlparse(args.gerrit).netloc
96
97 try:
98 return create_url_opener(args.gitcookies, domain)
99 except KeyError:
100 print('error: Cannot find the domain "{}" in "{}". '
101 .format(domain, args.gitcookies), file=sys.stderr)
102 print('error: Please check the Gerrit Code Review URL or follow the '
103 'instructions in '
104 'https://android.googlesource.com/platform/development/'
105 '+/master/tools/repo_pull#installation', file=sys.stderr)
106 sys.exit(1)
107
108
109def _decode_xssi_json(data):
110 """Trim XSSI protector and decode JSON objects."""
111
112 # Decode UTF-8
113 data = data.decode('utf-8')
114
115 # Trim cross site script inclusion (XSSI) protector
116 if data[0:4] != ')]}\'':
117 raise ValueError('unexpected responsed content: ' + data)
118 data = data[4:]
119
120 # Parse JSON objects
121 return json.loads(data)
122
123
124def query_change_lists(url_opener, gerrit, query_string, limits):
125 """Query change lists."""
126 data = [
127 ('q', query_string),
128 ('o', 'CURRENT_REVISION'),
129 ('o', 'CURRENT_COMMIT'),
130 ('n', str(limits)),
131 ]
132 url = gerrit + '/a/changes/?' + urlencode(data)
133
134 response_file = url_opener.open(url)
135 try:
136 return _decode_xssi_json(response_file.read())
137 finally:
138 response_file.close()
139
140
Logan Chien63be1582018-09-07 10:39:00 +0800141def _make_json_post_request(url_opener, url, data, method='POST'):
142 data = json.dumps(data).encode('utf-8')
143 headers = {
144 'Content-Type': 'application/json; charset=UTF-8',
145 }
146
147 request = Request(url, data, headers)
148 request.get_method = lambda: method
149 response_file = url_opener.open(request)
150 try:
151 res_code = response_file.getcode()
152 res_json = _decode_xssi_json(response_file.read())
153 return (res_code, res_json)
154 finally:
155 response_file.close()
156
157
Logan Chien3f67c282018-06-06 18:16:31 +0800158def set_review(url_opener, gerrit_url, change_id, labels, message):
159 """Set review votes to a change list."""
160
161 url = '{}/a/changes/{}/revisions/current/review'.format(
162 gerrit_url, change_id)
163
164 data = {}
165 if labels:
166 data['labels'] = labels
167 if message:
168 data['message'] = message
Logan Chien3f67c282018-06-06 18:16:31 +0800169
Logan Chien63be1582018-09-07 10:39:00 +0800170 return _make_json_post_request(url_opener, url, data)
Logan Chien3f67c282018-06-06 18:16:31 +0800171
172
Logan Chienb260a602018-07-03 10:27:55 +0800173def abandon(url_opener, gerrit_url, change_id, message):
174 """Abandon a change list."""
175
176 url = '{}/a/changes/{}/abandon'.format(gerrit_url, change_id)
177
178 data = {}
179 if message:
180 data['message'] = message
Logan Chienb260a602018-07-03 10:27:55 +0800181
Logan Chien63be1582018-09-07 10:39:00 +0800182 return _make_json_post_request(url_opener, url, data)
Logan Chienb260a602018-07-03 10:27:55 +0800183
184
Logan Chien2da2f272018-09-07 11:47:37 +0800185def set_topic(url_opener, gerrit_url, change_id, name):
186 """Set the topic name."""
187
188 url = '{}/a/changes/{}/topic'.format(gerrit_url, change_id)
189 data = {'topic': name}
190 return _make_json_post_request(url_opener, url, data, method='PUT')
191
192
193def delete_topic(url_opener, gerrit_url, change_id):
194 """Delete the topic name."""
195
196 url = '{}/a/changes/{}/topic'.format(gerrit_url, change_id)
197 request = Request(url)
198 request.get_method = lambda: 'DELETE'
199 response_file = url_opener.open(request)
200 try:
201 return (response_file.getcode(), response_file.read())
202 finally:
203 response_file.close()
204
205
206def set_hashtags(url_opener, gerrit_url, change_id, add_tags=None,
207 remove_tags=None):
208 """Add or remove hash tags."""
209
210 url = '{}/a/changes/{}/hashtags'.format(gerrit_url, change_id)
211
212 data = {}
213 if add_tags:
214 data['add'] = add_tags
215 if remove_tags:
216 data['remove'] = remove_tags
217
218 return _make_json_post_request(url_opener, url, data)
219
220
Logan Chien4721aa12018-06-07 10:50:15 +0800221def get_patch(url_opener, gerrit_url, change_id, revision_id='current'):
222 """Download the patch file."""
223
224 url = '{}/a/changes/{}/revisions/{}/patch'.format(
225 gerrit_url, change_id, revision_id)
226
227 response_file = url_opener.open(url)
228 try:
229 return base64.b64decode(response_file.read())
230 finally:
231 response_file.close()
232
233
Logan Chien3f67c282018-06-06 18:16:31 +0800234def _parse_args():
235 """Parse command line options."""
236 parser = argparse.ArgumentParser()
237
238 parser.add_argument('query', help='Change list query string')
239 parser.add_argument('-g', '--gerrit', required=True,
240 help='Gerrit review URL')
241
242 parser.add_argument('--gitcookies',
243 default=os.path.expanduser('~/.gitcookies'),
244 help='Gerrit cookie file')
245 parser.add_argument('--limits', default=1000,
246 help='Max number of change lists')
247
248 return parser.parse_args()
249
250
251def main():
Logan Chienbfb414f2018-06-15 16:06:56 +0800252 """Main function"""
Logan Chien3f67c282018-06-06 18:16:31 +0800253 args = _parse_args()
254
255 # Query change lists
256 url_opener = create_url_opener_from_args(args)
257 change_lists = query_change_lists(
258 url_opener, args.gerrit, args.query, args.limits)
259
260 # Print the result
261 json.dump(change_lists, sys.stdout, indent=4, separators=(', ', ': '))
262 print() # Print the end-of-line
263
264if __name__ == '__main__':
265 main()