blob: 6794ed7e4c3c6e17486a8d83b463b80d5a204b60 [file] [log] [blame]
# Copyright (c) 2011 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.
"""Module that handles the processing of patches to the source tree."""
import constants
import glob
import json
import os
import shutil
import tempfile
from chromite.lib import cros_build_lib as cros_lib
# The prefix of the temporary directory created to store local patches.
_TRYBOT_TEMP_PREFIX = 'trybot_patch-'
class PatchException(Exception):
"""Exception thrown by GetGerritPatchInfo."""
pass
class ApplyPatchException(Exception):
"""Exception thrown if we fail to apply a patch."""
pass
class Patch(object):
def __init__(self, project, tracking_branch):
"""Initialization of abstract Patch class.
Args:
project: The name of the project that the patch applies to.
tracking_branch: The remote branch of the project the patch applies to.
"""
self.project = project
self.tracking_branch = tracking_branch
def Apply(self, buildroot):
"""Applies the patch to specified buildroot. Implement in subclasses.
Raises:
PatchException
"""
raise NotImplementedError, 'Applies the patch to specified buildroot.'
class GerritPatch(Patch):
"""Object that represents a Gerrit CL."""
_PUBLIC_URL = os.path.join(constants.GERRIT_HTTP_URL, 'gerrit/p')
def __init__(self, patch_dict, internal):
"""Construct a GerritPatch object from Gerrit query results
Args:
patch_dict: A dictionary containing the parsed JSON gerrit query results.
internal: Whether the CL is an internal CL.
"""
Patch.__init__(self, patch_dict['project'], patch_dict['branch'])
self.internal = internal
# id - The CL's ChangeId
self.id = patch_dict['id']
# ref - The remote ref that contains the patch.
self.ref = patch_dict['currentPatchSet']['ref']
# revision - The CL's SHA1 hash.
self.revision = patch_dict['currentPatchSet']['revision']
self.patch_number = patch_dict['currentPatchSet']['number']
def Apply(self, buildroot):
"""Implementation of Patch.Apply()."""
if self.internal:
url_prefix = constants.GERRIT_INT_SSH_URL
else:
url_prefix = self._PUBLIC_URL
url = os.path.join(url_prefix, self.project)
project_dir = cros_lib.GetProjectDir(buildroot, self.project)
try:
cros_lib.RunCommand(['git', 'fetch', url, self.ref], cwd=project_dir)
cros_lib.RunCommand(['git', 'checkout', '--no-track',
'-b', constants.PATCH_BRANCH,
'FETCH_HEAD'], cwd=project_dir)
manifest_default_branch = cros_lib.GetManifestDefaultBranch(buildroot)
cros_lib.RunCommand(['git', 'rebase', 'm/' + manifest_default_branch],
cwd=project_dir)
except cros_lib.RunCommandError as e:
raise ApplyPatchException(e)
def Submit(self, helper, debug=False):
"""Submits patch using Gerrit Review."""
cmd = helper.GetGerritReviewCommand(['--submit', '%s,%s' % (
self.id, self.patch_number)])
if debug:
cros_lib.Info('Would have run: ' + ' '.join(cmd))
else:
cros_lib.RunCommand(cmd)
def __str__(self):
"""Returns custom string to identify this patch."""
return '%s:%s,%s' % (self.project, self.id, self.patch_number)
def RemovePatchRoot(patch_root):
"""Removes the temporary directory storing patches"""
assert(os.path.basename(patch_root).startswith(_TRYBOT_TEMP_PREFIX))
shutil.rmtree(patch_root)
class LocalPatch(Patch):
"""Object that represents a set of local commits that will be patched."""
def __init__(self, project, tracking_branch, patch_dir, local_branch):
"""Construct a LocalPatch object.
Args:
project: Same as Patch constructor arg.
tracking_branch: Same as Patch constructor arg.
patch_dir: The directory where the .patch files are stored.
local_branch: The local branch of the project that the patch came from.
"""
Patch.__init__(self, project, tracking_branch)
self.patch_dir = patch_dir
self.local_branch = local_branch
def _GetFileList(self):
"""Return a list of .patch files in sorted order."""
file_list = glob.glob(os.path.join(self.patch_dir, '*'))
file_list.sort()
return file_list
def Apply(self, buildroot):
"""Implementation of Patch.Apply().
Raises:
PatchException if the patch is for the wrong tracking branch.
"""
manifest_branch = _GetProjectManifestBranch(buildroot, self.project)
if self.tracking_branch != manifest_branch:
raise PatchException('branch %s for project %s is not tracking %s'
% (self.local_branch, self.project,
manifest_branch))
project_dir = cros_lib.GetProjectDir(buildroot, self.project)
try:
cros_lib.RunCommand(['repo', 'start', constants.PATCH_BRANCH, '.'],
cwd=project_dir)
cros_lib.RunCommand(['git', 'am', '--3way'] + self._GetFileList(),
cwd=project_dir)
except cros_lib.RunCommandError as e:
raise ApplyPatchException(e)
def GetGerritPatchInfo(patches):
"""Query Gerrit server for patch information.
Args:
patches: a list of patch ID's to query. Internal patches start with a '*'.
Returns:
A list of GerritPatch objects describing each patch.
Raises:
PatchException if a patch can't be found.
"""
parsed_patches = []
for patch in patches:
if patch.startswith('*'):
# Internal CL's have a '*' in front
internal = True
server, port = constants.GERRIT_INT_HOST, constants.GERRIT_INT_PORT
patch = patch[1:]
else:
internal = False
server, port = constants.GERRIT_HOST, constants.GERRIT_PORT
cmd = ['ssh', '-p', port, server, 'gerrit', 'query', '--current-patch-set',
'--format=JSON', patch]
result = cros_lib.RunCommand(cmd, redirect_stdout=True)
result_dict = json.loads(result.output.splitlines()[0])
if 'id' in result_dict:
parsed_patches.append(GerritPatch(result_dict, internal))
else:
raise PatchException('Change-ID %s not found on server %s.'
% (patch, server))
return parsed_patches
def _GetRemoteTrackingBranch(project_dir, branch):
"""Get the remote tracking branch of a local branch.
Raises:
cros_lib.NoTrackingBranchException if branch does not track anything.
"""
(remote, ref) = cros_lib.GetTrackingBranch(branch, project_dir)
return cros_lib.GetShortBranchName(remote, ref)
def _GetProjectManifestBranch(buildroot, project):
"""Get the branch specified in the manifest for the project."""
(remote, ref) = cros_lib.GetProjectManifestBranch(buildroot,
project)
return cros_lib.GetShortBranchName(remote, ref)
def PrepareLocalPatches(patches, manifest_branch):
"""Finish validation of parameters, and save patches to a temp folder.
Args:
patches: A list of user-specified patches, in project[:branch] form.
manifest_branch: The manifest branch of the buildroot.
Raises:
PatchException if:
1. The project branch isn't specified and the project isn't on a branch.
2. The project branch doesn't track a remote branch.
"""
patch_info = []
patch_root = tempfile.mkdtemp(prefix=_TRYBOT_TEMP_PREFIX)
for id in range(0, len(patches)):
project, branch = patches[id].split(':')
project_dir = cros_lib.GetProjectDir('.', project)
patch_dir = os.path.join(patch_root, str(id))
cmd = ['git', 'format-patch', '%s..%s' % ('m/' + manifest_branch, branch),
'-o', patch_dir]
cros_lib.RunCommand(cmd, redirect_stdout=True, cwd=project_dir)
if not len(os.listdir(patch_dir)):
raise PatchException('No changes found in %s:%s' % (project, branch))
# Store remote tracking branch for verification during patch stage.
try:
tracking_branch = _GetRemoteTrackingBranch(project_dir, branch)
except cros_lib.NoTrackingBranchException:
raise PatchException('%s:%s needs to track a remote branch!'
% (project, branch))
patch_info.append(LocalPatch(project, tracking_branch, patch_dir, branch))
return patch_info