| Index: dashboard/dashboard/pinpoint/models/change.py
|
| diff --git a/dashboard/dashboard/pinpoint/models/change.py b/dashboard/dashboard/pinpoint/models/change.py
|
| deleted file mode 100644
|
| index bf13aa3203adb99eadb08492cc3820599f19cd2e..0000000000000000000000000000000000000000
|
| --- a/dashboard/dashboard/pinpoint/models/change.py
|
| +++ /dev/null
|
| @@ -1,334 +0,0 @@
|
| -# Copyright 2016 The Chromium Authors. All rights reserved.
|
| -# Use of this source code is governed by a BSD-style license that can be
|
| -# found in the LICENSE file.
|
| -
|
| -import collections
|
| -
|
| -from dashboard.common import namespaced_stored_object
|
| -from dashboard.services import gitiles_service
|
| -
|
| -
|
| -_REPOSITORIES_KEY = 'repositories'
|
| -
|
| -
|
| -class NonLinearError(Exception):
|
| - """Raised when trying to find the midpoint of Changes that are not linear."""
|
| -
|
| -
|
| -class Change(collections.namedtuple('Change',
|
| - ('base_commit', 'deps', 'patch'))):
|
| - """A particular set of Deps with or without an additional patch applied.
|
| -
|
| - For example, a Change might sync to src@9064a40 and catapult@8f26966,
|
| - then apply patch 2423293002.
|
| - """
|
| -
|
| - def __new__(cls, base_commit, deps=frozenset(), patch=None):
|
| - """Create a Change.
|
| -
|
| - Args:
|
| - base_commit: A Dep representing the initial commit to sync to. The DEPS
|
| - file at that commit implies the default commits for any dependencies.
|
| - deps: An optional iterable of Deps to override the dependencies implied
|
| - by base_commit.
|
| - patch: An optional Patch to apply to the Change.
|
| - """
|
| - return super(Change, cls).__new__(cls, base_commit, frozenset(deps), patch)
|
| -
|
| - def __str__(self):
|
| - string = ' '.join(str(dep) for dep in self.all_deps)
|
| - if self.patch:
|
| - string += ' + ' + str(self.patch)
|
| - return string
|
| -
|
| - @property
|
| - def id_string(self):
|
| - string = ' '.join(dep.id_string for dep in self.all_deps)
|
| - if self.patch:
|
| - string += ' + ' + self.patch.id_string
|
| - return string
|
| -
|
| - @property
|
| - def all_deps(self):
|
| - return tuple([self.base_commit] + sorted(self.deps))
|
| -
|
| - def AsDict(self):
|
| - return {
|
| - 'base_commit': self.base_commit.AsDict(),
|
| - 'deps': [dep.AsDict() for dep in sorted(self.deps)],
|
| - 'patch': self.patch.AsDict() if self.patch else None,
|
| - }
|
| -
|
| - @classmethod
|
| - def FromDict(cls, data):
|
| - base_commit = Dep.FromDict(data['base_commit'])
|
| -
|
| - kwargs = {}
|
| - if 'deps' in data:
|
| - kwargs['deps'] = tuple(Dep.FromDict(dep) for dep in data['deps'])
|
| - if 'patch' in data:
|
| - kwargs['patch'] = Patch.FromDict(data['patch'])
|
| -
|
| - return cls(base_commit, **kwargs)
|
| -
|
| - @classmethod
|
| - def Midpoint(cls, change_a, change_b):
|
| - """Return a Change halfway between the two given Changes.
|
| -
|
| - A NonLinearError is raised if the Changes are not linear. The Changes are
|
| - not linear if any of the following is true:
|
| - * They have different base repositories.
|
| - * They have different patches.
|
| - * Their repositories differ even after expanding DEPS rolls.
|
| - See change_test.py for examples of linear and nonlinear Changes.
|
| -
|
| - The behavior is undefined if either of the Changes have multiple Deps with
|
| - the same repository.
|
| -
|
| - Args:
|
| - change_a: The first Change in the range.
|
| - change_b: The last Change in the range.
|
| -
|
| - Returns:
|
| - A new Change representing the midpoint.
|
| - The commit before the midpoint if the range has an even number of commits.
|
| - None if the range is empty, or the Changes are given in the wrong order.
|
| -
|
| - Raises:
|
| - NonLinearError: The Changes are not linear.
|
| - """
|
| - if change_a.base_commit.repository != change_b.base_commit.repository:
|
| - raise NonLinearError(
|
| - 'Change A has base repo "%s" and Change B has base repo "%s".' %
|
| - (change_a.base_commit.repository, change_b.base_commit.repository))
|
| -
|
| - if change_a.patch != change_b.patch:
|
| - raise NonLinearError(
|
| - 'Change A has patch "%s" and Change B has patch "%s".' %
|
| - (change_a.patch, change_b.patch))
|
| -
|
| - if change_a == change_b:
|
| - return None
|
| -
|
| - # Find the midpoint of every pair of Deps, expanding DEPS rolls as we go.
|
| - midpoint_deps = {}
|
| -
|
| - repositories_a = {dep.repository: dep for dep in change_a.all_deps}
|
| - repositories_b = {dep.repository: dep for dep in change_b.all_deps}
|
| -
|
| - # Match up all the Deps by repository.
|
| - while frozenset(repositories_a.iterkeys()).intersection(
|
| - frozenset(repositories_b.iterkeys())):
|
| - # Choose an arbitrary pair of Deps with the same repository.
|
| - shared_repositories = set(repositories_a.iterkeys()).intersection(
|
| - set(repositories_b.iterkeys()))
|
| - repository = shared_repositories.pop()
|
| - dep_a = repositories_a.pop(repository)
|
| - dep_b = repositories_b.pop(repository)
|
| -
|
| - if dep_a == dep_b:
|
| - # The Deps are the same.
|
| - midpoint_deps[repository] = dep_a
|
| - continue
|
| -
|
| - midpoint_dep = Dep.Midpoint(dep_a, dep_b)
|
| - if midpoint_dep:
|
| - # The Deps are not adjacent.
|
| - midpoint_deps[repository] = midpoint_dep
|
| - continue
|
| -
|
| - # The Deps are adjacent. Figure out if it's a DEPS roll.
|
| - deps_a = dep_a.Deps()
|
| - deps_b = dep_b.Deps()
|
| - if deps_a == deps_b:
|
| - # Not a DEPS roll. The Changes really are adjacent.
|
| - return None
|
| -
|
| - # DEPS roll! Expand the roll.
|
| - for dep in deps_a.difference(deps_b):
|
| - if dep.repository in midpoint_deps:
|
| - raise NonLinearError('Tried to take the midpoint across a DEPS roll, '
|
| - 'but the underlying Dep is already overriden in '
|
| - 'both Changes.')
|
| - if dep.repository not in repositories_a:
|
| - repositories_a[dep.repository] = dep
|
| - for dep in deps_b.difference(deps_a):
|
| - if dep.repository in midpoint_deps:
|
| - raise NonLinearError('Tried to take the midpoint across a DEPS roll, '
|
| - 'but the underlying Dep is already overriden in '
|
| - 'both Changes.')
|
| - if dep.repository not in repositories_b:
|
| - repositories_b[dep.repository] = dep
|
| - midpoint_deps[repository] = dep_a
|
| -
|
| - # Now that the DEPS are expanded, check to see if the repositories differ.
|
| - if repositories_a or repositories_b:
|
| - raise NonLinearError(
|
| - 'Repositories differ between Change A and Change B: %s' %
|
| - ', '.join(sorted(repositories_a.keys() + repositories_b.keys())))
|
| -
|
| - # Create our new Change!
|
| - base_commit = midpoint_deps.pop(change_a.base_commit.repository)
|
| - return cls(base_commit, midpoint_deps.itervalues(), change_a.patch)
|
| -
|
| -
|
| -class Dep(collections.namedtuple('Dep', ('repository', 'git_hash'))):
|
| - """A git repository pinned to a particular commit."""
|
| -
|
| - def __str__(self):
|
| - return self.repository + '@' + self.git_hash[:7]
|
| -
|
| - @property
|
| - def id_string(self):
|
| - return self.repository + '@' + self.git_hash
|
| -
|
| - @property
|
| - def repository_url(self):
|
| - """The HTTPS URL of the repository as passed to `git clone`."""
|
| - repositories = namespaced_stored_object.Get(_REPOSITORIES_KEY)
|
| - return repositories[self.repository]['repository_url']
|
| -
|
| - def Deps(self):
|
| - """Return the DEPS of this Dep as a frozenset of Deps."""
|
| - # Download and execute DEPS file.
|
| - deps_file_contents = gitiles_service.FileContents(
|
| - self.repository_url, self.git_hash, 'DEPS')
|
| - deps_data = {'Var': lambda variable: deps_data['vars'][variable]}
|
| - exec deps_file_contents in deps_data # pylint: disable=exec-used
|
| -
|
| - # Pull out deps dict, including OS-specific deps.
|
| - deps_dict = deps_data['deps']
|
| - for deps_os in deps_data.get('deps_os', {}).itervalues():
|
| - deps_dict.update(deps_os)
|
| -
|
| - # Convert deps strings to Dep objects.
|
| - deps = []
|
| - for dep_string in deps_dict.itervalues():
|
| - dep_string_parts = dep_string.split('@')
|
| - if len(dep_string_parts) < 2:
|
| - continue # Dep is not pinned to any particular revision.
|
| - if len(dep_string_parts) > 2:
|
| - raise NotImplementedError('Unknown DEP format: ' + dep_string)
|
| -
|
| - repository_url, git_hash = dep_string_parts
|
| - repository = _Repository(repository_url)
|
| - if not repository:
|
| - _AddRepository(repository_url)
|
| - repository = _Repository(repository_url)
|
| - deps.append(Dep(repository, git_hash))
|
| -
|
| - return frozenset(deps)
|
| -
|
| - def AsDict(self):
|
| - return {
|
| - 'repository': self.repository,
|
| - 'git_hash': self.git_hash,
|
| - 'url': self.repository_url + '/+/' + self.git_hash,
|
| - }
|
| -
|
| - @classmethod
|
| - def FromDict(cls, data):
|
| - """Create a Dep from a dict.
|
| -
|
| - If the repository is a repository URL, it will be translated to its short
|
| - form name.
|
| -
|
| - Raises:
|
| - KeyError: The repository name is not in the local datastore,
|
| - or the git hash is not valid.
|
| - """
|
| - repository = data['repository']
|
| -
|
| - # Translate repository if it's a URL.
|
| - repository_from_url = _Repository(repository)
|
| - if repository_from_url:
|
| - repository = repository_from_url
|
| -
|
| - dep = cls(repository, data['git_hash'])
|
| -
|
| - try:
|
| - gitiles_service.CommitInfo(dep.repository_url, dep.git_hash)
|
| - except gitiles_service.NotFoundError as e:
|
| - raise KeyError(str(e))
|
| -
|
| - return dep
|
| -
|
| - @classmethod
|
| - def Midpoint(cls, dep_a, dep_b):
|
| - """Return a Dep halfway between the two given Deps.
|
| -
|
| - Uses Gitiles to look up the commit range.
|
| -
|
| - Args:
|
| - dep_a: The first Dep in the range.
|
| - dep_b: The last Dep in the range.
|
| -
|
| - Returns:
|
| - A new Dep representing the midpoint.
|
| - The commit before the midpoint if the range has an even number of commits.
|
| - None if the range is empty, or the Deps are given in the wrong order.
|
| -
|
| - Raises:
|
| - ValueError: The Deps are in different repositories.
|
| - """
|
| - if dep_a.repository != dep_b.repository:
|
| - raise ValueError("Can't find the midpoint of Deps in differing "
|
| - 'repositories: "%s" and "%s"' % (dep_a, dep_b))
|
| -
|
| - commits = gitiles_service.CommitRange(dep_a.repository_url,
|
| - dep_a.git_hash, dep_b.git_hash)
|
| - # We don't handle NotFoundErrors because we assume that all Deps either came
|
| - # from this method or were already validated elsewhere.
|
| - if len(commits) <= 1:
|
| - return None
|
| - commits = commits[1:] # Remove dep_b from the range.
|
| -
|
| - return cls(dep_a.repository, commits[len(commits) / 2]['commit'])
|
| -
|
| -
|
| -class Patch(collections.namedtuple('Patch', ('server', 'issue', 'patchset'))):
|
| - """A patch in Rietveld."""
|
| - # TODO: Support Gerrit.
|
| - # https://github.com/catapult-project/catapult/issues/3599
|
| -
|
| - def __str__(self):
|
| - return self.id_string
|
| -
|
| - @property
|
| - def id_string(self):
|
| - return '%s/%d/%d' % (self.server, self.issue, self.patchset)
|
| -
|
| - def AsDict(self):
|
| - return self._asdict()
|
| -
|
| - @classmethod
|
| - def FromDict(cls, data):
|
| - # TODO: Validate to ensure the patch exists on the server.
|
| - return cls(data['server'], data['issue'], data['patchset'])
|
| -
|
| -
|
| -def _Repository(repository_url):
|
| - if repository_url.endswith('.git'):
|
| - repository_url = repository_url[:-4]
|
| -
|
| - repositories = namespaced_stored_object.Get(_REPOSITORIES_KEY)
|
| - for repo_label, repo_info in repositories.iteritems():
|
| - if repository_url == repo_info['repository_url']:
|
| - return repo_label
|
| -
|
| - return None
|
| -
|
| -
|
| -def _AddRepository(repository_url):
|
| - if repository_url.endswith('.git'):
|
| - repository_url = repository_url[:-4]
|
| -
|
| - repositories = namespaced_stored_object.Get(_REPOSITORIES_KEY)
|
| - repository = repository_url.split('/')[-1]
|
| -
|
| - if repository in repositories:
|
| - raise AssertionError("Attempted to add a repository that's already in the "
|
| - 'Datastore: %s: %s' % (repository, repository_url))
|
| -
|
| - repositories[repository] = {'repository_url': repository_url}
|
| - namespaced_stored_object.Set(_REPOSITORIES_KEY, repositories)
|
|
|