blob: 8adc54f6fa34290e4bafea433b6f244b903b0fd5 [file] [log] [blame]
James E. Blairee743612012-05-29 14:49:32 -07001# Copyright 2012 Hewlett-Packard Development Company, L.P.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15import re
James E. Blairff986a12012-05-30 14:56:51 -070016import time
James E. Blair4886cc12012-07-18 15:39:41 -070017from uuid import uuid4
James E. Blair5a9918a2013-08-27 10:06:27 -070018import extras
19
20OrderedDict = extras.try_imports(['collections.OrderedDict',
21 'ordereddict.OrderedDict'])
James E. Blair4886cc12012-07-18 15:39:41 -070022
23
James E. Blair19deff22013-08-25 13:17:35 -070024MERGER_MERGE = 1 # "git merge"
25MERGER_MERGE_RESOLVE = 2 # "git merge -s resolve"
26MERGER_CHERRY_PICK = 3 # "git cherry-pick"
27
28MERGER_MAP = {
29 'merge': MERGER_MERGE,
30 'merge-resolve': MERGER_MERGE_RESOLVE,
31 'cherry-pick': MERGER_CHERRY_PICK,
32}
James E. Blairee743612012-05-29 14:49:32 -070033
James E. Blair64ed6f22013-07-10 14:07:23 -070034PRECEDENCE_NORMAL = 0
35PRECEDENCE_LOW = 1
36PRECEDENCE_HIGH = 2
37
38PRECEDENCE_MAP = {
39 None: PRECEDENCE_NORMAL,
40 'low': PRECEDENCE_LOW,
41 'normal': PRECEDENCE_NORMAL,
42 'high': PRECEDENCE_HIGH,
43}
44
James E. Blair1e8dd892012-05-30 09:15:05 -070045
James E. Blairc053d022014-01-22 14:57:33 -080046def time_to_seconds(s):
47 if s.endswith('s'):
48 return int(s[:-1])
49 if s.endswith('m'):
50 return int(s[:-1]) * 60
51 if s.endswith('h'):
52 return int(s[:-1]) * 60 * 60
53 if s.endswith('d'):
54 return int(s[:-1]) * 24 * 60 * 60
55 if s.endswith('w'):
56 return int(s[:-1]) * 7 * 24 * 60 * 60
57 raise Exception("Unable to parse time value: %s" % s)
58
59
James E. Blair11041d22014-05-02 14:49:53 -070060def normalizeCategory(name):
61 name = name.lower()
62 return re.sub(' ', '-', name)
63
64
James E. Blair4aea70c2012-07-26 14:23:24 -070065class Pipeline(object):
66 """A top-level pipeline such as check, gate, post, etc."""
67 def __init__(self, name):
68 self.name = name
James E. Blair8dbd56a2012-12-22 10:55:10 -080069 self.description = None
James E. Blair56370192013-01-14 15:47:28 -080070 self.failure_message = None
Joshua Heskethb7179772014-01-30 23:30:46 +110071 self.merge_failure_message = None
James E. Blair56370192013-01-14 15:47:28 -080072 self.success_message = None
Joshua Hesketh3979e3e2014-03-04 11:21:10 +110073 self.footer_message = None
James E. Blair2fa50962013-01-30 21:50:41 -080074 self.dequeue_on_new_patchset = True
James E. Blair4aea70c2012-07-26 14:23:24 -070075 self.job_trees = {} # project -> JobTree
76 self.manager = None
James E. Blaire0487072012-08-29 17:38:31 -070077 self.queues = []
James E. Blair64ed6f22013-07-10 14:07:23 -070078 self.precedence = PRECEDENCE_NORMAL
James E. Blair6c358e72013-07-29 17:06:47 -070079 self.trigger = None
Joshua Hesketh1879cf72013-08-19 14:13:15 +100080 self.start_actions = None
81 self.success_actions = None
82 self.failure_actions = None
Clark Boylan7603a372014-01-21 11:43:20 -080083 self.window = None
84 self.window_floor = None
85 self.window_increase_type = None
86 self.window_increase_factor = None
87 self.window_decrease_type = None
88 self.window_decrease_factor = None
James E. Blair4aea70c2012-07-26 14:23:24 -070089
James E. Blaird09c17a2012-08-07 09:23:14 -070090 def __repr__(self):
91 return '<Pipeline %s>' % self.name
92
James E. Blair4aea70c2012-07-26 14:23:24 -070093 def setManager(self, manager):
94 self.manager = manager
95
96 def addProject(self, project):
97 job_tree = JobTree(None) # Null job == job tree root
98 self.job_trees[project] = job_tree
99 return job_tree
100
101 def getProjects(self):
James E. Blairc3d428e2013-12-03 15:06:48 -0800102 return sorted(self.job_trees.keys(), lambda a, b: cmp(a.name, b.name))
James E. Blair4aea70c2012-07-26 14:23:24 -0700103
James E. Blaire0487072012-08-29 17:38:31 -0700104 def addQueue(self, queue):
105 self.queues.append(queue)
106
107 def getQueue(self, project):
108 for queue in self.queues:
109 if project in queue.projects:
110 return queue
111 return None
112
James E. Blair4aea70c2012-07-26 14:23:24 -0700113 def getJobTree(self, project):
114 tree = self.job_trees.get(project)
115 return tree
116
117 def getJobs(self, changeish):
James E. Blaird09c17a2012-08-07 09:23:14 -0700118 tree = self.getJobTree(changeish.project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700119 if not tree:
120 return []
121 return changeish.filterJobs(tree.getJobs())
122
James E. Blairfee8d652013-06-07 08:57:52 -0700123 def _findJobsToRun(self, job_trees, item):
James E. Blair4aea70c2012-07-26 14:23:24 -0700124 torun = []
James E. Blairfee8d652013-06-07 08:57:52 -0700125 if item.item_ahead:
James E. Blair4aea70c2012-07-26 14:23:24 -0700126 # Only run jobs if any 'hold' jobs on the change ahead
127 # have completed successfully.
James E. Blairfee8d652013-06-07 08:57:52 -0700128 if self.isHoldingFollowingChanges(item.item_ahead):
James E. Blair4aea70c2012-07-26 14:23:24 -0700129 return []
130 for tree in job_trees:
131 job = tree.job
132 result = None
133 if job:
James E. Blairfee8d652013-06-07 08:57:52 -0700134 if not job.changeMatches(item.change):
James E. Blair4aea70c2012-07-26 14:23:24 -0700135 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700136 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700137 if build:
138 result = build.result
139 else:
140 # There is no build for the root of this job tree,
141 # so we should run it.
142 torun.append(job)
143 # If there is no job, this is a null job tree, and we should
144 # run all of its jobs.
145 if result == 'SUCCESS' or not job:
James E. Blairfee8d652013-06-07 08:57:52 -0700146 torun.extend(self._findJobsToRun(tree.job_trees, item))
James E. Blair4aea70c2012-07-26 14:23:24 -0700147 return torun
148
James E. Blairfee8d652013-06-07 08:57:52 -0700149 def findJobsToRun(self, item):
150 tree = self.getJobTree(item.change.project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700151 if not tree:
152 return []
James E. Blairfee8d652013-06-07 08:57:52 -0700153 return self._findJobsToRun(tree.job_trees, item)
James E. Blair4aea70c2012-07-26 14:23:24 -0700154
James E. Blairbea9ef12013-07-15 11:52:23 -0700155 def haveAllJobsStarted(self, item):
156 for job in self.getJobs(item.change):
157 build = item.current_build_set.getBuild(job.name)
158 if not build or not build.start_time:
159 return False
160 return True
161
James E. Blairfee8d652013-06-07 08:57:52 -0700162 def areAllJobsComplete(self, item):
163 for job in self.getJobs(item.change):
164 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700165 if not build or not build.result:
166 return False
167 return True
168
James E. Blairfee8d652013-06-07 08:57:52 -0700169 def didAllJobsSucceed(self, item):
170 for job in self.getJobs(item.change):
James E. Blair4ec821f2012-08-23 15:28:28 -0700171 if not job.voting:
172 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700173 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700174 if not build:
175 return False
176 if build.result != 'SUCCESS':
177 return False
178 return True
179
Joshua Heskethb7179772014-01-30 23:30:46 +1100180 def didMergerSucceed(self, item):
181 if item.current_build_set.unable_to_merge:
182 return False
183 return True
184
James E. Blairfee8d652013-06-07 08:57:52 -0700185 def didAnyJobFail(self, item):
186 for job in self.getJobs(item.change):
James E. Blair4ec821f2012-08-23 15:28:28 -0700187 if not job.voting:
188 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700189 build = item.current_build_set.getBuild(job.name)
James E. Blair0018a6c2013-02-27 14:11:45 -0800190 if build and build.result and (build.result != 'SUCCESS'):
James E. Blair4aea70c2012-07-26 14:23:24 -0700191 return True
192 return False
193
James E. Blairfee8d652013-06-07 08:57:52 -0700194 def isHoldingFollowingChanges(self, item):
195 for job in self.getJobs(item.change):
James E. Blair4aea70c2012-07-26 14:23:24 -0700196 if not job.hold_following_changes:
197 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700198 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700199 if not build:
200 return True
201 if build.result != 'SUCCESS':
202 return True
James E. Blair972e3c72013-08-29 12:04:55 -0700203
James E. Blairfee8d652013-06-07 08:57:52 -0700204 if not item.item_ahead:
James E. Blair4aea70c2012-07-26 14:23:24 -0700205 return False
James E. Blairfee8d652013-06-07 08:57:52 -0700206 return self.isHoldingFollowingChanges(item.item_ahead)
James E. Blair4aea70c2012-07-26 14:23:24 -0700207
James E. Blairfee8d652013-06-07 08:57:52 -0700208 def setResult(self, item, build):
James E. Blair4a28a882013-08-23 15:17:33 -0700209 if build.retry:
210 item.removeBuild(build)
211 elif build.result != 'SUCCESS':
James E. Blair4aea70c2012-07-26 14:23:24 -0700212 # Get a JobTree from a Job so we can find only its dependent jobs
James E. Blairfee8d652013-06-07 08:57:52 -0700213 root = self.getJobTree(item.change.project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700214 tree = root.getJobTreeForJob(build.job)
215 for job in tree.getJobs():
216 fakebuild = Build(job, None)
217 fakebuild.result = 'SKIPPED'
James E. Blairfee8d652013-06-07 08:57:52 -0700218 item.addBuild(fakebuild)
James E. Blair4aea70c2012-07-26 14:23:24 -0700219
Joshua Heskethb7179772014-01-30 23:30:46 +1100220 def setUnableToMerge(self, item):
James E. Blairfee8d652013-06-07 08:57:52 -0700221 item.current_build_set.unable_to_merge = True
222 root = self.getJobTree(item.change.project)
James E. Blair973721f2012-08-15 10:19:43 -0700223 for job in root.getJobs():
224 fakebuild = Build(job, None)
225 fakebuild.result = 'SKIPPED'
James E. Blairfee8d652013-06-07 08:57:52 -0700226 item.addBuild(fakebuild)
James E. Blair973721f2012-08-15 10:19:43 -0700227
James E. Blairfee8d652013-06-07 08:57:52 -0700228 def setDequeuedNeedingChange(self, item):
229 item.dequeued_needing_change = True
230 root = self.getJobTree(item.change.project)
James E. Blaircaec0c52012-08-22 14:52:22 -0700231 for job in root.getJobs():
232 fakebuild = Build(job, None)
233 fakebuild.result = 'SKIPPED'
James E. Blairfee8d652013-06-07 08:57:52 -0700234 item.addBuild(fakebuild)
James E. Blaircaec0c52012-08-22 14:52:22 -0700235
James E. Blaire0487072012-08-29 17:38:31 -0700236 def getChangesInQueue(self):
237 changes = []
238 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700239 changes.extend([x.change for x in shared_queue.queue])
James E. Blaire0487072012-08-29 17:38:31 -0700240 return changes
241
James E. Blairfee8d652013-06-07 08:57:52 -0700242 def getAllItems(self):
243 items = []
James E. Blaire0487072012-08-29 17:38:31 -0700244 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700245 items.extend(shared_queue.queue)
James E. Blairfee8d652013-06-07 08:57:52 -0700246 return items
James E. Blaire0487072012-08-29 17:38:31 -0700247
James E. Blair8dbd56a2012-12-22 10:55:10 -0800248 def formatStatusJSON(self):
249 j_pipeline = dict(name=self.name,
250 description=self.description)
251 j_queues = []
252 j_pipeline['change_queues'] = j_queues
253 for queue in self.queues:
254 j_queue = dict(name=queue.name)
255 j_queues.append(j_queue)
256 j_queue['heads'] = []
Clark Boylanaf2476f2014-01-23 14:47:36 -0800257 j_queue['window'] = queue.window
258 j_queue['dependent'] = queue.dependent
James E. Blair972e3c72013-08-29 12:04:55 -0700259
260 j_changes = []
261 for e in queue.queue:
262 if not e.item_ahead:
263 if j_changes:
264 j_queue['heads'].append(j_changes)
265 j_changes = []
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800266 j_changes.append(e.formatJSON())
James E. Blair972e3c72013-08-29 12:04:55 -0700267 if (len(j_changes) > 1 and
268 (j_changes[-2]['remaining_time'] is not None) and
269 (j_changes[-1]['remaining_time'] is not None)):
270 j_changes[-1]['remaining_time'] = max(
271 j_changes[-2]['remaining_time'],
272 j_changes[-1]['remaining_time'])
273 if j_changes:
James E. Blair8dbd56a2012-12-22 10:55:10 -0800274 j_queue['heads'].append(j_changes)
275 return j_pipeline
276
James E. Blair4aea70c2012-07-26 14:23:24 -0700277
Joshua Hesketh1879cf72013-08-19 14:13:15 +1000278class ActionReporter(object):
Alex Gaynor813d39b2014-05-17 16:17:16 -0700279 """An ActionReporter has a reporter and its configured parameters"""
Joshua Hesketh1879cf72013-08-19 14:13:15 +1000280
281 def __repr__(self):
282 return '<ActionReporter %s, %s>' % (self.reporter, self.params)
283
284 def __init__(self, reporter, params):
285 self.reporter = reporter
286 self.params = params
287
288 def report(self, change, message):
289 """Sends the built message off to the configured reporter.
290 Takes the change and message and adds the configured parameters.
291 """
292 return self.reporter.report(change, message, self.params)
293
294 def getSubmitAllowNeeds(self):
295 """Gets the submit allow needs from the reporter based off the
296 parameters."""
297 return self.reporter.getSubmitAllowNeeds(self.params)
298
299
James E. Blairee743612012-05-29 14:49:32 -0700300class ChangeQueue(object):
James E. Blair4aea70c2012-07-26 14:23:24 -0700301 """DependentPipelines have multiple parallel queues shared by
302 different projects; this is one of them. For instance, there may
303 a queue shared by interrelated projects foo and bar, and a second
304 queue for independent project baz. Pipelines have one or more
305 PipelineQueues."""
Clark Boylan7603a372014-01-21 11:43:20 -0800306 def __init__(self, pipeline, dependent=True, window=0, window_floor=1,
307 window_increase_type='linear', window_increase_factor=1,
308 window_decrease_type='exponential', window_decrease_factor=2):
James E. Blair4aea70c2012-07-26 14:23:24 -0700309 self.pipeline = pipeline
James E. Blairee743612012-05-29 14:49:32 -0700310 self.name = ''
James E. Blairc8a1e052014-02-25 09:29:26 -0800311 self.assigned_name = None
312 self.generated_name = None
James E. Blairee743612012-05-29 14:49:32 -0700313 self.projects = []
314 self._jobs = set()
315 self.queue = []
James E. Blaire0487072012-08-29 17:38:31 -0700316 self.dependent = dependent
Clark Boylan7603a372014-01-21 11:43:20 -0800317 self.window = window
318 self.window_floor = window_floor
319 self.window_increase_type = window_increase_type
320 self.window_increase_factor = window_increase_factor
321 self.window_decrease_type = window_decrease_type
322 self.window_decrease_factor = window_decrease_factor
James E. Blairee743612012-05-29 14:49:32 -0700323
James E. Blair9f9667e2012-06-12 17:51:08 -0700324 def __repr__(self):
James E. Blair4aea70c2012-07-26 14:23:24 -0700325 return '<ChangeQueue %s: %s>' % (self.pipeline.name, self.name)
James E. Blairee743612012-05-29 14:49:32 -0700326
327 def getJobs(self):
328 return self._jobs
329
330 def addProject(self, project):
331 if project not in self.projects:
332 self.projects.append(project)
James E. Blairc8a1e052014-02-25 09:29:26 -0800333 self._jobs |= set(self.pipeline.getJobTree(project).getJobs())
334
James E. Blairee743612012-05-29 14:49:32 -0700335 names = [x.name for x in self.projects]
336 names.sort()
James E. Blairc8a1e052014-02-25 09:29:26 -0800337 self.generated_name = ', '.join(names)
338
339 for job in self._jobs:
340 if job.queue_name:
341 if (self.assigned_name and
342 job.queue_name != self.assigned_name):
343 raise Exception("More than one name assigned to "
344 "change queue: %s != %s" %
345 (self.assigned_name, job.queue_name))
346 self.assigned_name = job.queue_name
347 self.name = self.assigned_name or self.generated_name
James E. Blairee743612012-05-29 14:49:32 -0700348
349 def enqueueChange(self, change):
James E. Blair4a035d92014-01-23 13:10:48 -0800350 item = QueueItem(self.pipeline, change)
James E. Blaircdccd972013-07-01 12:10:22 -0700351 self.enqueueItem(item)
352 item.enqueue_time = time.time()
353 return item
354
355 def enqueueItem(self, item):
James E. Blair4a035d92014-01-23 13:10:48 -0800356 item.pipeline = self.pipeline
James E. Blair75241582012-08-31 12:16:55 -0700357 if self.dependent and self.queue:
James E. Blairfee8d652013-06-07 08:57:52 -0700358 item.item_ahead = self.queue[-1]
James E. Blair972e3c72013-08-29 12:04:55 -0700359 item.item_ahead.items_behind.append(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700360 self.queue.append(item)
James E. Blairee743612012-05-29 14:49:32 -0700361
James E. Blairfee8d652013-06-07 08:57:52 -0700362 def dequeueItem(self, item):
363 if item in self.queue:
364 self.queue.remove(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700365 if item.item_ahead:
James E. Blair972e3c72013-08-29 12:04:55 -0700366 item.item_ahead.items_behind.remove(item)
367 for item_behind in item.items_behind:
368 if item.item_ahead:
369 item.item_ahead.items_behind.append(item_behind)
370 item_behind.item_ahead = item.item_ahead
James E. Blairfee8d652013-06-07 08:57:52 -0700371 item.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700372 item.items_behind = []
James E. Blairfee8d652013-06-07 08:57:52 -0700373 item.dequeue_time = time.time()
James E. Blaire0487072012-08-29 17:38:31 -0700374
James E. Blair972e3c72013-08-29 12:04:55 -0700375 def moveItem(self, item, item_ahead):
376 if not self.dependent:
377 return False
378 if item.item_ahead == item_ahead:
379 return False
380 # Remove from current location
381 if item.item_ahead:
382 item.item_ahead.items_behind.remove(item)
383 for item_behind in item.items_behind:
384 if item.item_ahead:
385 item.item_ahead.items_behind.append(item_behind)
386 item_behind.item_ahead = item.item_ahead
387 # Add to new location
388 item.item_ahead = item_ahead
James E. Blair00451262013-09-20 11:40:17 -0700389 item.items_behind = []
James E. Blair972e3c72013-08-29 12:04:55 -0700390 if item.item_ahead:
391 item.item_ahead.items_behind.append(item)
392 return True
James E. Blairee743612012-05-29 14:49:32 -0700393
394 def mergeChangeQueue(self, other):
395 for project in other.projects:
396 self.addProject(project)
Clark Boylan7603a372014-01-21 11:43:20 -0800397 self.window = min(self.window, other.window)
398 # TODO merge semantics
399
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800400 def isActionable(self, item):
Clark Boylan7603a372014-01-21 11:43:20 -0800401 if self.dependent and self.window:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800402 return item in self.queue[:self.window]
Clark Boylan7603a372014-01-21 11:43:20 -0800403 else:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800404 return True
Clark Boylan7603a372014-01-21 11:43:20 -0800405
406 def increaseWindowSize(self):
407 if self.dependent:
408 if self.window_increase_type == 'linear':
409 self.window += self.window_increase_factor
410 elif self.window_increase_type == 'exponential':
411 self.window *= self.window_increase_factor
412
413 def decreaseWindowSize(self):
414 if self.dependent:
415 if self.window_decrease_type == 'linear':
416 self.window = max(
417 self.window_floor,
418 self.window - self.window_decrease_factor)
419 elif self.window_decrease_type == 'exponential':
420 self.window = max(
421 self.window_floor,
422 self.window / self.window_decrease_factor)
James E. Blairee743612012-05-29 14:49:32 -0700423
James E. Blair1e8dd892012-05-30 09:15:05 -0700424
James E. Blair4aea70c2012-07-26 14:23:24 -0700425class Project(object):
426 def __init__(self, name):
427 self.name = name
James E. Blair19deff22013-08-25 13:17:35 -0700428 self.merge_mode = MERGER_MERGE_RESOLVE
James E. Blair4aea70c2012-07-26 14:23:24 -0700429
430 def __str__(self):
431 return self.name
432
433 def __repr__(self):
434 return '<Project %s>' % (self.name)
435
436
James E. Blairee743612012-05-29 14:49:32 -0700437class Job(object):
438 def __init__(self, name):
James E. Blair222d4982012-07-16 09:31:19 -0700439 # If you add attributes here, be sure to add them to the copy method.
James E. Blairee743612012-05-29 14:49:32 -0700440 self.name = name
James E. Blairc8a1e052014-02-25 09:29:26 -0800441 self.queue_name = None
James E. Blairee743612012-05-29 14:49:32 -0700442 self.failure_message = None
443 self.success_message = None
James E. Blair6aea36d2012-12-17 13:03:24 -0800444 self.failure_pattern = None
445 self.success_pattern = None
James E. Blaire5a847f2012-07-10 15:29:14 -0700446 self.parameter_function = None
James E. Blair222d4982012-07-16 09:31:19 -0700447 self.hold_following_changes = False
James E. Blair4ec821f2012-08-23 15:28:28 -0700448 self.voting = True
James E. Blaire421a232012-07-25 16:59:21 -0700449 self.branches = []
450 self._branches = []
James E. Blair70c71582013-03-06 08:50:50 -0800451 self.files = []
452 self._files = []
Joshua Hesketh36c3fa52014-01-22 11:40:52 +1100453 self.swift = {}
James E. Blairee743612012-05-29 14:49:32 -0700454
455 def __str__(self):
456 return self.name
457
458 def __repr__(self):
459 return '<Job %s>' % (self.name)
460
James E. Blairb0954652012-06-01 11:32:01 -0700461 def copy(self, other):
James E. Blairc28d1b02013-07-19 11:37:06 -0700462 if other.failure_message:
463 self.failure_message = other.failure_message
464 if other.success_message:
465 self.success_message = other.success_message
466 if other.failure_pattern:
467 self.failure_pattern = other.failure_pattern
468 if other.success_pattern:
469 self.success_pattern = other.success_pattern
470 if other.parameter_function:
471 self.parameter_function = other.parameter_function
472 if other.branches:
473 self.branches = other.branches[:]
474 self._branches = other._branches[:]
475 if other.files:
476 self.files = other.files[:]
477 self._files = other._files[:]
Joshua Hesketh36c3fa52014-01-22 11:40:52 +1100478 if other.swift:
479 self.swift.update(other.swift)
James E. Blair222d4982012-07-16 09:31:19 -0700480 self.hold_following_changes = other.hold_following_changes
James E. Blair4ec821f2012-08-23 15:28:28 -0700481 self.voting = other.voting
James E. Blairb0954652012-06-01 11:32:01 -0700482
James E. Blaire421a232012-07-25 16:59:21 -0700483 def changeMatches(self, change):
James E. Blair70c71582013-03-06 08:50:50 -0800484 matches_branch = False
James E. Blaire421a232012-07-25 16:59:21 -0700485 for branch in self.branches:
James E. Blair45865f32012-10-05 09:39:46 -0700486 if hasattr(change, 'branch') and branch.match(change.branch):
James E. Blair70c71582013-03-06 08:50:50 -0800487 matches_branch = True
James E. Blair45865f32012-10-05 09:39:46 -0700488 if hasattr(change, 'ref') and branch.match(change.ref):
James E. Blair70c71582013-03-06 08:50:50 -0800489 matches_branch = True
490 if self.branches and not matches_branch:
491 return False
492
493 matches_file = False
494 for f in self.files:
495 if hasattr(change, 'files'):
496 for cf in change.files:
497 if f.match(cf):
498 matches_file = True
499 if self.files and not matches_file:
500 return False
501
502 return True
James E. Blaire5a847f2012-07-10 15:29:14 -0700503
James E. Blair1e8dd892012-05-30 09:15:05 -0700504
James E. Blairee743612012-05-29 14:49:32 -0700505class JobTree(object):
506 """ A JobTree represents an instance of one Job, and holds JobTrees
507 whose jobs should be run if that Job succeeds. A root node of a
508 JobTree will have no associated Job. """
509
510 def __init__(self, job):
511 self.job = job
512 self.job_trees = []
513
514 def addJob(self, job):
James E. Blair12a92b12014-03-26 11:54:53 -0700515 if job not in [x.job for x in self.job_trees]:
516 t = JobTree(job)
517 self.job_trees.append(t)
518 return t
James E. Blairee743612012-05-29 14:49:32 -0700519
520 def getJobs(self):
521 jobs = []
522 for x in self.job_trees:
523 jobs.append(x.job)
524 jobs.extend(x.getJobs())
525 return jobs
526
527 def getJobTreeForJob(self, job):
528 if self.job == job:
529 return self
530 for tree in self.job_trees:
531 ret = tree.getJobTreeForJob(job)
532 if ret:
533 return ret
534 return None
535
James E. Blair1e8dd892012-05-30 09:15:05 -0700536
James E. Blair4aea70c2012-07-26 14:23:24 -0700537class Build(object):
538 def __init__(self, job, uuid):
539 self.job = job
540 self.uuid = uuid
James E. Blair4aea70c2012-07-26 14:23:24 -0700541 self.url = None
542 self.number = None
543 self.result = None
544 self.build_set = None
545 self.launch_time = time.time()
James E. Blair71e94122012-12-24 17:53:08 -0800546 self.start_time = None
547 self.end_time = None
James E. Blairbea9ef12013-07-15 11:52:23 -0700548 self.estimated_time = None
James E. Blair66eeebf2013-07-27 17:44:32 -0700549 self.pipeline = None
James E. Blair0aac4872013-08-23 14:02:38 -0700550 self.canceled = False
James E. Blair4a28a882013-08-23 15:17:33 -0700551 self.retry = False
James E. Blaird78576a2013-07-09 10:39:17 -0700552 self.parameters = {}
Joshua Heskethba8776a2014-01-12 14:35:40 +0800553 self.worker = Worker()
James E. Blairee743612012-05-29 14:49:32 -0700554
555 def __repr__(self):
Joshua Heskethba8776a2014-01-12 14:35:40 +0800556 return ('<Build %s of %s on %s>' %
557 (self.uuid, self.job.name, self.worker))
558
559
560class Worker(object):
561 """A model of the worker running a job"""
562 def __init__(self):
563 self.name = "Unknown"
564 self.hostname = None
565 self.ips = []
566 self.fqdn = None
567 self.program = None
568 self.version = None
569 self.extra = {}
570
571 def updateFromData(self, data):
572 """Update worker information if contained in the WORK_DATA response."""
573 self.name = data.get('worker_name', self.name)
574 self.hostname = data.get('worker_hostname', self.hostname)
575 self.ips = data.get('worker_ips', self.ips)
576 self.fqdn = data.get('worker_fqdn', self.fqdn)
577 self.program = data.get('worker_program', self.program)
578 self.version = data.get('worker_version', self.version)
579 self.extra = data.get('worker_extra', self.extra)
580
581 def __repr__(self):
582 return '<Worker %s>' % self.name
James E. Blairee743612012-05-29 14:49:32 -0700583
James E. Blair1e8dd892012-05-30 09:15:05 -0700584
James E. Blair7e530ad2012-07-03 16:12:28 -0700585class BuildSet(object):
James E. Blair4076e2b2014-01-28 12:42:20 -0800586 # Merge states:
587 NEW = 1
588 PENDING = 2
589 COMPLETE = 3
590
James E. Blairfee8d652013-06-07 08:57:52 -0700591 def __init__(self, item):
592 self.item = item
James E. Blair11700c32012-07-05 17:50:05 -0700593 self.other_changes = []
James E. Blair7e530ad2012-07-03 16:12:28 -0700594 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -0700595 self.result = None
596 self.next_build_set = None
597 self.previous_build_set = None
James E. Blair4886cc12012-07-18 15:39:41 -0700598 self.ref = None
James E. Blair81515ad2012-10-01 18:29:08 -0700599 self.commit = None
James E. Blair4076e2b2014-01-28 12:42:20 -0800600 self.zuul_url = None
James E. Blair973721f2012-08-15 10:19:43 -0700601 self.unable_to_merge = False
James E. Blair972e3c72013-08-29 12:04:55 -0700602 self.failing_reasons = []
James E. Blair4076e2b2014-01-28 12:42:20 -0800603 self.merge_state = self.NEW
James E. Blair7e530ad2012-07-03 16:12:28 -0700604
James E. Blair4886cc12012-07-18 15:39:41 -0700605 def setConfiguration(self):
James E. Blair11700c32012-07-05 17:50:05 -0700606 # The change isn't enqueued until after it's created
607 # so we don't know what the other changes ahead will be
608 # until jobs start.
609 if not self.other_changes:
James E. Blairfee8d652013-06-07 08:57:52 -0700610 next_item = self.item.item_ahead
611 while next_item:
612 self.other_changes.append(next_item.change)
613 next_item = next_item.item_ahead
James E. Blair4886cc12012-07-18 15:39:41 -0700614 if not self.ref:
615 self.ref = 'Z' + uuid4().hex
616
James E. Blair4886cc12012-07-18 15:39:41 -0700617 def addBuild(self, build):
618 self.builds[build.job.name] = build
619 build.build_set = self
James E. Blair11700c32012-07-05 17:50:05 -0700620
James E. Blair4a28a882013-08-23 15:17:33 -0700621 def removeBuild(self, build):
622 del self.builds[build.job.name]
623
James E. Blair7e530ad2012-07-03 16:12:28 -0700624 def getBuild(self, job_name):
625 return self.builds.get(job_name)
626
James E. Blair11700c32012-07-05 17:50:05 -0700627 def getBuilds(self):
628 keys = self.builds.keys()
629 keys.sort()
630 return [self.builds.get(x) for x in keys]
631
James E. Blair7e530ad2012-07-03 16:12:28 -0700632
James E. Blairfee8d652013-06-07 08:57:52 -0700633class QueueItem(object):
634 """A changish inside of a Pipeline queue"""
James E. Blair32663402012-06-01 10:04:18 -0700635
James E. Blair4a035d92014-01-23 13:10:48 -0800636 def __init__(self, pipeline, change):
James E. Blairfee8d652013-06-07 08:57:52 -0700637 self.pipeline = pipeline
638 self.change = change # a changeish
James E. Blair7e530ad2012-07-03 16:12:28 -0700639 self.build_sets = []
James E. Blaircaec0c52012-08-22 14:52:22 -0700640 self.dequeued_needing_change = False
James E. Blair11700c32012-07-05 17:50:05 -0700641 self.current_build_set = BuildSet(self)
642 self.build_sets.append(self.current_build_set)
James E. Blairfee8d652013-06-07 08:57:52 -0700643 self.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700644 self.items_behind = []
James E. Blair8fa16972013-01-15 16:57:20 -0800645 self.enqueue_time = None
646 self.dequeue_time = None
James E. Blairfee8d652013-06-07 08:57:52 -0700647 self.reported = False
Clark Boylanaf2476f2014-01-23 14:47:36 -0800648 self.active = False
James E. Blaire5a847f2012-07-10 15:29:14 -0700649
James E. Blair972e3c72013-08-29 12:04:55 -0700650 def __repr__(self):
651 if self.pipeline:
652 pipeline = self.pipeline.name
653 else:
654 pipeline = None
655 return '<QueueItem 0x%x for %s in %s>' % (
656 id(self), self.change, pipeline)
657
James E. Blairee743612012-05-29 14:49:32 -0700658 def resetAllBuilds(self):
James E. Blair11700c32012-07-05 17:50:05 -0700659 old = self.current_build_set
660 self.current_build_set.result = 'CANCELED'
661 self.current_build_set = BuildSet(self)
662 old.next_build_set = self.current_build_set
663 self.current_build_set.previous_build_set = old
James E. Blair7e530ad2012-07-03 16:12:28 -0700664 self.build_sets.append(self.current_build_set)
James E. Blairee743612012-05-29 14:49:32 -0700665
666 def addBuild(self, build):
James E. Blair7e530ad2012-07-03 16:12:28 -0700667 self.current_build_set.addBuild(build)
James E. Blair66eeebf2013-07-27 17:44:32 -0700668 build.pipeline = self.pipeline
James E. Blairee743612012-05-29 14:49:32 -0700669
James E. Blair4a28a882013-08-23 15:17:33 -0700670 def removeBuild(self, build):
671 self.current_build_set.removeBuild(build)
672
James E. Blairfee8d652013-06-07 08:57:52 -0700673 def setReportedResult(self, result):
674 self.current_build_set.result = result
675
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800676 def formatJSON(self):
677 changeish = self.change
678 ret = {}
679 ret['active'] = self.active
680 if hasattr(changeish, 'url') and changeish.url is not None:
681 ret['url'] = changeish.url
682 else:
683 ret['url'] = None
684 ret['id'] = changeish._id()
685 if self.item_ahead:
686 ret['item_ahead'] = self.item_ahead.change._id()
687 else:
688 ret['item_ahead'] = None
689 ret['items_behind'] = [i.change._id() for i in self.items_behind]
690 ret['failing_reasons'] = self.current_build_set.failing_reasons
691 ret['zuul_ref'] = self.current_build_set.ref
692 ret['project'] = changeish.project.name
693 ret['enqueue_time'] = int(self.enqueue_time * 1000)
694 ret['jobs'] = []
695 max_remaining = 0
696 for job in self.pipeline.getJobs(changeish):
697 now = time.time()
698 build = self.current_build_set.getBuild(job.name)
699 elapsed = None
700 remaining = None
701 result = None
702 url = None
703 worker = None
704 if build:
705 result = build.result
706 url = build.url
707 if build.start_time:
708 if build.end_time:
709 elapsed = int((build.end_time -
710 build.start_time) * 1000)
711 remaining = 0
712 else:
713 elapsed = int((now - build.start_time) * 1000)
714 if build.estimated_time:
715 remaining = max(
716 int(build.estimated_time * 1000) - elapsed,
717 0)
718 worker = {
719 'name': build.worker.name,
720 'hostname': build.worker.hostname,
721 'ips': build.worker.ips,
722 'fqdn': build.worker.fqdn,
723 'program': build.worker.program,
724 'version': build.worker.version,
725 'extra': build.worker.extra
726 }
727 if remaining and remaining > max_remaining:
728 max_remaining = remaining
729
730 ret['jobs'].append({
731 'name': job.name,
732 'elapsed_time': elapsed,
733 'remaining_time': remaining,
734 'url': url,
735 'result': result,
736 'voting': job.voting,
737 'uuid': build.uuid if build else None,
738 'launch_time': build.launch_time if build else None,
739 'start_time': build.start_time if build else None,
740 'end_time': build.end_time if build else None,
741 'estimated_time': build.estimated_time if build else None,
742 'pipeline': build.pipeline.name if build else None,
743 'canceled': build.canceled if build else None,
744 'retry': build.retry if build else None,
745 'number': build.number if build else None,
746 'parameters': build.parameters if build else None,
747 'worker': worker
748 })
749
750 if self.pipeline.haveAllJobsStarted(self):
751 ret['remaining_time'] = max_remaining
752 else:
753 ret['remaining_time'] = None
754 return ret
755
756 def formatStatus(self, indent=0, html=False):
757 changeish = self.change
758 indent_str = ' ' * indent
759 ret = ''
760 if html and hasattr(changeish, 'url') and changeish.url is not None:
761 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
762 indent_str,
763 changeish.project.name,
764 changeish.url,
765 changeish._id())
766 else:
767 ret += '%sProject %s change %s based on %s\n' % (
768 indent_str,
769 changeish.project.name,
770 changeish._id(),
771 self.item_ahead)
772 for job in self.pipeline.getJobs(changeish):
773 build = self.current_build_set.getBuild(job.name)
774 if build:
775 result = build.result
776 else:
777 result = None
778 job_name = job.name
779 if not job.voting:
780 voting = ' (non-voting)'
781 else:
782 voting = ''
783 if html:
784 if build:
785 url = build.url
786 else:
787 url = None
788 if url is not None:
789 job_name = '<a href="%s">%s</a>' % (url, job_name)
790 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
791 ret += '\n'
792 return ret
793
James E. Blairfee8d652013-06-07 08:57:52 -0700794
795class Changeish(object):
796 """Something like a change; either a change or a ref"""
James E. Blairfee8d652013-06-07 08:57:52 -0700797
798 def __init__(self, project):
799 self.project = project
800
Joshua Hesketh36c3fa52014-01-22 11:40:52 +1100801 def getBasePath(self):
802 base_path = ''
803 if hasattr(self, 'refspec'):
804 base_path = "%s/%s/%s" % (
805 self.number[-2:], self.number, self.patchset)
806 elif hasattr(self, 'ref'):
807 base_path = "%s/%s" % (self.newrev[:2], self.newrev)
808
809 return base_path
810
James E. Blairfee8d652013-06-07 08:57:52 -0700811 def equals(self, other):
812 raise NotImplementedError()
813
814 def isUpdateOf(self, other):
815 raise NotImplementedError()
816
817 def filterJobs(self, jobs):
818 return filter(lambda job: job.changeMatches(self), jobs)
819
820 def getRelatedChanges(self):
821 return set()
822
James E. Blair1e8dd892012-05-30 09:15:05 -0700823
James E. Blair4aea70c2012-07-26 14:23:24 -0700824class Change(Changeish):
James E. Blair4aea70c2012-07-26 14:23:24 -0700825 def __init__(self, project):
826 super(Change, self).__init__(project)
827 self.branch = None
828 self.number = None
829 self.url = None
830 self.patchset = None
831 self.refspec = None
832
James E. Blair70c71582013-03-06 08:50:50 -0800833 self.files = []
James E. Blair4aea70c2012-07-26 14:23:24 -0700834 self.needs_change = None
835 self.needed_by_changes = []
836 self.is_current_patchset = True
837 self.can_merge = False
838 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -0700839 self.failed_to_merge = False
James E. Blairc053d022014-01-22 14:57:33 -0800840 self.approvals = []
James E. Blair11041d22014-05-02 14:49:53 -0700841 self.open = None
842 self.status = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700843
844 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -0700845 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -0700846
847 def __repr__(self):
848 return '<Change 0x%x %s>' % (id(self), self._id())
849
850 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +0800851 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -0700852 return True
853 return False
854
James E. Blair2fa50962013-01-30 21:50:41 -0800855 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -0800856 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -0700857 (hasattr(other, 'patchset') and
858 self.patchset is not None and
859 other.patchset is not None and
860 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -0800861 return True
862 return False
863
James E. Blairfee8d652013-06-07 08:57:52 -0700864 def getRelatedChanges(self):
865 related = set()
866 if self.needs_change:
867 related.add(self.needs_change)
868 for c in self.needed_by_changes:
869 related.add(c)
870 related.update(c.getRelatedChanges())
871 return related
James E. Blair4aea70c2012-07-26 14:23:24 -0700872
873
874class Ref(Changeish):
James E. Blair4aea70c2012-07-26 14:23:24 -0700875 def __init__(self, project):
James E. Blairbe765db2012-08-07 08:36:20 -0700876 super(Ref, self).__init__(project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700877 self.ref = None
878 self.oldrev = None
879 self.newrev = None
880
James E. Blairbe765db2012-08-07 08:36:20 -0700881 def _id(self):
882 return self.newrev
883
Antoine Musso68bdcd72013-01-17 12:31:28 +0100884 def __repr__(self):
885 rep = None
886 if self.newrev == '0000000000000000000000000000000000000000':
887 rep = '<Ref 0x%x deletes %s from %s' % (
888 id(self), self.ref, self.oldrev)
889 elif self.oldrev == '0000000000000000000000000000000000000000':
890 rep = '<Ref 0x%x creates %s on %s>' % (
891 id(self), self.ref, self.newrev)
892 else:
893 # Catch all
894 rep = '<Ref 0x%x %s updated %s..%s>' % (
895 id(self), self.ref, self.oldrev, self.newrev)
896
897 return rep
898
James E. Blair4aea70c2012-07-26 14:23:24 -0700899 def equals(self, other):
James E. Blair9358c612012-09-28 08:29:39 -0700900 if (self.project == other.project
901 and self.ref == other.ref
902 and self.newrev == other.newrev):
James E. Blair4aea70c2012-07-26 14:23:24 -0700903 return True
904 return False
905
James E. Blair2fa50962013-01-30 21:50:41 -0800906 def isUpdateOf(self, other):
907 return False
908
James E. Blair4aea70c2012-07-26 14:23:24 -0700909
James E. Blair63bb0ef2013-07-29 17:14:51 -0700910class NullChange(Changeish):
James E. Blaire5910202013-12-27 09:50:31 -0800911 def __repr__(self):
912 return '<NullChange for %s>' % (self.project)
James E. Blair63bb0ef2013-07-29 17:14:51 -0700913
James E. Blair63bb0ef2013-07-29 17:14:51 -0700914 def _id(self):
Alex Gaynorddb9ef32013-09-16 21:04:58 -0700915 return None
James E. Blair63bb0ef2013-07-29 17:14:51 -0700916
917 def equals(self, other):
James E. Blair4f6033c2014-03-27 15:49:09 -0700918 if (self.project == other.project):
919 return True
James E. Blair63bb0ef2013-07-29 17:14:51 -0700920 return False
921
922 def isUpdateOf(self, other):
923 return False
924
925
James E. Blairee743612012-05-29 14:49:32 -0700926class TriggerEvent(object):
927 def __init__(self):
928 self.data = None
James E. Blair32663402012-06-01 10:04:18 -0700929 # common
James E. Blairee743612012-05-29 14:49:32 -0700930 self.type = None
931 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -0700932 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +0100933 # Representation of the user account that performed the event.
934 self.account = None
James E. Blair32663402012-06-01 10:04:18 -0700935 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -0700936 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -0700937 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -0700938 self.patch_number = None
James E. Blaira03262c2012-05-30 09:41:16 -0700939 self.refspec = None
James E. Blairee743612012-05-29 14:49:32 -0700940 self.approvals = []
941 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -0700942 self.comment = None
James E. Blair32663402012-06-01 10:04:18 -0700943 # ref-updated
James E. Blairee743612012-05-29 14:49:32 -0700944 self.ref = None
James E. Blair32663402012-06-01 10:04:18 -0700945 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -0700946 self.newrev = None
James E. Blair63bb0ef2013-07-29 17:14:51 -0700947 # timer
948 self.timespec = None
James E. Blairad28e912013-11-27 10:43:22 -0800949 # For events that arrive with a destination pipeline (eg, from
950 # an admin command, etc):
951 self.forced_pipeline = None
James E. Blairee743612012-05-29 14:49:32 -0700952
James E. Blair9f9667e2012-06-12 17:51:08 -0700953 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -0700954 ret = '<TriggerEvent %s %s' % (self.type, self.project_name)
James E. Blair1e8dd892012-05-30 09:15:05 -0700955
James E. Blairee743612012-05-29 14:49:32 -0700956 if self.branch:
957 ret += " %s" % self.branch
958 if self.change_number:
959 ret += " %s,%s" % (self.change_number, self.patch_number)
960 if self.approvals:
James E. Blair1e8dd892012-05-30 09:15:05 -0700961 ret += ' ' + ', '.join(
962 ['%s:%s' % (a['type'], a['value']) for a in self.approvals])
James E. Blairee743612012-05-29 14:49:32 -0700963 ret += '>'
964
965 return ret
966
James E. Blair4aea70c2012-07-26 14:23:24 -0700967 def getChange(self, project, trigger):
James E. Blaire421a232012-07-25 16:59:21 -0700968 if self.change_number:
James E. Blair4aea70c2012-07-26 14:23:24 -0700969 change = trigger.getChange(self.change_number, self.patch_number)
James E. Blair63bb0ef2013-07-29 17:14:51 -0700970 elif self.ref:
James E. Blair4aea70c2012-07-26 14:23:24 -0700971 change = Ref(project)
James E. Blaire421a232012-07-25 16:59:21 -0700972 change.ref = self.ref
973 change.oldrev = self.oldrev
974 change.newrev = self.newrev
James E. Blairc44b1382012-12-23 09:39:55 -0800975 change.url = trigger.getGitwebUrl(project, sha=self.newrev)
James E. Blair63bb0ef2013-07-29 17:14:51 -0700976 else:
977 change = NullChange(project)
James E. Blaire421a232012-07-25 16:59:21 -0700978
979 return change
980
James E. Blair1e8dd892012-05-30 09:15:05 -0700981
James E. Blair9c17dbf2014-06-23 14:21:58 -0700982class BaseFilter(object):
983 def __init__(self, required_approvals=[]):
984 self.required_approvals = required_approvals
985
986 for a in self.required_approvals:
987 for k, v in a.items():
988 if k == 'username':
989 pass
James E. Blair1fbfceb2014-06-23 14:42:53 -0700990 elif k in ['email', 'email-filter']:
991 a['email'] = re.compile(v)
James E. Blair9c17dbf2014-06-23 14:21:58 -0700992 elif k == 'newer-than':
993 a[k] = time_to_seconds(v)
994 elif k == 'older-than':
995 a[k] = time_to_seconds(v)
996 else:
997 if not isinstance(v, list):
998 a[k] = [v]
James E. Blair1fbfceb2014-06-23 14:42:53 -0700999 if 'email-filter' in a:
1000 del a['email-filter']
James E. Blair9c17dbf2014-06-23 14:21:58 -07001001
1002 def matchesRequiredApprovals(self, change):
1003 now = time.time()
1004 for rapproval in self.required_approvals:
1005 matches_approval = False
1006 for approval in change.approvals:
1007 if 'description' not in approval:
1008 continue
1009 found_approval = True
1010 by = approval.get('by', {})
1011 for k, v in rapproval.items():
1012 if k == 'username':
1013 if (by.get('username', '') != v):
1014 found_approval = False
James E. Blair1fbfceb2014-06-23 14:42:53 -07001015 elif k == 'email':
James E. Blair9c17dbf2014-06-23 14:21:58 -07001016 if (not v.search(by.get('email', ''))):
1017 found_approval = False
1018 elif k == 'newer-than':
1019 t = now - v
1020 if (approval['grantedOn'] < t):
1021 found_approval = False
1022 elif k == 'older-than':
1023 t = now - v
1024 if (approval['grantedOn'] >= t):
1025 found_approval = False
1026 else:
1027 if (normalizeCategory(approval['description']) != k or
1028 int(approval['value']) not in v):
1029 found_approval = False
1030 if found_approval:
1031 matches_approval = True
1032 break
1033 if not matches_approval:
1034 return False
1035 return True
1036
1037
1038class EventFilter(BaseFilter):
James E. Blairc053d022014-01-22 14:57:33 -08001039 def __init__(self, types=[], branches=[], refs=[], event_approvals={},
James E. Blair1fbfceb2014-06-23 14:42:53 -07001040 comments=[], emails=[], usernames=[], timespecs=[],
1041 required_approvals=[]):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001042 super(EventFilter, self).__init__(
1043 required_approvals=required_approvals)
James E. Blairee743612012-05-29 14:49:32 -07001044 self._types = types
1045 self._branches = branches
1046 self._refs = refs
James E. Blair1fbfceb2014-06-23 14:42:53 -07001047 self._comments = comments
1048 self._emails = emails
1049 self._usernames = usernames
James E. Blairee743612012-05-29 14:49:32 -07001050 self.types = [re.compile(x) for x in types]
1051 self.branches = [re.compile(x) for x in branches]
1052 self.refs = [re.compile(x) for x in refs]
James E. Blair1fbfceb2014-06-23 14:42:53 -07001053 self.comments = [re.compile(x) for x in comments]
1054 self.emails = [re.compile(x) for x in emails]
1055 self.usernames = [re.compile(x) for x in usernames]
James E. Blairc053d022014-01-22 14:57:33 -08001056 self.event_approvals = event_approvals
James E. Blair63bb0ef2013-07-29 17:14:51 -07001057 self.timespecs = timespecs
James E. Blairee743612012-05-29 14:49:32 -07001058
James E. Blair9f9667e2012-06-12 17:51:08 -07001059 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -07001060 ret = '<EventFilter'
James E. Blair1e8dd892012-05-30 09:15:05 -07001061
James E. Blairee743612012-05-29 14:49:32 -07001062 if self._types:
1063 ret += ' types: %s' % ', '.join(self._types)
1064 if self._branches:
1065 ret += ' branches: %s' % ', '.join(self._branches)
1066 if self._refs:
1067 ret += ' refs: %s' % ', '.join(self._refs)
James E. Blairc053d022014-01-22 14:57:33 -08001068 if self.event_approvals:
1069 ret += ' event_approvals: %s' % ', '.join(
1070 ['%s:%s' % a for a in self.event_approvals.items()])
James E. Blair9c17dbf2014-06-23 14:21:58 -07001071 if self.required_approvals:
1072 ret += ' required_approvals: %s' % ', '.join(
1073 ['%s' % a for a in self.required_approvals])
James E. Blair1fbfceb2014-06-23 14:42:53 -07001074 if self._comments:
1075 ret += ' comments: %s' % ', '.join(self._comments)
1076 if self._emails:
1077 ret += ' emails: %s' % ', '.join(self._emails)
1078 if self._usernames:
1079 ret += ' username_filters: %s' % ', '.join(self._usernames)
James E. Blair63bb0ef2013-07-29 17:14:51 -07001080 if self.timespecs:
1081 ret += ' timespecs: %s' % ', '.join(self.timespecs)
James E. Blairee743612012-05-29 14:49:32 -07001082 ret += '>'
1083
1084 return ret
1085
James E. Blairc053d022014-01-22 14:57:33 -08001086 def matches(self, event, change):
James E. Blairee743612012-05-29 14:49:32 -07001087 # event types are ORed
1088 matches_type = False
1089 for etype in self.types:
1090 if etype.match(event.type):
1091 matches_type = True
1092 if self.types and not matches_type:
1093 return False
1094
1095 # branches are ORed
1096 matches_branch = False
1097 for branch in self.branches:
1098 if branch.match(event.branch):
1099 matches_branch = True
1100 if self.branches and not matches_branch:
1101 return False
1102
1103 # refs are ORed
1104 matches_ref = False
1105 for ref in self.refs:
1106 if ref.match(event.ref):
1107 matches_ref = True
1108 if self.refs and not matches_ref:
1109 return False
1110
James E. Blair1fbfceb2014-06-23 14:42:53 -07001111 # comments are ORed
1112 matches_comment_re = False
1113 for comment_re in self.comments:
Clark Boylanb9bcb402012-06-29 17:44:05 -07001114 if (event.comment is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07001115 comment_re.search(event.comment)):
1116 matches_comment_re = True
1117 if self.comments and not matches_comment_re:
Clark Boylanb9bcb402012-06-29 17:44:05 -07001118 return False
1119
Antoine Mussob4e809e2012-12-06 16:58:06 +01001120 # We better have an account provided by Gerrit to do
1121 # email filtering.
1122 if event.account is not None:
James E. Blaircf429f32012-12-20 14:28:24 -08001123 account_email = event.account.get('email')
James E. Blair1fbfceb2014-06-23 14:42:53 -07001124 # emails are ORed
1125 matches_email_re = False
1126 for email_re in self.emails:
Antoine Mussob4e809e2012-12-06 16:58:06 +01001127 if (account_email is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07001128 email_re.search(account_email)):
1129 matches_email_re = True
1130 if self.emails and not matches_email_re:
Antoine Mussob4e809e2012-12-06 16:58:06 +01001131 return False
1132
James E. Blair1fbfceb2014-06-23 14:42:53 -07001133 # usernames are ORed
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001134 account_username = event.account.get('username')
James E. Blair1fbfceb2014-06-23 14:42:53 -07001135 matches_username_re = False
1136 for username_re in self.usernames:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001137 if (account_username is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07001138 username_re.search(account_username)):
1139 matches_username_re = True
1140 if self.usernames and not matches_username_re:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001141 return False
1142
James E. Blairee743612012-05-29 14:49:32 -07001143 # approvals are ANDed
James E. Blairc053d022014-01-22 14:57:33 -08001144 for category, value in self.event_approvals.items():
James E. Blairee743612012-05-29 14:49:32 -07001145 matches_approval = False
1146 for eapproval in event.approvals:
1147 if (normalizeCategory(eapproval['description']) == category and
1148 int(eapproval['value']) == int(value)):
1149 matches_approval = True
James E. Blair1e8dd892012-05-30 09:15:05 -07001150 if not matches_approval:
1151 return False
James E. Blair63bb0ef2013-07-29 17:14:51 -07001152
James E. Blair9c17dbf2014-06-23 14:21:58 -07001153 if self.required_approvals and not change.approvals:
James E. Blairc053d022014-01-22 14:57:33 -08001154 # A change with no approvals can not match
1155 return False
1156
James E. Blair9c17dbf2014-06-23 14:21:58 -07001157 # required approvals are ANDed
1158 if not self.matchesRequiredApprovals(change):
1159 return False
James E. Blairc053d022014-01-22 14:57:33 -08001160
James E. Blair63bb0ef2013-07-29 17:14:51 -07001161 # timespecs are ORed
1162 matches_timespec = False
1163 for timespec in self.timespecs:
1164 if (event.timespec == timespec):
1165 matches_timespec = True
1166 if self.timespecs and not matches_timespec:
1167 return False
1168
James E. Blairee743612012-05-29 14:49:32 -07001169 return True
James E. Blaireff88162013-07-01 12:44:14 -04001170
1171
James E. Blair9c17dbf2014-06-23 14:21:58 -07001172class ChangeishFilter(BaseFilter):
Clark Boylana9702ad2014-05-08 17:17:24 -07001173 def __init__(self, open=None, current_patchset=None,
James E. Blair9c17dbf2014-06-23 14:21:58 -07001174 statuses=[], required_approvals=[]):
1175 super(ChangeishFilter, self).__init__(
1176 required_approvals=required_approvals)
James E. Blair11041d22014-05-02 14:49:53 -07001177 self.open = open
Clark Boylana9702ad2014-05-08 17:17:24 -07001178 self.current_patchset = current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07001179 self.statuses = statuses
James E. Blair11041d22014-05-02 14:49:53 -07001180
1181 def __repr__(self):
1182 ret = '<ChangeishFilter'
1183
1184 if self.open is not None:
1185 ret += ' open: %s' % self.open
Clark Boylana9702ad2014-05-08 17:17:24 -07001186 if self.current_patchset is not None:
1187 ret += ' current-patchset: %s' % self.current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07001188 if self.statuses:
1189 ret += ' statuses: %s' % ', '.join(self.statuses)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001190 if self.required_approvals:
1191 ret += ' required_approvals: %s' % str(self.required_approvals)
James E. Blair11041d22014-05-02 14:49:53 -07001192 ret += '>'
1193
1194 return ret
1195
1196 def matches(self, change):
1197 if self.open is not None:
1198 if self.open != change.open:
1199 return False
1200
Clark Boylana9702ad2014-05-08 17:17:24 -07001201 if self.current_patchset is not None:
1202 if self.current_patchset != change.is_current_patchset:
1203 return False
1204
James E. Blair11041d22014-05-02 14:49:53 -07001205 if self.statuses:
1206 if change.status not in self.statuses:
1207 return False
1208
James E. Blair9c17dbf2014-06-23 14:21:58 -07001209 if self.required_approvals and not change.approvals:
James E. Blair11041d22014-05-02 14:49:53 -07001210 # A change with no approvals can not match
1211 return False
1212
James E. Blair9c17dbf2014-06-23 14:21:58 -07001213 # required approvals are ANDed
1214 if not self.matchesRequiredApprovals(change):
1215 return False
James E. Blair11041d22014-05-02 14:49:53 -07001216
1217 return True
1218
1219
James E. Blaireff88162013-07-01 12:44:14 -04001220class Layout(object):
1221 def __init__(self):
1222 self.projects = {}
James E. Blair5a9918a2013-08-27 10:06:27 -07001223 self.pipelines = OrderedDict()
James E. Blaireff88162013-07-01 12:44:14 -04001224 self.jobs = {}
James E. Blairc28d1b02013-07-19 11:37:06 -07001225 self.metajobs = []
James E. Blaireff88162013-07-01 12:44:14 -04001226
1227 def getJob(self, name):
1228 if name in self.jobs:
1229 return self.jobs[name]
1230 job = Job(name)
1231 if name.startswith('^'):
1232 # This is a meta-job
1233 regex = re.compile(name)
James E. Blairc28d1b02013-07-19 11:37:06 -07001234 self.metajobs.append((regex, job))
James E. Blaireff88162013-07-01 12:44:14 -04001235 else:
1236 # Apply attributes from matching meta-jobs
James E. Blairc28d1b02013-07-19 11:37:06 -07001237 for regex, metajob in self.metajobs:
James E. Blaireff88162013-07-01 12:44:14 -04001238 if regex.match(name):
1239 job.copy(metajob)
1240 self.jobs[name] = job
1241 return job