summaryrefslogtreecommitdiffstats
path: root/build/tools/repopick.py
diff options
context:
space:
mode:
Diffstat (limited to 'build/tools/repopick.py')
-rwxr-xr-xbuild/tools/repopick.py480
1 files changed, 0 insertions, 480 deletions
diff --git a/build/tools/repopick.py b/build/tools/repopick.py
deleted file mode 100755
index 7025dc67..00000000
--- a/build/tools/repopick.py
+++ /dev/null
@@ -1,480 +0,0 @@
-#!/usr/bin/env python
-#
-# Copyright (C) 2013-15 The CyanogenMod Project
-# (C) 2017 The LineageOS 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
-from functools import cmp_to_key
-from xml.etree import ElementTree
-
-try:
- import requests
-except ImportError:
- try:
- # For python3
- import urllib.error
- import urllib.request
- except ImportError:
- # For python2
- import imp
- import urllib2
- urllib = imp.new_module('urllib')
- urllib.error = urllib2
- urllib.request = urllib2
-
-
-# cmp() is not available in Python 3, define it manually
-# See https://docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons
-def cmp(a, b):
- return (a > b) - (a < b)
-
-
-# Verifies whether pathA is a subdirectory (or the same) as pathB
-def is_subdir(a, b):
- a = os.path.realpath(a) + '/'
- b = os.path.realpath(b) + '/'
- return b == a[:len(b)]
-
-
-def fetch_query_via_ssh(remote_url, query):
- """Given a remote_url and a query, return the list of changes that fit it
- This function is slightly messy - the ssh api does not return data in the same structure as the HTTP REST API
- We have to get the data, then transform it to match what we're expecting from the HTTP RESET API"""
- if remote_url.count(':') == 2:
- (uri, userhost, port) = remote_url.split(':')
- userhost = userhost[2:]
- elif remote_url.count(':') == 1:
- (uri, userhost) = remote_url.split(':')
- userhost = userhost[2:]
- port = 29418
- else:
- raise Exception('Malformed URI: Expecting ssh://[user@]host[:port]')
-
-
- out = subprocess.check_output(['ssh', '-x', '-p{0}'.format(port), userhost, 'gerrit', 'query', '--format=JSON --patch-sets --current-patch-set', query])
- if not hasattr(out, 'encode'):
- out = out.decode()
- reviews = []
- for line in out.split('\n'):
- try:
- data = json.loads(line)
- # make our data look like the http rest api data
- review = {
- 'branch': data['branch'],
- 'change_id': data['id'],
- 'current_revision': data['currentPatchSet']['revision'],
- 'number': int(data['number']),
- 'revisions': {patch_set['revision']: {
- '_number': int(patch_set['number']),
- 'fetch': {
- 'ssh': {
- 'ref': patch_set['ref'],
- 'url': 'ssh://{0}:{1}/{2}'.format(userhost, port, data['project'])
- }
- },
- 'commit': {
- 'parents': [{ 'commit': parent } for parent in patch_set['parents']]
- },
- } for patch_set in data['patchSets']},
- 'subject': data['subject'],
- 'project': data['project'],
- 'status': data['status']
- }
- reviews.append(review)
- except:
- pass
- args.quiet or print('Found {0} reviews'.format(len(reviews)))
- return reviews
-
-
-def fetch_query_via_http(remote_url, query):
- if "requests" in sys.modules:
- auth = None
- if os.path.isfile(os.getenv("HOME") + "/.gerritrc"):
- f = open(os.getenv("HOME") + "/.gerritrc", "r")
- for line in f:
- parts = line.rstrip().split("|")
- if parts[0] in remote_url:
- auth = requests.auth.HTTPBasicAuth(username=parts[1], password=parts[2])
- statusCode = '-1'
- if auth:
- url = '{0}/a/changes/?q={1}&o=CURRENT_REVISION&o=ALL_REVISIONS&o=ALL_COMMITS'.format(remote_url, query)
- data = requests.get(url, auth=auth)
- statusCode = str(data.status_code)
- if statusCode != '200':
- #They didn't get good authorization or data, Let's try the old way
- url = '{0}/changes/?q={1}&o=CURRENT_REVISION&o=ALL_REVISIONS&o=ALL_COMMITS'.format(remote_url, query)
- data = requests.get(url)
- reviews = json.loads(data.text[5:])
- else:
- """Given a query, fetch the change numbers via http"""
- url = '{0}/changes/?q={1}&o=CURRENT_REVISION&o=ALL_REVISIONS&o=ALL_COMMITS'.format(remote_url, query)
- data = urllib.request.urlopen(url).read().decode('utf-8')
- reviews = json.loads(data[5:])
-
- for review in reviews:
- review['number'] = review.pop('_number')
-
- return reviews
-
-
-def fetch_query(remote_url, query):
- """Wrapper for fetch_query_via_proto functions"""
- if remote_url[0:3] == 'ssh':
- return fetch_query_via_ssh(remote_url, query)
- elif remote_url[0:4] == 'http':
- return fetch_query_via_http(remote_url, query.replace(' ', '+'))
- else:
- raise Exception('Gerrit URL should be in the form http[s]://hostname/ or ssh://[user@]host[:port]')
-
-if __name__ == '__main__':
- # Default to LineageOS Gerrit
- default_gerrit = 'https://review.lineageos.org'
-
- parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description=textwrap.dedent('''\
- repopick.py is a utility to simplify the process of cherry picking
- patches from LineageOS's Gerrit instance (or any gerrit instance of your choosing)
-
- 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 conjunction 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. Use {change number}/{patchset number} to get a specific revision.')
- 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('-s', '--start-branch', nargs=1, help='start the specified branch before cherry picking')
- parser.add_argument('-r', '--reset', action='store_true', help='reset to initial state (abort cherry-pick) if there is a conflict')
- 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('-f', '--force', action='store_true', help='force cherry pick even if change is closed')
- parser.add_argument('-p', '--pull', action='store_true', help='execute pull instead of cherry-pick')
- parser.add_argument('-P', '--path', help='use the specified path for the change')
- parser.add_argument('-t', '--topic', help='pick all commits from a specified topic')
- parser.add_argument('-Q', '--query', help='pick all commits using the specified query')
- parser.add_argument('-g', '--gerrit', default=default_gerrit, help='Gerrit Instance to use. Form proto://[user@]host[:port]')
- parser.add_argument('-e', '--exclude', nargs=1, help='exclude a list of commit numbers separated by a ,')
- parser.add_argument('-c', '--check-picked', type=int, default=10, help='pass the amount of commits to check for already picked changes')
- args = parser.parse_args()
- if not args.start_branch 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 (1 << bool(args.change_number) << bool(args.topic) << bool(args.query)) != 2:
- parser.error('One (and only one) of change_number, topic, and query are allowed')
-
- # Change current directory to the top of the tree
- if 'ANDROID_BUILD_TOP' in os.environ:
- top = os.environ['ANDROID_BUILD_TOP']
-
- if not is_subdir(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'])
-
- # 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.check_output(['repo', 'info'])
- if not hasattr(plist, 'encode'):
- plist = plist.decode()
- needs_abandon = False
- for pline in plist.splitlines():
- matchObj = re.match(r'Local Branches.*\[(.*)\]', pline)
- 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])
- subprocess.check_output(['repo', 'abandon', args.start_branch[0]])
- if not args.quiet:
- print('')
-
- # Get the master manifest from repo
- # - convert project name and revision to a path
- project_name_to_data = {}
- manifest = subprocess.check_output(['repo', 'manifest'])
- xml_root = ElementTree.fromstring(manifest)
- projects = xml_root.findall('project')
- remotes = xml_root.findall('remote')
- default_revision = xml_root.findall('default')[0].get('revision')
-
- #dump project data into the a list of dicts with the following data:
- #{project: {path, revision}}
-
- for project in projects:
- name = project.get('name')
- # when name and path are equal, "repo manifest" doesn't return a path at all, so fall back to name
- path = project.get('path', name)
- revision = project.get('upstream')
- if revision is None:
- for remote in remotes:
- if remote.get('name') == project.get('remote'):
- revision = remote.get('revision')
- if revision is None:
- revision = default_revision
-
- if not name in project_name_to_data:
- project_name_to_data[name] = {}
- revision = revision.split('refs/heads/')[-1]
- project_name_to_data[name][revision] = path
-
- # get data on requested changes
- reviews = []
- change_numbers = []
-
- def cmp_reviews(review_a, review_b):
- current_a = review_a['current_revision']
- parents_a = [r['commit'] for r in review_a['revisions'][current_a]['commit']['parents']]
- current_b = review_b['current_revision']
- parents_b = [r['commit'] for r in review_b['revisions'][current_b]['commit']['parents']]
- if current_a in parents_b:
- return -1
- elif current_b in parents_a:
- return 1
- else:
- return cmp(review_a['number'], review_b['number'])
-
- if args.topic:
- reviews = fetch_query(args.gerrit, 'topic:{0}'.format(args.topic))
- change_numbers = [str(r['number']) for r in sorted(reviews, key=cmp_to_key(cmp_reviews))]
- if args.query:
- reviews = fetch_query(args.gerrit, args.query)
- change_numbers = [str(r['number']) for r in sorted(reviews, key=cmp_to_key(cmp_reviews))]
- if args.change_number:
- change_url_re = re.compile('https?://.+?/([0-9]+(?:/[0-9]+)?)/?')
- for c in args.change_number:
- change_number = change_url_re.findall(c)
- if change_number:
- change_numbers.extend(change_number)
- elif '-' in c:
- templist = c.split('-')
- for i in range(int(templist[0]), int(templist[1]) + 1):
- change_numbers.append(str(i))
- else:
- change_numbers.append(c)
- reviews = fetch_query(args.gerrit, ' OR '.join('change:{0}'.format(x.split('/')[0]) for x in change_numbers))
-
- # make list of things to actually merge
- mergables = []
-
- # If --exclude is given, create the list of commits to ignore
- exclude = []
- if args.exclude:
- exclude = args.exclude[0].split(',')
-
- for change in change_numbers:
- patchset = None
- if '/' in change:
- (change, patchset) = change.split('/')
-
- if change in exclude:
- continue
-
- change = int(change)
-
- if patchset:
- patchset = int(patchset)
-
- review = next((x for x in reviews if x['number'] == change), None)
- if review is None:
- print('Change %d not found, skipping' % change)
- continue
-
- mergables.append({
- 'subject': review['subject'],
- 'project': review['project'],
- 'branch': review['branch'],
- 'change_id': review['change_id'],
- 'change_number': review['number'],
- 'status': review['status'],
- 'fetch': None,
- 'patchset': review['revisions'][review['current_revision']]['_number'],
- })
-
- mergables[-1]['fetch'] = review['revisions'][review['current_revision']]['fetch']
- mergables[-1]['id'] = change
- if patchset:
- try:
- mergables[-1]['fetch'] = [review['revisions'][x]['fetch'] for x in review['revisions'] if review['revisions'][x]['_number'] == patchset][0]
- mergables[-1]['id'] = '{0}/{1}'.format(change, patchset)
- mergables[-1]['patchset'] = patchset
- except (IndexError, ValueError):
- args.quiet or print('ERROR: The patch set {0}/{1} could not be found, using CURRENT_REVISION instead.'.format(change, patchset))
-
- for item in mergables:
- args.quiet or print('Applying change number {0}...'.format(item['id']))
- # Check if change is open and exit if it's not, unless -f is specified
- if (item['status'] != 'OPEN' and item['status'] != 'NEW' and item['status'] != 'DRAFT') and not args.query:
- if args.force:
- print('!! Force-picking a closed change !!\n')
- else:
- print('Change status is ' + item['status'] + '. Skipping the cherry pick.\nUse -f to force this pick.')
- continue
-
- # Convert the project name to a project path
- # - check that the project path exists
- project_path = None
-
- if item['project'] in project_name_to_data and item['branch'] in project_name_to_data[item['project']]:
- project_path = project_name_to_data[item['project']][item['branch']]
- elif args.path:
- project_path = args.path
- elif args.ignore_missing:
- print('WARNING: Skipping {0} since there is no project directory for: {1}\n'.format(item['id'], item['project']))
- continue
- else:
- sys.stderr.write('ERROR: For {0}, could not determine the project path for project {1}\n'.format(item['id'], item['project']))
- sys.exit(1)
-
- # If --start-branch is given, create the branch (more than once per path is okay; repo ignores gracefully)
- if args.start_branch:
- subprocess.check_output(['repo', 'start', args.start_branch[0], project_path])
-
- # Determine the maximum commits to check already picked changes
- check_picked_count = args.check_picked
- branch_commits_count = int(subprocess.check_output(['git', 'rev-list', '--count', 'HEAD'], cwd=project_path))
- if branch_commits_count <= check_picked_count:
- check_picked_count = branch_commits_count - 1
-
- # Check if change is already picked to HEAD...HEAD~check_picked_count
- found_change = False
- for i in range(0, check_picked_count):
- if subprocess.call(['git', 'cat-file', '-e', 'HEAD~{0}'.format(i)], cwd=project_path, stderr=open(os.devnull, 'wb')):
- continue
- output = subprocess.check_output(['git', 'show', '-q', 'HEAD~{0}'.format(i)], cwd=project_path).split()
- if 'Change-Id:' in output:
- head_change_id = ''
- for j,t in enumerate(reversed(output)):
- if t == 'Change-Id:':
- head_change_id = output[len(output) - j]
- break
- if head_change_id.strip() == item['change_id']:
- print('Skipping {0} - already picked in {1} as HEAD~{2}'.format(item['id'], project_path, i))
- found_change = True
- break
- if found_change:
- continue
-
- # Print out some useful info
- if not args.quiet:
- print('--> Subject: "{0}"'.format(item['subject'].encode('utf-8')))
- print('--> Project path: {0}'.format(project_path))
- print('--> Change number: {0} (Patch Set {1})'.format(item['id'], item['patchset']))
-
- if 'anonymous http' in item['fetch']:
- method = 'anonymous http'
- else:
- method = 'ssh'
-
- # Try fetching from GitHub first if using default gerrit
- if args.gerrit == default_gerrit:
- if args.verbose:
- print('Trying to fetch the change from GitHub')
-
- if args.pull:
- cmd = ['git pull --no-edit github', item['fetch'][method]['ref']]
- else:
- cmd = ['git fetch github', item['fetch'][method]['ref']]
- if args.quiet:
- cmd.append('--quiet')
- else:
- print(cmd)
- result = subprocess.call([' '.join(cmd)], cwd=project_path, shell=True)
- FETCH_HEAD = '{0}/.git/FETCH_HEAD'.format(project_path)
- if result != 0 and os.stat(FETCH_HEAD).st_size != 0:
- print('ERROR: git command failed')
- sys.exit(result)
- # Check if it worked
- if args.gerrit != default_gerrit or os.stat(FETCH_HEAD).st_size == 0:
- # If not using the default gerrit or github failed, fetch from gerrit.
- if args.verbose:
- if args.gerrit == default_gerrit:
- print('Fetching from GitHub didn\'t work, trying to fetch the change from Gerrit')
- else:
- print('Fetching from {0}'.format(args.gerrit))
-
- if args.pull:
- cmd = ['git pull --no-edit', item['fetch'][method]['url'], item['fetch'][method]['ref']]
- else:
- cmd = ['git fetch', item['fetch'][method]['url'], item['fetch'][method]['ref']]
- if args.quiet:
- cmd.append('--quiet')
- else:
- print(cmd)
- result = subprocess.call([' '.join(cmd)], cwd=project_path, shell=True)
- if result != 0:
- print('ERROR: git command failed')
- sys.exit(result)
- # Perform the cherry-pick
- if not args.pull:
- cmd = ['git cherry-pick --ff FETCH_HEAD']
- if args.quiet:
- cmd_out = open(os.devnull, 'wb')
- else:
- cmd_out = None
- result = subprocess.call(cmd, cwd=project_path, shell=True, stdout=cmd_out, stderr=cmd_out)
- if result != 0:
- cmd = ['git diff-index --quiet HEAD --']
- result = subprocess.call(cmd, cwd=project_path, shell=True, stdout=cmd_out, stderr=cmd_out)
- if result == 0:
- print('WARNING: git command resulted with an empty commit, aborting cherry-pick')
- cmd = ['git cherry-pick --abort']
- subprocess.call(cmd, cwd=project_path, shell=True, stdout=cmd_out, stderr=cmd_out)
- elif args.reset:
- print('ERROR: git command failed, aborting cherry-pick')
- cmd = ['git cherry-pick --abort']
- subprocess.call(cmd, cwd=project_path, shell=True, stdout=cmd_out, stderr=cmd_out)
- sys.exit(result)
- else:
- print('ERROR: git command failed')
- sys.exit(result)
- if not args.quiet:
- print('')