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 """Returns an informal short string representation of this Change.""" | |
32 string = ' '.join(str(commit) for commit in self.commits) | |
33 if self.patch: | |
34 string += ' + ' + str(self.patch) | |
35 return string | |
36 | |
37 @property | |
38 def id_string(self): | |
39 """Returns a string that is unique to this list of commits and patch.""" | |
40 string = ' '.join(commit.id_string for commit in self.commits) | |
41 if self.patch: | |
42 string += ' + ' + self.patch.id_string | |
43 return string | |
44 | |
45 @property | |
46 def base_commit(self): | |
47 return self.commits[0] | |
48 | |
49 @property | |
50 def last_commit(self): | |
51 return self.commits[-1] | |
52 | |
53 @property | |
54 def deps(self): | |
55 return tuple(self.commits[1:]) | |
56 | |
57 def AsDict(self): | |
58 return { | |
59 'commits': [commit.AsDict() for commit in self.commits], | |
60 'patch': self.patch.AsDict() if self.patch else None, | |
61 } | |
62 | |
63 @classmethod | |
64 def FromDict(cls, data): | |
65 commits = tuple(commit_module.Commit.FromDict(commit) | |
66 for commit in data['commits']) | |
67 if 'patch' in data: | |
68 patch = patch_module.Patch.FromDict(data['patch']) | |
69 else: | |
70 patch = None | |
71 | |
72 return cls(commits, patch=patch) | |
73 | |
74 @classmethod | |
75 def Midpoint(cls, change_a, change_b): | |
76 """Returns a Change halfway between the two given Changes. | |
77 | |
78 This function does two passes over the Changes' Commits: | |
79 * The first pass attempts to match the lengths of the Commit lists by | |
80 expanding DEPS to fill in any repositories that are missing from one, | |
81 but included in the other. | |
82 * The second pass takes the midpoint of every matched pair of Commits, | |
83 expanding DEPS rolls as it comes across them. | |
84 | |
85 A NonLinearError is raised if there is no valid midpoint. The Changes are | |
86 not linear if any of the following is true: | |
87 * They have different patches. | |
88 * Their repositories don't match even after expanding DEPS rolls. | |
89 * The left Change comes after the right Change. | |
90 * They are the same or adjacent. | |
91 See change_test.py for examples of linear and nonlinear Changes. | |
92 | |
93 Args: | |
94 change_a: The first Change in the range. | |
95 change_b: The last Change in the range. | |
96 | |
97 Returns: | |
98 A new Change representing the midpoint. | |
99 The Change before the midpoint if the range has an even number of commits. | |
100 | |
101 Raises: | |
102 NonLinearError: The Changes are not linear. | |
103 """ | |
104 if change_a.patch != change_b.patch: | |
105 raise commit_module.NonLinearError( | |
106 'Change A has patch "%s" and Change B has patch "%s".' % | |
107 (change_a.patch, change_b.patch)) | |
108 | |
109 commits_a = list(change_a.commits) | |
110 commits_b = list(change_b.commits) | |
111 | |
112 _ExpandDepsToMatchRepositories(commits_a, commits_b) | |
113 commits_midpoint = _FindMidpoints(commits_a, commits_b) | |
114 | |
115 if commits_a == commits_midpoint: | |
116 raise commit_module.NonLinearError('Changes are the same or adjacent.') | |
117 | |
118 return cls(commits_midpoint, change_a.patch) | |
119 | |
120 | |
121 def _ExpandDepsToMatchRepositories(commits_a, commits_b): | |
122 """Expands DEPS in a Commit list to match the repositories in another. | |
123 | |
124 Given two lists of Commits, with one bigger than the other, this function | |
125 looks through the DEPS files for smaller commit list to fill out any missing | |
126 Commits that are already in the bigger commit list. | |
127 | |
128 Mutates the lists in-place, and doesn't return anything. The lists will not | |
129 have the same size if one Commit list contains a repository that is not found | |
130 in the DEPS of the other Commit list. | |
perezju
2017/09/13 12:52:24
(Let's revisit again on a follow up CL)
Add a tes
dtu
2017/09/13 15:49:26
testDifferingCommitCount is the test for this case
| |
131 | |
132 Example: | |
133 commits_a == [chromium@a, v8@c] | |
134 commits_b == [chromium@b] | |
135 This function looks through the DEPS file at chromium@b to find v8, then | |
136 appends that v8 Commit to commits_b, making the lists match. | |
137 | |
138 Args: | |
139 commits_a: A list of Commits. | |
140 commits_b: A list of Commits. | |
141 """ | |
142 # The lists may be given in any order. Let's make commits_b the bigger list. | |
143 if len(commits_a) > len(commits_b): | |
144 commits_a, commits_b = commits_b, commits_a | |
145 | |
146 # Loop through every DEPS file in commits_a. | |
147 for commit_a in commits_a: | |
148 if len(commits_a) == len(commits_b): | |
149 break | |
150 deps_a = commit_a.Deps() | |
151 | |
152 # Look through commits_b for any extra slots to fill with the DEPS. | |
153 for commit_b in commits_b[len(commits_a):]: | |
154 dep_a = _FindCommitWithRepository(deps_a, commit_b.repository) | |
155 if dep_a: | |
156 commits_a.append(dep_a) | |
157 else: | |
158 break | |
159 | |
160 | |
161 def _FindMidpoints(commits_a, commits_b): | |
162 """Returns the midpoint of two Commit lists. | |
163 | |
164 Loops through each pair of Commits and takes the midpoint. If the repositories | |
165 don't match, a NonLinearError is raised. If the Commits are adjacent and | |
166 represent a DEPS roll, the differing DEPs are added to the end of the lists. | |
167 | |
168 Args: | |
169 commits_a: A list of Commits. | |
170 commits_b: A list of Commits. | |
171 | |
172 Returns: | |
173 A list of Commits, each of which is the midpoint of the respective Commit in | |
174 commits_a and commits_b. | |
175 | |
176 Raises: | |
177 NonLinearError: The lists have a different number of commits even after | |
178 expanding DEPS rolls, a Commit pair contains differing repositories, or a | |
179 Commit pair is in the wrong order. | |
180 """ | |
181 commits_midpoint = [] | |
182 | |
183 for commit_a, commit_b in itertools.izip_longest(commits_a, commits_b): | |
184 if not (commit_a and commit_b): | |
185 # If the commit lists are not the same length, bail out. That could happen | |
186 # if commits_b has a repository that was not found in the DEPS of | |
187 # commits_a (or vice versa); or a DEPS roll added or removed a DEP. | |
188 raise commit_module.NonLinearError( | |
189 'Changes have a different number of commits.') | |
190 | |
191 commit_midpoint = commit_module.Commit.Midpoint(commit_a, commit_b) | |
192 commits_midpoint.append(commit_midpoint) | |
193 if commit_a == commit_midpoint != commit_b: | |
194 # Commits are adjacent. | |
195 # Add any DEPS changes to the commit lists. | |
196 deps_a = commit_a.Deps() | |
197 deps_b = commit_b.Deps() | |
198 commits_a += sorted( | |
perezju
2017/09/13 12:52:24
:( :( :(
dtu
2017/09/13 15:49:26
:( :( :(
| |
199 dep for dep in deps_a.difference(deps_b) | |
200 if not _FindCommitWithRepository(commits_a, dep.repository)) | |
201 commits_b += sorted( | |
202 dep for dep in deps_b.difference(deps_a) | |
203 if not _FindCommitWithRepository(commits_b, dep.repository)) | |
204 | |
205 return commits_midpoint | |
206 | |
207 | |
208 def _FindCommitWithRepository(commits, repository): | |
209 for commit in commits: | |
210 if commit.repository == repository: | |
211 return commit | |
212 return None | |
OLD | NEW |