| #!/usr/bin/env python |
| # |
| # Copyright (C) 2013 The CyanogenMod Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # |
| |
| # |
| # Run repopick.py -h for a description of this utility. |
| # |
| |
| from __future__ import print_function |
| |
| import sys |
| import json |
| import os |
| import subprocess |
| import re |
| import argparse |
| import textwrap |
| |
| try: |
| # For python3 |
| import urllib.request |
| except ImportError: |
| # For python2 |
| import imp |
| import urllib2 |
| urllib = imp.new_module('urllib') |
| urllib.request = urllib2 |
| |
| # Parse the command line |
| parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description=textwrap.dedent('''\ |
| repopick.py is a utility to simplify the process of cherry picking |
| patches from OmniROM's Gerrit instance. |
| |
| Given a list of change numbers, repopick will cd into the project path |
| and cherry pick the latest patch available. |
| |
| With the --start-branch argument, the user can specify that a branch |
| should be created before cherry picking. This is useful for |
| cherry-picking many patches into a common branch which can be easily |
| abandoned later (good for testing other's changes.) |
| |
| The --abandon-first argument, when used in conjuction with the |
| --start-branch option, will cause repopick to abandon the specified |
| branch in all repos first before performing any cherry picks.''')) |
| parser.add_argument('change_number', nargs='*', help='change number to cherry pick') |
| parser.add_argument('-i', '--ignore-missing', action='store_true', help='do not error out if a patch applies to a missing directory') |
| parser.add_argument('-c', '--checkout', action='store_true', help='checkout instead of cherry pick') |
| parser.add_argument('-s', '--start-branch', nargs=1, help='start the specified branch before cherry picking') |
| parser.add_argument('-a', '--abandon-first', action='store_true', help='before cherry picking, abandon the branch specified in --start-branch') |
| parser.add_argument('-b', '--auto-branch', action='store_true', help='shortcut to "--start-branch auto --abandon-first --ignore-missing"') |
| parser.add_argument('-q', '--quiet', action='store_true', help='print as little as possible') |
| parser.add_argument('-v', '--verbose', action='store_true', help='print extra information to aid in debug') |
| parser.add_argument('-t', '--topic', help='pick all commits from a specified topic') |
| args = parser.parse_args() |
| if args.start_branch == None and args.abandon_first: |
| parser.error('if --abandon-first is set, you must also give the branch name with --start-branch') |
| if args.auto_branch: |
| args.abandon_first = True |
| args.ignore_missing = True |
| if not args.start_branch: |
| args.start_branch = ['auto'] |
| if args.quiet and args.verbose: |
| parser.error('--quiet and --verbose cannot be specified together') |
| if len(args.change_number) > 0 and args.topic: |
| parser.error('cannot specify a topic and change number(s) together') |
| if len(args.change_number) == 0 and not args.topic: |
| parser.error('must specify at least one commit id or a topic') |
| |
| # Helper function to determine whether a path is an executable file |
| def is_exe(fpath): |
| return os.path.isfile(fpath) and os.access(fpath, os.X_OK) |
| |
| # Implementation of Unix 'which' in Python |
| # |
| # From: http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python |
| def which(program): |
| fpath, fname = os.path.split(program) |
| if fpath: |
| if is_exe(program): |
| return program |
| else: |
| for path in os.environ["PATH"].split(os.pathsep): |
| path = path.strip('"') |
| exe_file = os.path.join(path, program) |
| if is_exe(exe_file): |
| return exe_file |
| sys.stderr.write('ERROR: Could not find the %s program in $PATH\n' % program) |
| sys.exit(1) |
| |
| # Simple wrapper for os.system() that: |
| # - exits on error |
| # - prints out the command if --verbose |
| # - suppresses all output if --quiet |
| def execute_cmd(cmd, exit_on_fail=True): |
| if args.verbose: |
| print('Executing: %s' % cmd) |
| if args.quiet: |
| cmd = cmd.replace(' && ', ' &> /dev/null && ') |
| cmd = cmd + " &> /dev/null" |
| ret = os.system(cmd) |
| if ret and exit_on_fail: |
| if not args.verbose: |
| sys.stderr.write('\nERROR: Command that failed:\n%s' % cmd) |
| sys.exit(1) |
| return ret |
| |
| # Verifies whether pathA is a subdirectory (or the same) as pathB |
| def is_pathA_subdir_of_pathB(pathA, pathB): |
| pathA = os.path.realpath(pathA) + '/' |
| pathB = os.path.realpath(pathB) + '/' |
| return(pathB == pathA[:len(pathB)]) |
| |
| # Find the necessary bins - repo |
| repo_bin = which('repo') |
| |
| # Find the necessary bins - git |
| git_bin = which('git') |
| |
| # Change current directory to the top of the tree |
| if os.environ.get('ANDROID_BUILD_TOP', None): |
| top = os.environ['ANDROID_BUILD_TOP'] |
| if not is_pathA_subdir_of_pathB(os.getcwd(), top): |
| sys.stderr.write('ERROR: You must run this tool from within $ANDROID_BUILD_TOP!\n') |
| sys.exit(1) |
| os.chdir(os.environ['ANDROID_BUILD_TOP']) |
| else: |
| sys.stderr.write('ERROR: $ANDROID_BUILD_TOP is not defined. please check build/envsetup.sh\n') |
| sys.exit(1) |
| |
| # Sanity check that we are being run from the top level of the tree |
| if not os.path.isdir('.repo'): |
| sys.stderr.write('ERROR: No .repo directory found. Please run this from the top of your tree.\n') |
| sys.exit(1) |
| |
| # If --abandon-first is given, abandon the branch before starting |
| if args.abandon_first: |
| # Determine if the branch already exists; skip the abandon if it does not |
| plist = subprocess.Popen([repo_bin,"info"], stdout=subprocess.PIPE) |
| needs_abandon = False |
| while(True): |
| pline = plist.stdout.readline().rstrip() |
| if not pline: |
| break |
| matchObj = re.match(r'Local Branches.*\[(.*)\]', pline.decode()) |
| if matchObj: |
| local_branches = re.split('\s*,\s*', matchObj.group(1)) |
| if any(args.start_branch[0] in s for s in local_branches): |
| needs_abandon = True |
| |
| if needs_abandon: |
| # Perform the abandon only if the branch already exists |
| if not args.quiet: |
| print('Abandoning branch: %s' % args.start_branch[0]) |
| cmd = '%s abandon %s' % (repo_bin, args.start_branch[0]) |
| execute_cmd(cmd) |
| if not args.quiet: |
| print('') |
| |
| # Get the list of projects that repo knows about |
| # - convert the project name to a project path |
| project_name_to_path = {} |
| plist = subprocess.Popen([repo_bin,"list"], stdout=subprocess.PIPE) |
| project_path = None |
| while(True): |
| pline = plist.stdout.readline().rstrip() |
| if not pline: |
| break |
| ppaths = re.split('\s*:\s*', pline.decode()) |
| project_name_to_path[ppaths[1]] = ppaths[0] |
| |
| # Get all commits for a specified topic |
| if args.topic: |
| url = 'http://gerrit.omnirom.org/changes/?q=topic:%s' % args.topic |
| if args.verbose: |
| print('Fetching all commits from topic: %s\n' % args.topic) |
| f = urllib.request.urlopen(url) |
| d = f.read().decode("utf-8") |
| if args.verbose: |
| print('Result from request:\n' + d) |
| |
| # Clean up the result |
| d = d.lstrip(")]}'\n") |
| if re.match(r'\[\s*\]', d): |
| sys.stderr.write('ERROR: Topic %s was not found on the server\n' % args.topic) |
| sys.exit(1) |
| if args.verbose: |
| print('Result from request:\n' + d) |
| |
| try: |
| data = json.loads(d) |
| except ValueError: |
| sys.stderr.write('ERROR: Could not load json') |
| sys.exit(1) |
| |
| args.change_number = sorted(d['_number'] for d in data) |
| |
| # Check for range of commits and rebuild array |
| changelist = [] |
| for change in args.change_number: |
| c=str(change) |
| if '-' in c: |
| templist = c.split('-') |
| for i in range(int(templist[0]), int(templist[1]) + 1): |
| changelist.append(str(i)) |
| else: |
| changelist.append(c) |
| |
| args.change_number = changelist |
| |
| # Iterate through the requested change numbers |
| for change in args.change_number: |
| if not args.quiet: |
| print('Applying change number %s ...' % change) |
| |
| # Fetch information about the change from Gerrit's REST API |
| # |
| # gerrit returns two lines, a magic string and then valid JSON: |
| # )]}' |
| # [ ... valid JSON ... ] |
| url = 'https://gerrit.omnirom.org/changes/?q=%s&o=CURRENT_REVISION&o=CURRENT_COMMIT&pp=0' % change |
| if args.verbose: |
| print('Fetching from: %s\n' % url) |
| f = urllib.request.urlopen(url) |
| d = f.read().decode("utf-8") |
| if args.verbose: |
| print('Result from request:\n' + d) |
| |
| # Clean up the result |
| d = d.split('\n')[1] |
| if re.match(r'\[\s*\]', d): |
| sys.stderr.write('ERROR: Change number %s was not found on the server\n' % change) |
| sys.exit(1) |
| |
| # Parse the JSON |
| try: |
| data_array = json.loads(d) |
| except ValueError: |
| sys.stderr.write('ERROR: The response from the server could not be parsed properly\n') |
| if args.verbose: |
| sys.stderr.write('The malformed response was: %s\n' % d) |
| sys.exit(1) |
| # Enumerate through JSON response |
| for (i, data) in enumerate(data_array): |
| date_fluff = '.000000000' |
| project_name = data['project'] |
| change_number = data['_number'] |
| current_revision = data['revisions'][data['current_revision']] |
| status = data['status'] |
| patch_number = current_revision['_number'] |
| # Backwards compatibility |
| if 'http' in current_revision['fetch']: |
| fetch_url = current_revision['fetch']['http']['url'] |
| fetch_ref = current_revision['fetch']['http']['ref'] |
| else: |
| fetch_url = current_revision['fetch']['anonymous http']['url'] |
| fetch_ref = current_revision['fetch']['anonymous http']['ref'] |
| author_name = current_revision['commit']['author']['name'] |
| author_email = current_revision['commit']['author']['email'] |
| author_date = current_revision['commit']['author']['date'].replace(date_fluff, '') |
| committer_name = current_revision['commit']['committer']['name'] |
| committer_email = current_revision['commit']['committer']['email'] |
| committer_date = current_revision['commit']['committer']['date'].replace(date_fluff, '') |
| subject = current_revision['commit']['subject'] |
| |
| # remove symbols from committer name |
| # from http://stackoverflow.com/questions/9942594/unicodeencodeerror-ascii-codec-cant-encode-character-u-xa0-in-position-20?rq=1 |
| author_name = author_name.encode('ascii', 'ignore').decode('ascii') |
| committer_name = committer_name.encode('ascii', 'ignore').decode('ascii') |
| |
| # Check if commit is not open, skip it. |
| if (status != 'OPEN' and status != 'NEW'): |
| print("Change is not open. Skipping the cherry pick.") |
| continue; |
| |
| # Convert the project name to a project path |
| # - check that the project path exists |
| if project_name in project_name_to_path: |
| project_path = project_name_to_path[project_name]; |
| elif args.ignore_missing: |
| print('WARNING: Skipping %d since there is no project directory for: %s\n' % (change_number, project_name)) |
| continue; |
| else: |
| sys.stderr.write('ERROR: For %d, could not determine the project path for project %s\n' % (change_number, project_name)) |
| continue; |
| |
| # If --start-branch is given, create the branch (more than once per path is okay; repo ignores gracefully) |
| if args.start_branch: |
| cmd = '%s start %s %s' % (repo_bin, args.start_branch[0], project_path) |
| execute_cmd(cmd) |
| |
| # Print out some useful info |
| if not args.quiet: |
| print('--> Subject: "%s"' % subject) |
| print('--> Project path: %s' % project_path) |
| print('--> Change number: %d (Patch Set %d)' % (change_number, patch_number)) |
| print('--> Author: %s <%s> %s' % (author_name, author_email, author_date)) |
| print('--> Committer: %s <%s> %s' % (committer_name, committer_email, committer_date)) |
| |
| if args.verbose: |
| print('Trying to fetch the change %d (Patch Set %d) from Gerrit') |
| cmd = 'cd %s && git fetch %s %s' % (project_path, fetch_url, fetch_ref) |
| execute_cmd(cmd) |
| # Check if it worked |
| FETCH_HEAD = '%s/.git/FETCH_HEAD' % project_path |
| if os.stat(FETCH_HEAD).st_size == 0: |
| # That didn't work, print error and exit |
| sys.stderr.write('ERROR: Fetching change from Gerrit failed. Exiting...') |
| continue; |
| # Perform the cherry-pick or checkout |
| if args.checkout: |
| cmd = 'cd %s && git checkout FETCH_HEAD' % (project_path) |
| else: |
| cmd = 'cd %s && git cherry-pick FETCH_HEAD' % (project_path) |
| |
| execute_cmd(cmd) |
| if not args.quiet: |
| print('Change #%d (Patch Set %d) %s into %s' % (change_number, patch_number, 'checked out' if args.checkout else 'cherry-picked', project_path)) |