blob: 2178858df8762bd7f0e8584eb5f714bbfa542a8c [file] [log] [blame]
Michael Bestas3952f6c2016-08-26 01:12:08 +03001#!/usr/bin/env python
2#
3# Copyright (C) 2013-15 The CyanogenMod Project
Dan Pasanen0fdc0852016-12-27 10:32:16 -06004# (C) 2017 The LineageOS Project
Michael Bestas3952f6c2016-08-26 01:12:08 +03005#
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
23from __future__ import print_function
24
25import sys
26import json
27import os
28import subprocess
29import re
30import argparse
31import textwrap
Gabriele M5b610ae2018-03-31 14:26:59 +020032from functools import cmp_to_key
Michael Bestas3952f6c2016-08-26 01:12:08 +030033from xml.etree import ElementTree
34
35try:
Dan Pasanen1cdd3802017-01-23 15:08:52 -060036 import requests
Michael Bestas3952f6c2016-08-26 01:12:08 +030037except ImportError:
Dan Pasanen1cdd3802017-01-23 15:08:52 -060038 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 Bestas3952f6c2016-08-26 01:12:08 +030049
50
Luca Weiss5ee35ea2018-11-25 14:07:12 +010051# cmp() is not available in Python 3, define it manually
52# See https://docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons
53def cmp(a, b):
54 return (a > b) - (a < b)
55
56
Michael Bestas3952f6c2016-08-26 01:12:08 +030057# Verifies whether pathA is a subdirectory (or the same) as pathB
58def is_subdir(a, b):
59 a = os.path.realpath(a) + '/'
60 b = os.path.realpath(b) + '/'
61 return b == a[:len(b)]
62
63
64def 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 Bestas3952f6c2016-08-26 01:12:08 +030078 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 M0fcc1222018-04-10 18:35:12 +020092 '_number': int(patch_set['number']),
Michael Bestas3952f6c2016-08-26 01:12:08 +030093 'fetch': {
94 'ssh': {
95 'ref': patch_set['ref'],
96 'url': 'ssh://{0}:{1}/{2}'.format(userhost, port, data['project'])
97 }
Gabriele M0fcc1222018-04-10 18:35:12 +020098 },
99 'commit': {
Aayush Guptad0a36cd2020-07-26 07:19:19 +0000100 'parents': [{'commit': parent} for parent in patch_set['parents']]
Gabriele M0fcc1222018-04-10 18:35:12 +0200101 },
Michael Bestas3952f6c2016-08-26 01:12:08 +0300102 } 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
114def fetch_query_via_http(remote_url, query):
Dan Pasanen1cdd3802017-01-23 15:08:52 -0600115 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 Guptad0a36cd2020-07-26 07:19:19 +0000123 status_code = '-1'
Dan Pasanen1cdd3802017-01-23 15:08:52 -0600124 if auth:
Gabriele M5b610ae2018-03-31 14:26:59 +0200125 url = '{0}/a/changes/?q={1}&o=CURRENT_REVISION&o=ALL_REVISIONS&o=ALL_COMMITS'.format(remote_url, query)
Dan Pasanen1cdd3802017-01-23 15:08:52 -0600126 data = requests.get(url, auth=auth)
Aayush Guptad0a36cd2020-07-26 07:19:19 +0000127 status_code = str(data.status_code)
128 if status_code != '200':
Dan Pasanen1cdd3802017-01-23 15:08:52 -0600129 #They didn't get good authorization or data, Let's try the old way
Gabriele M5b610ae2018-03-31 14:26:59 +0200130 url = '{0}/changes/?q={1}&o=CURRENT_REVISION&o=ALL_REVISIONS&o=ALL_COMMITS'.format(remote_url, query)
Dan Pasanen1cdd3802017-01-23 15:08:52 -0600131 data = requests.get(url)
132 reviews = json.loads(data.text[5:])
133 else:
134 """Given a query, fetch the change numbers via http"""
Gabriele M5b610ae2018-03-31 14:26:59 +0200135 url = '{0}/changes/?q={1}&o=CURRENT_REVISION&o=ALL_REVISIONS&o=ALL_COMMITS'.format(remote_url, query)
Dan Pasanen1cdd3802017-01-23 15:08:52 -0600136 data = urllib.request.urlopen(url).read().decode('utf-8')
137 reviews = json.loads(data[5:])
Michael Bestas3952f6c2016-08-26 01:12:08 +0300138
139 for review in reviews:
140 review['number'] = review.pop('_number')
141
142 return reviews
143
144
145def 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 Guptad0a36cd2020-07-26 07:19:19 +0000154
Michael Bestas3952f6c2016-08-26 01:12:08 +0300155if __name__ == '__main__':
Dan Pasanen0fdc0852016-12-27 10:32:16 -0600156 # Default to LineageOS Gerrit
rohanfa44f372020-03-14 23:40:54 -0400157 default_gerrit = 'https://review.blissroms.com'
Michael Bestas3952f6c2016-08-26 01:12:08 +0300158
159 parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description=textwrap.dedent('''\
160 repopick.py is a utility to simplify the process of cherry picking
Dan Pasanen0fdc0852016-12-27 10:32:16 -0600161 patches from LineageOS's Gerrit instance (or any gerrit instance of your choosing)
Michael Bestas3952f6c2016-08-26 01:12:08 +0300162
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 Gupta1cb94762020-07-26 06:47:01 +0000174 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 Bestas3952f6c2016-08-26 01:12:08 +0300186 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 Gupta1cb94762020-07-26 06:47:01 +0000190 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 Bestas3952f6c2016-08-26 01:12:08 +0300199 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 Guptad0a36cd2020-07-26 07:19:19 +0000258 # dump project data into the a list of dicts with the following data:
259 # {project: {path, revision}}
Michael Bestas3952f6c2016-08-26 01:12:08 +0300260
261 for project in projects:
262 name = project.get('name')
Aaron Kling149d66a2020-06-19 18:43:16 -0500263 # 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 Kling1e16a0f2020-06-30 14:22:55 -0500265 revision = project.get('upstream')
Michael Bestas3952f6c2016-08-26 01:12:08 +0300266 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:
Simon Shields2bdb18f2016-09-26 17:52:03 +1000271 revision = default_revision
Michael Bestas3952f6c2016-08-26 01:12:08 +0300272
Aayush Guptad0a36cd2020-07-26 07:19:19 +0000273 if name not in project_name_to_data:
Michael Bestas3952f6c2016-08-26 01:12:08 +0300274 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 M5b610ae2018-03-31 14:26:59 +0200281
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 Bestas3952f6c2016-08-26 01:12:08 +0300294 if args.topic:
Giuseppe Maggiobcf8e892019-03-18 17:24:06 +0100295 for t in args.topic:
296 # Store current topic to process for change_numbers
297 topic = fetch_query(args.gerrit, 'topic:{0}'.format(t))
298 # Append topic to reviews, for later reference
299 reviews += topic
300 # Cycle through the current topic to get the change numbers
301 change_numbers += sorted([str(r['number']) for r in topic], key=int)
Michael Bestas3952f6c2016-08-26 01:12:08 +0300302 if args.query:
303 reviews = fetch_query(args.gerrit, args.query)
Gabriele M5b610ae2018-03-31 14:26:59 +0200304 change_numbers = [str(r['number']) for r in sorted(reviews, key=cmp_to_key(cmp_reviews))]
Michael Bestas3952f6c2016-08-26 01:12:08 +0300305 if args.change_number:
Gabriele Maf970b62018-04-01 17:50:58 +0200306 change_url_re = re.compile('https?://.+?/([0-9]+(?:/[0-9]+)?)/?')
Michael Bestas3952f6c2016-08-26 01:12:08 +0300307 for c in args.change_number:
Gabriele Maf970b62018-04-01 17:50:58 +0200308 change_number = change_url_re.findall(c)
309 if change_number:
310 change_numbers.extend(change_number)
311 elif '-' in c:
Michael Bestas3952f6c2016-08-26 01:12:08 +0300312 templist = c.split('-')
313 for i in range(int(templist[0]), int(templist[1]) + 1):
314 change_numbers.append(str(i))
315 else:
316 change_numbers.append(c)
317 reviews = fetch_query(args.gerrit, ' OR '.join('change:{0}'.format(x.split('/')[0]) for x in change_numbers))
318
319 # make list of things to actually merge
320 mergables = []
321
322 # If --exclude is given, create the list of commits to ignore
323 exclude = []
324 if args.exclude:
325 exclude = args.exclude[0].split(',')
326
327 for change in change_numbers:
328 patchset = None
329 if '/' in change:
330 (change, patchset) = change.split('/')
331
332 if change in exclude:
333 continue
334
335 change = int(change)
LuK1337ad5d9a02017-03-24 23:25:13 +0100336
Gabriele Mde9e0ae2018-04-01 17:50:55 +0200337 if patchset:
LuK1337ad5d9a02017-03-24 23:25:13 +0100338 patchset = int(patchset)
339
Michael Bestas3952f6c2016-08-26 01:12:08 +0300340 review = next((x for x in reviews if x['number'] == change), None)
341 if review is None:
342 print('Change %d not found, skipping' % change)
343 continue
344
345 mergables.append({
346 'subject': review['subject'],
347 'project': review['project'],
348 'branch': review['branch'],
349 'change_id': review['change_id'],
350 'change_number': review['number'],
351 'status': review['status'],
Gabriele M88c0e5d2018-04-01 17:50:57 +0200352 'fetch': None,
353 'patchset': review['revisions'][review['current_revision']]['_number'],
Michael Bestas3952f6c2016-08-26 01:12:08 +0300354 })
Gabriele M88c0e5d2018-04-01 17:50:57 +0200355
Michael Bestas3952f6c2016-08-26 01:12:08 +0300356 mergables[-1]['fetch'] = review['revisions'][review['current_revision']]['fetch']
357 mergables[-1]['id'] = change
358 if patchset:
359 try:
LuK133727564182017-03-24 20:00:48 +0100360 mergables[-1]['fetch'] = [review['revisions'][x]['fetch'] for x in review['revisions'] if review['revisions'][x]['_number'] == patchset][0]
Michael Bestas3952f6c2016-08-26 01:12:08 +0300361 mergables[-1]['id'] = '{0}/{1}'.format(change, patchset)
Gabriele M88c0e5d2018-04-01 17:50:57 +0200362 mergables[-1]['patchset'] = patchset
Michael Bestas3952f6c2016-08-26 01:12:08 +0300363 except (IndexError, ValueError):
364 args.quiet or print('ERROR: The patch set {0}/{1} could not be found, using CURRENT_REVISION instead.'.format(change, patchset))
365
366 for item in mergables:
367 args.quiet or print('Applying change number {0}...'.format(item['id']))
368 # Check if change is open and exit if it's not, unless -f is specified
Dan Pasanen63f767e2017-07-09 09:41:33 -0500369 if (item['status'] != 'OPEN' and item['status'] != 'NEW' and item['status'] != 'DRAFT') and not args.query:
Michael Bestas3952f6c2016-08-26 01:12:08 +0300370 if args.force:
371 print('!! Force-picking a closed change !!\n')
372 else:
373 print('Change status is ' + item['status'] + '. Skipping the cherry pick.\nUse -f to force this pick.')
374 continue
375
376 # Convert the project name to a project path
377 # - check that the project path exists
378 project_path = None
379
380 if item['project'] in project_name_to_data and item['branch'] in project_name_to_data[item['project']]:
381 project_path = project_name_to_data[item['project']][item['branch']]
382 elif args.path:
383 project_path = args.path
Adrian DCff74c6a2019-10-13 12:34:06 +0200384 elif item['project'] in project_name_to_data and len(project_name_to_data[item['project']]) == 1:
385 local_branch = list(project_name_to_data[item['project']])[0]
386 project_path = project_name_to_data[item['project']][local_branch]
387 print('WARNING: Project {0} has a different branch ("{1}" != "{2}")'.format(project_path, local_branch, item['branch']))
Michael Bestas3952f6c2016-08-26 01:12:08 +0300388 elif args.ignore_missing:
389 print('WARNING: Skipping {0} since there is no project directory for: {1}\n'.format(item['id'], item['project']))
390 continue
391 else:
392 sys.stderr.write('ERROR: For {0}, could not determine the project path for project {1}\n'.format(item['id'], item['project']))
393 sys.exit(1)
394
395 # If --start-branch is given, create the branch (more than once per path is okay; repo ignores gracefully)
396 if args.start_branch:
397 subprocess.check_output(['repo', 'start', args.start_branch[0], project_path])
398
399 # Determine the maximum commits to check already picked changes
Adrian DCdf290222017-09-07 22:59:54 +0200400 check_picked_count = args.check_picked
Michael Bestas3952f6c2016-08-26 01:12:08 +0300401 branch_commits_count = int(subprocess.check_output(['git', 'rev-list', '--count', 'HEAD'], cwd=project_path))
402 if branch_commits_count <= check_picked_count:
403 check_picked_count = branch_commits_count - 1
404
405 # Check if change is already picked to HEAD...HEAD~check_picked_count
406 found_change = False
407 for i in range(0, check_picked_count):
Adrian DC13b02ff2016-12-04 12:30:26 +0100408 if subprocess.call(['git', 'cat-file', '-e', 'HEAD~{0}'.format(i)], cwd=project_path, stderr=open(os.devnull, 'wb')):
409 continue
Michael Bestas3952f6c2016-08-26 01:12:08 +0300410 output = subprocess.check_output(['git', 'show', '-q', 'HEAD~{0}'.format(i)], cwd=project_path).split()
411 if 'Change-Id:' in output:
412 head_change_id = ''
Aayush Guptad0a36cd2020-07-26 07:19:19 +0000413 for j, t in enumerate(reversed(output)):
Michael Bestas3952f6c2016-08-26 01:12:08 +0300414 if t == 'Change-Id:':
415 head_change_id = output[len(output) - j]
416 break
417 if head_change_id.strip() == item['change_id']:
418 print('Skipping {0} - already picked in {1} as HEAD~{2}'.format(item['id'], project_path, i))
419 found_change = True
420 break
421 if found_change:
422 continue
423
424 # Print out some useful info
425 if not args.quiet:
LuK1337c62a9fb2019-09-21 11:47:33 +0200426 print(u'--> Subject: "{0}"'.format(item['subject']))
Michael Bestas3952f6c2016-08-26 01:12:08 +0300427 print('--> Project path: {0}'.format(project_path))
Gabriele M88c0e5d2018-04-01 17:50:57 +0200428 print('--> Change number: {0} (Patch Set {1})'.format(item['id'], item['patchset']))
Michael Bestas3952f6c2016-08-26 01:12:08 +0300429
430 if 'anonymous http' in item['fetch']:
431 method = 'anonymous http'
432 else:
433 method = 'ssh'
434
435 # Try fetching from GitHub first if using default gerrit
436 if args.gerrit == default_gerrit:
437 if args.verbose:
438 print('Trying to fetch the change from GitHub')
439
440 if args.pull:
441 cmd = ['git pull --no-edit github', item['fetch'][method]['ref']]
442 else:
443 cmd = ['git fetch github', item['fetch'][method]['ref']]
444 if args.quiet:
445 cmd.append('--quiet')
446 else:
447 print(cmd)
448 result = subprocess.call([' '.join(cmd)], cwd=project_path, shell=True)
449 FETCH_HEAD = '{0}/.git/FETCH_HEAD'.format(project_path)
450 if result != 0 and os.stat(FETCH_HEAD).st_size != 0:
451 print('ERROR: git command failed')
452 sys.exit(result)
453 # Check if it worked
454 if args.gerrit != default_gerrit or os.stat(FETCH_HEAD).st_size == 0:
455 # If not using the default gerrit or github failed, fetch from gerrit.
456 if args.verbose:
457 if args.gerrit == default_gerrit:
458 print('Fetching from GitHub didn\'t work, trying to fetch the change from Gerrit')
459 else:
460 print('Fetching from {0}'.format(args.gerrit))
461
462 if args.pull:
463 cmd = ['git pull --no-edit', item['fetch'][method]['url'], item['fetch'][method]['ref']]
464 else:
465 cmd = ['git fetch', item['fetch'][method]['url'], item['fetch'][method]['ref']]
466 if args.quiet:
467 cmd.append('--quiet')
468 else:
469 print(cmd)
470 result = subprocess.call([' '.join(cmd)], cwd=project_path, shell=True)
471 if result != 0:
472 print('ERROR: git command failed')
473 sys.exit(result)
474 # Perform the cherry-pick
475 if not args.pull:
Tim Schumacher49d26ba2018-10-13 14:37:54 +0200476 cmd = ['git cherry-pick --ff FETCH_HEAD']
Michael Bestas3952f6c2016-08-26 01:12:08 +0300477 if args.quiet:
478 cmd_out = open(os.devnull, 'wb')
479 else:
480 cmd_out = None
481 result = subprocess.call(cmd, cwd=project_path, shell=True, stdout=cmd_out, stderr=cmd_out)
482 if result != 0:
Adrian DC0f8230b2018-08-30 23:07:23 +0200483 cmd = ['git diff-index --quiet HEAD --']
484 result = subprocess.call(cmd, cwd=project_path, shell=True, stdout=cmd_out, stderr=cmd_out)
485 if result == 0:
486 print('WARNING: git command resulted with an empty commit, aborting cherry-pick')
487 cmd = ['git cherry-pick --abort']
488 subprocess.call(cmd, cwd=project_path, shell=True, stdout=cmd_out, stderr=cmd_out)
489 elif args.reset:
Harry Youd1c9c5a32017-07-18 18:52:42 +0100490 print('ERROR: git command failed, aborting cherry-pick')
491 cmd = ['git cherry-pick --abort']
492 subprocess.call(cmd, cwd=project_path, shell=True, stdout=cmd_out, stderr=cmd_out)
Adrian DC0f8230b2018-08-30 23:07:23 +0200493 sys.exit(result)
Harry Youd1c9c5a32017-07-18 18:52:42 +0100494 else:
495 print('ERROR: git command failed')
Adrian DC0f8230b2018-08-30 23:07:23 +0200496 sys.exit(result)
Michael Bestas3952f6c2016-08-26 01:12:08 +0300497 if not args.quiet:
498 print('')