blob: ea7227e59394a16f29a1db3b67f3715e14bdf12c [file] [log] [blame]
Chirayu Desai4a319b82013-06-05 20:14:33 +05301#!/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 CyanogenMod'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('-s', '--start-branch', nargs=1, help='start the specified branch before cherry picking')
61parser.add_argument('-a', '--abandon-first', action='store_true', help='before cherry picking, abandon the branch specified in --start-branch')
62parser.add_argument('-b', '--auto-branch', action='store_true', help='shortcut to "--start-branch auto --abandon-first --ignore-missing"')
63parser.add_argument('-q', '--quiet', action='store_true', help='print as little as possible')
64parser.add_argument('-v', '--verbose', action='store_true', help='print extra information to aid in debug')
65args = parser.parse_args()
66if args.start_branch == None and args.abandon_first:
67 parser.error('if --abandon-first is set, you must also give the branch name with --start-branch')
68if args.auto_branch:
69 args.abandon_first = True
70 args.ignore_missing = True
71 if not args.start_branch:
72 args.start_branch = ['auto']
73if args.quiet and args.verbose:
74 parser.error('--quiet and --verbose cannot be specified together')
75
76# Helper function to determine whether a path is an executable file
77def is_exe(fpath):
78 return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
79
80# Implementation of Unix 'which' in Python
81#
82# From: http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python
83def which(program):
84 fpath, fname = os.path.split(program)
85 if fpath:
86 if is_exe(program):
87 return program
88 else:
89 for path in os.environ["PATH"].split(os.pathsep):
90 path = path.strip('"')
91 exe_file = os.path.join(path, program)
92 if is_exe(exe_file):
93 return exe_file
94
95 return None
96
97# Simple wrapper for os.system() that:
98# - exits on error
99# - prints out the command if --verbose
100# - suppresses all output if --quiet
101def execute_cmd(cmd):
102 if args.verbose:
103 print('Executing: %s' % cmd)
104 if args.quiet:
105 cmd = cmd.replace(' && ', ' &> /dev/null && ')
106 cmd = cmd + " &> /dev/null"
107 if os.system(cmd):
108 if not args.verbose:
109 print('\nCommand that failed:\n%s' % cmd)
110 sys.exit(1)
111
112# Verifies whether pathA is a subdirectory (or the same) as pathB
113def is_pathA_subdir_of_pathB(pathA, pathB):
114 pathA = os.path.realpath(pathA) + '/'
115 pathB = os.path.realpath(pathB) + '/'
116 return(pathB == pathA[:len(pathB)])
117
118# Find the necessary bins - repo
119repo_bin = which('repo')
120if repo_bin == None:
121 repo_bin = os.path.join(os.environ["HOME"], 'repo')
122 if not is_exe(repo_bin):
123 sys.stderr.write('ERROR: Could not find the repo program in either $PATH or $HOME/bin\n')
124 sys.exit(1)
125
126# Find the necessary bins - git
127git_bin = which('git')
128if not is_exe(git_bin):
129 sys.stderr.write('ERROR: Could not find the git program in $PATH\n')
130 sys.exit(1)
131
132# Change current directory to the top of the tree
133if 'ANDROID_BUILD_TOP' in os.environ:
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'])
139
140# Sanity check that we are being run from the top level of the tree
141if not os.path.isdir('.repo'):
142 sys.stderr.write('ERROR: No .repo directory found. Please run this from the top of your tree.\n')
143 sys.exit(1)
144
145# If --abandon-first is given, abandon the branch before starting
146if args.abandon_first:
147 # Determine if the branch already exists; skip the abandon if it does not
148 plist = subprocess.Popen([repo_bin,"info"], stdout=subprocess.PIPE)
149 needs_abandon = False
150 while(True):
151 pline = plist.stdout.readline().rstrip()
152 if not pline:
153 break
154 matchObj = re.match(r'Local Branches.*\[(.*)\]', pline.decode())
155 if matchObj:
156 local_branches = re.split('\s*,\s*', matchObj.group(1))
157 if any(args.start_branch[0] in s for s in local_branches):
158 needs_abandon = True
159 break
160
161 if needs_abandon:
162 # Perform the abandon only if the branch already exists
163 if not args.quiet:
164 print('Abandoning branch: %s' % args.start_branch[0])
165 cmd = '%s abandon %s' % (repo_bin, args.start_branch[0])
166 execute_cmd(cmd)
167 if not args.quiet:
168 print('')
169
170# Iterate through the requested change numbers
171for change in args.change_number:
172 if not args.quiet:
173 print('Applying change number %s ...' % change)
174
175 # Fetch information about the change from Gerrit's REST API
176 #
177 # gerrit returns two lines, a magic string and then valid JSON:
178 # )]}'
179 # [ ... valid JSON ... ]
180 url = 'http://review.cyanogenmod.org/changes/?q=%s&o=CURRENT_REVISION&o=CURRENT_COMMIT&pp=0' % change
181 if args.verbose:
182 print('Fetching from: %s\n' % url)
183 f = urllib.request.urlopen(url)
184 d = f.read().decode()
185
186 # Parse the result
187 if args.verbose:
188 print('Result from request:\n' + d)
189 d = d.split('\n')[1]
190 d = re.sub(r'\[(.*)\]', r'\1', d)
191 data = json.loads(d)
192
193 # Extract information from the JSON response
194 project_name = data['project']
195 change_number = data['_number']
196 current_revision = data['revisions'][data['current_revision']]
197 patch_number = current_revision['_number']
198 fetch_url = current_revision['fetch']['http']['url']
199 fetch_ref = current_revision['fetch']['http']['ref']
200 author_name = current_revision['commit']['author']['name']
201 author_email = current_revision['commit']['author']['email']
202 author_date = current_revision['commit']['author']['date']
203 committer_name = current_revision['commit']['committer']['name']
204 committer_email = current_revision['commit']['committer']['email']
205 committer_date = current_revision['commit']['committer']['date']
206 subject = current_revision['commit']['subject']
207
208 # Get the list of projects that repo knows about
209 # - convert the project name to a project path
210 plist = subprocess.Popen([repo_bin,"list"], stdout=subprocess.PIPE)
211 while(True):
212 pline = plist.stdout.readline().rstrip()
213 if not pline:
214 break
215 ppaths = re.split('\s*:\s*', pline.decode())
216 if ppaths[1] == project_name:
217 project_path = ppaths[0]
218 break
219 if 'project_path' not in locals():
220 sys.stderr.write('ERROR: Could not determine the project path for project %s\n' % project_name)
221 sys.exit(1)
222
223 # Check that the project path exists
224 if not os.path.isdir(project_path):
225 if args.ignore_missing:
226 print('WARNING: Skipping %d since there is no project directory: %s\n' % (change_number, project_path))
227 continue;
228 else:
229 sys.stderr.write('ERROR: For %d, there is no project directory: %s\n' % (change_number, project_path))
230 sys.exit(1)
231
232 # If --start-branch is given, create the branch (more than once per path is okay; repo ignores gracefully)
233 if args.start_branch:
234 cmd = '%s start %s %s' % (repo_bin, args.start_branch[0], project_path)
235 execute_cmd(cmd)
236
237 # Print out some useful info
238 if not args.quiet:
239 print('--> Subject: "%s"' % subject)
240 print('--> Project path: %s' % project_path)
241 print('--> Change number: %d (Patch Set %d)' % (change_number, patch_number))
242 print('--> Author: %s <%s> %s' % (author_name, author_email, author_date))
243 print('--> Committer: %s <%s> %s' % (committer_name, committer_email, committer_date))
244
245 # Perform the cherry-pick
246 cmd = 'cd %s && git fetch %s %s && git cherry-pick FETCH_HEAD' % (project_path, fetch_url, fetch_ref)
247 execute_cmd(cmd)
248 if not args.quiet:
249 print('')
250