OSDN Git Service

repopick: initial commit
authorChirayu Desai <cdesai@cyanogenmod.org>
Wed, 5 Jun 2013 14:44:33 +0000 (20:14 +0530)
committerSteve Kondik <steve@cyngn.com>
Sun, 4 Sep 2016 04:47:36 +0000 (21:47 -0700)
Change-Id: Ie42c11d335da07f6d164a0fcb887011e5fd6dcf4

repopick: add -b shortcut option

  * useful for people like me who want to pick into temp branch so
    that the picks survive a repo sync
  * shortcut to "--start-branch auto --abandon-first --ignore-missing"

Change-Id: Ia4d4309d27e46a15ff4b74525668d974f4251dcb

repopick: handle additional error cases around subprocesses

  * gracefully error if the project path cannot be found
  * poll() was not a reliable determination of EOF on my
    Darwin-Python 2.7 system.

Change-Id: I203c2a75820f8acc079a5c9751d1c04daf6f3a16

repopick: allow running from a subdir of ANDROID_BUILD_TOP

Change-Id: I6dfcd5dfe700174ed87dc8627b23519c62b4cb27

envsetup: hmm repopick

Change-Id: I483cb36721f31dbf1f67e46cbe8a306b2a9e2c15

repopick: decode the result as UTF-8

Change-Id: I51f2defa861c86b51baa7bad1df1d9666b952160

repopick: gracefully handle empty/non-JSON server responses

Change-Id: Idbabdbfb4a910de0ad405f02b2a84cf2bc9ef3dc

repopick: remove the superfluous information from date

  * '2011-11-17 06:54:52.000000000' -> '2011-11-17 06:54:52'

Change-Id: I73d37c9aba13d4be6b4d2d2fc0f2f83971a3e77b

repopick: let's be nice to our servers :)

* Prefer fetching changes from GitHub, and if that fails,
  silently fall-back to Gerrit.

Change-Id: Ibf68d4b69a7e8dbee2adb8f7f4250340b8be629c

repopick: skip a cherry pick if its already been merged

* override this behavior with the -f argument

Change-Id: I280b4945344e7483a192d1c9769c42b8fd2a2aef

RepoPick : Add support for git pull

Allow user to git pull patchsets and dependencies.

Change-Id: If5520b45fe79836eac628b3caf0526f11f8953d9
(cherry picked from commit df646304bdcef329e3fe7c12b68107de1f4cd42a)

repopick: allow specifying a topic to pick all commits from

Change-Id: I4fb60120794a77986bf641de063a8d41f4f45a23

repopick: support specifying a range of commits to pick

* for example: repopick 12345-12350

Change-Id: I21dd0ef5f19d7b3899fa40ec4c28a0e9dba8a4df

repopick: handle variant hal branching

Change-Id: Ib0dee19abc13a7fb8d8e42c09b73c1b4f35ca938

Allow repopick to cherry-pick a specific patch set

Use 'repopick 123456/9' where '123456' is the change number
and '9' is the desired patchset.

Change-Id: I2d9f6939fbde50b2a6057b75d2e7f722be5a3e21

repopick: Catch errors on url load failure

Exit gracefully if server cannot be reached or if it reports an error.

Change-Id: I86a1a45d3a1f8dfdb49a0400cb728c965dbad8df

repopick: Allow using a custom query

Change-Id: I87e92d3cbfa35367d87f55c92a6d12a6d4282232

repopick: Allow the github fetch to fail

* This is optional and done to save gerrit server
  bandwidth, however it may fail in cases where
  the 'github' remote is a mirror which doesn't
  sync the changes.
* Let it try fetching from gerrit if fetching from
  github fails.

Change-Id: I6d183ff83572d817d78633280d8b20e3efdaf8f0

Support custom gerrit servers via HTTP or SSH

Please test/provide feedback.

Change-Id: Id7043dc009c1fe5614f73a5da2aa021c0d784a4b
Ticket: RM-14

repopick: skip abandoned commits

Change-Id: I7f203fc4f790bf26acce1112c5c24c703acd9326

repopick: handle hardware_ril/ril-caf

Change-Id: Ib72255d899529effb0d6121034f42a7532d4a02b

Fix custom URL error. Merged from cm_build_config

Change-Id: Iffe7766f137889f113bc5a94254058dc4dbf0ea3
(cherry picked from commit 04d470c5efa7510cc86e614cb401a9b6a41947f0)

repopick: open changes are either NEW or OPEN, not just OPEN

Gerrit's API is terrible. I believe it was written by monkeys.

Change-Id: Idb39a357c241ce7923549a3d217f7ee632326e92

repopick: print the status of a skipped cherry pick

Change-Id: I5181910dc80fe4cf2d69cc22089e191b36a80c9e

repopick: Update script with some fixes

Make quiet actually be quiet.
Allow overriding the path for a repopick.
Sort query and topic results to have a better chance of them applying
cleanly.
Don't try to pull from github when using a custom gerrit address.
Catch git command failures and properly return failure codes.

Change-Id: I7ff010fbfbf1026c6fe03cb27649f677637e1bb5

repopick: support projects which get checked out multiple times

see: caf branches for multimedia, where different branches get checked
out under different paths.

Change removes hacks needed for -caf branches, as this data is taken
directly out of 'repo manifest'

Change-Id: Id7aa24709c3dc6bc874899ef605e58271fec082c

repopick: Actually try to fetch from Gerrit if fetching from GitHub fails

* Exit on failure only if fetching didn't fail.

Change-Id: Iae75dfcff590ed72afd93f58772440e4f3b0ca27

repopick: Allow commits to be excluded from a topic importation

 * Add the ability to 'repopick -t TOPIC -e 120586,120587'

Change-Id: Ib99c095174a0987f68cb8261b7af3f59272b05b5
Signed-off-by: AdrianDC <radian.dc@gmail.com>
py3: repopick

Change-Id: I65e6bb295bd475cc391579023940642b5747f521

py3: update all the things

Change-Id: I5e11b46b7c2f7f8760d6c0e713ca99c1e88b7cd3

repopick: Don't crash if change not found

If change is not located, skip it. Resolves the following:

Traceback (most recent call last):
  File "(omitted)/build/tools/repopick.py", line 258,
in <module>
    review = [x for x in reviews if x['number'] == change][0]
IndexError: list index out of range

Change-Id: I96423ddc809ac1c7994998db4e1929ca813f20ca

repopick: support specifying a range of commits to pick

* for example: repopick 12345-12350

Change-Id: I3b8f2c331a6ac5488032432e133bd4b44d0bf007

envsetup.sh
tools/repopick.py [new file with mode: 0755]

index 29d26fd..3818162 100644 (file)
@@ -29,6 +29,7 @@ Invoke ". build/envsetup.sh" from your shell to add the following functions to y
 - cmrebase:  Rebase a Gerrit change and push it again
 - mka:       Builds using SCHED_BATCH on all processors
 - reposync:  Parallel repo sync using ionice and SCHED_BATCH
+- repopick:  Utility to fetch changes from Gerrit.
 - installboot: Installs a boot.img to the connected device.
 - installrecovery: Installs a recovery.img to the connected device.
 - repodiff:  Diff 2 different branches or tags within the same repo
@@ -2155,6 +2156,11 @@ alias mmmp='dopush mmm'
 alias mkap='dopush mka'
 alias cmkap='dopush cmka'
 
+function repopick() {
+    T=$(gettop)
+    $T/build/tools/repopick.py $@
+}
+
 # Force JAVA_HOME to point to java 1.7/1.8 if it isn't already set.
 function set_java_home() {
     # Clear the existing JAVA_HOME value if we set it ourselves, so that
diff --git a/tools/repopick.py b/tools/repopick.py
new file mode 100755 (executable)
index 0000000..a53c043
--- /dev/null
@@ -0,0 +1,377 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2013-15 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
+from xml.etree import ElementTree
+
+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
+
+
+# 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'])
+                        }
+                    }
+                } 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):
+
+    """Given a query, fetch the change numbers via http"""
+    url = '{0}/changes/?q={1}&o=CURRENT_REVISION&o=ALL_REVISIONS'.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 CyanogenMod Gerrit
+    default_gerrit = 'http://review.cyanogenmod.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 CyanogenMod'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('-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 ,')
+    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')
+    default_revision = xml_root.findall('default')[0].get('revision').split('/')[-1]
+
+    #dump project data into the a list of dicts with the following data:
+    #{project: {path, revision}}
+
+    for project in projects:
+        name = project.get('name')
+        path = project.get('path')
+        revision = project.get('revision')
+        if revision is None:
+            revision = default_revision
+
+        if not name in project_name_to_data:
+            project_name_to_data[name] = {}
+        project_name_to_data[name][revision] = path
+
+    # get data on requested changes
+    reviews = []
+    change_numbers = []
+    if args.topic:
+        reviews = fetch_query(args.gerrit, 'topic:{0}'.format(args.topic))
+        change_numbers = sorted([str(r['number']) for r in reviews])
+    if args.query:
+        reviews = fetch_query(args.gerrit, args.query)
+        change_numbers = sorted([str(r['number']) for r in reviews])
+    if args.change_number:
+        for c in args.change_number:
+            if '-' 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)
+        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_number': review['number'],
+            'status': review['status'],
+            'fetch': None
+        })
+        mergables[-1]['fetch'] = review['revisions'][review['current_revision']]['fetch']
+        mergables[-1]['id'] = change
+        if patchset:
+            try:
+                mergables[-1]['fetch'] = [x['fetch'] for x in review['revisions'] if x['_number'] == patchset][0]
+                mergables[-1]['id'] = '{0}/{1}'.format(change, 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 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])
+
+        # Print out some useful info
+        if not args.quiet:
+            print('--> Subject:       "{0}"'.format(item['subject']))
+            print('--> Project path:  {0}'.format(project_path))
+            print('--> Change number: {0} (Patch Set {0})'.format(item['id']))
+
+        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 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:
+                print('ERROR: git command failed')
+                sys.exit(result)
+        if not args.quiet:
+            print('')