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..14d7d331d6cbef763ca28daefa7507ff0bee3f57 |
--- /dev/null |
+++ b/dashboard/dashboard/pinpoint/models/change/commit.py |
@@ -0,0 +1,172 @@ |
+# 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 AdjacentCommitsWarning(Exception): |
perezju
2017/09/12 10:59:54
I'm not trilled about "raising" warnings as except
dtu
2017/09/12 22:31:16
Done.
|
+ """Raised when trying to find the midpoint of Commits that are adjacent.""" |
+ |
+ |
+class SameCommitWarning(Exception): |
+ """Raised when trying to find the midpoint of identical Commits.""" |
+ |
+ |
+class Commit(collections.namedtuple('Commit', ('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 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 |
+ repository = _Repository(repository_url) |
+ if not repository: |
+ _AddRepository(repository_url) |
perezju
2017/09/12 10:59:54
nit: make _AddRepository to return the repository
dtu
2017/09/12 22:31:16
Done.
|
+ repository = _Repository(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. |
+ repository_from_url = _Repository(repository) |
+ if repository_from_url: |
+ repository = repository_from_url |
perezju
2017/09/12 10:59:54
Wouldn't it be safer to check for leading 'https?:
dtu
2017/09/12 22:31:16
Done.
|
+ |
+ 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. |
+ |
+ Raises: |
+ AdjacentCommitsWarning: The Commits are adjacent. |
+ SameCommitWarning: The Commits are the same. |
perezju
2017/09/12 10:59:54
I think these warnings raised as exceptions are aw
dtu
2017/09/12 22:31:16
Done.
|
+ NonLinearError: The Commits are in different repositories or are in the |
+ wrong order. |
+ """ |
+ if commit_a == commit_b: |
+ raise SameCommitWarning() |
+ |
+ 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('The commits are in the wrong order: %s and %s' % |
perezju
2017/09/12 10:59:54
nit: Rephrase as "Commit {a} does not come before
dtu
2017/09/12 22:31:16
Done. Do you mean e.g. they're in the same reposit
perezju
2017/09/13 12:52:23
Yep exactly. This may not be common since we tend
|
+ commit_a, commit_b) |
+ if len(commits) == 1: |
+ raise AdjacentCommitsWarning() |
+ commits = commits[1:] # 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 |
+ |
+ 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) |