Chirayu Desai | 4a319b8 | 2013-06-05 20:14:33 +0530 | [diff] [blame^] | 1 | #!/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 | |
| 22 | from __future__ import print_function |
| 23 | |
| 24 | import sys |
| 25 | import json |
| 26 | import os |
| 27 | import subprocess |
| 28 | import re |
| 29 | import argparse |
| 30 | import textwrap |
| 31 | |
| 32 | try: |
| 33 | # For python3 |
| 34 | import urllib.request |
| 35 | except 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 |
| 43 | parser = 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.''')) |
| 58 | parser.add_argument('change_number', nargs='+', help='change number to cherry pick') |
| 59 | parser.add_argument('-i', '--ignore-missing', action='store_true', help='do not error out if a patch applies to a missing directory') |
| 60 | parser.add_argument('-s', '--start-branch', nargs=1, help='start the specified branch before cherry picking') |
| 61 | parser.add_argument('-a', '--abandon-first', action='store_true', help='before cherry picking, abandon the branch specified in --start-branch') |
| 62 | parser.add_argument('-b', '--auto-branch', action='store_true', help='shortcut to "--start-branch auto --abandon-first --ignore-missing"') |
| 63 | parser.add_argument('-q', '--quiet', action='store_true', help='print as little as possible') |
| 64 | parser.add_argument('-v', '--verbose', action='store_true', help='print extra information to aid in debug') |
| 65 | args = parser.parse_args() |
| 66 | if 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') |
| 68 | if args.auto_branch: |
| 69 | args.abandon_first = True |
| 70 | args.ignore_missing = True |
| 71 | if not args.start_branch: |
| 72 | args.start_branch = ['auto'] |
| 73 | if 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 |
| 77 | def 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 |
| 83 | def 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 |
| 101 | def 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 |
| 113 | def 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 |
| 119 | repo_bin = which('repo') |
| 120 | if 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 |
| 127 | git_bin = which('git') |
| 128 | if 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 |
| 133 | if '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 |
| 141 | if 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 |
| 146 | if 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 |
| 171 | for 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 | |