| # Copyright (c) 2011-2012 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 logging |
| import os |
| import random |
| import re |
| |
| from chromite.lib import cros_build_lib |
| |
| |
| class PatchException(Exception): |
| """Base exception class all patch exception derive from.""" |
| |
| # Unless instances override it, default all exceptions to ToT. |
| inflight = False |
| |
| def __init__(self, patch, message=None): |
| Exception.__init__(self) |
| self.patch = patch |
| self.message = message |
| self.args = (patch,) |
| if message is not None: |
| self.args += (message,) |
| |
| def __str__(self): |
| return "Change %s failed: %s" % (self.patch, self.message) |
| |
| |
| class ApplyPatchException(PatchException): |
| """Exception thrown if we fail to apply a patch.""" |
| |
| def __init__(self, patch, inflight=False): |
| PatchException.__init__(self, patch) |
| self.inflight = inflight |
| self.args += (inflight,) |
| |
| def __str__(self): |
| return 'Change %s no longer cleanly applies against %s.' % ( |
| self.patch, 'current patch series' if self.inflight else 'ToT') |
| |
| |
| class DependencyError(PatchException): |
| |
| """Exception thrown when a change cannot be applied due to a failure in a |
| dependency.""" |
| |
| def __init__(self, patch, error): |
| """ |
| Args: |
| patch: The GitRepoPatch instance that this exception concerns. |
| error: A PatchException object that can be stringified to describe |
| the error. |
| """ |
| PatchException.__init__(self, patch) |
| self.inflight = error.inflight |
| self.error = error |
| self.args += (error,) |
| |
| def __str__(self): |
| return "Patch %s depends on %s which has an error: %s" % ( |
| self.patch, self.error.patch.id, self.error) |
| |
| |
| class BrokenCQDepends(PatchException): |
| """Raised if a patch has a CQ-DEPEND line that is ill formated.""" |
| |
| def __init__(self, patch, text): |
| PatchException.__init__(self, patch) |
| self.text = text |
| self.args += (text,) |
| |
| def __str__(self): |
| return "Change %s has a malformed CQ-DEPEND target: %s" % ( |
| self.patch, self.text) |
| |
| |
| class BrokenChangeID(PatchException): |
| """Raised if a patch has an invalid or missing Change-ID.""" |
| |
| def __init__(self, patch, message): |
| PatchException.__init__(self, patch) |
| self.message = message |
| self.args += (message,) |
| |
| def __str__(self): |
| return "Change %s has a broken ChangeId: %s" % (self.patch, self.message) |
| |
| |
| def MakeChangeId(unusable=False): |
| """Create a random Change-Id. |
| |
| Args: |
| unusable: If set to True, return a Change-Id like string that gerrit |
| will explicitly fail on. This is primarily used for internal ids, |
| as a fallback when a Change-Id could not be parsed. |
| """ |
| s = "%x" % (random.randint(0, 2**160),) |
| s = s.rjust(40, '0') |
| if unusable: |
| return 'Fake-ID %s' % s |
| return 'I%s' % s |
| |
| |
| def FormatChangeId(text): |
| """Format a Change-ID into a standardized form. Use this anywhere |
| we're accepting user input of Change-IDs.""" |
| if not text: |
| raise ValueError("FormatChangeId invoked w/ an empty value: %r" % (text,)) |
| elif not text[0] in 'iI': |
| raise ValueError("FormatChangeId invoked w/ a malformed Change-Id: %r" % |
| (text,)) |
| return 'I%s' % text[1:].lower() |
| |
| |
| class GitRepoPatch(object): |
| """Representing a patch from a branch of a local or remote git repository.""" |
| |
| # Note the selective case insensitivity; gerrit allows only this. |
| _VALID_CHANGE_ID_RE = re.compile(r'^I[0-9a-fA-F]{40}$') |
| _GIT_CHANGE_ID_RE = re.compile(r'^Change-Id:[\t ]*(\w+)\s*$', |
| re.I|re.MULTILINE) |
| _PALADIN_DEPENDENCY_RE = re.compile(r'^CQ-DEPEND=(.*)$', re.MULTILINE) |
| _PALADIN_BUG_RE = re.compile(r'(\w+)') |
| |
| def __init__(self, project_url, project, ref, tracking_branch, sha1=None, |
| change_id=None): |
| """Initialization of abstract Patch class. |
| |
| Args: |
| project_url: The url of the git repo (can be local or remote) to pull the |
| patch from. |
| project: The name of the project that the patch applies to. |
| ref: The refspec to pull from the git repo. |
| tracking_branch: The remote branch of the project the patch applies to. |
| sha1: The sha1 of the commit, if known. This *must* be accurate. Can |
| be None if not yet known- in which case Fetch will update it. |
| change_id: The Change-Id of the commit, if known. This *must* be |
| accurate. Can be None if not yet known- in which case Fetch will |
| parse it. |
| """ |
| self._commit_message = None |
| self.project_url = project_url |
| self.project = project |
| self.ref = ref |
| self.tracking_branch = os.path.basename(tracking_branch) |
| self.sha1 = sha1 |
| # TODO(ferringb): remove this attribute. |
| # apply_error_message is currently used by ValidationPool as a way to |
| # track what changes have failed. Patch objects shouldn't use this |
| # attribute, instead leaving it purely for validation_pool and friends |
| # to mutate. |
| self.apply_error_message = None |
| # change_id must always be a valid Change-Id (local or gerrit), or None. |
| # id is an internal value- if change-id exists, we use that. If not, |
| # we make up a fake change-id to keep internal apis happy/simple. |
| self.change_id = self.id = change_id |
| self._is_fetched = set() |
| self._projectdir_cache = {} |
| |
| def ProjectDir(self, checkout_root): |
| """Returns the local directory where this patch will be applied.""" |
| checkout_root = os.path.normpath(checkout_root) |
| val = self._projectdir_cache.get(checkout_root) |
| if val is None: |
| val = cros_build_lib.GetProjectDir(checkout_root, self.project) |
| self._projectdir_cache[checkout_root] = val |
| return val |
| |
| def Fetch(self, target_repo): |
| """Fetch this patch into the target repository. |
| |
| FETCH_HEAD is implicitly reset by this operation. Additionally, |
| if the sha1 of the patch was not yet known, it is pulled and stored |
| on this object and the target_repo is updated w/ the requested git |
| object. |
| |
| While doing so, we'll load the commit message and Change-Id if not |
| already known. |
| |
| Finally, if the sha1 is known and it's already available in the target |
| repository, this will skip the actual fetch operation (it's unneeded). |
| """ |
| |
| target_repo = os.path.normpath(target_repo) |
| if target_repo in self._is_fetched: |
| return self.sha1 |
| |
| def _PullData(rev): |
| ret = cros_build_lib.RunCommandCaptureOutput( |
| ['git', 'log', '--format=%H%x00%B', '-n1', rev], print_cmd=False, |
| error_code_ok=True, cwd=target_repo) |
| if ret.returncode != 0: |
| return None, None |
| output = ret.output.split('\0') |
| if len(output) != 2: |
| return None, None |
| return [x.strip() for x in output] |
| |
| if self.sha1 is not None: |
| # See if we've already got the object |
| sha1, msg = _PullData(self.sha1) |
| if sha1 is not None: |
| assert sha1 == self.sha1 |
| self._commit_message = msg |
| self._EnsureId(msg) |
| return self.sha1 |
| |
| cros_build_lib.RunCommand(['git', 'fetch', self.project_url, self.ref], |
| cwd=target_repo, print_cmd=False) |
| |
| sha1, msg = _PullData('FETCH_HEAD') |
| |
| # Even if we know the sha1, still do a sanity check to ensure we |
| # actually just fetched it. |
| if self.sha1 is not None: |
| if sha1 != self.sha1: |
| raise PatchException('Patch %s specifies sha1 %s, yet in fetching from ' |
| '%s we could not find that sha1. Internal error ' |
| 'most likely.' % (self, self.sha1, self.ref)) |
| else: |
| self.sha1 = sha1 |
| self._commit_message = msg |
| self._EnsureId(msg) |
| self._is_fetched.add(target_repo) |
| return self.sha1 |
| |
| def _RebaseOnto(self, branch, upstream, project_dir, rev, trivial): |
| """Attempts to rebase the given rev into branch. |
| |
| Raises: |
| cros_build_lib.RunCommandError: If the rebase operation returns an error code. |
| In this case, we still rebase --abort (or equivalent) before |
| returning. |
| """ |
| try: |
| cmd = ['git', 'rebase'] |
| if trivial: |
| cmd.extend(['--strategy', 'resolve', '-X', 'trivial']) |
| cmd.extend(['--onto', branch, upstream, rev]) |
| |
| # Run the rebase command. |
| cros_build_lib.RunCommand(cmd, cwd=project_dir, print_cmd=False) |
| |
| except cros_build_lib.RunCommandError: |
| cros_build_lib.RunCommand(['git', 'rebase', '--abort'], |
| cwd=project_dir, print_cmd=False, |
| error_code_ok=True) |
| raise |
| |
| def _RebasePatch(self, upstream, project_dir, rev, trivial): |
| """Rebase patch fetched from gerrit onto constants.PATCH_BRANCH. |
| |
| When the function completes, the constants.PATCH_BRANCH branch will be |
| pointing to the rebased change. |
| |
| Arguments: |
| upstream: The upstream branch to base patch on. |
| project_dir: Directory of the project that is being patched. |
| rev: The rev we're rebasing into the tree. |
| trivial: Use trivial logic that only allows trivial merges. Note: |
| Requires Git >= 1.7.6 -- bug <. Bots have 1.7.6 installed. |
| |
| Raises: |
| ApplyPatchException: If the patch failed to apply. |
| """ |
| try: |
| self._RebaseOnto(constants.PATCH_BRANCH, upstream, project_dir, rev, |
| trivial) |
| except cros_build_lib.RunCommandError: |
| # Ignore this error; next we fiddle with the patch a bit to |
| # discern what exception to throw. |
| pass |
| else: |
| cros_build_lib.RunCommand( |
| ['git', 'checkout', '-B', constants.PATCH_BRANCH, 'HEAD'], |
| cwd=project_dir, print_cmd=False) |
| return |
| |
| try: |
| self._RebaseOnto(upstream, upstream, project_dir, rev, trivial) |
| raise ApplyPatchException(self, inflight=True) |
| except cros_build_lib.RunCommandError: |
| # Cleanup the rebase attempt. |
| cros_build_lib.RunCommandCaptureOutput( |
| ['git', 'rebase', '--abort'], cwd=project_dir, print_cmd=False, |
| error_code_ok=True) |
| raise ApplyPatchException(self, inflight=False) |
| |
| finally: |
| # Ensure we're on the correct branch on the way out. |
| cros_build_lib.RunCommand(['git', 'checkout', constants.PATCH_BRANCH], |
| cwd=project_dir, print_cmd=False) |
| |
| def _GetUpstreamBranch(self, buildroot, force_default=False): |
| """Get the branch specified in the manifest for this patch.""" |
| # TODO(ferringb): remove this via cherry-picking; it's broken |
| # now since it assumes the tracking_branch (which is local |
| # to the originating repo) is what should be used for rebasing |
| # this patch into the current tree). |
| manifest = cros_build_lib.ManifestCheckout.Cached(buildroot) |
| if force_default: |
| return manifest.default_branch |
| return manifest.GetProjectsLocalRevision(self.project) |
| |
| def ApplyIntoGitRepo(self, project_dir, upstream, trivial=False, |
| do_check=True): |
| """Apply patch into a standalone git repo. |
| |
| The git repo does not need to be part of a repo checkout. |
| |
| Arguments: |
| project_dir: The directory of the project. |
| upstream: The branch to base the patch on. |
| trivial: Only allow trivial merges when applying change. |
| do_check: Check if tracking is the same as the given upstream. |
| """ |
| # Check that the patch is based on the same branch as project we are |
| # patching into. |
| # TODO(ferringb): remove this when cherry-pick lands; has questionable |
| # value now, has no relevance/value once cherry-picking is in. |
| branch_base = os.path.basename(upstream) |
| if self.tracking_branch != branch_base and do_check: |
| raise PatchException('branch %s for project %s is not tracking %s' |
| % (self.ref, self.project, branch_base)) |
| |
| rev = self.Fetch(project_dir) |
| |
| if not cros_build_lib.DoesLocalBranchExist(project_dir, |
| constants.PATCH_BRANCH): |
| cmd = ['checkout', '-b', constants.PATCH_BRANCH, '-t', upstream] |
| cros_build_lib.RunGitCommand(project_dir, cmd) |
| |
| self._RebasePatch(upstream, project_dir, rev, trivial) |
| |
| def Apply(self, buildroot, trivial=False): |
| """Applies the patch to specified buildroot. |
| |
| buildroot: The buildroot. |
| trivial: Only allow trivial merges when applying change. |
| |
| Raises: |
| ApplyPatchException: If the patch failed to apply. |
| """ |
| logging.info('Attempting to apply change %s', self) |
| manifest_branch = self._GetUpstreamBranch(buildroot) |
| do_check = manifest_branch.startswith('refs/') |
| if not do_check: |
| # Revision locked. Use the manifest default branch which |
| # is set to the manifest locked revision, and |
| # suppress the tracking/upstream check. |
| manifest_branch = self._GetUpstreamBranch(buildroot, True) |
| project_dir = self.ProjectDir(buildroot) |
| self.ApplyIntoGitRepo(project_dir, manifest_branch, trivial, |
| do_check=do_check) |
| |
| def GerritDependencies(self, buildroot): |
| """Returns an ordered list of dependencies from Gerrit. |
| |
| The list of changes are in order from FETCH_HEAD back to m/master. |
| |
| Arguments: |
| buildroot: The buildroot. |
| Returns: |
| An ordered list of Gerrit revisions that this patch depends on. |
| Raises: |
| BrokenChangeID: If a dependent change is missing its ChangeID. |
| """ |
| dependencies = [] |
| logging.info('Checking for Gerrit dependencies for change %s', self) |
| project_dir = self.ProjectDir(buildroot) |
| |
| rev = self.Fetch(project_dir) |
| |
| tracking_branch = self._GetUpstreamBranch(buildroot) |
| try: |
| return_obj = cros_build_lib.RunGitCommand( |
| project_dir, |
| ['log', '--format=%B%x00', '%s..%s^' % (tracking_branch, rev)]) |
| except cros_build_lib.RunCommandError, e: |
| if e.result.returncode != 128: |
| raise |
| # Errorcode 128 means "object not found"; either we've got an |
| # internal bug (tracking_branch was wrong, fetch somehow didn't get |
| # that actual rev, etc), or... this is the first commit in a repository. |
| # The following code checks for that, raising the original |
| # exception if not. |
| result = cros_build_lib.RunGitCommand(project_dir, |
| ['rev-list', '-n2', rev]) |
| if len(result.output.split()) != 1: |
| raise |
| # First commit of a repository; obviously, it has no dependencies. |
| return [] |
| |
| patches = [] |
| if return_obj.output: |
| # Only do this if we have output; else it leads |
| # to an invalid [''] result which we can't identify |
| # as differing from actual output for a single patch that |
| # lacks a commit message. |
| # Because the explicit null addition, strip off the last record. |
| patches = return_obj.output.split('\0')[:-1] |
| |
| for patch_output in patches: |
| change_id = self._ParseChangeId(patch_output) |
| dependencies.append(change_id) |
| |
| if dependencies: |
| logging.info('Found %s Gerrit dependencies for change %s', dependencies, |
| self) |
| |
| return dependencies |
| |
| def _EnsureId(self, commit_message): |
| """Ensure we have a usable Change-Id. This will parse the Change-Id out |
| of the given commit message- if it cannot find one, it logs a warning |
| and creates a fake ID. |
| |
| By it's nature, that fake ID is useless- it's created to simplify API |
| usage for patch consumers. |
| |
| If CQ were to see and try operating on one of these, it would fail for |
| example.""" |
| if self.id is not None: |
| return self.id |
| try: |
| self.change_id = self.id = self._ParseChangeId(commit_message) |
| except BrokenChangeID: |
| logging.warning( |
| 'Change %s, sha1 %s lacks a change-id in its commit ' |
| 'message. CQ-DEPEND against this rev will not work, nor ' |
| 'will any gerrit querying. Please add the appropriate ' |
| 'Change-Id into the commit message to resolve this.', |
| self, self.sha1) |
| |
| # We still need an internal id to address it via- thus we |
| # make a fake one to keep our caches/resolvers happy, while |
| # leaving the authoritive .change_id set to None. |
| self.id = MakeChangeId(unusable=True) |
| return self.id |
| |
| def _ParseChangeId(self, data): |
| """Parse a Change-Id out of a block of text.""" |
| # Grab just the last pararaph. |
| git_metadata = re.split('\n{2,}', data.rstrip())[-1] |
| change_id_match = self._GIT_CHANGE_ID_RE.findall(git_metadata) |
| if not change_id_match: |
| raise BrokenChangeID(self, 'Missing Change-Id in %s' % (data,)) |
| |
| # Now, validate it. This has no real effect on actual gerrit patches, |
| # but for local patches the validation is useful for general sanity |
| # enforcement. |
| change_id_match = change_id_match[-1] |
| if not self._VALID_CHANGE_ID_RE.match(change_id_match): |
| raise BrokenChangeID(self, change_id_match) |
| |
| # Force a standard for the formatting; I must be caps, force the rest |
| # of the hex to lower case. |
| return FormatChangeId(change_id_match) |
| |
| def PaladinDependencies(self, buildroot): |
| """Returns an ordered list of dependencies based on the Commit Message. |
| |
| Parses the Commit message for this change looking for lines that follow |
| the format: |
| |
| CQ-DEPEND:change_num+ e.g. |
| |
| A commit which depends on a couple others. |
| |
| BUG=blah |
| TEST=blah |
| CQ-DEPEND=10001,10002 |
| """ |
| dependencies = [] |
| logging.info('Checking for CQ-DEPEND dependencies for change %s', self) |
| self.Fetch(self.ProjectDir(buildroot)) |
| matches = self._PALADIN_DEPENDENCY_RE.findall(self._commit_message) |
| for match in matches: |
| chunks = ' '.join(match.split(',')) |
| chunks = chunks.split() |
| for chunk in chunks: |
| if not chunk.isdigit(): |
| if not self._VALID_CHANGE_ID_RE.match(chunk): |
| raise BrokenCQDepends(self, chunk) |
| chunk = FormatChangeId(chunk) |
| if chunk not in dependencies: |
| dependencies.append(chunk) |
| |
| if dependencies: |
| logging.info('Found %s Paladin dependencies for change %s', dependencies, |
| self) |
| return dependencies |
| |
| def __str__(self): |
| """Returns custom string to identify this patch.""" |
| s = '%s:%s' % (self.project, self.ref) |
| if self.sha1 is not None: |
| s = '%s:%s' % (s, self.sha1[:8]) |
| return s |
| |
| |
| class LocalPatch(GitRepoPatch): |
| """Represents patch coming from an on-disk git repo.""" |
| |
| def __init__(self, project_url, project, ref, tracking_branch, sha1): |
| GitRepoPatch.__init__(self, project_url, project, ref, tracking_branch, |
| sha1=sha1) |
| |
| def Sha1Hash(self): |
| """Get the Sha1 of the branch.""" |
| return self.sha1 |
| |
| def _GetCarbonCopy(self): |
| """Returns a copy of this commit object, with a different sha1. |
| |
| This is used to work around a Gerrit bug, where a commit object cannot be |
| uploaded for review if an existing branch (in refs/tryjobs/*) points to |
| that same sha1. So instead we create a copy of the commit object and upload |
| that to refs/tryjobs/*. |
| |
| Returns: |
| The sha1 of the new commit object. |
| """ |
| hash_fields = [('tree_hash', '%T'), ('parent_hash', '%P')] |
| transfer_fields = [('GIT_AUTHOR_NAME', '%an'), |
| ('GIT_AUTHOR_EMAIL', '%ae'), |
| ('GIT_AUTHOR_DATE', '%ad'), |
| ('GIT_COMMITTER_NAME', '%cn'), |
| ('GIT_COMMITTER_EMAIL', '%ce'), |
| ('GIT_COMMITER_DATE', '%ct')] |
| fields = hash_fields + transfer_fields |
| |
| format_string = '%n'.join([code for _, code in fields] + ['%B']) |
| result = cros_build_lib.RunGitCommand( |
| self.project_url, |
| ['log', '--format=%s' % format_string, '-z', '-n1', self.sha1]) |
| lines = result.output.splitlines() |
| field_value = dict(zip([name for name, _ in fields], |
| [line.strip() for line in lines])) |
| commit_body = '\n'.join(lines[len(fields):]) |
| |
| if len(field_value['parent_hash'].split()) != 1: |
| raise PatchException('Branch %s:%s contains merge result %s!' |
| % (self.project, self.ref, self.sha1)) |
| |
| extra_env = dict([(field, field_value[field]) for field, _ in |
| transfer_fields]) |
| |
| # Reset the commit date to a value that can't conflict; if we |
| # leave this to git, it's possible for a fast moving set of commit/uploads |
| # to all occur within the same second (thus the same commit date), |
| # resulting in the same sha1. |
| extra_env['GIT_COMMITTER_DATE'] = str( |
| int(extra_env["GIT_COMMITER_DATE"]) - 1) |
| |
| result = cros_build_lib.RunGitCommand( |
| self.project_url, |
| ['commit-tree', field_value['tree_hash'], '-p', |
| field_value['parent_hash']], |
| extra_env=extra_env, input=commit_body) |
| |
| new_sha1 = result.output.strip() |
| if new_sha1 == self.sha1: |
| raise PatchException( |
| 'Internal error! Carbon copy of %s is the same as original!' |
| % self.sha1) |
| |
| return new_sha1 |
| |
| def Upload(self, push_url, remote_ref, dryrun=False): |
| """Upload the patch to a remote git branch. |
| |
| Arguments: |
| push_url: Which url to push to. |
| remote_ref: The ref on the remote host to push to. |
| dryrun: Do the git push with --dry-run |
| """ |
| carbon_copy = self._GetCarbonCopy() |
| cmd = ['push', push_url, '%s:%s' % (carbon_copy, remote_ref)] |
| if dryrun: |
| cmd.append('--dry-run') |
| |
| cros_build_lib.RunGitCommand(self.project_url, cmd) |
| |
| |
| class UploadedLocalPatch(GitRepoPatch): |
| """Represents an uploaded local patch passed in using --remote-patch.""" |
| def __init__(self, project_url, project, ref, tracking_branch, |
| original_branch, original_sha1, carbon_copy_sha1=None): |
| GitRepoPatch.__init__(self, project_url, project, ref, tracking_branch, |
| sha1=carbon_copy_sha1) |
| self.original_branch = original_branch |
| self.original_sha1 = original_sha1 |
| |
| def __str__(self): |
| """Returns custom string to identify this patch.""" |
| # TODO(ferringb): fold subject line in here. |
| s = '%s:%s:%s' % (self.project, self.original_branch, |
| self.original_sha1[:8]) |
| return s |
| |
| |
| class GerritPatch(GitRepoPatch): |
| """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. |
| |
| Gerrit query JSON fields are documented at: |
| http://gerrit-documentation.googlecode.com/svn/Documentation/2.2.1/json.html |
| |
| Args: |
| patch_dict: A dictionary containing the parsed JSON gerrit query results. |
| internal: Whether the CL is an internal CL. |
| """ |
| self.patch_dict = patch_dict |
| super(GerritPatch, self).__init__( |
| self._GetProjectUrl(patch_dict['project'], internal), |
| patch_dict['project'], |
| patch_dict['currentPatchSet']['ref'], |
| patch_dict['branch'], |
| sha1=patch_dict['currentPatchSet']['revision'], |
| change_id=FormatChangeId(patch_dict['id'])) |
| |
| self.internal = internal |
| # id - The CL's ChangeId |
| # revision - The CL's SHA1 hash. |
| self.revision = patch_dict['currentPatchSet']['revision'] |
| self.patch_number = patch_dict['currentPatchSet']['number'] |
| self.commit = patch_dict['currentPatchSet']['revision'] |
| self.owner, _, _ = patch_dict['owner']['email'].partition('@') |
| self.gerrit_number = patch_dict['number'] |
| self.url = patch_dict['url'] |
| # status - Current state of this change. Can be one of |
| # ['NEW', 'SUBMITTED', 'MERGED', 'ABANDONED']. |
| self.status = patch_dict['status'] |
| |
| def __reduce__(self): |
| """Used for pickling to re-create patch object.""" |
| return self.__class__, (self.patch_dict.copy(), self.internal) |
| |
| def IsAlreadyMerged(self): |
| """Returns whether the patch has already been merged in Gerrit.""" |
| return self.status == 'MERGED' |
| |
| @classmethod |
| def _GetProjectUrl(cls, project, internal): |
| """Returns the url to the gerrit project.""" |
| if internal: |
| url_prefix = constants.GERRIT_INT_SSH_URL |
| else: |
| url_prefix = cls._PUBLIC_URL |
| |
| return os.path.join(url_prefix, project) |
| |
| def _EnsureId(self, commit_message): |
| """Ensure we have a usable Change-Id, validating what we received |
| from gerrit against what the commit message states.""" |
| # GerritPatch instances get their Change-Id from gerrit |
| # directly; for this to fail, there is an internal bug. |
| assert self.id is not None |
| |
| # For GerritPatches, we still parse the ID- this is |
| # primarily so we can throw an appropriate warning, |
| # and also validate our parsing against gerrit's in |
| # the process. |
| try: |
| parsed_id = self._ParseChangeId(commit_message) |
| if parsed_id != self.change_id: |
| raise AssertionError( |
| 'For Change-Id %s, sha %s, our parsing of the Change-Id did not ' |
| 'match what gerrit told us. This is an internal bug: either our ' |
| "parsing no longer matches gerrit's, or somehow this instance's " |
| 'stored change_id was invalidly modified. Our parsing of the ' |
| 'Change-Id yielded: %s' |
| % (self.change_id, self.sha1, parsed_id)) |
| |
| except BrokenChangeID: |
| logging.warning( |
| 'Change %s, Change-Id %s, sha1 %s lacks a change-id in its commit ' |
| 'message. This breaks the ability for any dependencies ' |
| 'to be committed as a batch (instead having to be committed one ' |
| 'by one through CQ). Please add the appropriate ' |
| 'Change-Id into the commit message to resolve this.', |
| self, self.change_id, self.sha1) |
| |
| return self.id |
| |
| def __str__(self): |
| """Returns custom string to identify this patch.""" |
| s = '%s:%s' % (self.owner, self.gerrit_number) |
| if self.sha1 is not None: |
| s = '%s:%s' % (s, self.sha1[:8]) |
| return s |
| |
| # Define methods to use patches in sets. We uniquely identify patches |
| # by Gerrit change numbers. |
| def __hash__(self): |
| return hash(self.id) |
| |
| def __eq__(self, other): |
| return self.id == other.id |
| |
| |
| |
| def PrepareLocalPatches(manifest, patches): |
| """Finish validation of parameters, and save patches to a temp folder. |
| |
| Args: |
| manifest: The manifest object for the checkout in question. |
| patches: A list of user-specified patches, in project[:branch] form. |
| """ |
| patch_info = [] |
| for patch in patches: |
| project, branch = patch.split(':') |
| project_dir = manifest.GetProjectPath(project, True) |
| tracking_branch = manifest.GetProjectsLocalRevision(project) |
| |
| cmd = ['git', 'rev-list', '-n1', '%s..%s' % (tracking_branch, branch)] |
| result = cros_build_lib.RunCommand(cmd, redirect_stdout=True, |
| cwd=project_dir) |
| sha1 = result.output.strip() |
| |
| if not sha1: |
| cros_build_lib.Die('No changes found in %s:%s' % (project, branch)) |
| |
| patch_info.append(LocalPatch(os.path.join(project_dir, '.git'), |
| project, branch, tracking_branch, |
| sha1)) |
| |
| return patch_info |
| |
| |
| def PrepareRemotePatches(patches): |
| """Generate patch objects from list of --remote-patch parameters. |
| |
| Args: |
| patches: A list of --remote-patches strings that the user specified on |
| the commandline. Patch strings are colon-delimited. Patches come |
| in the format |
| <project>:<original_branch>:<ref>:<tracking_branch>:<tag>. |
| A description of each element: |
| project: The manifest project name that the patch is for. |
| original_branch: The name of the development branch that the local |
| patch came from. |
| ref: The remote ref that points to the patch. |
| tracking_branch: The upstream branch that the original_branch was |
| tracking. Should be a manifest branch. |
| tag: Denotes whether the project is an internal or external |
| project. |
| """ |
| patch_info = [] |
| for patch in patches: |
| try: |
| project, original_branch, ref, tracking_branch, tag = patch.split(':') |
| except ValueError: |
| cros_build_lib.Die( |
| "Unexpected tryjob format. You may be running an " |
| "older version of chromite. Run 'repo sync " |
| "chromiumos/chromite'.") |
| |
| if tag == constants.EXTERNAL_PATCH_TAG: |
| push_url = constants.GERRIT_SSH_URL |
| elif tag == constants.INTERNAL_PATCH_TAG: |
| push_url = constants.GERRIT_INT_SSH_URL |
| else: |
| raise ValueError('Bad remote patch format. Unknown tag %s' % tag) |
| |
| patch_info.append(UploadedLocalPatch(os.path.join(push_url, project), |
| project, ref, tracking_branch, |
| original_branch, |
| os.path.basename(ref))) |
| |
| return patch_info |