Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # |
| 3 | # Copyright (C) 2013-15 The CyanogenMod Project |
Dan Pasanen | 0fdc085 | 2016-12-27 10:32:16 -0600 | [diff] [blame] | 4 | # (C) 2017 The LineageOS Project |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 5 | # |
| 6 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 7 | # you may not use this file except in compliance with the License. |
| 8 | # You may obtain a copy of the License at |
| 9 | # |
| 10 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | # |
| 12 | # Unless required by applicable law or agreed to in writing, software |
| 13 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 15 | # See the License for the specific language governing permissions and |
| 16 | # limitations under the License. |
| 17 | # |
| 18 | |
| 19 | # |
| 20 | # Run repopick.py -h for a description of this utility. |
| 21 | # |
| 22 | |
| 23 | from __future__ import print_function |
| 24 | |
| 25 | import sys |
| 26 | import json |
| 27 | import os |
| 28 | import subprocess |
| 29 | import re |
| 30 | import argparse |
| 31 | import textwrap |
Gabriele M | 5b610ae | 2018-03-31 14:26:59 +0200 | [diff] [blame] | 32 | from functools import cmp_to_key |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 33 | from xml.etree import ElementTree |
| 34 | |
| 35 | try: |
Dan Pasanen | 1cdd380 | 2017-01-23 15:08:52 -0600 | [diff] [blame] | 36 | import requests |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 37 | except ImportError: |
Dan Pasanen | 1cdd380 | 2017-01-23 15:08:52 -0600 | [diff] [blame] | 38 | try: |
| 39 | # For python3 |
| 40 | import urllib.error |
| 41 | import urllib.request |
| 42 | except ImportError: |
| 43 | # For python2 |
| 44 | import imp |
| 45 | import urllib2 |
| 46 | urllib = imp.new_module('urllib') |
| 47 | urllib.error = urllib2 |
| 48 | urllib.request = urllib2 |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 49 | |
| 50 | |
Luca Weiss | 5ee35ea | 2018-11-25 14:07:12 +0100 | [diff] [blame] | 51 | # cmp() is not available in Python 3, define it manually |
| 52 | # See https://docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons |
| 53 | def cmp(a, b): |
| 54 | return (a > b) - (a < b) |
| 55 | |
| 56 | |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 57 | # Verifies whether pathA is a subdirectory (or the same) as pathB |
| 58 | def is_subdir(a, b): |
| 59 | a = os.path.realpath(a) + '/' |
| 60 | b = os.path.realpath(b) + '/' |
| 61 | return b == a[:len(b)] |
| 62 | |
| 63 | |
| 64 | def fetch_query_via_ssh(remote_url, query): |
| 65 | """Given a remote_url and a query, return the list of changes that fit it |
| 66 | This function is slightly messy - the ssh api does not return data in the same structure as the HTTP REST API |
| 67 | We have to get the data, then transform it to match what we're expecting from the HTTP RESET API""" |
| 68 | if remote_url.count(':') == 2: |
| 69 | (uri, userhost, port) = remote_url.split(':') |
| 70 | userhost = userhost[2:] |
| 71 | elif remote_url.count(':') == 1: |
| 72 | (uri, userhost) = remote_url.split(':') |
| 73 | userhost = userhost[2:] |
| 74 | port = 29418 |
| 75 | else: |
| 76 | raise Exception('Malformed URI: Expecting ssh://[user@]host[:port]') |
| 77 | |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 78 | out = subprocess.check_output(['ssh', '-x', '-p{0}'.format(port), userhost, 'gerrit', 'query', '--format=JSON --patch-sets --current-patch-set', query]) |
| 79 | if not hasattr(out, 'encode'): |
| 80 | out = out.decode() |
| 81 | reviews = [] |
| 82 | for line in out.split('\n'): |
| 83 | try: |
| 84 | data = json.loads(line) |
| 85 | # make our data look like the http rest api data |
| 86 | review = { |
| 87 | 'branch': data['branch'], |
| 88 | 'change_id': data['id'], |
| 89 | 'current_revision': data['currentPatchSet']['revision'], |
| 90 | 'number': int(data['number']), |
| 91 | 'revisions': {patch_set['revision']: { |
Gabriele M | 0fcc122 | 2018-04-10 18:35:12 +0200 | [diff] [blame] | 92 | '_number': int(patch_set['number']), |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 93 | 'fetch': { |
| 94 | 'ssh': { |
| 95 | 'ref': patch_set['ref'], |
| 96 | 'url': 'ssh://{0}:{1}/{2}'.format(userhost, port, data['project']) |
| 97 | } |
Gabriele M | 0fcc122 | 2018-04-10 18:35:12 +0200 | [diff] [blame] | 98 | }, |
| 99 | 'commit': { |
Aayush Gupta | d395aa2 | 2020-07-26 07:19:19 +0000 | [diff] [blame] | 100 | 'parents': [{'commit': parent} for parent in patch_set['parents']] |
Gabriele M | 0fcc122 | 2018-04-10 18:35:12 +0200 | [diff] [blame] | 101 | }, |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 102 | } for patch_set in data['patchSets']}, |
| 103 | 'subject': data['subject'], |
| 104 | 'project': data['project'], |
| 105 | 'status': data['status'] |
| 106 | } |
| 107 | reviews.append(review) |
| 108 | except: |
| 109 | pass |
| 110 | args.quiet or print('Found {0} reviews'.format(len(reviews))) |
| 111 | return reviews |
| 112 | |
| 113 | |
| 114 | def fetch_query_via_http(remote_url, query): |
Dan Pasanen | 1cdd380 | 2017-01-23 15:08:52 -0600 | [diff] [blame] | 115 | if "requests" in sys.modules: |
| 116 | auth = None |
| 117 | if os.path.isfile(os.getenv("HOME") + "/.gerritrc"): |
| 118 | f = open(os.getenv("HOME") + "/.gerritrc", "r") |
| 119 | for line in f: |
| 120 | parts = line.rstrip().split("|") |
| 121 | if parts[0] in remote_url: |
| 122 | auth = requests.auth.HTTPBasicAuth(username=parts[1], password=parts[2]) |
Aayush Gupta | d395aa2 | 2020-07-26 07:19:19 +0000 | [diff] [blame] | 123 | status_code = '-1' |
Dan Pasanen | 1cdd380 | 2017-01-23 15:08:52 -0600 | [diff] [blame] | 124 | if auth: |
Gabriele M | 5b610ae | 2018-03-31 14:26:59 +0200 | [diff] [blame] | 125 | url = '{0}/a/changes/?q={1}&o=CURRENT_REVISION&o=ALL_REVISIONS&o=ALL_COMMITS'.format(remote_url, query) |
Dan Pasanen | 1cdd380 | 2017-01-23 15:08:52 -0600 | [diff] [blame] | 126 | data = requests.get(url, auth=auth) |
Aayush Gupta | d395aa2 | 2020-07-26 07:19:19 +0000 | [diff] [blame] | 127 | status_code = str(data.status_code) |
| 128 | if status_code != '200': |
Dan Pasanen | 1cdd380 | 2017-01-23 15:08:52 -0600 | [diff] [blame] | 129 | #They didn't get good authorization or data, Let's try the old way |
Gabriele M | 5b610ae | 2018-03-31 14:26:59 +0200 | [diff] [blame] | 130 | url = '{0}/changes/?q={1}&o=CURRENT_REVISION&o=ALL_REVISIONS&o=ALL_COMMITS'.format(remote_url, query) |
Dan Pasanen | 1cdd380 | 2017-01-23 15:08:52 -0600 | [diff] [blame] | 131 | data = requests.get(url) |
| 132 | reviews = json.loads(data.text[5:]) |
| 133 | else: |
| 134 | """Given a query, fetch the change numbers via http""" |
Gabriele M | 5b610ae | 2018-03-31 14:26:59 +0200 | [diff] [blame] | 135 | url = '{0}/changes/?q={1}&o=CURRENT_REVISION&o=ALL_REVISIONS&o=ALL_COMMITS'.format(remote_url, query) |
Dan Pasanen | 1cdd380 | 2017-01-23 15:08:52 -0600 | [diff] [blame] | 136 | data = urllib.request.urlopen(url).read().decode('utf-8') |
| 137 | reviews = json.loads(data[5:]) |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 138 | |
| 139 | for review in reviews: |
| 140 | review['number'] = review.pop('_number') |
| 141 | |
| 142 | return reviews |
| 143 | |
| 144 | |
| 145 | def fetch_query(remote_url, query): |
| 146 | """Wrapper for fetch_query_via_proto functions""" |
| 147 | if remote_url[0:3] == 'ssh': |
| 148 | return fetch_query_via_ssh(remote_url, query) |
| 149 | elif remote_url[0:4] == 'http': |
| 150 | return fetch_query_via_http(remote_url, query.replace(' ', '+')) |
| 151 | else: |
| 152 | raise Exception('Gerrit URL should be in the form http[s]://hostname/ or ssh://[user@]host[:port]') |
| 153 | |
Aayush Gupta | d395aa2 | 2020-07-26 07:19:19 +0000 | [diff] [blame] | 154 | |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 155 | if __name__ == '__main__': |
Jackeagle | d6811aa | 2019-09-24 08:26:40 +0200 | [diff] [blame] | 156 | # Default to BlissRoms Gerrit |
Jackeagle | a324932 | 2021-07-15 05:43:47 +0200 | [diff] [blame] | 157 | default_gerrit = 'https://review.blissroms.org' |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 158 | |
| 159 | parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description=textwrap.dedent('''\ |
| 160 | repopick.py is a utility to simplify the process of cherry picking |
Jackeagle | a324932 | 2021-07-15 05:43:47 +0200 | [diff] [blame] | 161 | patches from BlissRoms Gerrit instance (or any gerrit instance of your choosing) |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 162 | |
| 163 | Given a list of change numbers, repopick will cd into the project path |
| 164 | and cherry pick the latest patch available. |
| 165 | |
| 166 | With the --start-branch argument, the user can specify that a branch |
| 167 | should be created before cherry picking. This is useful for |
| 168 | cherry-picking many patches into a common branch which can be easily |
| 169 | abandoned later (good for testing other's changes.) |
| 170 | |
| 171 | The --abandon-first argument, when used in conjunction with the |
| 172 | --start-branch option, will cause repopick to abandon the specified |
| 173 | branch in all repos first before performing any cherry picks.''')) |
Aayush Gupta | 5fd8e2b | 2020-07-26 06:47:01 +0000 | [diff] [blame] | 174 | parser.add_argument('change_number', nargs='*', |
| 175 | help='change number to cherry pick. Use {change number}/{patchset number} to get a specific revision.') |
| 176 | parser.add_argument('-i', '--ignore-missing', action='store_true', |
| 177 | help='do not error out if a patch applies to a missing directory') |
| 178 | parser.add_argument('-s', '--start-branch', nargs=1, |
| 179 | metavar='', help='start the specified branch before cherry picking') |
| 180 | parser.add_argument('-r', '--reset', action='store_true', |
| 181 | help='reset to initial state (abort cherry-pick) if there is a conflict') |
| 182 | parser.add_argument('-a', '--abandon-first', action='store_true', |
| 183 | help='before cherry picking, abandon the branch specified in --start-branch') |
| 184 | parser.add_argument('-b', '--auto-branch', action='store_true', |
| 185 | help='shortcut to "--start-branch auto --abandon-first --ignore-missing"') |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 186 | parser.add_argument('-q', '--quiet', action='store_true', help='print as little as possible') |
| 187 | parser.add_argument('-v', '--verbose', action='store_true', help='print extra information to aid in debug') |
| 188 | parser.add_argument('-f', '--force', action='store_true', help='force cherry pick even if change is closed') |
| 189 | parser.add_argument('-p', '--pull', action='store_true', help='execute pull instead of cherry-pick') |
Aayush Gupta | 5fd8e2b | 2020-07-26 06:47:01 +0000 | [diff] [blame] | 190 | parser.add_argument('-P', '--path', metavar='', help='use the specified path for the change') |
| 191 | parser.add_argument('-t', '--topic', metavar='', help='pick all commits from a specified topic') |
| 192 | parser.add_argument('-Q', '--query', metavar='', help='pick all commits using the specified query') |
| 193 | parser.add_argument('-g', '--gerrit', default=default_gerrit, |
| 194 | metavar='', help='Gerrit Instance to use. Form proto://[user@]host[:port]') |
| 195 | parser.add_argument('-e', '--exclude', nargs=1, |
| 196 | metavar='', help='exclude a list of commit numbers separated by a ,') |
| 197 | parser.add_argument('-c', '--check-picked', type=int, default=10, |
| 198 | metavar='', help='pass the amount of commits to check for already picked changes') |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 199 | args = parser.parse_args() |
| 200 | if not args.start_branch and args.abandon_first: |
| 201 | parser.error('if --abandon-first is set, you must also give the branch name with --start-branch') |
| 202 | if args.auto_branch: |
| 203 | args.abandon_first = True |
| 204 | args.ignore_missing = True |
| 205 | if not args.start_branch: |
| 206 | args.start_branch = ['auto'] |
| 207 | if args.quiet and args.verbose: |
| 208 | parser.error('--quiet and --verbose cannot be specified together') |
| 209 | |
| 210 | if (1 << bool(args.change_number) << bool(args.topic) << bool(args.query)) != 2: |
| 211 | parser.error('One (and only one) of change_number, topic, and query are allowed') |
| 212 | |
| 213 | # Change current directory to the top of the tree |
| 214 | if 'ANDROID_BUILD_TOP' in os.environ: |
| 215 | top = os.environ['ANDROID_BUILD_TOP'] |
| 216 | |
| 217 | if not is_subdir(os.getcwd(), top): |
| 218 | sys.stderr.write('ERROR: You must run this tool from within $ANDROID_BUILD_TOP!\n') |
| 219 | sys.exit(1) |
| 220 | os.chdir(os.environ['ANDROID_BUILD_TOP']) |
| 221 | |
| 222 | # Sanity check that we are being run from the top level of the tree |
| 223 | if not os.path.isdir('.repo'): |
| 224 | sys.stderr.write('ERROR: No .repo directory found. Please run this from the top of your tree.\n') |
| 225 | sys.exit(1) |
| 226 | |
| 227 | # If --abandon-first is given, abandon the branch before starting |
| 228 | if args.abandon_first: |
| 229 | # Determine if the branch already exists; skip the abandon if it does not |
| 230 | plist = subprocess.check_output(['repo', 'info']) |
| 231 | if not hasattr(plist, 'encode'): |
| 232 | plist = plist.decode() |
| 233 | needs_abandon = False |
| 234 | for pline in plist.splitlines(): |
| 235 | matchObj = re.match(r'Local Branches.*\[(.*)\]', pline) |
| 236 | if matchObj: |
| 237 | local_branches = re.split('\s*,\s*', matchObj.group(1)) |
| 238 | if any(args.start_branch[0] in s for s in local_branches): |
| 239 | needs_abandon = True |
| 240 | |
| 241 | if needs_abandon: |
| 242 | # Perform the abandon only if the branch already exists |
| 243 | if not args.quiet: |
| 244 | print('Abandoning branch: %s' % args.start_branch[0]) |
| 245 | subprocess.check_output(['repo', 'abandon', args.start_branch[0]]) |
| 246 | if not args.quiet: |
| 247 | print('') |
| 248 | |
| 249 | # Get the master manifest from repo |
| 250 | # - convert project name and revision to a path |
| 251 | project_name_to_data = {} |
| 252 | manifest = subprocess.check_output(['repo', 'manifest']) |
| 253 | xml_root = ElementTree.fromstring(manifest) |
| 254 | projects = xml_root.findall('project') |
| 255 | remotes = xml_root.findall('remote') |
| 256 | default_revision = xml_root.findall('default')[0].get('revision') |
| 257 | |
Aayush Gupta | d395aa2 | 2020-07-26 07:19:19 +0000 | [diff] [blame] | 258 | # dump project data into the a list of dicts with the following data: |
| 259 | # {project: {path, revision}} |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 260 | |
| 261 | for project in projects: |
| 262 | name = project.get('name') |
Aaron Kling | d97bfec | 2020-06-19 18:43:16 -0500 | [diff] [blame] | 263 | # when name and path are equal, "repo manifest" doesn't return a path at all, so fall back to name |
| 264 | path = project.get('path', name) |
Aaron Kling | 7d6601c | 2020-06-30 14:22:55 -0500 | [diff] [blame] | 265 | revision = project.get('upstream') |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 266 | if revision is None: |
| 267 | for remote in remotes: |
| 268 | if remote.get('name') == project.get('remote'): |
| 269 | revision = remote.get('revision') |
| 270 | if revision is None: |
LuK1337 | 9098b94 | 2020-11-23 10:55:24 +0100 | [diff] [blame] | 271 | revision = project.get('revision', default_revision) |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 272 | |
Aayush Gupta | d395aa2 | 2020-07-26 07:19:19 +0000 | [diff] [blame] | 273 | if name not in project_name_to_data: |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 274 | project_name_to_data[name] = {} |
| 275 | revision = revision.split('refs/heads/')[-1] |
| 276 | project_name_to_data[name][revision] = path |
| 277 | |
| 278 | # get data on requested changes |
| 279 | reviews = [] |
| 280 | change_numbers = [] |
Gabriele M | 5b610ae | 2018-03-31 14:26:59 +0200 | [diff] [blame] | 281 | |
| 282 | def cmp_reviews(review_a, review_b): |
| 283 | current_a = review_a['current_revision'] |
| 284 | parents_a = [r['commit'] for r in review_a['revisions'][current_a]['commit']['parents']] |
| 285 | current_b = review_b['current_revision'] |
| 286 | parents_b = [r['commit'] for r in review_b['revisions'][current_b]['commit']['parents']] |
| 287 | if current_a in parents_b: |
| 288 | return -1 |
| 289 | elif current_b in parents_a: |
| 290 | return 1 |
| 291 | else: |
| 292 | return cmp(review_a['number'], review_b['number']) |
| 293 | |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 294 | if args.topic: |
| 295 | reviews = fetch_query(args.gerrit, 'topic:{0}'.format(args.topic)) |
Gabriele M | 5b610ae | 2018-03-31 14:26:59 +0200 | [diff] [blame] | 296 | change_numbers = [str(r['number']) for r in sorted(reviews, key=cmp_to_key(cmp_reviews))] |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 297 | if args.query: |
| 298 | reviews = fetch_query(args.gerrit, args.query) |
Gabriele M | 5b610ae | 2018-03-31 14:26:59 +0200 | [diff] [blame] | 299 | change_numbers = [str(r['number']) for r in sorted(reviews, key=cmp_to_key(cmp_reviews))] |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 300 | if args.change_number: |
Gabriele M | af970b6 | 2018-04-01 17:50:58 +0200 | [diff] [blame] | 301 | change_url_re = re.compile('https?://.+?/([0-9]+(?:/[0-9]+)?)/?') |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 302 | for c in args.change_number: |
Gabriele M | af970b6 | 2018-04-01 17:50:58 +0200 | [diff] [blame] | 303 | change_number = change_url_re.findall(c) |
| 304 | if change_number: |
| 305 | change_numbers.extend(change_number) |
| 306 | elif '-' in c: |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 307 | templist = c.split('-') |
| 308 | for i in range(int(templist[0]), int(templist[1]) + 1): |
| 309 | change_numbers.append(str(i)) |
| 310 | else: |
| 311 | change_numbers.append(c) |
| 312 | reviews = fetch_query(args.gerrit, ' OR '.join('change:{0}'.format(x.split('/')[0]) for x in change_numbers)) |
| 313 | |
| 314 | # make list of things to actually merge |
| 315 | mergables = [] |
| 316 | |
| 317 | # If --exclude is given, create the list of commits to ignore |
| 318 | exclude = [] |
| 319 | if args.exclude: |
| 320 | exclude = args.exclude[0].split(',') |
| 321 | |
| 322 | for change in change_numbers: |
| 323 | patchset = None |
| 324 | if '/' in change: |
| 325 | (change, patchset) = change.split('/') |
| 326 | |
| 327 | if change in exclude: |
| 328 | continue |
| 329 | |
| 330 | change = int(change) |
LuK1337 | ad5d9a0 | 2017-03-24 23:25:13 +0100 | [diff] [blame] | 331 | |
Gabriele M | de9e0ae | 2018-04-01 17:50:55 +0200 | [diff] [blame] | 332 | if patchset: |
LuK1337 | ad5d9a0 | 2017-03-24 23:25:13 +0100 | [diff] [blame] | 333 | patchset = int(patchset) |
| 334 | |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 335 | review = next((x for x in reviews if x['number'] == change), None) |
| 336 | if review is None: |
| 337 | print('Change %d not found, skipping' % change) |
| 338 | continue |
| 339 | |
| 340 | mergables.append({ |
| 341 | 'subject': review['subject'], |
| 342 | 'project': review['project'], |
| 343 | 'branch': review['branch'], |
| 344 | 'change_id': review['change_id'], |
| 345 | 'change_number': review['number'], |
| 346 | 'status': review['status'], |
Gabriele M | 88c0e5d | 2018-04-01 17:50:57 +0200 | [diff] [blame] | 347 | 'fetch': None, |
| 348 | 'patchset': review['revisions'][review['current_revision']]['_number'], |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 349 | }) |
Gabriele M | 88c0e5d | 2018-04-01 17:50:57 +0200 | [diff] [blame] | 350 | |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 351 | mergables[-1]['fetch'] = review['revisions'][review['current_revision']]['fetch'] |
| 352 | mergables[-1]['id'] = change |
| 353 | if patchset: |
| 354 | try: |
LuK1337 | 2756418 | 2017-03-24 20:00:48 +0100 | [diff] [blame] | 355 | mergables[-1]['fetch'] = [review['revisions'][x]['fetch'] for x in review['revisions'] if review['revisions'][x]['_number'] == patchset][0] |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 356 | mergables[-1]['id'] = '{0}/{1}'.format(change, patchset) |
Gabriele M | 88c0e5d | 2018-04-01 17:50:57 +0200 | [diff] [blame] | 357 | mergables[-1]['patchset'] = patchset |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 358 | except (IndexError, ValueError): |
| 359 | args.quiet or print('ERROR: The patch set {0}/{1} could not be found, using CURRENT_REVISION instead.'.format(change, patchset)) |
| 360 | |
| 361 | for item in mergables: |
| 362 | args.quiet or print('Applying change number {0}...'.format(item['id'])) |
| 363 | # Check if change is open and exit if it's not, unless -f is specified |
Dan Pasanen | 63f767e | 2017-07-09 09:41:33 -0500 | [diff] [blame] | 364 | if (item['status'] != 'OPEN' and item['status'] != 'NEW' and item['status'] != 'DRAFT') and not args.query: |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 365 | if args.force: |
| 366 | print('!! Force-picking a closed change !!\n') |
| 367 | else: |
| 368 | print('Change status is ' + item['status'] + '. Skipping the cherry pick.\nUse -f to force this pick.') |
| 369 | continue |
| 370 | |
| 371 | # Convert the project name to a project path |
| 372 | # - check that the project path exists |
| 373 | project_path = None |
| 374 | |
| 375 | if item['project'] in project_name_to_data and item['branch'] in project_name_to_data[item['project']]: |
| 376 | project_path = project_name_to_data[item['project']][item['branch']] |
| 377 | elif args.path: |
| 378 | project_path = args.path |
Adrian DC | 045f664 | 2019-10-13 12:34:06 +0200 | [diff] [blame] | 379 | elif item['project'] in project_name_to_data and len(project_name_to_data[item['project']]) == 1: |
| 380 | local_branch = list(project_name_to_data[item['project']])[0] |
| 381 | project_path = project_name_to_data[item['project']][local_branch] |
| 382 | print('WARNING: Project {0} has a different branch ("{1}" != "{2}")'.format(project_path, local_branch, item['branch'])) |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 383 | elif args.ignore_missing: |
| 384 | print('WARNING: Skipping {0} since there is no project directory for: {1}\n'.format(item['id'], item['project'])) |
| 385 | continue |
| 386 | else: |
| 387 | sys.stderr.write('ERROR: For {0}, could not determine the project path for project {1}\n'.format(item['id'], item['project'])) |
| 388 | sys.exit(1) |
| 389 | |
| 390 | # If --start-branch is given, create the branch (more than once per path is okay; repo ignores gracefully) |
| 391 | if args.start_branch: |
| 392 | subprocess.check_output(['repo', 'start', args.start_branch[0], project_path]) |
| 393 | |
| 394 | # Determine the maximum commits to check already picked changes |
Adrian DC | df29022 | 2017-09-07 22:59:54 +0200 | [diff] [blame] | 395 | check_picked_count = args.check_picked |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 396 | branch_commits_count = int(subprocess.check_output(['git', 'rev-list', '--count', 'HEAD'], cwd=project_path)) |
| 397 | if branch_commits_count <= check_picked_count: |
| 398 | check_picked_count = branch_commits_count - 1 |
| 399 | |
| 400 | # Check if change is already picked to HEAD...HEAD~check_picked_count |
| 401 | found_change = False |
| 402 | for i in range(0, check_picked_count): |
Adrian DC | 13b02ff | 2016-12-04 12:30:26 +0100 | [diff] [blame] | 403 | if subprocess.call(['git', 'cat-file', '-e', 'HEAD~{0}'.format(i)], cwd=project_path, stderr=open(os.devnull, 'wb')): |
| 404 | continue |
Simon Shields | d5e35c9 | 2019-11-18 23:56:08 +1100 | [diff] [blame] | 405 | output = subprocess.check_output(['git', 'show', '-q', 'HEAD~{0}'.format(i)], cwd=project_path) |
| 406 | # make sure we have a string on Python 3 |
| 407 | if isinstance(output, bytes): |
| 408 | output = output.decode('utf-8') |
| 409 | output = output.split() |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 410 | if 'Change-Id:' in output: |
| 411 | head_change_id = '' |
Aayush Gupta | d395aa2 | 2020-07-26 07:19:19 +0000 | [diff] [blame] | 412 | for j, t in enumerate(reversed(output)): |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 413 | if t == 'Change-Id:': |
| 414 | head_change_id = output[len(output) - j] |
| 415 | break |
| 416 | if head_change_id.strip() == item['change_id']: |
| 417 | print('Skipping {0} - already picked in {1} as HEAD~{2}'.format(item['id'], project_path, i)) |
| 418 | found_change = True |
| 419 | break |
| 420 | if found_change: |
| 421 | continue |
| 422 | |
| 423 | # Print out some useful info |
| 424 | if not args.quiet: |
LuK1337 | c62a9fb | 2019-09-21 11:47:33 +0200 | [diff] [blame] | 425 | print(u'--> Subject: "{0}"'.format(item['subject'])) |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 426 | print('--> Project path: {0}'.format(project_path)) |
Gabriele M | 88c0e5d | 2018-04-01 17:50:57 +0200 | [diff] [blame] | 427 | print('--> Change number: {0} (Patch Set {1})'.format(item['id'], item['patchset'])) |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 428 | |
| 429 | if 'anonymous http' in item['fetch']: |
| 430 | method = 'anonymous http' |
| 431 | else: |
| 432 | method = 'ssh' |
| 433 | |
| 434 | # Try fetching from GitHub first if using default gerrit |
| 435 | if args.gerrit == default_gerrit: |
| 436 | if args.verbose: |
| 437 | print('Trying to fetch the change from GitHub') |
| 438 | |
| 439 | if args.pull: |
Jackeagle | a324932 | 2021-07-15 05:43:47 +0200 | [diff] [blame] | 440 | cmd = ['git pull --no-edit BlissRoms', item['fetch'][method]['ref']] |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 441 | else: |
Jackeagle | a324932 | 2021-07-15 05:43:47 +0200 | [diff] [blame] | 442 | cmd = ['git fetch BlissRoms', item['fetch'][method]['ref']] |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 443 | if args.quiet: |
| 444 | cmd.append('--quiet') |
| 445 | else: |
| 446 | print(cmd) |
| 447 | result = subprocess.call([' '.join(cmd)], cwd=project_path, shell=True) |
| 448 | FETCH_HEAD = '{0}/.git/FETCH_HEAD'.format(project_path) |
| 449 | if result != 0 and os.stat(FETCH_HEAD).st_size != 0: |
| 450 | print('ERROR: git command failed') |
| 451 | sys.exit(result) |
| 452 | # Check if it worked |
| 453 | if args.gerrit != default_gerrit or os.stat(FETCH_HEAD).st_size == 0: |
| 454 | # If not using the default gerrit or github failed, fetch from gerrit. |
| 455 | if args.verbose: |
| 456 | if args.gerrit == default_gerrit: |
Jackeagle | a324932 | 2021-07-15 05:43:47 +0200 | [diff] [blame] | 457 | print('Fetching from BlissRoms didn\'t work, trying to fetch the change from Gerrit') |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 458 | else: |
| 459 | print('Fetching from {0}'.format(args.gerrit)) |
| 460 | |
| 461 | if args.pull: |
| 462 | cmd = ['git pull --no-edit', item['fetch'][method]['url'], item['fetch'][method]['ref']] |
| 463 | else: |
| 464 | cmd = ['git fetch', item['fetch'][method]['url'], item['fetch'][method]['ref']] |
| 465 | if args.quiet: |
| 466 | cmd.append('--quiet') |
| 467 | else: |
| 468 | print(cmd) |
| 469 | result = subprocess.call([' '.join(cmd)], cwd=project_path, shell=True) |
| 470 | if result != 0: |
| 471 | print('ERROR: git command failed') |
| 472 | sys.exit(result) |
| 473 | # Perform the cherry-pick |
| 474 | if not args.pull: |
Tim Schumacher | 49d26ba | 2018-10-13 14:37:54 +0200 | [diff] [blame] | 475 | cmd = ['git cherry-pick --ff FETCH_HEAD'] |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 476 | if args.quiet: |
| 477 | cmd_out = open(os.devnull, 'wb') |
| 478 | else: |
| 479 | cmd_out = None |
| 480 | result = subprocess.call(cmd, cwd=project_path, shell=True, stdout=cmd_out, stderr=cmd_out) |
| 481 | if result != 0: |
Adrian DC | 0f8230b | 2018-08-30 23:07:23 +0200 | [diff] [blame] | 482 | cmd = ['git diff-index --quiet HEAD --'] |
| 483 | result = subprocess.call(cmd, cwd=project_path, shell=True, stdout=cmd_out, stderr=cmd_out) |
| 484 | if result == 0: |
| 485 | print('WARNING: git command resulted with an empty commit, aborting cherry-pick') |
| 486 | cmd = ['git cherry-pick --abort'] |
| 487 | subprocess.call(cmd, cwd=project_path, shell=True, stdout=cmd_out, stderr=cmd_out) |
| 488 | elif args.reset: |
Harry Youd | 1c9c5a3 | 2017-07-18 18:52:42 +0100 | [diff] [blame] | 489 | print('ERROR: git command failed, aborting cherry-pick') |
| 490 | cmd = ['git cherry-pick --abort'] |
| 491 | subprocess.call(cmd, cwd=project_path, shell=True, stdout=cmd_out, stderr=cmd_out) |
Adrian DC | 0f8230b | 2018-08-30 23:07:23 +0200 | [diff] [blame] | 492 | sys.exit(result) |
Harry Youd | 1c9c5a3 | 2017-07-18 18:52:42 +0100 | [diff] [blame] | 493 | else: |
| 494 | print('ERROR: git command failed') |
Adrian DC | 0f8230b | 2018-08-30 23:07:23 +0200 | [diff] [blame] | 495 | sys.exit(result) |
Michael Bestas | 3952f6c | 2016-08-26 01:12:08 +0300 | [diff] [blame] | 496 | if not args.quiet: |
| 497 | print('') |
Jackeagle | a324932 | 2021-07-15 05:43:47 +0200 | [diff] [blame] | 498 | |