blob: a2ee36ea40606953839a9839bf316a341b622932 [file] [log] [blame]
Chirayu Desai4a319b82013-06-05 20:14:33 +05301#!/usr/bin/env python
2#
Tom Powellc8580302015-08-04 15:37:12 -07003# Copyright (C) 2013-15 The CyanogenMod Project
Chirayu Desai4a319b82013-06-05 20:14:33 +05304#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17
18#
19# Run repopick.py -h for a description of this utility.
20#
21
22from __future__ import print_function
23
24import sys
25import json
26import os
27import subprocess
28import re
29import argparse
30import textwrap
31
32try:
Tom Powellc8580302015-08-04 15:37:12 -070033 # For python3
34 import urllib.error
35 import urllib.request
Chirayu Desai4a319b82013-06-05 20:14:33 +053036except ImportError:
Tom Powellc8580302015-08-04 15:37:12 -070037 # For python2
38 import imp
39 import urllib2
40 urllib = imp.new_module('urllib')
41 urllib.error = urllib2
42 urllib.request = urllib2
Chirayu Desai4a319b82013-06-05 20:14:33 +053043
Chirayu Desai4a319b82013-06-05 20:14:33 +053044
45# Verifies whether pathA is a subdirectory (or the same) as pathB
Tom Powellc8580302015-08-04 15:37:12 -070046def is_subdir(a, b):
47 a = os.path.realpath(a) + '/'
48 b = os.path.realpath(b) + '/'
49 return b == a[:len(b)]
Chirayu Desai4a319b82013-06-05 20:14:33 +053050
Tom Powellc8580302015-08-04 15:37:12 -070051
52def fetch_query_via_ssh(remote_url, query):
53 """Given a remote_url and a query, return the list of changes that fit it
54 This function is slightly messy - the ssh api does not return data in the same structure as the HTTP REST API
55 We have to get the data, then transform it to match what we're expecting from the HTTP RESET API"""
56 if remote_url.count(':') == 2:
57 (uri, userhost, port) = remote_url.split(':')
Tom Powellff0032a2015-09-01 16:52:40 -070058 userhost = userhost[2:]
Tom Powellc8580302015-08-04 15:37:12 -070059 elif remote_url.count(':') == 1:
60 (uri, userhost) = remote_url.split(':')
Tom Powellff0032a2015-09-01 16:52:40 -070061 userhost = userhost[2:]
Tom Powellc8580302015-08-04 15:37:12 -070062 port = 29418
63 else:
64 raise Exception('Malformed URI: Expecting ssh://[user@]host[:port]')
65
66
67 out = subprocess.check_output(['ssh', '-x', '-p{0}'.format(port), userhost, 'gerrit', 'query', '--format=JSON --patch-sets --current-patch-set', query])
68
69 reviews = []
70 for line in out.split('\n'):
71 try:
72 data = json.loads(line)
73 # make our data look like the http rest api data
74 review = {
75 'branch': data['branch'],
76 'change_id': data['id'],
77 'current_revision': data['currentPatchSet']['revision'],
78 'number': int(data['number']),
79 'revisions': {patch_set['revision']: {
80 'number': int(patch_set['number']),
81 'fetch': {
82 'ssh': {
83 'ref': patch_set['ref'],
84 'url': u'ssh://{0}:{1}/{2}'.format(userhost, port, data['project'])
85 }
86 }
87 } for patch_set in data['patchSets']},
88 'subject': data['subject'],
89 'project': data['project'],
90 'status': data['status']
91 }
92 reviews.append(review)
Tom Powellff0032a2015-09-01 16:52:40 -070093 except:
Tom Powellc8580302015-08-04 15:37:12 -070094 pass
Brint E. Kriebel9c1a3c32015-09-09 22:29:28 -070095 args.quiet or print('Found {0} reviews'.format(len(reviews)))
Tom Powellc8580302015-08-04 15:37:12 -070096 return reviews
97
98
99def fetch_query_via_http(remote_url, query):
100
101 """Given a query, fetch the change numbers via http"""
102 url = '{0}/changes/?q={1}&o=CURRENT_REVISION&o=ALL_REVISIONS'.format(remote_url, query)
103 data = urllib.request.urlopen(url).read().decode('utf-8')
104 reviews = json.loads(data[5:])
105
106 for review in reviews:
107 review[u'number'] = review.pop('_number')
108
109 return reviews
110
111
112def fetch_query(remote_url, query):
113 """Wrapper for fetch_query_via_proto functions"""
Tom Powellff0032a2015-09-01 16:52:40 -0700114 if remote_url[0:3] == 'ssh':
Tom Powellc8580302015-08-04 15:37:12 -0700115 return fetch_query_via_ssh(remote_url, query)
116 elif remote_url[0:4] == 'http':
117 return fetch_query_via_http(remote_url, query.replace(' ', '+'))
118 else:
119 raise Exception('Gerrit URL should be in the form http[s]://hostname/ or ssh://[user@]host[:port]')
120
121if __name__ == '__main__':
Brint E. Kriebel9c1a3c32015-09-09 22:29:28 -0700122 # Default to CyanogenMod Gerrit
123 default_gerrit = 'http://review.cyanogenmod.org'
124
Tom Powellc8580302015-08-04 15:37:12 -0700125 parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description=textwrap.dedent('''\
126 repopick.py is a utility to simplify the process of cherry picking
127 patches from CyanogenMod's Gerrit instance (or any gerrit instance of your choosing)
128
129 Given a list of change numbers, repopick will cd into the project path
130 and cherry pick the latest patch available.
131
132 With the --start-branch argument, the user can specify that a branch
133 should be created before cherry picking. This is useful for
134 cherry-picking many patches into a common branch which can be easily
135 abandoned later (good for testing other's changes.)
136
137 The --abandon-first argument, when used in conjunction with the
138 --start-branch option, will cause repopick to abandon the specified
139 branch in all repos first before performing any cherry picks.'''))
140 parser.add_argument('change_number', nargs='*', help='change number to cherry pick. Use {change number}/{patchset number} to get a specific revision.')
141 parser.add_argument('-i', '--ignore-missing', action='store_true', help='do not error out if a patch applies to a missing directory')
142 parser.add_argument('-s', '--start-branch', nargs=1, help='start the specified branch before cherry picking')
143 parser.add_argument('-a', '--abandon-first', action='store_true', help='before cherry picking, abandon the branch specified in --start-branch')
144 parser.add_argument('-b', '--auto-branch', action='store_true', help='shortcut to "--start-branch auto --abandon-first --ignore-missing"')
145 parser.add_argument('-q', '--quiet', action='store_true', help='print as little as possible')
146 parser.add_argument('-v', '--verbose', action='store_true', help='print extra information to aid in debug')
jrior001fd11d072015-08-21 17:23:25 -0400147 parser.add_argument('-f', '--force', action='store_true', help='force cherry pick even if change is closed')
Tom Powellc8580302015-08-04 15:37:12 -0700148 parser.add_argument('-p', '--pull', action='store_true', help='execute pull instead of cherry-pick')
Brint E. Kriebel9c1a3c32015-09-09 22:29:28 -0700149 parser.add_argument('-P', '--path', help='use the specified path for the change')
Tom Powellc8580302015-08-04 15:37:12 -0700150 parser.add_argument('-t', '--topic', help='pick all commits from a specified topic')
151 parser.add_argument('-Q', '--query', help='pick all commits using the specified query')
Brint E. Kriebel9c1a3c32015-09-09 22:29:28 -0700152 parser.add_argument('-g', '--gerrit', default=default_gerrit, help='Gerrit Instance to use. Form proto://[user@]host[:port]')
Tom Powellc8580302015-08-04 15:37:12 -0700153 args = parser.parse_args()
Tom Powellc8580302015-08-04 15:37:12 -0700154 if not args.start_branch and args.abandon_first:
155 parser.error('if --abandon-first is set, you must also give the branch name with --start-branch')
156 if args.auto_branch:
157 args.abandon_first = True
158 args.ignore_missing = True
159 if not args.start_branch:
160 args.start_branch = ['auto']
161 if args.quiet and args.verbose:
162 parser.error('--quiet and --verbose cannot be specified together')
163
164 if (1 << bool(args.change_number) << bool(args.topic) << bool(args.query)) != 2:
165 parser.error('One (and only one) of change_number, topic, and query are allowed')
166
167 # Change current directory to the top of the tree
168 if 'ANDROID_BUILD_TOP' in os.environ:
169 top = os.environ['ANDROID_BUILD_TOP']
170
171 if not is_subdir(os.getcwd(), top):
172 sys.stderr.write('ERROR: You must run this tool from within $ANDROID_BUILD_TOP!\n')
173 sys.exit(1)
174 os.chdir(os.environ['ANDROID_BUILD_TOP'])
175
176 # Sanity check that we are being run from the top level of the tree
177 if not os.path.isdir('.repo'):
178 sys.stderr.write('ERROR: No .repo directory found. Please run this from the top of your tree.\n')
Chirayu Desai4a319b82013-06-05 20:14:33 +0530179 sys.exit(1)
180
Tom Powellc8580302015-08-04 15:37:12 -0700181 # If --abandon-first is given, abandon the branch before starting
182 if args.abandon_first:
183 # Determine if the branch already exists; skip the abandon if it does not
184 plist = subprocess.check_output(['repo', 'info'])
185 needs_abandon = False
186 for pline in plist:
187 matchObj = re.match(r'Local Branches.*\[(.*)\]', pline)
188 if matchObj:
189 local_branches = re.split('\s*,\s*', matchObj.group(1))
190 if any(args.start_branch[0] in s for s in local_branches):
191 needs_abandon = True
Chirayu Desai4a319b82013-06-05 20:14:33 +0530192
Tom Powellc8580302015-08-04 15:37:12 -0700193 if needs_abandon:
194 # Perform the abandon only if the branch already exists
195 if not args.quiet:
196 print('Abandoning branch: %s' % args.start_branch[0])
197 subprocess.check_output(['repo', 'abandon', args.start_branch[0]])
198 if not args.quiet:
199 print('')
Chirayu Desai4a319b82013-06-05 20:14:33 +0530200
Tom Powellc8580302015-08-04 15:37:12 -0700201 # Get the list of projects that repo knows about
202 # - convert the project name to a project path
203 project_name_to_path = {}
204 plist = subprocess.check_output(['repo', 'list']).split('\n')
Chirayu Desai4a319b82013-06-05 20:14:33 +0530205
Tom Powellc8580302015-08-04 15:37:12 -0700206 for pline in plist:
Chirayu Desai4a319b82013-06-05 20:14:33 +0530207 if not pline:
208 break
Tom Powellc8580302015-08-04 15:37:12 -0700209 ppaths = pline.split(' : ')
Chirayu Desai4a319b82013-06-05 20:14:33 +0530210
Tom Powellc8580302015-08-04 15:37:12 -0700211 project_name_to_path[ppaths[1]] = ppaths[0]
212
213 # get data on requested changes
214 reviews = []
215 change_numbers = []
216 if args.topic:
217 reviews = fetch_query(args.gerrit, 'topic:{0}'.format(args.topic))
Brint E. Kriebel9c1a3c32015-09-09 22:29:28 -0700218 change_numbers = sorted([str(r['number']) for r in reviews])
Tom Powellc8580302015-08-04 15:37:12 -0700219 if args.query:
220 reviews = fetch_query(args.gerrit, args.query)
Brint E. Kriebel9c1a3c32015-09-09 22:29:28 -0700221 change_numbers = sorted([str(r['number']) for r in reviews])
Tom Powellc8580302015-08-04 15:37:12 -0700222 if args.change_number:
223 reviews = fetch_query(args.gerrit, ' OR '.join('change:{0}'.format(x.split('/')[0]) for x in args.change_number))
224 change_numbers = args.change_number
Tom Powellc8580302015-08-04 15:37:12 -0700225
Brint E. Kriebel9c1a3c32015-09-09 22:29:28 -0700226 # make list of things to actually merge
Tom Powellc8580302015-08-04 15:37:12 -0700227 mergables = []
228
229 for change in change_numbers:
230 patchset = None
231 if '/' in change:
232 (change, patchset) = change.split('/')
233 change = int(change)
234
235 review = [x for x in reviews if x['number'] == change][0]
236 mergables.append({
237 'subject': review['subject'],
238 'project': review['project'],
239 'branch': review['branch'],
240 'change_number': review['number'],
241 'status': review['status'],
242 'fetch': None
243 })
244 mergables[-1]['fetch'] = review['revisions'][review['current_revision']]['fetch']
245 mergables[-1]['id'] = change
246 if patchset:
247 try:
248 mergables[-1]['fetch'] = [x['fetch'] for x in review['revisions'] if x['_number'] == patchset][0]
249 mergables[-1]['id'] = '{0}/{1}'.format(change, patchset)
250 except (IndexError, ValueError):
Brint E. Kriebel9c1a3c32015-09-09 22:29:28 -0700251 args.quiet or print('ERROR: The patch set {0}/{1} could not be found, using CURRENT_REVISION instead.'.format(change, patchset))
Tom Powellc8580302015-08-04 15:37:12 -0700252
253 for item in mergables:
Brint E. Kriebel9c1a3c32015-09-09 22:29:28 -0700254 args.quiet or print('Applying change number {0}...'.format(item['id']))
jrior001fd11d072015-08-21 17:23:25 -0400255 # Check if change is open and exit if it's not, unless -f is specified
Tom Powellc627f072015-09-02 05:46:55 +0000256 if (item['status'] != 'OPEN' and item['status'] != 'NEW') and not args.query:
Tom Powellc8580302015-08-04 15:37:12 -0700257 if args.force:
jrior001fd11d072015-08-21 17:23:25 -0400258 print('!! Force-picking a closed change !!\n')
Tom Powellc8580302015-08-04 15:37:12 -0700259 else:
Dan Pasanenfe636282015-09-09 23:38:16 -0500260 print('Change status is ' + item['status'] + '. Skipping the cherry pick.\nUse -f to force this pick.')
Tom Powellc8580302015-08-04 15:37:12 -0700261 continue
262
263 # Convert the project name to a project path
264 # - check that the project path exists
265 project_path = None
266 if item['project'] in project_name_to_path:
267 project_path = project_name_to_path[item['project']]
268
269 if project_path.startswith('hardware/qcom/'):
270 split_path = project_path.split('/')
271 # split_path[2] might be display or it might be display-caf, trim the -caf
272 split_path[2] = split_path[2].split('-')[0]
273
274 # Need to treat hardware/qcom/{audio,display,media} specially
275 if split_path[2] == 'audio' or split_path[2] == 'display' or split_path[2] == 'media':
276 split_branch = item['branch'].split('-')
277
278 # display is extra special
279 if split_path[2] == 'display' and len(split_path) == 3:
280 project_path = '/'.join(split_path)
281 else:
282 project_path = '/'.join(split_path[:-1])
283
284 if len(split_branch) == 4 and split_branch[0] == 'cm' and split_branch[2] == 'caf':
285 project_path += '-caf/msm' + split_branch[3]
286 # audio and media are different from display
287 elif split_path[2] == 'audio' or split_path[2] == 'media':
288 project_path += '/default'
Anthony King8e1ccda2015-07-20 17:39:20 -0400289 elif project_path.startswith('hardware/ril'):
290 project_path = project_path.rstrip('-caf')
291 if item["branch"].split('-')[-1] == 'caf':
292 project_path += '-caf'
Brint E. Kriebel9c1a3c32015-09-09 22:29:28 -0700293 elif args.path:
294 project_path = args.path
Tom Powellc8580302015-08-04 15:37:12 -0700295 elif args.ignore_missing:
296 print('WARNING: Skipping {0} since there is no project directory for: {1}\n'.format(item['id'], item['project']))
297 continue
298 else:
299 sys.stderr.write('ERROR: For {0}, could not determine the project path for project {1}\n'.format(item['id'], item['project']))
300 sys.exit(1)
301
302 # If --start-branch is given, create the branch (more than once per path is okay; repo ignores gracefully)
303 if args.start_branch:
304 subprocess.check_output(['repo', 'start', args.start_branch[0], project_path])
305
306 # Print out some useful info
Chirayu Desai4a319b82013-06-05 20:14:33 +0530307 if not args.quiet:
Tom Powellc8580302015-08-04 15:37:12 -0700308 print('--> Subject: "{0}"'.format(item['subject']))
309 print('--> Project path: {0}'.format(project_path))
310 print('--> Change number: {0} (Patch Set {0})'.format(item['id']))
311
Tom Powellc8580302015-08-04 15:37:12 -0700312 if 'anonymous http' in item['fetch']:
313 method = 'anonymous http'
314 else:
315 method = 'ssh'
316
Brint E. Kriebel9c1a3c32015-09-09 22:29:28 -0700317 # Try fetching from GitHub first if using default gerrit
318 if args.gerrit == default_gerrit:
Tom Powellc8580302015-08-04 15:37:12 -0700319 if args.verbose:
Brint E. Kriebel9c1a3c32015-09-09 22:29:28 -0700320 print('Trying to fetch the change from GitHub')
321
322 if args.pull:
323 cmd = ['git pull --no-edit github', item['fetch'][method]['ref']]
324 else:
325 cmd = ['git fetch github', item['fetch'][method]['ref']]
326 if args.quiet:
327 cmd.append('--quiet')
328 else:
329 print(cmd)
330 result = subprocess.call([' '.join(cmd)], cwd=project_path, shell=True)
331 if result != 0:
332 print('ERROR: git command failed')
333 sys.exit(result)
334 FETCH_HEAD = '{0}/.git/FETCH_HEAD'.format(project_path)
335 # Check if it worked
336 if args.gerrit != default_gerrit or os.stat(FETCH_HEAD).st_size == 0:
337 # If not using the default gerrit or github failed, fetch from gerrit.
338 if args.verbose:
339 if args.gerrit == default_gerrit:
340 print('Fetching from GitHub didn\'t work, trying to fetch the change from Gerrit')
341 else:
342 print('Fetching from {0}'.format(args.gerrit))
343
Tom Powellc8580302015-08-04 15:37:12 -0700344 if args.pull:
345 cmd = ['git pull --no-edit', item['fetch'][method]['url'], item['fetch'][method]['ref']]
346 else:
347 cmd = ['git fetch', item['fetch'][method]['url'], item['fetch'][method]['ref']]
Brint E. Kriebel9c1a3c32015-09-09 22:29:28 -0700348 if args.quiet:
349 cmd.append('--quiet')
350 else:
351 print(cmd)
352 result = subprocess.call([' '.join(cmd)], cwd=project_path, shell=True)
353 if result != 0:
354 print('ERROR: git command failed')
355 sys.exit(result)
Tom Powellc8580302015-08-04 15:37:12 -0700356 # Perform the cherry-pick
357 if not args.pull:
358 cmd = ['git cherry-pick FETCH_HEAD']
Brint E. Kriebel9c1a3c32015-09-09 22:29:28 -0700359 if args.quiet:
360 cmd_out = open(os.devnull, 'wb')
361 else:
362 cmd_out = None
363 result = subprocess.call(cmd, cwd=project_path, shell=True, stdout=cmd_out, stderr=cmd_out)
364 if result != 0:
365 print('ERROR: git command failed')
366 sys.exit(result)
Chirayu Desai4a319b82013-06-05 20:14:33 +0530367 if not args.quiet:
368 print('')
369