]> git.ipfire.org Git - people/jschlag/pbs.git/blob - src/buildservice/builds.py
builds: Drop functionality to reset a build
[people/jschlag/pbs.git] / src / buildservice / builds.py
1 #!/usr/bin/python
2
3 import logging
4 import os
5 import re
6 import uuid
7
8 import pakfire.packages
9
10 from . import base
11 from . import logs
12 from . import updates
13 from . import users
14
15 log = logging.getLogger("builds")
16 log.propagate = 1
17
18 from .constants import *
19 from .decorators import *
20
21 class Builds(base.Object):
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
34 def get_by_id(self, id, data=None):
35 return Build(self.backend, id, data=data)
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):
44 query = "SELECT * FROM builds ORDER BY time_created DESC"
45
46 if limit:
47 query += " LIMIT %d" % limit
48
49 return [self.get_by_id(b.id, b) for b in self.db.query(query)]
50
51 def get_by_user(self, user, type=None, public=None):
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
68 query = "SELECT builds.* AS id FROM builds \
69 JOIN packages ON builds.pkg_id = packages.id"
70
71 if conditions:
72 query += " WHERE %s" % " AND ".join(conditions)
73
74 query += " ORDER BY builds.time_created DESC"
75
76 builds = []
77 for build in self.db.query(query, *args):
78 build = Build(self.backend, build.id, build)
79 builds.append(build)
80
81 return builds
82
83 def get_by_name(self, name, type=None, public=None, user=None, limit=None, offset=None):
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
103 query = "SELECT builds.* AS id FROM builds \
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
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"
116
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
125 return [Build(self.backend, b.id, b) for b in self.db.query(query, *args)]
126
127 def get_latest_by_name(self, name, type=None, public=None):
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"
132 args = [name,]
133
134 if type:
135 query += " AND builds_latest.build_type = %s"
136 args.append(type)
137
138 if public is True:
139 query += " AND builds.public = %s"
140 args.append("Y")
141 elif public is False:
142 query += " AND builds.public = %s"
143 args.append("N")
144
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"
152
153 res = self.db.get(query, *args)
154
155 if res:
156 return Build(self.backend, res.id, res)
157
158 def get_active_builds(self, name, public=None):
159 query = "\
160 SELECT * FROM builds \
161 LEFT JOIN builds_latest ON builds.id = builds_latest.build_id \
162 WHERE builds_latest.package_name = %s AND builds.type = %s"
163 args = [name, "release"]
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
172 builds = []
173 for row in self.db.query(query, *args):
174 b = Build(self.backend, row.id, row)
175 builds.append(b)
176
177 # Sort the result. Lastest build first.
178 builds.sort(reverse=True)
179
180 return builds
181
182 def count(self):
183 builds = self.db.get("SELECT COUNT(*) AS count FROM builds")
184 if builds:
185 return builds.count
186
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:
212 build = Build(self.backend, build.id)
213 builds.append(build)
214
215 return builds
216
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
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):
319 b = Build(self.backend, b.id, b)
320 builds.append(b)
321
322 builds.sort(reverse=True)
323
324 return builds
325
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):
354 comment = logs.CommentLogEntry(self.backend, comment)
355 comments.append(comment)
356
357 return comments
358
359 def get_build_times_summary(self, name=None, arch=None):
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
380 # Filter by arch.
381 if arch:
382 conditions.append("builds_times.arch = %s")
383 args.append(arch)
384
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
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
403
404 class Build(base.DataObject):
405 table = "builds"
406
407 def __repr__(self):
408 return "<%s id=%s %s>" % (self.__class__.__name__, self.id, self.pkg)
409
410 def __eq__(self, other):
411 if isinstance(other, self.__class__):
412 return self.id == other.id
413
414 def __lt__(self, other):
415 if isinstance(other, self.__class__):
416 return self.pkg < other.pkg
417
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
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)
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
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
494 @lazy_property
495 def pkg(self):
496 """
497 Get package that is to be built in the build.
498 """
499 return self.backend.packages.get_by_id(self.data.pkg_id)
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
512 def get_owner(self):
513 """
514 The owner of this build.
515 """
516 if self.data.owner_id:
517 return self.backend.users.get_by_id(self.data.owner_id)
518
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)
524
525 owner = lazy_property(get_owner, set_owner)
526
527 @lazy_property
528 def distro(self):
529 return self.backend.distros.get_by_id(self.data.distro_id)
530
531 @property
532 def user(self):
533 if self.type == "scratch":
534 return self.owner
535
536 def get_depends_on(self):
537 if self.data.depends_on:
538 return self.backend.builds.get_by_id(self.data.depends_on)
539
540 def set_depends_on(self, build):
541 self._set_attribute("depends_on", build.id)
542
543 depends_on = lazy_property(get_depends_on, set_depends_on)
544
545 @property
546 def created(self):
547 return self.data.time_created
548
549 @property
550 def date(self):
551 return self.created.date()
552
553 @property
554 def public(self):
555 """
556 Is this build public?
557 """
558 return self.data.public
559
560 @lazy_property
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
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
599 self._set_attribute("state", state)
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
626 def is_broken(self):
627 return self.state == "broken"
628
629 def obsolete_others(self):
630 if not self.type == "release":
631 return
632
633 for build in self.backend.builds.get_by_name(self.pkg.name, type="release"):
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):
646 self._set_attribute("severity", severity)
647
648 def get_severity(self):
649 return self.data.severity
650
651 severity = property(get_severity, set_severity)
652
653 @lazy_property
654 def commit(self):
655 if self.pkg and self.pkg.commit:
656 return self.pkg.commit
657
658 def update_message(self, message):
659 self._set_attribute("message", message)
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
718 self._set_attribute("priority", priority)
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):
745 return "/".join((self.backend.settings.get("download_baseurl"), "packages"))
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>""" % \
765 (job.state, job.type, job.uuid, job.arch))
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
780 def get_jobs(self, type=None):
781 """
782 Returns a list of jobs of this build.
783 """
784 return self.backend.jobs.get_by_build(self.id, self, type=type)
785
786 @lazy_property
787 def jobs(self):
788 """
789 Get a list of all build jobs that are in this build.
790 """
791 return self.get_jobs(type="build")
792
793 @property
794 def test_jobs(self):
795 return self.get_jobs(type="test")
796
797 @property
798 def all_jobs_finished(self):
799 ret = True
800
801 for job in self.jobs:
802 if not job.state == "finished":
803 ret = False
804 break
805
806 return ret
807
808 def create_autojobs(self, arches=None, type="build"):
809 jobs = []
810
811 # Arches may be passed to this function. If not we use all arches
812 # this package supports.
813 if arches is None:
814 arches = self.supported_arches
815
816 # Create a new job for every given archirecture.
817 for arch in self.backend.arches.expand(arches):
818 # Don't create jobs for src
819 if arch == "src":
820 continue
821
822 job = self.add_job(arch, type=type)
823 jobs.append(job)
824
825 # Return all newly created jobs.
826 return jobs
827
828 def add_job(self, arch, type="build"):
829 job = self.backend.jobs.create(self, arch, type=type)
830
831 # Add new job to cache.
832 self.jobs.append(job)
833
834 return job
835
836 ## Update stuff
837
838 @property
839 def update_id(self):
840 if not self.type == "release":
841 return
842
843 # Generate an update ID if none does exist, yet.
844 self.generate_update_id()
845
846 s = [
847 "%s" % self.distro.name.replace(" ", "").upper(),
848 "%04d" % (self.data.update_year or 0),
849 "%04d" % (self.data.update_num or 0),
850 ]
851
852 return "-".join(s)
853
854 def generate_update_id(self):
855 if not self.type == "release":
856 return
857
858 if self.data.update_num:
859 return
860
861 update = self.db.get("SELECT update_num AS num FROM builds \
862 WHERE update_year = EXTRACT(year FROM NOW()) ORDER BY update_num DESC LIMIT 1")
863
864 if update:
865 update_num = update.num + 1
866 else:
867 update_num = 1
868
869 self.db.execute("UPDATE builds SET update_year = EXTRACT(year FROM NOW()), update_num = %s \
870 WHERE id = %s", update_num, self.id)
871
872 ## Comment stuff
873
874 def get_comments(self, limit=10, offset=0):
875 query = "SELECT * FROM builds_comments \
876 JOIN users ON builds_comments.user_id = users.id \
877 WHERE build_id = %s ORDER BY time_created ASC"
878
879 comments = []
880 for comment in self.db.query(query, self.id):
881 comment = logs.CommentLogEntry(self.backend, comment)
882 comments.append(comment)
883
884 return comments
885
886 def add_comment(self, user, text, score):
887 # Add the new comment to the database.
888 id = self.db.execute("INSERT INTO \
889 builds_comments(build_id, user_id, text, credit, time_created) \
890 VALUES(%s, %s, %s, %s, NOW())",
891 self.id, user.id, text, score)
892
893 # Update the credit cache
894 self.score += score
895
896 # Send the new comment to all watchers and stuff.
897 self.send_comment_message(id)
898
899 # Return the ID of the newly created comment.
900 return id
901
902 @lazy_property
903 def score(self):
904 res = self.db.get("SELECT SUM(credit) AS score \
905 FROM builds_comments WHERE build_id = %s", self.id)
906
907 return res.score or 0
908
909 @property
910 def credits(self):
911 # XXX COMPAT
912 return self.score
913
914 def get_commenters(self):
915 users = self.db.query("SELECT DISTINCT users.id AS id FROM builds_comments \
916 JOIN users ON builds_comments.user_id = users.id \
917 WHERE builds_comments.build_id = %s AND NOT users.deleted = 'Y' \
918 AND NOT users.activated = 'Y' ORDER BY users.id", self.id)
919
920 return [users.User(self.backend, u.id) for u in users]
921
922 def send_comment_message(self, comment_id):
923 comment = self.db.get("SELECT * FROM builds_comments WHERE id = %s",
924 comment_id)
925
926 assert comment
927 assert comment.build_id == self.id
928
929 # Get user who wrote the comment.
930 user = self.backend.users.get_by_id(comment.user_id)
931
932 format = {
933 "build_name" : self.name,
934 "user_name" : user.realname,
935 }
936
937 # XXX create beautiful message
938
939 self.backend.messages.send_to_all(self.message_recipients,
940 N_("%(user_name)s commented on %(build_name)s"),
941 comment.text, format)
942
943 ## Logging stuff
944
945 def get_log(self, comments=True, repo=True, limit=None):
946 entries = []
947
948 # Created entry.
949 created_entry = logs.CreatedLogEntry(self.backend, self)
950 entries.append(created_entry)
951
952 if comments:
953 entries += self.get_comments(limit=limit)
954
955 if repo:
956 entries += self.get_repo_moves(limit=limit)
957
958 # Sort all entries in chronological order.
959 entries.sort()
960
961 if limit:
962 entries = entries[:limit]
963
964 return entries
965
966 ## Watchers stuff
967
968 def get_watchers(self):
969 query = self.db.query("SELECT DISTINCT users.id AS id FROM builds_watchers \
970 JOIN users ON builds_watchers.user_id = users.id \
971 WHERE builds_watchers.build_id = %s AND NOT users.deleted = 'Y' \
972 AND users.activated = 'Y' ORDER BY users.id", self.id)
973
974 return [users.User(self.backend, u.id) for u in query]
975
976 def add_watcher(self, user):
977 # Don't add a user twice.
978 if user in self.get_watchers():
979 return
980
981 self.db.execute("INSERT INTO builds_watchers(build_id, user_id) \
982 VALUES(%s, %s)", self.id, user.id)
983
984 @property
985 def message_recipients(self):
986 ret = []
987
988 for watcher in self.get_watchers():
989 ret.append("%s <%s>" % (watcher.realname, watcher.email))
990
991 return ret
992
993 @property
994 def update(self):
995 if self._update is None:
996 update = self.db.get("SELECT update_id AS id FROM updates_builds \
997 WHERE build_id = %s", self.id)
998
999 if update:
1000 self._update = updates.Update(self.backend, update.id)
1001
1002 return self._update
1003
1004 @lazy_property
1005 def repo(self):
1006 res = self.db.get("SELECT repo_id FROM repositories_builds \
1007 WHERE build_id = %s", self.id)
1008
1009 if res:
1010 return self.backend.repos.get_by_id(res.repo_id)
1011
1012 def get_repo_moves(self, limit=None):
1013 query = "SELECT * FROM repositories_history \
1014 WHERE build_id = %s ORDER BY time ASC"
1015
1016 actions = []
1017 for action in self.db.query(query, self.id):
1018 action = logs.RepositoryLogEntry(self.backend, action)
1019 actions.append(action)
1020
1021 return actions
1022
1023 @property
1024 def is_loose(self):
1025 if self.repo:
1026 return False
1027
1028 return True
1029
1030 @property
1031 def repo_time(self):
1032 repo = self.db.get("SELECT time_added FROM repositories_builds \
1033 WHERE build_id = %s", self.id)
1034
1035 if repo:
1036 return repo.time_added
1037
1038 def get_auto_move(self):
1039 return self.data.auto_move == "Y"
1040
1041 def set_auto_move(self, state):
1042 self._set_attribute("auto_move", state)
1043
1044 auto_move = property(get_auto_move, set_auto_move)
1045
1046 @property
1047 def can_move_forward(self):
1048 if not self.repo:
1049 return False
1050
1051 # If there is no next repository, we cannot move anything.
1052 if not self.repo.next:
1053 return False
1054
1055 # If the needed amount of score is reached, we can move forward.
1056 if self.score >= self.repo.next.score_needed:
1057 return True
1058
1059 # If the repository does not require a minimal time,
1060 # we can move forward immediately.
1061 if not self.repo.time_min:
1062 return True
1063
1064 query = self.db.get("SELECT NOW() - time_added AS duration FROM repositories_builds \
1065 WHERE build_id = %s", self.id)
1066 duration = query.duration
1067
1068 if duration >= self.repo.time_min:
1069 return True
1070
1071 return False
1072
1073 ## Bugs
1074
1075 def get_bug_ids(self):
1076 query = self.db.query("SELECT bug_id FROM builds_bugs \
1077 WHERE build_id = %s", self.id)
1078
1079 return [b.bug_id for b in query]
1080
1081 def add_bug(self, bug_id, user=None, log=True):
1082 # Check if this bug is already in the list of bugs.
1083 if bug_id in self.get_bug_ids():
1084 return
1085
1086 self.db.execute("INSERT INTO builds_bugs(build_id, bug_id) \
1087 VALUES(%s, %s)", self.id, bug_id)
1088
1089 # Log the event.
1090 if log:
1091 self.log("bug_added", user=user, bug_id=bug_id)
1092
1093 def rem_bug(self, bug_id, user=None, log=True):
1094 self.db.execute("DELETE FROM builds_bugs WHERE build_id = %s AND \
1095 bug_id = %s", self.id, bug_id)
1096
1097 # Log the event.
1098 if log:
1099 self.log("bug_removed", user=user, bug_id=bug_id)
1100
1101 def search_for_bugs(self):
1102 if not self.commit:
1103 return
1104
1105 pattern = re.compile(r"(bug\s?|#)(\d+)")
1106
1107 for txt in (self.commit.subject, self.commit.message):
1108 for bug in re.finditer(pattern, txt):
1109 try:
1110 bugid = int(bug.group(2))
1111 except ValueError:
1112 continue
1113
1114 # Check if a bug with the given ID exists in BZ.
1115 bug = self.backend.bugzilla.get_bug(bugid)
1116 if not bug:
1117 continue
1118
1119 self.add_bug(bugid)
1120
1121 def get_bugs(self):
1122 bugs = []
1123 for bug_id in self.get_bug_ids():
1124 bug = self.backend.bugzilla.get_bug(bug_id)
1125 if not bug:
1126 continue
1127
1128 bugs.append(bug)
1129
1130 return bugs
1131
1132 def _update_bugs_helper(self, repo):
1133 """
1134 This function takes a new status and generates messages that
1135 are appended to all bugs.
1136 """
1137 try:
1138 kwargs = BUG_MESSAGES[repo.type].copy()
1139 except KeyError:
1140 return
1141
1142 baseurl = self.backend.settings.get("baseurl", "")
1143 args = {
1144 "build_url" : "%s/build/%s" % (baseurl, self.uuid),
1145 "distro_name" : self.distro.name,
1146 "package_name" : self.name,
1147 "repo_name" : repo.name,
1148 }
1149 kwargs["comment"] = kwargs["comment"] % args
1150
1151 self.update_bugs(**kwargs)
1152
1153 def _update_bug(self, bug_id, status=None, resolution=None, comment=None):
1154 self.db.execute("INSERT INTO builds_bugs_updates(bug_id, status, resolution, comment, time) \
1155 VALUES(%s, %s, %s, %s, NOW())", bug_id, status, resolution, comment)
1156
1157 def update_bugs(self, status, resolution=None, comment=None):
1158 # Update all bugs linked to this build.
1159 for bug_id in self.get_bug_ids():
1160 self._update_bug(bug_id, status=status, resolution=resolution, comment=comment)