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 import itertools | |
| 7 | |
| 8 from dashboard.pinpoint.models.change import commit as commit_module | |
| 9 from dashboard.pinpoint.models.change import patch as patch_module | |
| 10 | |
| 11 | |
| 12 class Change(collections.namedtuple('Change', ('commits', 'patch'))): | |
| 13 """A particular set of Commits with or without an additional patch applied. | |
| 14 | |
| 15 For example, a Change might sync to src@9064a40 and catapult@8f26966, | |
| 16 then apply patch 2423293002. | |
| 17 """ | |
| 18 | |
| 19 def __new__(cls, commits, patch=None): | |
| 20 """Creates a Change. | |
| 21 | |
| 22 Args: | |
| 23 commits: An iterable of Commits representing this Change's dependencies. | |
| 24 patch: An optional Patch to apply to the Change. | |
| 25 """ | |
| 26 if not commits: | |
| 27 raise TypeError('At least one commit required.') | |
| 28 return super(Change, cls).__new__(cls, tuple(commits), patch) | |
| 29 | |
| 30 def __str__(self): | |
| 31 string = ' '.join(str(commit) for commit in self.commits) | |
| 32 if self.patch: | |
| 33 string += ' + ' + str(self.patch) | |
| 34 return string | |
| 35 | |
| 36 @property | |
| 37 def id_string(self): | |
|
perezju
2017/09/12 10:59:54
add docstrings explaining difference between str(c
dtu
2017/09/12 22:31:16
Done.
| |
| 38 string = ' '.join(commit.id_string for commit in self.commits) | |
| 39 if self.patch: | |
| 40 string += ' + ' + self.patch.id_string | |
| 41 return string | |
| 42 | |
| 43 @property | |
| 44 def base_commit(self): | |
| 45 return self.commits[0] | |
| 46 | |
| 47 @property | |
| 48 def deps(self): | |
| 49 return tuple(self.commits[1:]) | |
| 50 | |
| 51 def AsDict(self): | |
| 52 return { | |
| 53 'commits': [commit.AsDict() for commit in self.commits], | |
| 54 'patch': self.patch.AsDict() if self.patch else None, | |
| 55 } | |
| 56 | |
| 57 @classmethod | |
| 58 def FromDict(cls, data): | |
| 59 commits = tuple(commit_module.Commit.FromDict(commit) | |
| 60 for commit in data['commits']) | |
| 61 if 'patch' in data: | |
| 62 patch = patch_module.Patch.FromDict(data['patch']) | |
| 63 else: | |
| 64 patch = None | |
| 65 | |
| 66 return cls(commits, patch=patch) | |
| 67 | |
| 68 @classmethod | |
| 69 def Midpoint(cls, change_a, change_b): | |
| 70 """Returns a Change halfway between the two given Changes. | |
| 71 | |
| 72 This function does two passes over the Changes' Commits: | |
| 73 * The first pass attempts to match the lengths of the Commit lists by | |
| 74 expanding DEPS to fill in any repositories that are missing from one, | |
| 75 but included in the other. | |
| 76 * The second pass takes the midpoint of every matched pair of Commits, | |
| 77 expanding DEPS rolls as it comes across them. | |
| 78 | |
| 79 A NonLinearError is raised if there is no valid midpoint. The Changes are | |
| 80 not linear if any of the following is true: | |
| 81 * They have different patches. | |
| 82 * Their repositories don't match even after expanding DEPS rolls. | |
| 83 * The left Change comes after the right Change. | |
| 84 * They are the same or adjacent. | |
| 85 See change_test.py for examples of linear and nonlinear Changes. | |
| 86 | |
| 87 Args: | |
| 88 change_a: The first Change in the range. | |
| 89 change_b: The last Change in the range. | |
| 90 | |
| 91 Returns: | |
| 92 A new Change representing the midpoint. | |
| 93 The Change before the midpoint if the range has an even number of commits. | |
| 94 | |
| 95 Raises: | |
| 96 NonLinearError: The Changes are not linear. | |
| 97 """ | |
| 98 if change_a.patch != change_b.patch: | |
| 99 raise commit_module.NonLinearError( | |
| 100 'Change A has patch "%s" and Change B has patch "%s".' % | |
| 101 (change_a.patch, change_b.patch)) | |
| 102 | |
| 103 commits_a = list(change_a.commits) | |
| 104 commits_b = list(change_b.commits) | |
| 105 | |
| 106 _ExpandDepsToMatchRepositories(commits_a, commits_b) | |
| 107 commits_midpoint = _FindMidpoints(commits_a, commits_b) | |
| 108 | |
| 109 if commits_a == commits_midpoint: | |
| 110 raise commit_module.NonLinearError('Changes are the same or adjacent.') | |
| 111 | |
| 112 return cls(commits_midpoint, change_a.patch) | |
| 113 | |
| 114 | |
| 115 def _ExpandDepsToMatchRepositories(commits_a, commits_b): | |
| 116 """Expands DEPS in a Commit list to match the repositories in another. | |
| 117 | |
| 118 Given two lists of Commits, with one bigger than the other, this function | |
| 119 looks through the DEPS files for smaller commit list to fill out any missing | |
| 120 Commits that are already in the bigger commit list. | |
| 121 | |
| 122 Mutates the lists in-place, and doesn't return anything. | |
| 123 | |
| 124 Example: | |
| 125 commits_a == [chromium@a, v8@c] | |
| 126 commits_b == [chromium@b] | |
| 127 This function looks through the DEPS file at chromium@b to find v8, then | |
| 128 appends that v8 Commit to commits_b, making the lists match. | |
| 129 | |
| 130 Args: | |
| 131 commits_a: A list of Commits. | |
| 132 commits_b: A list of Commits. | |
| 133 """ | |
| 134 # The lists may be given in any order. Let's make commits_b the bigger list. | |
| 135 if len(commits_a) > len(commits_b): | |
| 136 commits_a, commits_b = commits_b, commits_a | |
| 137 | |
| 138 # Loop through every DEPS file in commits_a. | |
| 139 for commit_a in commits_a: | |
|
perezju
2017/09/12 10:59:54
this looks scary, modifying the list while iterati
dtu
2017/09/12 22:31:16
Is it any less scary if we're not modifying the ex
perezju
2017/09/13 12:52:23
(Let's revisit again on a follow up CL)
It's stil
dtu
2017/09/13 15:49:26
It keeps going. I've used izip specifically instea
| |
| 140 if len(commits_a) == len(commits_b): | |
| 141 break | |
| 142 deps_a = commit_a.Deps() | |
| 143 | |
| 144 # Look through commits_b for any extra slots to fill with the DEPS. | |
| 145 for commit_b in commits_b[len(commits_a):]: | |
| 146 dep_a = _FindCommitWithRepository(deps_a, commit_b.repository) | |
| 147 if dep_a: | |
| 148 commits_a.append(dep_a) | |
| 149 else: | |
| 150 break | |
| 151 | |
| 152 | |
| 153 def _FindMidpoints(commits_a, commits_b): | |
| 154 """Returns the midpoint of two Commit lists. | |
| 155 | |
| 156 Loops through each pair of Commits and takes the midpoint. If the repositories | |
| 157 don't match, a NonLinearError is raised. If the Commits are adjacent and | |
| 158 represent a DEPS roll, the differing DEPs are added to the end of the lists. | |
| 159 | |
| 160 Args: | |
| 161 commits_a: A list of Commits. | |
| 162 commits_b: A list of Commits. | |
| 163 | |
| 164 Returns: | |
| 165 A list of Commits, each of which is the midpoint of the respective Commit in | |
| 166 commits_a and commits_b. | |
| 167 | |
| 168 Raises: | |
| 169 NonLinearError: The lists have a different number of commits even after | |
| 170 expanding DEPS rolls, a Commit pair contains differing repositories, or a | |
| 171 Commit pair is in the wrong order. | |
| 172 """ | |
| 173 commits_midpoint = [] | |
| 174 | |
| 175 for commit_a, commit_b in itertools.izip_longest(commits_a, commits_b): | |
|
perezju
2017/09/12 10:59:54
at this point the two lists should be the same siz
dtu
2017/09/12 22:31:16
In practical cases, yes, but there's no invariant
perezju
2017/09/13 12:52:23
(Let's revisit again on a follow up CL)
I was goi
dtu
2017/09/13 15:49:26
That's right. If a DEPS roll adds or removes a DEP
| |
| 176 if not (commit_a and commit_b): | |
| 177 raise commit_module.NonLinearError( | |
| 178 'Changes have a different number of commits.') | |
| 179 | |
| 180 try: | |
| 181 # Commits are not adjacent. | |
| 182 commit_midpoint = commit_module.Commit.Midpoint(commit_a, commit_b) | |
| 183 except commit_module.SameCommitWarning: | |
| 184 # Commits are the same. | |
| 185 commit_midpoint = commit_a | |
| 186 except commit_module.AdjacentCommitsWarning: | |
| 187 # Commits are adjacent. | |
| 188 commit_midpoint = commit_a | |
| 189 | |
| 190 # Add any DEPS changes to the commit lists. | |
| 191 deps_a = commit_a.Deps() | |
| 192 deps_b = commit_b.Deps() | |
| 193 commits_a += sorted( | |
| 194 dep for dep in deps_a.difference(deps_b) | |
| 195 if not _FindCommitWithRepository(commits_a, dep.repository)) | |
| 196 commits_b += sorted( | |
| 197 dep for dep in deps_b.difference(deps_a) | |
| 198 if not _FindCommitWithRepository(commits_b, dep.repository)) | |
| 199 | |
| 200 commits_midpoint.append(commit_midpoint) | |
| 201 | |
| 202 return commits_midpoint | |
| 203 | |
| 204 | |
| 205 def _FindCommitWithRepository(commits, repository): | |
| 206 for commit in commits: | |
| 207 if commit.repository == repository: | |
| 208 return commit | |
| 209 return None | |
| OLD | NEW |