]>
Commit | Line | Data |
---|---|---|
f6e6ff79 MT |
1 | #!/usr/bin/python |
2 | ||
f6e6ff79 MT |
3 | import logging |
4 | import os | |
5 | import re | |
f6e6ff79 MT |
6 | import uuid |
7 | ||
f6e6ff79 MT |
8 | import pakfire.packages |
9 | ||
2c909128 | 10 | from . import base |
2c909128 | 11 | from . import logs |
2c909128 MT |
12 | from . import updates |
13 | from . import users | |
14 | ||
e153d3f6 MT |
15 | log = logging.getLogger("builds") |
16 | log.propagate = 1 | |
17 | ||
2c909128 | 18 | from .constants import * |
044a9c43 | 19 | from .decorators import * |
f6e6ff79 | 20 | |
f6e6ff79 | 21 | class Builds(base.Object): |
764b87d2 MT |
22 | def _get_build(self, query, *args): |
23 | res = self.db.get(query, *args) | |
24 | ||
25 | if res: | |
26 | return Build(self.backend, res.id, data=res) | |
27 | ||
28 | def _get_builds(self, query, *args): | |
29 | res = self.db.query(query, *args) | |
30 | ||
31 | for row in res: | |
32 | yield Build(self.backend, row.id, data=row) | |
33 | ||
eedc6432 | 34 | def get_by_id(self, id, data=None): |
326664a5 | 35 | return Build(self.backend, id, data=data) |
f6e6ff79 MT |
36 | |
37 | def get_by_uuid(self, uuid): | |
38 | build = self.db.get("SELECT id FROM builds WHERE uuid = %s LIMIT 1", uuid) | |
39 | ||
40 | if build: | |
41 | return self.get_by_id(build.id) | |
42 | ||
43 | def get_all(self, limit=50): | |
eedc6432 | 44 | query = "SELECT * FROM builds ORDER BY time_created DESC" |
f6e6ff79 MT |
45 | |
46 | if limit: | |
47 | query += " LIMIT %d" % limit | |
48 | ||
eedc6432 | 49 | return [self.get_by_id(b.id, b) for b in self.db.query(query)] |
f6e6ff79 | 50 | |
eedc6432 | 51 | def get_by_user(self, user, type=None, public=None): |
f6e6ff79 MT |
52 | args = [] |
53 | conditions = [] | |
54 | ||
55 | if not type or type == "scratch": | |
56 | # On scratch builds the user id equals the owner id. | |
57 | conditions.append("(builds.type = 'scratch' AND owner_id = %s)") | |
58 | args.append(user.id) | |
59 | ||
60 | elif not type or type == "release": | |
61 | pass # TODO | |
62 | ||
63 | if public is True: | |
64 | conditions.append("public = 'Y'") | |
65 | elif public is False: | |
66 | conditions.append("public = 'N'") | |
67 | ||
eedc6432 | 68 | query = "SELECT builds.* AS id FROM builds \ |
f6e6ff79 MT |
69 | JOIN packages ON builds.pkg_id = packages.id" |
70 | ||
71 | if conditions: | |
72 | query += " WHERE %s" % " AND ".join(conditions) | |
73 | ||
eedc6432 | 74 | query += " ORDER BY builds.time_created DESC" |
f6e6ff79 | 75 | |
eedc6432 | 76 | builds = [] |
f6e6ff79 | 77 | for build in self.db.query(query, *args): |
326664a5 | 78 | build = Build(self.backend, build.id, build) |
eedc6432 MT |
79 | builds.append(build) |
80 | ||
81 | return builds | |
f6e6ff79 | 82 | |
a15d6139 | 83 | def get_by_name(self, name, type=None, public=None, user=None, limit=None, offset=None): |
f6e6ff79 MT |
84 | args = [name,] |
85 | conditions = [ | |
86 | "packages.name = %s", | |
87 | ] | |
88 | ||
89 | if type: | |
90 | conditions.append("builds.type = %s") | |
91 | args.append(type) | |
92 | ||
93 | or_conditions = [] | |
94 | if public is True: | |
95 | or_conditions.append("public = 'Y'") | |
96 | elif public is False: | |
97 | or_conditions.append("public = 'N'") | |
98 | ||
99 | if user and not user.is_admin(): | |
100 | or_conditions.append("builds.owner_id = %s") | |
101 | args.append(user.id) | |
102 | ||
a15d6139 | 103 | query = "SELECT builds.* AS id FROM builds \ |
f6e6ff79 MT |
104 | JOIN packages ON builds.pkg_id = packages.id" |
105 | ||
106 | if or_conditions: | |
107 | conditions.append(" OR ".join(or_conditions)) | |
108 | ||
109 | if conditions: | |
110 | query += " WHERE %s" % " AND ".join(conditions) | |
111 | ||
a15d6139 MT |
112 | if type == "release": |
113 | query += " ORDER BY packages.name,packages.epoch,packages.version,packages.release,id ASC" | |
114 | elif type == "scratch": | |
115 | query += " ORDER BY time_created DESC" | |
f6e6ff79 | 116 | |
a15d6139 MT |
117 | if limit: |
118 | if offset: | |
119 | query += " LIMIT %s,%s" | |
120 | args.extend([offset, limit]) | |
121 | else: | |
122 | query += " LIMIT %s" | |
123 | args.append(limit) | |
124 | ||
326664a5 | 125 | return [Build(self.backend, b.id, b) for b in self.db.query(query, *args)] |
f6e6ff79 MT |
126 | |
127 | def get_latest_by_name(self, name, type=None, public=None): | |
2f45327a MT |
128 | query = "\ |
129 | SELECT * FROM builds \ | |
130 | LEFT JOIN builds_latest ON builds.id = builds_latest.build_id \ | |
131 | WHERE builds_latest.package_name = %s" | |
f6e6ff79 MT |
132 | args = [name,] |
133 | ||
2f45327a MT |
134 | if type: |
135 | query += " AND builds_latest.build_type = %s" | |
136 | args.append(type) | |
137 | ||
f6e6ff79 | 138 | if public is True: |
2f45327a MT |
139 | query += " AND builds.public = %s" |
140 | args.append("Y") | |
f6e6ff79 | 141 | elif public is False: |
2f45327a MT |
142 | query += " AND builds.public = %s" |
143 | args.append("N") | |
f6e6ff79 | 144 | |
2f45327a MT |
145 | # Get the last one only. |
146 | # Prefer release builds over scratch builds. | |
147 | query += "\ | |
148 | ORDER BY \ | |
149 | CASE builds.type WHEN 'release' THEN 0 ELSE 1 END, \ | |
150 | builds.time_created DESC \ | |
151 | LIMIT 1" | |
f6e6ff79 | 152 | |
2f45327a | 153 | res = self.db.get(query, *args) |
f6e6ff79 | 154 | |
2f45327a | 155 | if res: |
326664a5 | 156 | return Build(self.backend, res.id, res) |
f6e6ff79 | 157 | |
fd0e70ec MT |
158 | def get_active_builds(self, name, public=None): |
159 | query = "\ | |
aff0187d MT |
160 | SELECT * FROM builds \ |
161 | LEFT JOIN builds_latest ON builds.id = builds_latest.build_id \ | |
2f83864f MT |
162 | WHERE builds_latest.package_name = %s AND builds.type = %s" |
163 | args = [name, "release"] | |
fd0e70ec MT |
164 | |
165 | if public is True: | |
166 | query += " AND builds.public = %s" | |
167 | args.append("Y") | |
168 | elif public is False: | |
169 | query += " AND builds.public = %s" | |
170 | args.append("N") | |
171 | ||
fd0e70ec MT |
172 | builds = [] |
173 | for row in self.db.query(query, *args): | |
326664a5 | 174 | b = Build(self.backend, row.id, row) |
fd0e70ec MT |
175 | builds.append(b) |
176 | ||
177 | # Sort the result. Lastest build first. | |
178 | builds.sort(reverse=True) | |
179 | ||
180 | return builds | |
181 | ||
f6e6ff79 | 182 | def count(self): |
966498de MT |
183 | builds = self.db.get("SELECT COUNT(*) AS count FROM builds") |
184 | if builds: | |
185 | return builds.count | |
f6e6ff79 | 186 | |
f6e6ff79 MT |
187 | def get_obsolete(self, repo=None): |
188 | """ | |
189 | Get all obsoleted builds. | |
190 | ||
191 | If repo is True: which are in any repository. | |
192 | If repo is some Repository object: which are in this repository. | |
193 | """ | |
194 | args = [] | |
195 | ||
196 | if repo is None: | |
197 | query = "SELECT id FROM builds WHERE state = 'obsolete'" | |
198 | ||
199 | else: | |
200 | query = "SELECT build_id AS id FROM repositories_builds \ | |
201 | JOIN builds ON builds.id = repositories_builds.build_id \ | |
202 | WHERE builds.state = 'obsolete'" | |
203 | ||
204 | if repo and not repo is True: | |
205 | query += " AND repositories_builds.repo_id = %s" | |
206 | args.append(repo.id) | |
207 | ||
208 | res = self.db.query(query, *args) | |
209 | ||
210 | builds = [] | |
211 | for build in res: | |
326664a5 | 212 | build = Build(self.backend, build.id) |
f6e6ff79 MT |
213 | builds.append(build) |
214 | ||
215 | return builds | |
216 | ||
e153d3f6 MT |
217 | def create(self, pkg, type="release", owner=None, distro=None): |
218 | assert type in ("release", "scratch", "test") | |
219 | assert distro, "You need to specify the distribution of this build." | |
220 | ||
221 | # Check if scratch build has an owner. | |
222 | if type == "scratch" and not owner: | |
223 | raise Exception, "Scratch builds require an owner" | |
224 | ||
225 | # Set the default priority of this build. | |
226 | if type == "release": | |
227 | priority = 0 | |
228 | ||
229 | elif type == "scratch": | |
230 | priority = 1 | |
231 | ||
232 | elif type == "test": | |
233 | priority = -1 | |
234 | ||
235 | # Create build in database | |
236 | build = self._get_build("INSERT INTO builds(uuid, pkg_id, type, distro_id, priority) \ | |
237 | VALUES(%s, %s, %s, %s, %s) RETURNING *", "%s" % uuid.uuid4(), pkg.id, type, distro.id, priority) | |
238 | ||
239 | # Set the owner of this build | |
240 | if owner: | |
241 | build.owner = owner | |
242 | ||
243 | # Log that the build has been created. | |
244 | build.log("created", user=owner) | |
245 | ||
246 | # Create directory where the files live | |
247 | if not os.path.exists(build.path): | |
248 | os.makedirs(build.path) | |
249 | ||
250 | # Move package file to the directory of the build. | |
251 | build.pkg.move(os.path.join(build.path, "src")) | |
252 | ||
253 | # Generate an update id. | |
254 | build.generate_update_id() | |
255 | ||
256 | # Obsolete all other builds with the same name to track updates. | |
257 | build.obsolete_others() | |
258 | ||
259 | # Search for possible bug IDs in the commit message. | |
260 | build.search_for_bugs() | |
261 | ||
262 | return build | |
263 | ||
264 | def create_from_source_package(self, filename, distro, commit=None, type="release", | |
265 | arches=None, check_for_duplicates=True, owner=None): | |
266 | assert distro | |
267 | ||
268 | # Open the package file to read some basic information. | |
269 | pkg = pakfire.packages.open(None, None, filename) | |
270 | ||
271 | if check_for_duplicates: | |
272 | if distro.has_package(pkg.name, pkg.epoch, pkg.version, pkg.release): | |
273 | log.warning("Duplicate package detected: %s. Skipping." % pkg) | |
274 | return | |
275 | ||
276 | # Open the package and add it to the database | |
277 | pkg = self.backend.packages.create(filename) | |
278 | ||
279 | # Associate the package to the processed commit | |
280 | if commit: | |
281 | pkg.commit = commit | |
282 | ||
283 | # Create a new build object from the package | |
284 | build = self.create(pkg, type=type, owner=owner, distro=distro) | |
285 | ||
286 | # Create all automatic jobs | |
287 | build.create_autojobs(arches=arches) | |
288 | ||
289 | return build | |
290 | ||
4b1e87c4 MT |
291 | def get_changelog(self, name, public=None, limit=5, offset=0): |
292 | query = "SELECT builds.* FROM builds \ | |
293 | JOIN packages ON builds.pkg_id = packages.id \ | |
294 | WHERE \ | |
295 | builds.type = %s \ | |
296 | AND \ | |
297 | packages.name = %s" | |
298 | args = ["release", name,] | |
299 | ||
300 | if public == True: | |
301 | query += " AND builds.public = %s" | |
302 | args.append("Y") | |
303 | elif public == False: | |
304 | query += " AND builds.public = %s" | |
305 | args.append("N") | |
306 | ||
307 | query += " ORDER BY builds.time_created DESC" | |
308 | ||
309 | if limit: | |
310 | if offset: | |
311 | query += " LIMIT %s,%s" | |
312 | args += [offset, limit] | |
313 | else: | |
314 | query += " LIMIT %s" | |
315 | args.append(limit) | |
316 | ||
317 | builds = [] | |
318 | for b in self.db.query(query, *args): | |
326664a5 | 319 | b = Build(self.backend, b.id, b) |
4b1e87c4 MT |
320 | builds.append(b) |
321 | ||
322 | builds.sort(reverse=True) | |
323 | ||
324 | return builds | |
325 | ||
62c7e7cd MT |
326 | def get_comments(self, limit=10, offset=None, user=None): |
327 | query = "SELECT * FROM builds_comments \ | |
328 | JOIN users ON builds_comments.user_id = users.id" | |
329 | args = [] | |
330 | ||
331 | wheres = [] | |
332 | if user: | |
333 | wheres.append("users.id = %s") | |
334 | args.append(user.id) | |
335 | ||
336 | if wheres: | |
337 | query += " WHERE %s" % " AND ".join(wheres) | |
338 | ||
339 | # Sort everything. | |
340 | query += " ORDER BY time_created DESC" | |
341 | ||
342 | # Limits. | |
343 | if limit: | |
344 | if offset: | |
345 | query += " LIMIT %s,%s" | |
346 | args.append(offset) | |
347 | else: | |
348 | query += " LIMIT %s" | |
349 | ||
350 | args.append(limit) | |
351 | ||
352 | comments = [] | |
353 | for comment in self.db.query(query, *args): | |
326664a5 | 354 | comment = logs.CommentLogEntry(self.backend, comment) |
62c7e7cd MT |
355 | comments.append(comment) |
356 | ||
357 | return comments | |
358 | ||
4f90cf84 | 359 | def get_build_times_summary(self, name=None, arch=None): |
bc293d03 MT |
360 | query = "\ |
361 | SELECT \ | |
362 | builds_times.arch AS arch, \ | |
363 | MAX(duration) AS maximum, \ | |
364 | MIN(duration) AS minimum, \ | |
365 | AVG(duration) AS average, \ | |
366 | SUM(duration) AS sum, \ | |
367 | STDDEV_POP(duration) AS stddev \ | |
368 | FROM builds_times \ | |
369 | LEFT JOIN builds ON builds_times.build_id = builds.id \ | |
370 | LEFT JOIN packages ON builds.pkg_id = packages.id" | |
371 | ||
372 | args = [] | |
373 | conditions = [] | |
374 | ||
375 | # Filter for name. | |
376 | if name: | |
377 | conditions.append("packages.name = %s") | |
378 | args.append(name) | |
379 | ||
a90bd9b0 MT |
380 | # Filter by arch. |
381 | if arch: | |
382 | conditions.append("builds_times.arch = %s") | |
383 | args.append(arch) | |
384 | ||
bc293d03 MT |
385 | # Add conditions. |
386 | if conditions: | |
387 | query += " WHERE %s" % " AND ".join(conditions) | |
388 | ||
389 | # Grouping and sorting. | |
390 | query += " GROUP BY arch ORDER BY arch DESC" | |
391 | ||
392 | return self.db.query(query, *args) | |
393 | ||
a90bd9b0 MT |
394 | def get_build_times_by_arch(self, arch, **kwargs): |
395 | kwargs.update({ | |
396 | "arch" : arch, | |
397 | }) | |
398 | ||
399 | build_times = self.get_build_times_summary(**kwargs) | |
400 | if build_times: | |
401 | return build_times[0] | |
402 | ||
f6e6ff79 | 403 | |
326664a5 | 404 | class Build(base.DataObject): |
e153d3f6 MT |
405 | table = "builds" |
406 | ||
f6e6ff79 MT |
407 | def __repr__(self): |
408 | return "<%s id=%s %s>" % (self.__class__.__name__, self.id, self.pkg) | |
409 | ||
326664a5 MT |
410 | def __eq__(self, other): |
411 | if isinstance(other, self.__class__): | |
412 | return self.id == other.id | |
f6e6ff79 | 413 | |
326664a5 MT |
414 | def __lt__(self, other): |
415 | if isinstance(other, self.__class__): | |
416 | return self.pkg < other.pkg | |
f6e6ff79 | 417 | |
764b87d2 MT |
418 | def __iter__(self): |
419 | jobs = self.backend.jobs._get_jobs("SELECT * FROM jobs \ | |
420 | WHERE build_id = %s", self.id) | |
421 | ||
422 | return iter(sorted(jobs)) | |
423 | ||
f6e6ff79 MT |
424 | def delete(self): |
425 | """ | |
426 | Deletes this build including all jobs, packages and the source | |
427 | package. | |
428 | """ | |
429 | # If the build is in a repository, we need to remove it. | |
430 | if self.repo: | |
431 | self.repo.rem_build(self) | |
432 | ||
433 | for job in self.jobs + self.test_jobs: | |
434 | job.delete() | |
435 | ||
436 | if self.pkg: | |
437 | self.pkg.delete() | |
438 | ||
439 | # Delete everything related to this build. | |
440 | self.__delete_bugs() | |
441 | self.__delete_comments() | |
442 | self.__delete_history() | |
443 | self.__delete_watchers() | |
444 | ||
445 | # Delete the build itself. | |
446 | self.db.execute("DELETE FROM builds WHERE id = %s", self.id) | |
f6e6ff79 MT |
447 | |
448 | def __delete_bugs(self): | |
449 | """ | |
450 | Delete all associated bugs. | |
451 | """ | |
452 | self.db.execute("DELETE FROM builds_bugs WHERE build_id = %s", self.id) | |
453 | ||
454 | def __delete_comments(self): | |
455 | """ | |
456 | Delete all comments. | |
457 | """ | |
458 | self.db.execute("DELETE FROM builds_comments WHERE build_id = %s", self.id) | |
459 | ||
460 | def __delete_history(self): | |
461 | """ | |
462 | Delete the repository history. | |
463 | """ | |
464 | self.db.execute("DELETE FROM repositories_history WHERE build_id = %s", self.id) | |
465 | ||
466 | def __delete_watchers(self): | |
467 | """ | |
468 | Delete all watchers. | |
469 | """ | |
470 | self.db.execute("DELETE FROM builds_watchers WHERE build_id = %s", self.id) | |
471 | ||
f6e6ff79 MT |
472 | @property |
473 | def info(self): | |
474 | """ | |
475 | A set of information that is sent to the XMLRPC client. | |
476 | """ | |
477 | return { "uuid" : self.uuid } | |
478 | ||
479 | def log(self, action, user=None, bug_id=None): | |
480 | user_id = None | |
481 | if user: | |
482 | user_id = user.id | |
483 | ||
484 | self.db.execute("INSERT INTO builds_history(build_id, action, user_id, time, bug_id) \ | |
485 | VALUES(%s, %s, %s, NOW(), %s)", self.id, action, user_id, bug_id) | |
486 | ||
487 | @property | |
488 | def uuid(self): | |
489 | """ | |
490 | The UUID of this build. | |
491 | """ | |
492 | return self.data.uuid | |
493 | ||
326664a5 | 494 | @lazy_property |
f6e6ff79 MT |
495 | def pkg(self): |
496 | """ | |
497 | Get package that is to be built in the build. | |
498 | """ | |
326664a5 | 499 | return self.backend.packages.get_by_id(self.data.pkg_id) |
f6e6ff79 MT |
500 | |
501 | @property | |
502 | def name(self): | |
503 | return "%s-%s" % (self.pkg.name, self.pkg.friendly_version) | |
504 | ||
505 | @property | |
506 | def type(self): | |
507 | """ | |
508 | The type of this build. | |
509 | """ | |
510 | return self.data.type | |
511 | ||
e153d3f6 | 512 | def get_owner(self): |
f6e6ff79 MT |
513 | """ |
514 | The owner of this build. | |
515 | """ | |
e153d3f6 MT |
516 | if self.data.owner_id: |
517 | return self.backend.users.get_by_id(self.data.owner_id) | |
f6e6ff79 | 518 | |
e153d3f6 MT |
519 | def set_owner(self, owner): |
520 | if owner: | |
521 | self._set_attribute("owner_id", owner.id) | |
522 | else: | |
523 | self._set_attribute("owner_id", None) | |
f6e6ff79 | 524 | |
e153d3f6 | 525 | owner = lazy_property(get_owner, set_owner) |
f6e6ff79 | 526 | |
326664a5 | 527 | @lazy_property |
f6e6ff79 | 528 | def distro(self): |
326664a5 | 529 | return self.backend.distros.get_by_id(self.data.distro_id) |
f6e6ff79 MT |
530 | |
531 | @property | |
532 | def user(self): | |
533 | if self.type == "scratch": | |
534 | return self.owner | |
535 | ||
536 | def get_depends_on(self): | |
326664a5 MT |
537 | if self.data.depends_on: |
538 | return self.backend.builds.get_by_id(self.data.depends_on) | |
f6e6ff79 MT |
539 | |
540 | def set_depends_on(self, build): | |
326664a5 | 541 | self._set_attribute("depends_on", build.id) |
f6e6ff79 | 542 | |
326664a5 | 543 | depends_on = lazy_property(get_depends_on, set_depends_on) |
f6e6ff79 MT |
544 | |
545 | @property | |
546 | def created(self): | |
547 | return self.data.time_created | |
548 | ||
eedc6432 MT |
549 | @property |
550 | def date(self): | |
551 | return self.created.date() | |
552 | ||
f6e6ff79 MT |
553 | @property |
554 | def public(self): | |
555 | """ | |
556 | Is this build public? | |
557 | """ | |
326664a5 | 558 | return self.data.public |
f6e6ff79 | 559 | |
326664a5 | 560 | @lazy_property |
eedc6432 MT |
561 | def size(self): |
562 | """ | |
563 | Returns the size on disk of this build. | |
564 | """ | |
565 | s = 0 | |
566 | ||
567 | # Add the source package. | |
568 | if self.pkg: | |
569 | s += self.pkg.size | |
570 | ||
571 | # Add all jobs. | |
572 | s += sum((j.size for j in self.jobs)) | |
573 | ||
574 | return s | |
575 | ||
f6e6ff79 MT |
576 | def auto_update_state(self): |
577 | """ | |
578 | Check if the state of this build can be updated and perform | |
579 | the change if possible. | |
580 | """ | |
581 | # Do not change the broken/obsolete state automatically. | |
582 | if self.state in ("broken", "obsolete"): | |
583 | return | |
584 | ||
585 | if self.repo and self.repo.type == "stable": | |
586 | self.update_state("stable") | |
587 | return | |
588 | ||
589 | # If any of the build jobs are finished, the build will be put in testing | |
590 | # state. | |
591 | for job in self.jobs: | |
592 | if job.state == "finished": | |
593 | self.update_state("testing") | |
594 | break | |
595 | ||
596 | def update_state(self, state, user=None, remove=False): | |
597 | assert state in ("stable", "testing", "obsolete", "broken") | |
598 | ||
326664a5 | 599 | self._set_attribute("state", state) |
f6e6ff79 MT |
600 | |
601 | # In broken state, the removal from the repository is forced and | |
602 | # all jobs that are not finished yet will be aborted. | |
603 | if state == "broken": | |
604 | remove = True | |
605 | ||
606 | for job in self.jobs: | |
607 | if job.state in ("new", "pending", "running", "dependency_error"): | |
608 | job.state = "aborted" | |
609 | ||
610 | # If this build is in a repository, it will leave it. | |
611 | if remove and self.repo: | |
612 | self.repo.rem_build(self) | |
613 | ||
614 | # If a release build is now in testing state, we put it into the | |
615 | # first repository of the distribution. | |
616 | elif self.type == "release" and state == "testing": | |
617 | # If the build is not in a repository, yet and if there is | |
618 | # a first repository, we put the build there. | |
619 | if not self.repo and self.distro.first_repo: | |
620 | self.distro.first_repo.add_build(self, user=user) | |
621 | ||
622 | @property | |
623 | def state(self): | |
624 | return self.data.state | |
625 | ||
9fa1787c MT |
626 | def is_broken(self): |
627 | return self.state == "broken" | |
628 | ||
f6e6ff79 MT |
629 | def obsolete_others(self): |
630 | if not self.type == "release": | |
631 | return | |
632 | ||
326664a5 | 633 | for build in self.backend.builds.get_by_name(self.pkg.name, type="release"): |
f6e6ff79 MT |
634 | # Don't modify ourself. |
635 | if self.id == build.id: | |
636 | continue | |
637 | ||
638 | # Don't touch broken builds. | |
639 | if build.state in ("obsolete", "broken"): | |
640 | continue | |
641 | ||
642 | # Obsolete the build. | |
643 | build.update_state("obsolete") | |
644 | ||
645 | def set_severity(self, severity): | |
326664a5 | 646 | self._set_attribute("severity", severity) |
f6e6ff79 MT |
647 | |
648 | def get_severity(self): | |
649 | return self.data.severity | |
650 | ||
651 | severity = property(get_severity, set_severity) | |
652 | ||
326664a5 | 653 | @lazy_property |
f6e6ff79 MT |
654 | def commit(self): |
655 | if self.pkg and self.pkg.commit: | |
656 | return self.pkg.commit | |
657 | ||
326664a5 MT |
658 | def update_message(self, message): |
659 | self._set_attribute("message", message) | |
f6e6ff79 MT |
660 | |
661 | def has_perm(self, user): | |
662 | """ | |
663 | Check, if the given user has the right to perform administrative | |
664 | operations on this build. | |
665 | """ | |
666 | if user is None: | |
667 | return False | |
668 | ||
669 | if user.is_admin(): | |
670 | return True | |
671 | ||
672 | # Check if the user is allowed to manage packages from the critical path. | |
673 | if self.critical_path and not user.has_perm("manage_critical_path"): | |
674 | return False | |
675 | ||
676 | # Search for maintainers... | |
677 | ||
678 | # Scratch builds. | |
679 | if self.type == "scratch": | |
680 | # The owner of a scratch build has the right to do anything with it. | |
681 | if self.owner_id == user.id: | |
682 | return True | |
683 | ||
684 | # Release builds. | |
685 | elif self.type == "release": | |
686 | # The maintainer also is allowed to manage the build. | |
687 | if self.pkg.maintainer == user: | |
688 | return True | |
689 | ||
690 | # Deny permission for all other cases. | |
691 | return False | |
692 | ||
693 | @property | |
694 | def message(self): | |
695 | message = "" | |
696 | ||
697 | if self.data.message: | |
698 | message = self.data.message | |
699 | ||
700 | elif self.commit: | |
701 | if self.commit.message: | |
702 | message = "\n".join((self.commit.subject, self.commit.message)) | |
703 | else: | |
704 | message = self.commit.subject | |
705 | ||
706 | prefix = "%s: " % self.pkg.name | |
707 | if message.startswith(prefix): | |
708 | message = message[len(prefix):] | |
709 | ||
710 | return message | |
711 | ||
712 | def get_priority(self): | |
713 | return self.data.priority | |
714 | ||
715 | def set_priority(self, priority): | |
716 | assert priority in (-2, -1, 0, 1, 2) | |
717 | ||
326664a5 | 718 | self._set_attribute("priority", priority) |
f6e6ff79 MT |
719 | |
720 | priority = property(get_priority, set_priority) | |
721 | ||
722 | @property | |
723 | def path(self): | |
724 | path = [] | |
725 | if self.type == "scratch": | |
726 | path.append(BUILD_SCRATCH_DIR) | |
727 | path.append(self.uuid) | |
728 | ||
729 | elif self.type == "release": | |
730 | path.append(BUILD_RELEASE_DIR) | |
731 | path.append("%s/%s-%s-%s" % \ | |
732 | (self.pkg.name, self.pkg.epoch, self.pkg.version, self.pkg.release)) | |
733 | ||
734 | else: | |
735 | raise Exception, "Unknown build type: %s" % self.type | |
736 | ||
737 | return os.path.join(*path) | |
738 | ||
739 | @property | |
740 | def source_filename(self): | |
741 | return os.path.basename(self.pkg.path) | |
742 | ||
743 | @property | |
744 | def download_prefix(self): | |
326664a5 | 745 | return "/".join((self.backend.settings.get("download_baseurl"), "packages")) |
f6e6ff79 MT |
746 | |
747 | @property | |
748 | def source_download(self): | |
749 | return "/".join((self.download_prefix, self.pkg.path)) | |
750 | ||
751 | @property | |
752 | def source_hash_sha512(self): | |
753 | return self.pkg.hash_sha512 | |
754 | ||
755 | @property | |
756 | def link(self): | |
757 | # XXX maybe this should rather live in a uimodule. | |
758 | # zlib-1.2.3-2.ip3 [src, i686, blah...] | |
759 | s = """<a class="state_%s %s" href="/build/%s">%s</a>""" % \ | |
760 | (self.state, self.type, self.uuid, self.name) | |
761 | ||
762 | s_jobs = [] | |
763 | for job in self.jobs: | |
764 | s_jobs.append("""<a class="state_%s %s" href="/job/%s">%s</a>""" % \ | |
8f04a9e9 | 765 | (job.state, "test" if job.test else "build", job.uuid, job.arch)) |
f6e6ff79 MT |
766 | |
767 | if s_jobs: | |
768 | s += " [%s]" % ", ".join(s_jobs) | |
769 | ||
770 | return s | |
771 | ||
772 | @property | |
773 | def supported_arches(self): | |
774 | return self.pkg.supported_arches | |
775 | ||
776 | @property | |
777 | def critical_path(self): | |
778 | return self.pkg.critical_path | |
779 | ||
326664a5 | 780 | @lazy_property |
f6e6ff79 MT |
781 | def jobs(self): |
782 | """ | |
783 | Get a list of all build jobs that are in this build. | |
784 | """ | |
8f04a9e9 MT |
785 | return self.backend.jobs._get_jobs("SELECT * FROM jobs \ |
786 | WHERE build_id = %s AND test IS FALSE AND deleted_at IS NULL", self.id) | |
f6e6ff79 MT |
787 | |
788 | @property | |
789 | def test_jobs(self): | |
8f04a9e9 MT |
790 | return self.backend.jobs._get_jobs("SELECT * FROM jobs \ |
791 | WHERE build_id = %s AND test IS TRUE AND deleted_at IS NULL", self.id) | |
f6e6ff79 MT |
792 | |
793 | @property | |
794 | def all_jobs_finished(self): | |
795 | ret = True | |
796 | ||
797 | for job in self.jobs: | |
798 | if not job.state == "finished": | |
799 | ret = False | |
800 | break | |
801 | ||
802 | return ret | |
803 | ||
8f04a9e9 | 804 | def create_autojobs(self, arches=None, **kwargs): |
f6e6ff79 MT |
805 | jobs = [] |
806 | ||
807 | # Arches may be passed to this function. If not we use all arches | |
808 | # this package supports. | |
809 | if arches is None: | |
810 | arches = self.supported_arches | |
811 | ||
812 | # Create a new job for every given archirecture. | |
326664a5 | 813 | for arch in self.backend.arches.expand(arches): |
e153d3f6 MT |
814 | # Don't create jobs for src |
815 | if arch == "src": | |
f6e6ff79 MT |
816 | continue |
817 | ||
8f04a9e9 | 818 | job = self.add_job(arch, **kwargs) |
f6e6ff79 MT |
819 | jobs.append(job) |
820 | ||
821 | # Return all newly created jobs. | |
822 | return jobs | |
823 | ||
8f04a9e9 MT |
824 | def add_job(self, arch, **kwargs): |
825 | job = self.backend.jobs.create(self, arch, **kwargs) | |
f6e6ff79 MT |
826 | |
827 | # Add new job to cache. | |
326664a5 | 828 | self.jobs.append(job) |
f6e6ff79 MT |
829 | |
830 | return job | |
831 | ||
832 | ## Update stuff | |
833 | ||
834 | @property | |
835 | def update_id(self): | |
836 | if not self.type == "release": | |
837 | return | |
838 | ||
839 | # Generate an update ID if none does exist, yet. | |
840 | self.generate_update_id() | |
841 | ||
842 | s = [ | |
843 | "%s" % self.distro.name.replace(" ", "").upper(), | |
844 | "%04d" % (self.data.update_year or 0), | |
845 | "%04d" % (self.data.update_num or 0), | |
846 | ] | |
847 | ||
848 | return "-".join(s) | |
849 | ||
850 | def generate_update_id(self): | |
851 | if not self.type == "release": | |
852 | return | |
853 | ||
854 | if self.data.update_num: | |
855 | return | |
856 | ||
857 | update = self.db.get("SELECT update_num AS num FROM builds \ | |
e153d3f6 | 858 | WHERE update_year = EXTRACT(year FROM NOW()) ORDER BY update_num DESC LIMIT 1") |
f6e6ff79 MT |
859 | |
860 | if update: | |
861 | update_num = update.num + 1 | |
862 | else: | |
863 | update_num = 1 | |
864 | ||
e153d3f6 | 865 | self.db.execute("UPDATE builds SET update_year = EXTRACT(year FROM NOW()), update_num = %s \ |
f6e6ff79 MT |
866 | WHERE id = %s", update_num, self.id) |
867 | ||
868 | ## Comment stuff | |
869 | ||
870 | def get_comments(self, limit=10, offset=0): | |
871 | query = "SELECT * FROM builds_comments \ | |
872 | JOIN users ON builds_comments.user_id = users.id \ | |
873 | WHERE build_id = %s ORDER BY time_created ASC" | |
874 | ||
875 | comments = [] | |
876 | for comment in self.db.query(query, self.id): | |
326664a5 | 877 | comment = logs.CommentLogEntry(self.backend, comment) |
f6e6ff79 MT |
878 | comments.append(comment) |
879 | ||
880 | return comments | |
881 | ||
326664a5 | 882 | def add_comment(self, user, text, score): |
f6e6ff79 MT |
883 | # Add the new comment to the database. |
884 | id = self.db.execute("INSERT INTO \ | |
885 | builds_comments(build_id, user_id, text, credit, time_created) \ | |
886 | VALUES(%s, %s, %s, %s, NOW())", | |
326664a5 | 887 | self.id, user.id, text, score) |
f6e6ff79 | 888 | |
326664a5 MT |
889 | # Update the credit cache |
890 | self.score += score | |
f6e6ff79 MT |
891 | |
892 | # Send the new comment to all watchers and stuff. | |
893 | self.send_comment_message(id) | |
894 | ||
895 | # Return the ID of the newly created comment. | |
896 | return id | |
897 | ||
326664a5 | 898 | @lazy_property |
f6e6ff79 | 899 | def score(self): |
326664a5 MT |
900 | res = self.db.get("SELECT SUM(credit) AS score \ |
901 | FROM builds_comments WHERE build_id = %s", self.id) | |
f6e6ff79 | 902 | |
326664a5 | 903 | return res.score or 0 |
f6e6ff79 MT |
904 | |
905 | @property | |
906 | def credits(self): | |
907 | # XXX COMPAT | |
908 | return self.score | |
909 | ||
910 | def get_commenters(self): | |
911 | users = self.db.query("SELECT DISTINCT users.id AS id FROM builds_comments \ | |
912 | JOIN users ON builds_comments.user_id = users.id \ | |
913 | WHERE builds_comments.build_id = %s AND NOT users.deleted = 'Y' \ | |
914 | AND NOT users.activated = 'Y' ORDER BY users.id", self.id) | |
915 | ||
326664a5 | 916 | return [users.User(self.backend, u.id) for u in users] |
f6e6ff79 MT |
917 | |
918 | def send_comment_message(self, comment_id): | |
919 | comment = self.db.get("SELECT * FROM builds_comments WHERE id = %s", | |
920 | comment_id) | |
921 | ||
922 | assert comment | |
923 | assert comment.build_id == self.id | |
924 | ||
925 | # Get user who wrote the comment. | |
326664a5 | 926 | user = self.backend.users.get_by_id(comment.user_id) |
f6e6ff79 MT |
927 | |
928 | format = { | |
929 | "build_name" : self.name, | |
930 | "user_name" : user.realname, | |
931 | } | |
932 | ||
933 | # XXX create beautiful message | |
934 | ||
326664a5 | 935 | self.backend.messages.send_to_all(self.message_recipients, |
f6e6ff79 MT |
936 | N_("%(user_name)s commented on %(build_name)s"), |
937 | comment.text, format) | |
938 | ||
939 | ## Logging stuff | |
940 | ||
941 | def get_log(self, comments=True, repo=True, limit=None): | |
942 | entries = [] | |
943 | ||
fd681905 | 944 | # Created entry. |
326664a5 | 945 | created_entry = logs.CreatedLogEntry(self.backend, self) |
fd681905 MT |
946 | entries.append(created_entry) |
947 | ||
f6e6ff79 MT |
948 | if comments: |
949 | entries += self.get_comments(limit=limit) | |
950 | ||
951 | if repo: | |
952 | entries += self.get_repo_moves(limit=limit) | |
953 | ||
954 | # Sort all entries in chronological order. | |
955 | entries.sort() | |
956 | ||
957 | if limit: | |
958 | entries = entries[:limit] | |
959 | ||
960 | return entries | |
961 | ||
962 | ## Watchers stuff | |
963 | ||
964 | def get_watchers(self): | |
fe8e7f02 | 965 | query = self.db.query("SELECT DISTINCT users.id AS id FROM builds_watchers \ |
f6e6ff79 MT |
966 | JOIN users ON builds_watchers.user_id = users.id \ |
967 | WHERE builds_watchers.build_id = %s AND NOT users.deleted = 'Y' \ | |
968 | AND users.activated = 'Y' ORDER BY users.id", self.id) | |
969 | ||
326664a5 | 970 | return [users.User(self.backend, u.id) for u in query] |
f6e6ff79 MT |
971 | |
972 | def add_watcher(self, user): | |
973 | # Don't add a user twice. | |
974 | if user in self.get_watchers(): | |
975 | return | |
976 | ||
977 | self.db.execute("INSERT INTO builds_watchers(build_id, user_id) \ | |
978 | VALUES(%s, %s)", self.id, user.id) | |
979 | ||
980 | @property | |
981 | def message_recipients(self): | |
982 | ret = [] | |
983 | ||
984 | for watcher in self.get_watchers(): | |
985 | ret.append("%s <%s>" % (watcher.realname, watcher.email)) | |
986 | ||
987 | return ret | |
988 | ||
989 | @property | |
990 | def update(self): | |
991 | if self._update is None: | |
992 | update = self.db.get("SELECT update_id AS id FROM updates_builds \ | |
993 | WHERE build_id = %s", self.id) | |
994 | ||
995 | if update: | |
326664a5 | 996 | self._update = updates.Update(self.backend, update.id) |
f6e6ff79 MT |
997 | |
998 | return self._update | |
999 | ||
326664a5 | 1000 | @lazy_property |
f6e6ff79 | 1001 | def repo(self): |
326664a5 MT |
1002 | res = self.db.get("SELECT repo_id FROM repositories_builds \ |
1003 | WHERE build_id = %s", self.id) | |
f6e6ff79 | 1004 | |
326664a5 MT |
1005 | if res: |
1006 | return self.backend.repos.get_by_id(res.repo_id) | |
f6e6ff79 MT |
1007 | |
1008 | def get_repo_moves(self, limit=None): | |
1009 | query = "SELECT * FROM repositories_history \ | |
1010 | WHERE build_id = %s ORDER BY time ASC" | |
1011 | ||
1012 | actions = [] | |
1013 | for action in self.db.query(query, self.id): | |
326664a5 | 1014 | action = logs.RepositoryLogEntry(self.backend, action) |
f6e6ff79 MT |
1015 | actions.append(action) |
1016 | ||
1017 | return actions | |
1018 | ||
1019 | @property | |
1020 | def is_loose(self): | |
1021 | if self.repo: | |
1022 | return False | |
1023 | ||
1024 | return True | |
1025 | ||
1026 | @property | |
1027 | def repo_time(self): | |
1028 | repo = self.db.get("SELECT time_added FROM repositories_builds \ | |
1029 | WHERE build_id = %s", self.id) | |
1030 | ||
1031 | if repo: | |
1032 | return repo.time_added | |
1033 | ||
1034 | def get_auto_move(self): | |
1035 | return self.data.auto_move == "Y" | |
1036 | ||
1037 | def set_auto_move(self, state): | |
326664a5 | 1038 | self._set_attribute("auto_move", state) |
f6e6ff79 MT |
1039 | |
1040 | auto_move = property(get_auto_move, set_auto_move) | |
1041 | ||
1042 | @property | |
1043 | def can_move_forward(self): | |
1044 | if not self.repo: | |
1045 | return False | |
1046 | ||
1047 | # If there is no next repository, we cannot move anything. | |
d629da45 | 1048 | if not self.repo.next: |
f6e6ff79 MT |
1049 | return False |
1050 | ||
1051 | # If the needed amount of score is reached, we can move forward. | |
d629da45 | 1052 | if self.score >= self.repo.next.score_needed: |
f6e6ff79 MT |
1053 | return True |
1054 | ||
1055 | # If the repository does not require a minimal time, | |
1056 | # we can move forward immediately. | |
1057 | if not self.repo.time_min: | |
1058 | return True | |
1059 | ||
1060 | query = self.db.get("SELECT NOW() - time_added AS duration FROM repositories_builds \ | |
1061 | WHERE build_id = %s", self.id) | |
1062 | duration = query.duration | |
1063 | ||
1064 | if duration >= self.repo.time_min: | |
1065 | return True | |
1066 | ||
1067 | return False | |
1068 | ||
1069 | ## Bugs | |
1070 | ||
1071 | def get_bug_ids(self): | |
1072 | query = self.db.query("SELECT bug_id FROM builds_bugs \ | |
1073 | WHERE build_id = %s", self.id) | |
1074 | ||
1075 | return [b.bug_id for b in query] | |
1076 | ||
1077 | def add_bug(self, bug_id, user=None, log=True): | |
1078 | # Check if this bug is already in the list of bugs. | |
1079 | if bug_id in self.get_bug_ids(): | |
1080 | return | |
1081 | ||
1082 | self.db.execute("INSERT INTO builds_bugs(build_id, bug_id) \ | |
1083 | VALUES(%s, %s)", self.id, bug_id) | |
1084 | ||
1085 | # Log the event. | |
1086 | if log: | |
1087 | self.log("bug_added", user=user, bug_id=bug_id) | |
1088 | ||
1089 | def rem_bug(self, bug_id, user=None, log=True): | |
1090 | self.db.execute("DELETE FROM builds_bugs WHERE build_id = %s AND \ | |
1091 | bug_id = %s", self.id, bug_id) | |
1092 | ||
1093 | # Log the event. | |
1094 | if log: | |
1095 | self.log("bug_removed", user=user, bug_id=bug_id) | |
1096 | ||
1097 | def search_for_bugs(self): | |
1098 | if not self.commit: | |
1099 | return | |
1100 | ||
1101 | pattern = re.compile(r"(bug\s?|#)(\d+)") | |
1102 | ||
1103 | for txt in (self.commit.subject, self.commit.message): | |
1104 | for bug in re.finditer(pattern, txt): | |
1105 | try: | |
1106 | bugid = int(bug.group(2)) | |
1107 | except ValueError: | |
1108 | continue | |
1109 | ||
1110 | # Check if a bug with the given ID exists in BZ. | |
326664a5 | 1111 | bug = self.backend.bugzilla.get_bug(bugid) |
f6e6ff79 MT |
1112 | if not bug: |
1113 | continue | |
1114 | ||
1115 | self.add_bug(bugid) | |
1116 | ||
1117 | def get_bugs(self): | |
1118 | bugs = [] | |
1119 | for bug_id in self.get_bug_ids(): | |
326664a5 | 1120 | bug = self.backend.bugzilla.get_bug(bug_id) |
f6e6ff79 MT |
1121 | if not bug: |
1122 | continue | |
1123 | ||
1124 | bugs.append(bug) | |
1125 | ||
1126 | return bugs | |
1127 | ||
1128 | def _update_bugs_helper(self, repo): | |
1129 | """ | |
1130 | This function takes a new status and generates messages that | |
1131 | are appended to all bugs. | |
1132 | """ | |
1133 | try: | |
1134 | kwargs = BUG_MESSAGES[repo.type].copy() | |
1135 | except KeyError: | |
1136 | return | |
1137 | ||
326664a5 | 1138 | baseurl = self.backend.settings.get("baseurl", "") |
f6e6ff79 MT |
1139 | args = { |
1140 | "build_url" : "%s/build/%s" % (baseurl, self.uuid), | |
1141 | "distro_name" : self.distro.name, | |
1142 | "package_name" : self.name, | |
1143 | "repo_name" : repo.name, | |
1144 | } | |
1145 | kwargs["comment"] = kwargs["comment"] % args | |
1146 | ||
1147 | self.update_bugs(**kwargs) | |
1148 | ||
1149 | def _update_bug(self, bug_id, status=None, resolution=None, comment=None): | |
1150 | self.db.execute("INSERT INTO builds_bugs_updates(bug_id, status, resolution, comment, time) \ | |
1151 | VALUES(%s, %s, %s, %s, NOW())", bug_id, status, resolution, comment) | |
1152 | ||
1153 | def update_bugs(self, status, resolution=None, comment=None): | |
1154 | # Update all bugs linked to this build. | |
1155 | for bug_id in self.get_bug_ids(): | |
1156 | self._update_bug(bug_id, status=status, resolution=resolution, comment=comment) |