blob: aa1d07bbcfd6415a855410fd2e1c84a019e87104 [file] [log] [blame]
Pulser72e23242013-09-29 09:56:55 +01001#!/usr/bin/env python
2#
3# Copyright (C) 2013 The CyanogenMod Project
4#
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:
33 # For python3
34 import urllib.request
35except ImportError:
36 # For python2
37 import imp
38 import urllib2
39 urllib = imp.new_module('urllib')
40 urllib.request = urllib2
41
42# Parse the command line
43parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description=textwrap.dedent('''\
44 repopick.py is a utility to simplify the process of cherry picking
45 patches from OmniROM's Gerrit instance.
46
47 Given a list of change numbers, repopick will cd into the project path
48 and cherry pick the latest patch available.
49
50 With the --start-branch argument, the user can specify that a branch
51 should be created before cherry picking. This is useful for
52 cherry-picking many patches into a common branch which can be easily
53 abandoned later (good for testing other's changes.)
54
55 The --abandon-first argument, when used in conjuction with the
56 --start-branch option, will cause repopick to abandon the specified
57 branch in all repos first before performing any cherry picks.'''))
58parser.add_argument('change_number', nargs='*', help='change number to cherry pick')
59parser.add_argument('-i', '--ignore-missing', action='store_true', help='do not error out if a patch applies to a missing directory')
60parser.add_argument('-c', '--checkout', action='store_true', help='checkout instead of cherry pick')
61parser.add_argument('-s', '--start-branch', nargs=1, help='start the specified branch before cherry picking')
62parser.add_argument('-a', '--abandon-first', action='store_true', help='before cherry picking, abandon the branch specified in --start-branch')
63parser.add_argument('-b', '--auto-branch', action='store_true', help='shortcut to "--start-branch auto --abandon-first --ignore-missing"')
64parser.add_argument('-q', '--quiet', action='store_true', help='print as little as possible')
65parser.add_argument('-v', '--verbose', action='store_true', help='print extra information to aid in debug')
66parser.add_argument('-t', '--topic', help='pick all commits from a specified topic')
67args = parser.parse_args()
68if args.start_branch == None and args.abandon_first:
69 parser.error('if --abandon-first is set, you must also give the branch name with --start-branch')
70if args.auto_branch:
71 args.abandon_first = True
72 args.ignore_missing = True
73 if not args.start_branch:
74 args.start_branch = ['auto']
75if args.quiet and args.verbose:
76 parser.error('--quiet and --verbose cannot be specified together')
77if len(args.change_number) > 0 and args.topic:
78 parser.error('cannot specify a topic and change number(s) together')
79if len(args.change_number) == 0 and not args.topic:
80 parser.error('must specify at least one commit id or a topic')
81
82# Helper function to determine whether a path is an executable file
83def is_exe(fpath):
84 return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
85
86# Implementation of Unix 'which' in Python
87#
88# From: http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python
89def which(program):
90 fpath, fname = os.path.split(program)
91 if fpath:
92 if is_exe(program):
93 return program
94 else:
95 for path in os.environ["PATH"].split(os.pathsep):
96 path = path.strip('"')
97 exe_file = os.path.join(path, program)
98 if is_exe(exe_file):
99 return exe_file
100 sys.stderr.write('ERROR: Could not find the %s program in $PATH\n' % program)
101 sys.exit(1)
102
103# Simple wrapper for os.system() that:
104# - exits on error
105# - prints out the command if --verbose
106# - suppresses all output if --quiet
107def execute_cmd(cmd, exit_on_fail=True):
108 if args.verbose:
109 print('Executing: %s' % cmd)
110 if args.quiet:
111 cmd = cmd.replace(' && ', ' &> /dev/null && ')
112 cmd = cmd + " &> /dev/null"
113 ret = os.system(cmd)
114 if ret and exit_on_fail:
115 if not args.verbose:
116 sys.stderr.write('\nERROR: Command that failed:\n%s' % cmd)
117 sys.exit(1)
118 return ret
119
120# Verifies whether pathA is a subdirectory (or the same) as pathB
121def is_pathA_subdir_of_pathB(pathA, pathB):
122 pathA = os.path.realpath(pathA) + '/'
123 pathB = os.path.realpath(pathB) + '/'
124 return(pathB == pathA[:len(pathB)])
125
126# Find the necessary bins - repo
127repo_bin = which('repo')
128
129# Find the necessary bins - git
130git_bin = which('git')
131
132# Change current directory to the top of the tree
133if os.environ.get('ANDROID_BUILD_TOP', None):
134 top = os.environ['ANDROID_BUILD_TOP']
135 if not is_pathA_subdir_of_pathB(os.getcwd(), top):
136 sys.stderr.write('ERROR: You must run this tool from within $ANDROID_BUILD_TOP!\n')
137 sys.exit(1)
138 os.chdir(os.environ['ANDROID_BUILD_TOP'])
139else:
140 sys.stderr.write('ERROR: $ANDROID_BUILD_TOP is not defined. please check build/envsetup.sh\n')
141 sys.exit(1)
142
143# Sanity check that we are being run from the top level of the tree
144if not os.path.isdir('.repo'):
145 sys.stderr.write('ERROR: No .repo directory found. Please run this from the top of your tree.\n')
146 sys.exit(1)
147
148# If --abandon-first is given, abandon the branch before starting
149if args.abandon_first:
150 # Determine if the branch already exists; skip the abandon if it does not
151 plist = subprocess.Popen([repo_bin,"info"], stdout=subprocess.PIPE)
152 needs_abandon = False
153 while(True):
154 pline = plist.stdout.readline().rstrip()
155 if not pline:
156 break
157 matchObj = re.match(r'Local Branches.*\[(.*)\]', pline.decode())
158 if matchObj:
159 local_branches = re.split('\s*,\s*', matchObj.group(1))
160 if any(args.start_branch[0] in s for s in local_branches):
161 needs_abandon = True
162
163 if needs_abandon:
164 # Perform the abandon only if the branch already exists
165 if not args.quiet:
166 print('Abandoning branch: %s' % args.start_branch[0])
167 cmd = '%s abandon %s' % (repo_bin, args.start_branch[0])
168 execute_cmd(cmd)
169 if not args.quiet:
170 print('')
171
172# Get the list of projects that repo knows about
173# - convert the project name to a project path
174project_name_to_path = {}
175plist = subprocess.Popen([repo_bin,"list"], stdout=subprocess.PIPE)
176project_path = None
177while(True):
178 pline = plist.stdout.readline().rstrip()
179 if not pline:
180 break
181 ppaths = re.split('\s*:\s*', pline.decode())
182 project_name_to_path[ppaths[1]] = ppaths[0]
183
184# Get all commits for a specified topic
185if args.topic:
186 url = 'http://gerrit.omnirom.org/changes/?q=topic:%s' % args.topic
187 if args.verbose:
188 print('Fetching all commits from topic: %s\n' % args.topic)
189 f = urllib.request.urlopen(url)
190 d = f.read().decode("utf-8")
191 if args.verbose:
192 print('Result from request:\n' + d)
193
194 # Clean up the result
195 d = d.lstrip(")]}'\n")
196 if re.match(r'\[\s*\]', d):
197 sys.stderr.write('ERROR: Topic %s was not found on the server\n' % args.topic)
198 sys.exit(1)
199 if args.verbose:
200 print('Result from request:\n' + d)
201
202 try:
203 data = json.loads(d)
204 except ValueError:
205 sys.stderr.write('ERROR: Could not load json')
206 sys.exit(1)
207
208 args.change_number = sorted(d['_number'] for d in data)
209
210# Check for range of commits and rebuild array
211changelist = []
212for change in args.change_number:
213 c=str(change)
214 if '-' in c:
215 templist = c.split('-')
216 for i in range(int(templist[0]), int(templist[1]) + 1):
217 changelist.append(str(i))
218 else:
219 changelist.append(c)
220
221args.change_number = changelist
222
223# Iterate through the requested change numbers
224for change in args.change_number:
225 if not args.quiet:
226 print('Applying change number %s ...' % change)
227
228 # Fetch information about the change from Gerrit's REST API
229 #
230 # gerrit returns two lines, a magic string and then valid JSON:
231 # )]}'
232 # [ ... valid JSON ... ]
233 url = 'https://gerrit.omnirom.org/changes/?q=%s&o=CURRENT_REVISION&o=CURRENT_COMMIT&pp=0' % change
234 if args.verbose:
235 print('Fetching from: %s\n' % url)
236 f = urllib.request.urlopen(url)
237 d = f.read().decode("utf-8")
238 if args.verbose:
239 print('Result from request:\n' + d)
240
241 # Clean up the result
242 d = d.split('\n')[1]
243 if re.match(r'\[\s*\]', d):
244 sys.stderr.write('ERROR: Change number %s was not found on the server\n' % change)
245 sys.exit(1)
246
247 # Parse the JSON
248 try:
249 data_array = json.loads(d)
250 except ValueError:
251 sys.stderr.write('ERROR: The response from the server could not be parsed properly\n')
252 if args.verbose:
253 sys.stderr.write('The malformed response was: %s\n' % d)
254 sys.exit(1)
255 # Enumerate through JSON response
256 for (i, data) in enumerate(data_array):
257 date_fluff = '.000000000'
258 project_name = data['project']
259 change_number = data['_number']
260 current_revision = data['revisions'][data['current_revision']]
261 status = data['status']
262 patch_number = current_revision['_number']
263 # Backwards compatibility
264 if 'http' in current_revision['fetch']:
265 fetch_url = current_revision['fetch']['http']['url']
266 fetch_ref = current_revision['fetch']['http']['ref']
267 else:
268 fetch_url = current_revision['fetch']['anonymous http']['url']
269 fetch_ref = current_revision['fetch']['anonymous http']['ref']
270 author_name = current_revision['commit']['author']['name']
271 author_email = current_revision['commit']['author']['email']
272 author_date = current_revision['commit']['author']['date'].replace(date_fluff, '')
273 committer_name = current_revision['commit']['committer']['name']
274 committer_email = current_revision['commit']['committer']['email']
275 committer_date = current_revision['commit']['committer']['date'].replace(date_fluff, '')
276 subject = current_revision['commit']['subject']
277
278 # remove symbols from committer name
279 # from http://stackoverflow.com/questions/9942594/unicodeencodeerror-ascii-codec-cant-encode-character-u-xa0-in-position-20?rq=1
280 author_name = author_name.encode('ascii', 'ignore').decode('ascii')
281 committer_name = committer_name.encode('ascii', 'ignore').decode('ascii')
282
283 # Check if commit is not open, skip it.
284 if (status != 'OPEN' and status != 'NEW'):
285 print("Change is not open. Skipping the cherry pick.")
286 continue;
287
288 # Convert the project name to a project path
289 # - check that the project path exists
290 if project_name in project_name_to_path:
291 project_path = project_name_to_path[project_name];
292 elif args.ignore_missing:
293 print('WARNING: Skipping %d since there is no project directory for: %s\n' % (change_number, project_name))
294 continue;
295 else:
296 sys.stderr.write('ERROR: For %d, could not determine the project path for project %s\n' % (change_number, project_name))
297 continue;
298
299 # If --start-branch is given, create the branch (more than once per path is okay; repo ignores gracefully)
300 if args.start_branch:
301 cmd = '%s start %s %s' % (repo_bin, args.start_branch[0], project_path)
302 execute_cmd(cmd)
303
304 # Print out some useful info
305 if not args.quiet:
306 print('--> Subject: "%s"' % subject)
307 print('--> Project path: %s' % project_path)
308 print('--> Change number: %d (Patch Set %d)' % (change_number, patch_number))
309 print('--> Author: %s <%s> %s' % (author_name, author_email, author_date))
310 print('--> Committer: %s <%s> %s' % (committer_name, committer_email, committer_date))
311
312 if args.verbose:
313 print('Trying to fetch the change %d (Patch Set %d) from Gerrit')
314 cmd = 'cd %s && git fetch %s %s' % (project_path, fetch_url, fetch_ref)
315 execute_cmd(cmd)
316 # Check if it worked
317 FETCH_HEAD = '%s/.git/FETCH_HEAD' % project_path
318 if os.stat(FETCH_HEAD).st_size == 0:
319 # That didn't work, print error and exit
320 sys.stderr.write('ERROR: Fetching change from Gerrit failed. Exiting...')
321 continue;
322 # Perform the cherry-pick or checkout
323 if args.checkout:
324 cmd = 'cd %s && git checkout FETCH_HEAD' % (project_path)
325 else:
326 cmd = 'cd %s && git cherry-pick FETCH_HEAD' % (project_path)
327
328 execute_cmd(cmd)
329 if not args.quiet:
330 print('Change #%d (Patch Set %d) %s into %s' % (change_number, patch_number, 'checked out' if args.checkout else 'cherry-picked', project_path))