]> git.ipfire.org Git - people/jschlag/pbs.git/blame - src/buildservice/builds.py
builds: Drop functionality to reset a build
[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>""" % \
044a9c43 765 (job.state, job.type, 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
780 def get_jobs(self, type=None):
781 """
782 Returns a list of jobs of this build.
783 """
326664a5 784 return self.backend.jobs.get_by_build(self.id, self, type=type)
f6e6ff79 785
326664a5 786 @lazy_property
f6e6ff79
MT
787 def jobs(self):
788 """
789 Get a list of all build jobs that are in this build.
790 """
326664a5 791 return self.get_jobs(type="build")
f6e6ff79
MT
792
793 @property
794 def test_jobs(self):
326664a5 795 return self.get_jobs(type="test")
f6e6ff79
MT
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.
326664a5 817 for arch in self.backend.arches.expand(arches):
e153d3f6
MT
818 # Don't create jobs for src
819 if arch == "src":
f6e6ff79
MT
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"):
3ee30276 829 job = self.backend.jobs.create(self, arch, type=type)
f6e6ff79
MT
830
831 # Add new job to cache.
326664a5 832 self.jobs.append(job)
f6e6ff79
MT
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 \
e153d3f6 862 WHERE update_year = EXTRACT(year FROM NOW()) ORDER BY update_num DESC LIMIT 1")
f6e6ff79
MT
863
864 if update:
865 update_num = update.num + 1
866 else:
867 update_num = 1
868
e153d3f6 869 self.db.execute("UPDATE builds SET update_year = EXTRACT(year FROM NOW()), update_num = %s \
f6e6ff79
MT
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):
326664a5 881 comment = logs.CommentLogEntry(self.backend, comment)
f6e6ff79
MT
882 comments.append(comment)
883
884 return comments
885
326664a5 886 def add_comment(self, user, text, score):
f6e6ff79
MT
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())",
326664a5 891 self.id, user.id, text, score)
f6e6ff79 892
326664a5
MT
893 # Update the credit cache
894 self.score += score
f6e6ff79
MT
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
326664a5 902 @lazy_property
f6e6ff79 903 def score(self):
326664a5
MT
904 res = self.db.get("SELECT SUM(credit) AS score \
905 FROM builds_comments WHERE build_id = %s", self.id)
f6e6ff79 906
326664a5 907 return res.score or 0
f6e6ff79
MT
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
326664a5 920 return [users.User(self.backend, u.id) for u in users]
f6e6ff79
MT
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.
326664a5 930 user = self.backend.users.get_by_id(comment.user_id)
f6e6ff79
MT
931
932 format = {
933 "build_name" : self.name,
934 "user_name" : user.realname,
935 }
936
937 # XXX create beautiful message
938
326664a5 939 self.backend.messages.send_to_all(self.message_recipients,
f6e6ff79
MT
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
fd681905 948 # Created entry.
326664a5 949 created_entry = logs.CreatedLogEntry(self.backend, self)
fd681905
MT
950 entries.append(created_entry)
951
f6e6ff79
MT
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):
fe8e7f02 969 query = self.db.query("SELECT DISTINCT users.id AS id FROM builds_watchers \
f6e6ff79
MT
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
326664a5 974 return [users.User(self.backend, u.id) for u in query]
f6e6ff79
MT
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:
326664a5 1000 self._update = updates.Update(self.backend, update.id)
f6e6ff79
MT
1001
1002 return self._update
1003
326664a5 1004 @lazy_property
f6e6ff79 1005 def repo(self):
326664a5
MT
1006 res = self.db.get("SELECT repo_id FROM repositories_builds \
1007 WHERE build_id = %s", self.id)
f6e6ff79 1008
326664a5
MT
1009 if res:
1010 return self.backend.repos.get_by_id(res.repo_id)
f6e6ff79
MT
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):
326664a5 1018 action = logs.RepositoryLogEntry(self.backend, action)
f6e6ff79
MT
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):
326664a5 1042 self._set_attribute("auto_move", state)
f6e6ff79
MT
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.
d629da45 1052 if not self.repo.next:
f6e6ff79
MT
1053 return False
1054
1055 # If the needed amount of score is reached, we can move forward.
d629da45 1056 if self.score >= self.repo.next.score_needed:
f6e6ff79
MT
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.
326664a5 1115 bug = self.backend.bugzilla.get_bug(bugid)
f6e6ff79
MT
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():
326664a5 1124 bug = self.backend.bugzilla.get_bug(bug_id)
f6e6ff79
MT
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
326664a5 1142 baseurl = self.backend.settings.get("baseurl", "")
f6e6ff79
MT
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)