]> git.ipfire.org Git - people/jschlag/pbs.git/blame - src/buildservice/builds.py
jobs: Remove deps to type field
[people/jschlag/pbs.git] / src / buildservice / builds.py
CommitLineData
f6e6ff79
MT
1#!/usr/bin/python
2
f6e6ff79
MT
3import logging
4import os
5import re
f6e6ff79
MT
6import uuid
7
f6e6ff79
MT
8import pakfire.packages
9
2c909128 10from . import base
2c909128 11from . import logs
2c909128
MT
12from . import updates
13from . import users
14
e153d3f6
MT
15log = logging.getLogger("builds")
16log.propagate = 1
17
2c909128 18from .constants import *
044a9c43 19from .decorators import *
f6e6ff79 20
f6e6ff79 21class 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 404class 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)