blob: 16a9b6893f763e846c7293c68d288da58b12d298 [file] [log] [blame]
#!/usr/bin/env python
# Copyright 2017 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Download patches from Gerrit.
This tool will first checkout BRANCH, and cherry-pick changes on Gerrit with
specific topic or hashtag. If BOARD is given, will also cherry-pick changes in
private overlay. Please refer to tools/README.md for more detailed information.
"""
from __future__ import print_function
import argparse
import logging
import os
import subprocess
import sys
import factory_common # pylint: disable=unused-import
from cros.factory.test.env import paths
from cros.factory.utils import cros_board_utils
from cros.factory.utils import process_utils
try:
DEPOT_TOOLS_PATH = os.path.join(
os.path.dirname(os.environ.get('CROS_WORKON_SRCROOT',
'/mnt/host/source')),
'depot_tools')
if DEPOT_TOOLS_PATH not in sys.path:
sys.path.append(DEPOT_TOOLS_PATH)
import gerrit_util # pylint: disable=import-error
except ImportError:
logging.exception('cannot find module gerrit_util, which should be found '
'under %s, are you in chroot?', DEPOT_TOOLS_PATH)
raise
def GetFactoryRepoInfo(options):
return {
'url': 'chromium-review.googlesource.com',
'project': 'chromiumos/platform/factory',
'dir': paths.FACTORY_DIR,
'branch': 'cros/' + options.branch}
def GetBoardRepoInfo(options):
board = options.board
files_dir = cros_board_utils.GetChromeOSFactoryBoardPath(board)
if not files_dir:
raise RuntimeError('cannot find private overlay for board: ' + board)
overlay_dir = os.path.dirname(files_dir)
# expected output would be like:
#
# Manifest branch: ...
# Manifest merge branch: ...
# Manifest groups: ...
# ----------------------------
# Project: chromeos/overlays/overlay-*-private
# Mount path: /mnt/host/source/src/private-overlays/*
# Current revision: ...
# Local Branches: ...
# ----------------------------
#
PROJECT_LINE_PREFIX = 'Project: '
repo_info = process_utils.CheckOutput(['repo', '--color=never', 'info', '.'],
cwd=overlay_dir)
project = [s for s in repo_info.splitlines()
if s.startswith(PROJECT_LINE_PREFIX)][0] # find project line
project = project[len(PROJECT_LINE_PREFIX):] # remove prefix
return {
'url': 'chrome-internal-review.googlesource.com',
'project': project,
'dir': overlay_dir,
'branch': 'cros-internal/' + options.branch}
def QueryChanges(info, options):
"""Fetch list of changes with specific topic and branch.
Args:
info: required information for the repo we are working on, should be the
value returned by `GetFactoryRepoInfo` or `GetBoardRepoInfo`.
options: parsed command line argument.
"""
param = [('project', info['project']),
('status', 'open')]
if options.branch:
param.append(('branch', options.branch))
if options.topic:
param.append(('topic', options.topic))
if options.hashtag:
param.append(('hashtag', options.hashtag))
logging.debug('query change list from gerrit: ')
logging.debug(' url: %s', info['url'])
logging.debug(' param: %r', param)
results = list(
gerrit_util.GenerateAllChanges(
info['url'],
param,
o_params=['CURRENT_REVISION', 'CURRENT_COMMIT', ]))
changes = {}
for change in results:
current_revision_hash = change['current_revision']
current_revision = change['revisions'][current_revision_hash]
changes[current_revision_hash] = {
'subject': change['subject'],
'change_id': change['change_id'],
'parent': current_revision['commit']['parents'][0]['commit'],
'fetch': current_revision['fetch']['http'],
'url': 'https://%s/%d' % (info['url'],
change['_number']), }
return changes
def TopologicalSort(changes):
"""Sort changes in topological order.
Args:
changes: a dictionary, which maps commit hashes to commit objects::
{
'<commit hash>': {
'subject': '<subject>',
'change_id': '<gerrit change ID>',
'parent': '<parent commit hash>',
'fetch': {'url': '<url of the project>',
'ref': '<ref of the patch set>'},
'url': '<link to review change>',
},
}
:type changes: dict
Returns:
A list of dict objects, each object contains the following attributes::
{
'commit': '<commit hash>',
'url': '<link to review change>',
'fetch': {'url': '<url of the project>',
'ref': '<ref of the patch set>'},
'subject': '<subject>',
}
"""
not_root = set()
for key in changes.keys():
parent = changes[key]['parent']
if parent in changes.keys():
parent = changes[parent]
if 'kids' not in parent:
parent['kids'] = {key: changes[key]}
else:
parent['kids'][key] = changes[key]
not_root.add(key)
changes = {key: changes[key] for key in set(changes) - not_root}
# sort CLs in topological order
def walk(d):
for k, v in d.iteritems():
yield {
'url': v['url'],
'fetch': v['fetch'],
'subject': v['subject'],
'commit': k}
if 'kids' in v:
for change in walk(v['kids']):
yield change
return list(walk(changes))
def CherryPickChanges(info, changes):
"""Cherry-pick a list of changes.
Would try to respect dependencies between each changes, that is, will download
changes in topological order.
Args:
info: required information for the repo we are working on, should be the
value returned by `GetFactoryRepoInfo` or `GetBoardRepoInfo`.
changes: a dictionary, which maps commit hashes to commit objects::
{
'<commit hash>': {
'subject': '<subject>',
'change_id': '<gerrit change ID>',
'parent': '<parent commit hash>',
'fetch': {'url': '<url of the project>',
'ref': '<ref of the patch set>'},
'url': '<link to review change>',
},
}
:type changes: dict
"""
changes = TopologicalSort(changes)
try:
process_utils.LogAndCheckCall(
['git', 'checkout', info['branch']],
cwd=info['dir'])
except subprocess.CalledProcessError:
logging.exception('failed to checkout branch %s', info['branch'])
else:
head = process_utils.CheckOutput(['git', 'rev-parse', 'HEAD'],
cwd=info['dir']).strip()
successes = set()
for idx, change in enumerate(changes):
print('cherry-picking %s ...' % change['url'])
try:
process_utils.LogAndCheckCall(
['git', 'fetch', change['fetch']['url'], change['fetch']['ref']],
cwd=info['dir'])
except subprocess.CalledProcessError:
logging.exception('Cannot download change %s', change['url'])
continue
try:
process_utils.LogAndCheckCall(
['git', 'cherry-pick', 'FETCH_HEAD'],
cwd=info['dir'])
except subprocess.CalledProcessError:
logging.exception('Failed to cherry pick %s', change['url'])
logging.error('Revert this change...')
try:
process_utils.LogAndCheckCall(['git', 'cherry-pick', '--abort'],
cwd=info['dir'])
except subprocess.CalledProcessError:
# somehow we cannot recover git state, abort
logging.exception('Cannot revert git state, abort...')
break
continue
successes.add(idx)
print()
print()
print('Summary:')
print('BASE:', head)
for idx, change in enumerate(changes):
print('%s: %s' % (change['url'],
'success' if idx in successes else 'failed'))
print()
print()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--topic',
help='limit to specific topic')
parser.add_argument('--hashtag',
help='search changes with this hashtag')
parser.add_argument('--branch', default='master',
help='limit to specific branch')
parser.add_argument('--board',
help='board name (to specify the private overlay)')
parser.add_argument('-v', '--verbose',
help='verbose mode', action='store_true')
options = parser.parse_args()
logging_level = logging.DEBUG if options.verbose else logging.WARNING
logging.basicConfig(
format=('[%(levelname)s] %(filename)s:%(lineno)d: %(message)s'),
level=logging_level)
if not options.topic and not options.hashtag:
logging.error('At least one of --topic and --hashtag must be specified')
parser.print_usage()
return
info = GetFactoryRepoInfo(options)
changes = QueryChanges(info, options)
CherryPickChanges(info, changes)
if options.board:
info = GetBoardRepoInfo(options)
changes = QueryChanges(info, options)
CherryPickChanges(info, changes)
if __name__ == '__main__':
main()