]> git.ipfire.org Git - pbs.git/blob - src/buildservice/sources.py
Upvote builds when it has testers
[pbs.git] / src / buildservice / sources.py
1 #!/usr/bin/python
2
3 import datetime
4 import logging
5 import os
6 import pakfire
7 import pakfire.config
8 import re
9 import shutil
10 import subprocess
11 import tempfile
12
13 from . import base
14 from . import git
15
16 from .constants import *
17 from .decorators import *
18
19 VALID_TAGS = (
20 "Acked-by",
21 "Cc",
22 "Fixes",
23 "Reported-by",
24 "Reviewed-by",
25 "Signed-off-by",
26 "Suggested-by",
27 "Tested-by",
28 )
29
30 class Sources(base.Object):
31 def _get_source(self, query, *args):
32 res = self.db.get(query, *args)
33
34 if res:
35 return Source(self.backend, res.id, data=res)
36
37 def _get_sources(self, query, *args):
38 res = self.db.query(query, *args)
39
40 for row in res:
41 yield Source(self.backend, row.id, data=row)
42
43 def _get_commit(self, query, *args):
44 res = self.db.get(query, *args)
45
46 if res:
47 return Commit(self.backend, res.id, data=res)
48
49 def _get_commits(self, query, *args):
50 res = self.db.query(query, *args)
51
52 for row in res:
53 yield Commit(self.backend, row.id, data=row)
54
55 def __iter__(self):
56 return self._get_sources("SELECT * FROM sources")
57
58 def get_by_id(self, id):
59 return self._get_source("SELECT * FROM sources \
60 WHERE id = %s", id)
61
62 def get_by_distro(self, distro):
63 return self._get_sources("SELECT * FROM sources \
64 WHERE distro_id = %s", distro.id)
65
66 def update_revision(self, source_id, revision):
67 query = "UPDATE sources SET revision = %s WHERE id = %s"
68
69 return self.db.execute(query, revision, source_id)
70
71 def get_commit_by_id(self, commit_id):
72 commit = self.db.get("SELECT id FROM sources_commits WHERE id = %s", commit_id)
73
74 if commit:
75 return Commit(self.pakfire, commit.id)
76
77 def pull(self):
78 for source in self:
79 with git.Repo(self.backend, source, mode="mirror") as repo:
80 # Fetch the latest updates
81 repo.fetch()
82
83 # Import all new revisions
84 repo.import_revisions()
85
86 def dist(self):
87 # Walk through all source repositories
88 for source in self:
89 # Get access to the git repo
90 with git.Repo(self.pakfire, source) as repo:
91 # Walk through all pending commits
92 for commit in source.pending_commits:
93 commit.state = "running"
94
95 logging.debug("Processing commit %s: %s" % (commit.revision, commit.subject))
96
97 # Navigate to the right revision.
98 repo.checkout(commit.revision)
99
100 # Get all changed makefiles.
101 deleted_files = []
102 updated_files = []
103
104 for file in repo.changed_files(commit.revision):
105 # Don't care about files that are not a makefile.
106 if not file.endswith(".%s" % MAKEFILE_EXTENSION):
107 continue
108
109 if os.path.exists(file):
110 updated_files.append(file)
111 else:
112 deleted_files.append(file)
113
114 if updated_files:
115 # Create a temporary directory where to put all the files
116 # that are generated here.
117 pkg_dir = tempfile.mkdtemp()
118
119 try:
120 config = pakfire.config.Config(["general.conf",])
121 config.parse(source.distro.get_config())
122
123 p = pakfire.PakfireServer(config=config)
124
125 pkgs = []
126 for file in updated_files:
127 try:
128 pkg_file = p.dist(file, pkg_dir)
129 pkgs.append(pkg_file)
130 except:
131 raise
132
133 # Import all packages in one swoop.
134 for pkg in pkgs:
135 with self.db.transaction():
136 build = self.backend.builds.create_from_source_package(pkg,
137 source.distro, commit=commit, type="release")
138
139 # Import any testers from the commit message
140 for tester in commit.testers:
141 build.upvote(tester)
142
143 except:
144 if commit:
145 commit.state = "failed"
146
147 raise
148
149 finally:
150 if os.path.exists(pkg_dir):
151 shutil.rmtree(pkg_dir)
152
153 for file in deleted_files:
154 # Determine the name of the package.
155 name = os.path.basename(file)
156 name = name[:len(MAKEFILE_EXTENSION) + 1]
157
158 source.distro.delete_package(name)
159
160 if commit:
161 commit.state = "finished"
162
163
164 class Commit(base.DataObject):
165 table = "sources_commits"
166
167 @property
168 def revision(self):
169 return self.data.revision
170
171 @lazy_property
172 def source(self):
173 return self.backend.sources.get_by_id(self.data.source_id)
174
175 @property
176 def distro(self):
177 """
178 A shortcut to the distribution this commit
179 belongs to.
180 """
181 return self.source.distro
182
183 def set_state(self, state):
184 self._set_attribute("state", state)
185
186 state = property(lambda s: s.data.state, set_state)
187
188 @lazy_property
189 def author(self):
190 return self.backend.users.find_maintainer(self.data.author) or self.data.author
191
192 @lazy_property
193 def committer(self):
194 return self.backend.users.find_maintainer(self.data.committer) or self.data.committer
195
196 @property
197 def subject(self):
198 return self.data.subject.strip()
199
200 @property
201 def body(self):
202 return self.data.body.strip()
203
204 @lazy_property
205 def message(self):
206 """
207 Returns the message without any Git tags
208 """
209 # Compile regex
210 r = re.compile("^(%s):" % "|".join(VALID_TAGS), re.IGNORECASE)
211
212 message = []
213 for line in self.body.splitlines():
214 # Find lines that start with a known Git tag
215 if r.match(line):
216 continue
217
218 message.append(line)
219
220 # If all lines are empty lines, we send back an empty message
221 if all((l == "" for l in message)):
222 return
223
224 # We will now break the message into paragraphs
225 paragraphs = re.split("\n\n+", "\n".join(message))
226 print paragraphs
227
228 message = []
229 for paragraph in paragraphs:
230 # Remove all line breaks that are not following a colon
231 # and where the next line does not start with a star.
232 paragraph = re.sub("(?<=\:)\n(?=[\*\s])", " ", paragraph)
233
234 message.append(paragraph)
235
236 return "\n\n".join(message)
237
238 @property
239 def message_full(self):
240 message = self.subject
241
242 if self.message:
243 message += "\n\n%s" % self.message
244
245 return message
246
247 def get_tag(self, tag):
248 """
249 Returns a list of the values of this Git tag
250 """
251 if not tag in VALID_TAGS:
252 raise ValueError("Unknown tag: %s" % tag)
253
254 # Compile regex
255 r = re.compile("^%s: (.*)$" % tag, re.IGNORECASE)
256
257 values = []
258 for line in self.body.splitlines():
259 # Skip all empty lines
260 if not line:
261 continue
262
263 # Check if line matches the regex
264 m = r.match(line)
265 if m:
266 values.append(m.group(1))
267
268 return values
269
270 @lazy_property
271 def contributors(self):
272 contributors = [
273 self.data.author,
274 self.data.committer,
275 ]
276
277 for tag in ("Acked-by", "Cc", "Reported-by", "Reviewed-by", "Signed-off-by", "Suggested-by", "Tested-by"):
278 contributors += self.get_tag(tag)
279
280 # Get all user accounts that we know
281 users = self.backend.users.find_maintainers(contributors)
282
283 # Add all email addresses where a user could not be found
284 for contributor in contributors[:]:
285 for user in users:
286 if user.has_email_address(contributor):
287 try:
288 contributors.remove(contributor)
289 except:
290 pass
291
292 return sorted(contributors + users)
293
294 @lazy_property
295 def testers(self):
296 users = []
297
298 for tag in ("Acked-by", "Reviewed-by", "Signed-off-by", "Tested-by"):
299 users += self.get_tag(tag)
300
301 return self.backend.users.find_maintainers(users)
302
303 @property
304 def date(self):
305 return self.data.date
306
307 @lazy_property
308 def packages(self):
309 return self.backend.packages._get_packages("SELECT * FROM packages \
310 WHERE commit_id = %s", self.id)
311
312 def reset(self):
313 """
314 Removes all packages that have been created by this commit and
315 resets the state so it will be processed again.
316 """
317 # Remove all packages and corresponding builds.
318 for pkg in self.packages:
319 # Check if there is a build associated with the package.
320 # If so, the whole build will be deleted.
321 if pkg.build:
322 pkg.build.delete()
323
324 else:
325 # Delete the package.
326 pkg.delete()
327
328 # Clear the cache.
329 del self.packages
330
331 # Reset the state to 'pending'.
332 self.state = "pending"
333
334
335 class Source(base.DataObject):
336 table = "sources"
337
338 def __eq__(self, other):
339 return self.id == other.id
340
341 def __len__(self):
342 ret = self.db.get("SELECT COUNT(*) AS len FROM sources_commits \
343 WHERE source_id = %s", self.id)
344
345 return ret.len
346
347 def create_commit(self, revision, author, committer, subject, body, date):
348 commit = self.backend.sources._get_commit("INSERT INTO sources_commits(source_id, \
349 revision, author, committer, subject, body, date) VALUES(%s, %s, %s, %s, %s, %s, %s) \
350 RETURNING *", self.id, revision, author, committer, subject, body, date)
351
352 # Commit
353 commit.source = self
354
355 return commit
356
357 @property
358 def info(self):
359 return {
360 "id" : self.id,
361 "name" : self.name,
362 "url" : self.url,
363 "path" : self.path,
364 "targetpath" : self.targetpath,
365 "revision" : self.revision,
366 "branch" : self.branch,
367 }
368
369 @property
370 def name(self):
371 return self.data.name
372
373 @property
374 def identifier(self):
375 return self.data.identifier
376
377 @property
378 def url(self):
379 return self.data.url
380
381 @property
382 def gitweb(self):
383 return self.data.gitweb
384
385 @property
386 def revision(self):
387 return self.data.revision
388
389 @property
390 def branch(self):
391 return self.data.branch
392
393 @property
394 def builds(self):
395 return self.pakfire.builds.get_by_source(self.id)
396
397 @lazy_property
398 def distro(self):
399 return self.pakfire.distros.get_by_id(self.data.distro_id)
400
401 @property
402 def start_revision(self):
403 return self.data.revision
404
405 @lazy_property
406 def head_revision(self):
407 return self.backend.sources._get_commit("SELECT * FROM sources_commits \
408 WHERE source_id = %s ORDER BY id DESC LIMIT 1", self.id)
409
410 def get_commits(self, limit=None, offset=None):
411 return self.backend.sources._get_commits("SELECT * FROM sources_commits \
412 WHERE source_id = %s ORDER BY id DESC LIMIT %s OFFSET %s", limit, offset)
413
414 def get_commit(self, revision):
415 commit = self.backend.sources._get_commit("SELECT * FROM sources_commits \
416 WHERE source_id = %s AND revision = %s", self.id, revision)
417
418 if commit:
419 commit.source = self
420 return commit
421
422 @property
423 def pending_commits(self):
424 return self.backend.sources._get_commits("SELECT * FROM sources_commits \
425 WHERE state = %s ORDER BY imported_at", "pending")