Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. | |
| 2 # Use of this source code is governed by a BSD-style license that can be | |
| 3 # found in the LICENSE file. | |
| 4 | |
| 5 import collections | |
| 6 | |
| 7 from dashboard.common import namespaced_stored_object | |
| 8 from dashboard.services import gitiles_service | |
| 9 | |
| 10 | |
| 11 _REPOSITORIES_KEY = 'repositories' | |
| 12 | |
| 13 | |
| 14 class NonLinearError(Exception): | |
| 15 """Raised when trying to find the midpoint of Changes that are not linear.""" | |
| 16 | |
| 17 | |
| 18 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.
| |
| 19 """Raised when trying to find the midpoint of Commits that are adjacent.""" | |
| 20 | |
| 21 | |
| 22 class SameCommitWarning(Exception): | |
| 23 """Raised when trying to find the midpoint of identical Commits.""" | |
| 24 | |
| 25 | |
| 26 class Commit(collections.namedtuple('Commit', ('repository', 'git_hash'))): | |
| 27 """A git repository pinned to a particular commit.""" | |
| 28 | |
| 29 def __str__(self): | |
| 30 return self.repository + '@' + self.git_hash[:7] | |
| 31 | |
| 32 @property | |
| 33 def id_string(self): | |
| 34 return self.repository + '@' + self.git_hash | |
| 35 | |
| 36 @property | |
| 37 def repository_url(self): | |
| 38 """The HTTPS URL of the repository as passed to `git clone`.""" | |
| 39 repositories = namespaced_stored_object.Get(_REPOSITORIES_KEY) | |
| 40 return repositories[self.repository]['repository_url'] | |
| 41 | |
| 42 def Deps(self): | |
| 43 """Return the DEPS of this Commit as a frozenset of Commits.""" | |
| 44 # Download and execute DEPS file. | |
| 45 deps_file_contents = gitiles_service.FileContents( | |
| 46 self.repository_url, self.git_hash, 'DEPS') | |
| 47 deps_data = {'Var': lambda variable: deps_data['vars'][variable]} | |
| 48 exec deps_file_contents in deps_data # pylint: disable=exec-used | |
| 49 | |
| 50 # Pull out deps dict, including OS-specific deps. | |
| 51 deps_dict = deps_data['deps'] | |
| 52 for deps_os in deps_data.get('deps_os', {}).itervalues(): | |
| 53 deps_dict.update(deps_os) | |
| 54 | |
| 55 # Convert deps strings to Commit objects. | |
| 56 commits = [] | |
| 57 for dep_string in deps_dict.itervalues(): | |
| 58 dep_string_parts = dep_string.split('@') | |
| 59 if len(dep_string_parts) < 2: | |
| 60 continue # Dep is not pinned to any particular revision. | |
| 61 if len(dep_string_parts) > 2: | |
| 62 raise NotImplementedError('Unknown DEP format: ' + dep_string) | |
| 63 | |
| 64 repository_url, git_hash = dep_string_parts | |
| 65 repository = _Repository(repository_url) | |
| 66 if not repository: | |
| 67 _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.
| |
| 68 repository = _Repository(repository_url) | |
| 69 commits.append(Commit(repository, git_hash)) | |
| 70 | |
| 71 return frozenset(commits) | |
| 72 | |
| 73 def AsDict(self): | |
| 74 return { | |
| 75 'repository': self.repository, | |
| 76 'git_hash': self.git_hash, | |
| 77 'url': self.repository_url + '/+/' + self.git_hash, | |
| 78 } | |
| 79 | |
| 80 @classmethod | |
| 81 def FromDict(cls, data): | |
| 82 """Create a Commit from a dict. | |
| 83 | |
| 84 If the repository is a repository URL, it will be translated to its short | |
| 85 form name. | |
| 86 | |
| 87 Raises: | |
| 88 KeyError: The repository name is not in the local datastore, | |
| 89 or the git hash is not valid. | |
| 90 """ | |
| 91 repository = data['repository'] | |
| 92 | |
| 93 # Translate repository if it's a URL. | |
| 94 repository_from_url = _Repository(repository) | |
| 95 if repository_from_url: | |
| 96 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.
| |
| 97 | |
| 98 commit = cls(repository, data['git_hash']) | |
| 99 | |
| 100 try: | |
| 101 gitiles_service.CommitInfo(commit.repository_url, commit.git_hash) | |
| 102 except gitiles_service.NotFoundError as e: | |
| 103 raise KeyError(str(e)) | |
| 104 | |
| 105 return commit | |
| 106 | |
| 107 @classmethod | |
| 108 def Midpoint(cls, commit_a, commit_b): | |
| 109 """Return a Commit halfway between the two given Commits. | |
| 110 | |
| 111 Uses Gitiles to look up the commit range. | |
| 112 | |
| 113 Args: | |
| 114 commit_a: The first Commit in the range. | |
| 115 commit_b: The last Commit in the range. | |
| 116 | |
| 117 Returns: | |
| 118 A new Commit representing the midpoint. | |
| 119 The commit before the midpoint if the range has an even number of commits. | |
| 120 | |
| 121 Raises: | |
| 122 AdjacentCommitsWarning: The Commits are adjacent. | |
| 123 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.
| |
| 124 NonLinearError: The Commits are in different repositories or are in the | |
| 125 wrong order. | |
| 126 """ | |
| 127 if commit_a == commit_b: | |
| 128 raise SameCommitWarning() | |
| 129 | |
| 130 if commit_a.repository != commit_b.repository: | |
| 131 raise NonLinearError('Repositories differ between Commits: %s vs %s' % | |
| 132 (commit_a.repository, commit_b.repository)) | |
| 133 | |
| 134 commits = gitiles_service.CommitRange(commit_a.repository_url, | |
| 135 commit_a.git_hash, commit_b.git_hash) | |
| 136 # We don't handle NotFoundErrors because we assume that all Commits either | |
| 137 # came from this method or were already validated elsewhere. | |
| 138 if len(commits) == 0: | |
| 139 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
| |
| 140 commit_a, commit_b) | |
| 141 if len(commits) == 1: | |
| 142 raise AdjacentCommitsWarning() | |
| 143 commits = commits[1:] # Remove commit_b from the range. | |
| 144 | |
| 145 return cls(commit_a.repository, commits[len(commits) / 2]['commit']) | |
| 146 | |
| 147 | |
| 148 def _Repository(repository_url): | |
| 149 if repository_url.endswith('.git'): | |
| 150 repository_url = repository_url[:-4] | |
| 151 | |
| 152 repositories = namespaced_stored_object.Get(_REPOSITORIES_KEY) | |
| 153 for repo_label, repo_info in repositories.iteritems(): | |
| 154 if repository_url == repo_info['repository_url']: | |
| 155 return repo_label | |
| 156 | |
| 157 return None | |
| 158 | |
| 159 | |
| 160 def _AddRepository(repository_url): | |
| 161 if repository_url.endswith('.git'): | |
| 162 repository_url = repository_url[:-4] | |
| 163 | |
| 164 repositories = namespaced_stored_object.Get(_REPOSITORIES_KEY) | |
| 165 repository = repository_url.split('/')[-1] | |
| 166 | |
| 167 if repository in repositories: | |
| 168 raise AssertionError("Attempted to add a repository that's already in the " | |
| 169 'Datastore: %s: %s' % (repository, repository_url)) | |
| 170 | |
| 171 repositories[repository] = {'repository_url': repository_url} | |
| 172 namespaced_stored_object.Set(_REPOSITORIES_KEY, repositories) | |
| OLD | NEW |