| Index: dashboard/dashboard/pinpoint/models/change/commit.py
|
| diff --git a/dashboard/dashboard/pinpoint/models/change/commit.py b/dashboard/dashboard/pinpoint/models/change/commit.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..f7357279de5c7bc9385e39d07640f0d188d42657
|
| --- /dev/null
|
| +++ b/dashboard/dashboard/pinpoint/models/change/commit.py
|
| @@ -0,0 +1,166 @@
|
| +# 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 Commit(collections.namedtuple('Commit', ('repository', 'git_hash'))):
|
| + """A git repository pinned to a particular commit."""
|
| +
|
| + def __str__(self):
|
| + """Returns an informal short string representation of this Commit."""
|
| + return self.repository + '@' + self.git_hash[:7]
|
| +
|
| + @property
|
| + def id_string(self):
|
| + """Returns a string that is unique to this repository and git hash."""
|
| + 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 Commit as a frozenset of Commits."""
|
| + # 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 Commit objects.
|
| + commits = []
|
| + 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
|
| + try:
|
| + repository = _Repository(repository_url)
|
| + except KeyError:
|
| + repository = _AddRepository(repository_url)
|
| + commits.append(Commit(repository, git_hash))
|
| +
|
| + return frozenset(commits)
|
| +
|
| + 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 Commit 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.
|
| + if repository.startswith('https://'):
|
| + repository = _Repository(repository)
|
| +
|
| + commit = cls(repository, data['git_hash'])
|
| +
|
| + try:
|
| + gitiles_service.CommitInfo(commit.repository_url, commit.git_hash)
|
| + except gitiles_service.NotFoundError as e:
|
| + raise KeyError(str(e))
|
| +
|
| + return commit
|
| +
|
| + @classmethod
|
| + def Midpoint(cls, commit_a, commit_b):
|
| + """Return a Commit halfway between the two given Commits.
|
| +
|
| + Uses Gitiles to look up the commit range.
|
| +
|
| + Args:
|
| + commit_a: The first Commit in the range.
|
| + commit_b: The last Commit in the range.
|
| +
|
| + Returns:
|
| + A new Commit representing the midpoint.
|
| + The commit before the midpoint if the range has an even number of commits.
|
| + commit_a if the Commits are the same or adjacent.
|
| +
|
| + Raises:
|
| + NonLinearError: The Commits are in different repositories or commit_a does
|
| + not come before commit_b.
|
| + """
|
| + if commit_a == commit_b:
|
| + return commit_a
|
| +
|
| + if commit_a.repository != commit_b.repository:
|
| + raise NonLinearError('Repositories differ between Commits: %s vs %s' %
|
| + (commit_a.repository, commit_b.repository))
|
| +
|
| + commits = gitiles_service.CommitRange(commit_a.repository_url,
|
| + commit_a.git_hash, commit_b.git_hash)
|
| + # We don't handle NotFoundErrors because we assume that all Commits either
|
| + # came from this method or were already validated elsewhere.
|
| + if len(commits) == 0:
|
| + raise NonLinearError('Commit "%s" does not come before commit "%s".' %
|
| + commit_a, commit_b)
|
| + if len(commits) == 1:
|
| + return commit_a
|
| + commits.pop(0) # Remove commit_b from the range.
|
| +
|
| + return cls(commit_a.repository, commits[len(commits) / 2]['commit'])
|
| +
|
| +
|
| +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
|
| +
|
| + raise KeyError('Unknown repository URL: ' + repository_url)
|
| +
|
| +
|
| +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)
|
| +
|
| + return repository
|
|
|