1 | #!/usr/bin/python |
---|
2 | from RepSys import Error, config, RepSysTree |
---|
3 | from RepSys.svn import SVN |
---|
4 | from RepSys.util import execcmd |
---|
5 | |
---|
6 | from Cheetah.Template import Template |
---|
7 | |
---|
8 | import sys |
---|
9 | import os |
---|
10 | import re |
---|
11 | import time |
---|
12 | import locale |
---|
13 | import glob |
---|
14 | import tempfile |
---|
15 | import shutil |
---|
16 | |
---|
17 | |
---|
18 | locale.setlocale(locale.LC_ALL, "C") |
---|
19 | |
---|
20 | default_template = """ |
---|
21 | #for $rel in $releases_by_author |
---|
22 | * $rel.date $rel.author_name <$rel.author_email> $rel.version-$rel.release |
---|
23 | ## |
---|
24 | #if not $rel.released |
---|
25 | (not released yet) |
---|
26 | #end if |
---|
27 | #for $rev in $rel.release_revisions |
---|
28 | #for $line in $rev.lines |
---|
29 | $line |
---|
30 | #end for |
---|
31 | #end for |
---|
32 | |
---|
33 | #for $author in $rel.authors |
---|
34 | + $author.name <$author.email> |
---|
35 | #for $rev in $author.revisions |
---|
36 | #for $line in $rev.lines |
---|
37 | $line |
---|
38 | #end for |
---|
39 | #end for |
---|
40 | |
---|
41 | #end for |
---|
42 | #end for |
---|
43 | """ |
---|
44 | |
---|
45 | def getrelease(pkgdirurl, rev=None): |
---|
46 | """Tries to obtain the version-release of the package for a |
---|
47 | yet-not-markrelease revision of the package. |
---|
48 | |
---|
49 | Is here where things should be changed if "automatic release increasing" |
---|
50 | will be used. |
---|
51 | """ |
---|
52 | svn = SVN(baseurl=pkgdirurl) |
---|
53 | tmpdir = tempfile.mktemp() |
---|
54 | try: |
---|
55 | pkgname = RepSysTree.pkgname(pkgdirurl) |
---|
56 | pkgcurrenturl = os.path.join(pkgdirurl, "current") |
---|
57 | specurl = os.path.join(pkgcurrenturl, "SPECS") |
---|
58 | if svn.ls(specurl, noerror=1): |
---|
59 | svn.export(specurl, tmpdir, rev=rev) |
---|
60 | found = glob.glob(os.path.join(tmpdir, "*.spec")) |
---|
61 | if found: |
---|
62 | specpath = found[0] |
---|
63 | command = (("rpm -q --qf '%%{VERSION}-%%{RELEASE}\n' " |
---|
64 | "--specfile %s 2>/dev/null") % specpath) |
---|
65 | status, output = execcmd(command) |
---|
66 | if status != 0: |
---|
67 | raise Error, "Error in command %s: %s" % (command, output) |
---|
68 | releases = output.split() |
---|
69 | try: |
---|
70 | version, release = releases[0].split("-", 1) |
---|
71 | except ValueError: |
---|
72 | raise Error, "Invalid command output: %s: %s" % \ |
---|
73 | (command, output) |
---|
74 | return version, release |
---|
75 | finally: |
---|
76 | if os.path.isdir(tmpdir): |
---|
77 | shutil.rmtree(tmpdir) |
---|
78 | |
---|
79 | |
---|
80 | class _Revision: |
---|
81 | lines = [] |
---|
82 | date = None |
---|
83 | raw_date = None |
---|
84 | revision = None |
---|
85 | author_name = None |
---|
86 | author_email = None |
---|
87 | |
---|
88 | def __init__(self, **kwargs): |
---|
89 | self.__dict__.update(kwargs) |
---|
90 | |
---|
91 | |
---|
92 | class _Release(_Revision): |
---|
93 | version = None |
---|
94 | release = None |
---|
95 | revisions = None |
---|
96 | |
---|
97 | def __init__(self, **kwargs): |
---|
98 | self.revisions = [] |
---|
99 | _Revision.__init__(self, **kwargs) |
---|
100 | |
---|
101 | |
---|
102 | def format_lines(lines): |
---|
103 | first = 1 |
---|
104 | entrylines = [] |
---|
105 | perexpr = re.compile(r"([^%])%([^%])") |
---|
106 | for line in lines: |
---|
107 | line = line.lstrip() |
---|
108 | if line: |
---|
109 | line = perexpr.sub("\\1%%\\2", line) |
---|
110 | if first: |
---|
111 | first = 0 |
---|
112 | line = line.lstrip() |
---|
113 | if line[0] != "-": |
---|
114 | nextline = "- " + line |
---|
115 | else: |
---|
116 | nextline = line |
---|
117 | elif line[0] != " " and line[0] != "-": |
---|
118 | nextline = " " + line |
---|
119 | else: |
---|
120 | nextline = line |
---|
121 | if nextline not in entrylines: |
---|
122 | entrylines.append(nextline) |
---|
123 | return entrylines |
---|
124 | |
---|
125 | |
---|
126 | class _Author: |
---|
127 | name = None |
---|
128 | email = None |
---|
129 | revisions = None |
---|
130 | |
---|
131 | |
---|
132 | def group_releases_by_author(releases): |
---|
133 | allauthors = [] |
---|
134 | for release in releases: |
---|
135 | authors = {} |
---|
136 | for revision in release.revisions: |
---|
137 | authors.setdefault(revision.author, []).append(revision) |
---|
138 | |
---|
139 | # all the mess below is to sort by author and by revision number |
---|
140 | decorated = [] |
---|
141 | for authorname, revs in authors.iteritems(): |
---|
142 | author = _Author() |
---|
143 | author.name = revs[0].author_name |
---|
144 | author.email = revs[0].author_email |
---|
145 | revdeco = [(r.revision, r) for r in revs] |
---|
146 | revdeco.sort(reverse=1) |
---|
147 | author.revisions = [t[1] for t in revdeco] |
---|
148 | decorated.append((max(revdeco)[0], author)) |
---|
149 | |
---|
150 | decorated.sort(reverse=1) |
---|
151 | release.authors = [t[1] for t in decorated] |
---|
152 | # the difference between a released and a not released _Release is |
---|
153 | # the way the release numbers is obtained. So, when this is a |
---|
154 | # released, we already have it, but if we don't, we should get de |
---|
155 | # version/release string using getrelease and then get the first |
---|
156 | first, release.authors = release.authors[0], release.authors[1:] |
---|
157 | release.author_name = first.name |
---|
158 | release.author_email = first.email |
---|
159 | release.date = first.revisions[0].date |
---|
160 | release.raw_date = first.revisions[0].raw_date |
---|
161 | release.release_revisions = first.revisions |
---|
162 | release.revision = first.revisions[0].revision |
---|
163 | |
---|
164 | return releases |
---|
165 | |
---|
166 | |
---|
167 | def group_revisions_by_author(currentlog): |
---|
168 | revisions = [] |
---|
169 | last_author = None |
---|
170 | for entry in currentlog: |
---|
171 | revision = _Revision() |
---|
172 | revision.lines = format_lines(entry.lines) |
---|
173 | revision.raw_date = entry.date |
---|
174 | revision.date = parse_raw_date(entry.date) |
---|
175 | revision.revision = entry.revision |
---|
176 | if entry.author == last_author: |
---|
177 | revisions[-1].revisions.append(revision) |
---|
178 | else: |
---|
179 | author = _Author() |
---|
180 | author.name, author.email = get_author_name(entry.author) |
---|
181 | author.revisions = [revision] |
---|
182 | revisions.append(author) |
---|
183 | last_author = entry.author |
---|
184 | return revisions |
---|
185 | |
---|
186 | |
---|
187 | emailpat = re.compile("(?P<name>.*?)\s*<(?P<email>.*?)>") |
---|
188 | |
---|
189 | def get_author_name(author): |
---|
190 | found = emailpat.match(config.get("users", author, author)) |
---|
191 | name = ((found and found.group("name")) or author) |
---|
192 | email = ((found and found.group("email")) or author) |
---|
193 | return name, email |
---|
194 | |
---|
195 | def parse_raw_date(rawdate): |
---|
196 | return time.strftime("%a %b %d %Y", rawdate) |
---|
197 | |
---|
198 | def filter_log_lines(lines): |
---|
199 | # lines in commit messages containing SILENT at any position will be |
---|
200 | # skipped; commits with their log messages beggining with SILENT in the |
---|
201 | # first positionj of the first line will have all lines ignored. |
---|
202 | ignstr = config.get("log", "ignore-string", "SILENT") |
---|
203 | if len(lines) and lines[0].startswith(ignstr): |
---|
204 | return [] |
---|
205 | filtered = [line for line in lines if ignstr not in line] |
---|
206 | return filtered |
---|
207 | |
---|
208 | |
---|
209 | def make_release(author=None, revision=None, date=None, lines=None, |
---|
210 | entries=[], released=True, version=None, release=None): |
---|
211 | rel = _Release() |
---|
212 | rel.author = author |
---|
213 | if author: |
---|
214 | rel.author_name, rel.author_email = get_author_name(author) |
---|
215 | rel.revision = revision |
---|
216 | rel.version = version |
---|
217 | rel.release = release |
---|
218 | rel.date = (date and parse_raw_date(date)) or None |
---|
219 | rel.lines = lines |
---|
220 | rel.released = released |
---|
221 | for entry in entries: |
---|
222 | lines = filter_log_lines(entry.lines) |
---|
223 | revision = _Revision() |
---|
224 | revision.revision = entry.revision |
---|
225 | revision.lines = format_lines(lines) |
---|
226 | revision.date = parse_raw_date(entry.date) |
---|
227 | revision.raw_date = entry.date |
---|
228 | revision.author = entry.author |
---|
229 | (revision.author_name, revision.author_email) = \ |
---|
230 | get_author_name(entry.author) |
---|
231 | rel.revisions.append(revision) |
---|
232 | return rel |
---|
233 | |
---|
234 | |
---|
235 | def dump_file(releases, currentlog=None, template=None): |
---|
236 | templpath = template or config.get("template", "path", None) |
---|
237 | params = {} |
---|
238 | if templpath is None or not os.path.exists(templpath): |
---|
239 | params["source"] = default_template |
---|
240 | sys.stderr.write("warning: %s not found. using built-in template.\n"% |
---|
241 | templpath) |
---|
242 | else: |
---|
243 | params["file"] = templpath |
---|
244 | releases_author = group_releases_by_author(releases) |
---|
245 | revisions_author = group_revisions_by_author(currentlog) |
---|
246 | params["searchList"] = [{"releases_by_author" : releases_author, |
---|
247 | "releases" : releases, |
---|
248 | "revisions_by_author": revisions_author}] |
---|
249 | t = Template(**params) |
---|
250 | return repr(t) |
---|
251 | |
---|
252 | |
---|
253 | class InvalidEntryError(Exception): |
---|
254 | pass |
---|
255 | |
---|
256 | def parse_repsys_entry(revlog): |
---|
257 | # parse entries in the format: |
---|
258 | # %repsys <operation> |
---|
259 | # key: value |
---|
260 | # .. |
---|
261 | # <newline> |
---|
262 | # <comments> |
---|
263 | # |
---|
264 | if len(revlog.lines) == 0 or not revlog.lines[0].startswith("%repsys"): |
---|
265 | raise InvalidEntryError |
---|
266 | try: |
---|
267 | data = {"operation" : revlog.lines[0].split()[1]} |
---|
268 | except IndexError: |
---|
269 | raise InvalidEntryError |
---|
270 | for line in revlog.lines[1:]: |
---|
271 | if not line: |
---|
272 | break |
---|
273 | try: |
---|
274 | key, value = line.split(":", 1) |
---|
275 | except ValueError: |
---|
276 | raise InvalidEntryError |
---|
277 | data[key.strip().lower()] = value.strip() # ??? |
---|
278 | return data |
---|
279 | |
---|
280 | |
---|
281 | def get_revision_offset(): |
---|
282 | try: |
---|
283 | revoffset = config.getint("log", "revision-offset", 0) |
---|
284 | except (ValueError, TypeError): |
---|
285 | raise Error, ("Invalid revision-offset number in configuration " |
---|
286 | "file(s).") |
---|
287 | return revoffset or 0 |
---|
288 | |
---|
289 | oldmsgpat = re.compile( |
---|
290 | r"Copying release (?P<rel>[^\s]+) to (?P<dir>[^\s]+) directory\.") |
---|
291 | |
---|
292 | def parse_markrelease_log(relentry): |
---|
293 | if not ((relentry.lines and oldmsgpat.match(relentry.lines[0]) \ |
---|
294 | or parse_repsys_entry(relentry))): |
---|
295 | raise InvalidEntryError |
---|
296 | from_rev = None |
---|
297 | path = None |
---|
298 | for changed in relentry.changed: |
---|
299 | if changed["action"] == "A" and changed["from_rev"]: |
---|
300 | from_rev = changed["from_rev"] |
---|
301 | path = changed["path"] |
---|
302 | break |
---|
303 | else: |
---|
304 | raise InvalidEntryError |
---|
305 | # get the version and release from the names in the path, do not relay |
---|
306 | # on log messages |
---|
307 | version, release = path.rsplit(os.path.sep, 3)[-2:] |
---|
308 | return version, release, from_rev |
---|
309 | |
---|
310 | |
---|
311 | def svn2rpm(pkgdirurl, rev=None, size=None, submit=False, template=None): |
---|
312 | concat = config.get("log", "concat", "").split() |
---|
313 | revoffset = get_revision_offset() |
---|
314 | svn = SVN(baseurl=pkgdirurl) |
---|
315 | pkgreleasesurl = os.path.join(pkgdirurl, "releases") |
---|
316 | pkgcurrenturl = os.path.join(pkgdirurl, "current") |
---|
317 | releaseslog = svn.log(pkgreleasesurl, noerror=1) |
---|
318 | currentlog = svn.log(pkgcurrenturl, limit=size, start=rev, |
---|
319 | end=revoffset) |
---|
320 | |
---|
321 | # sort releases by copyfrom-revision, so that markreleases for same |
---|
322 | # revisions won't look empty |
---|
323 | releasesdata = [] |
---|
324 | if releaseslog: |
---|
325 | for relentry in releaseslog[::-1]: |
---|
326 | try: |
---|
327 | (version, release, relrevision) = \ |
---|
328 | parse_markrelease_log(relentry) |
---|
329 | except InvalidEntryError: |
---|
330 | continue |
---|
331 | releasesdata.append((relrevision, -relentry.revision, relentry, |
---|
332 | version, release)) |
---|
333 | releasesdata.sort() |
---|
334 | |
---|
335 | # collect valid releases using the versions provided by the changes and |
---|
336 | # the packages |
---|
337 | prevrevision = 0 |
---|
338 | releases = [] |
---|
339 | for (relrevision, dummy, relentry, version, release) in releasesdata: |
---|
340 | if prevrevision == relrevision: |
---|
341 | # ignore older markrelease of the same revision, since they |
---|
342 | # will have no history |
---|
343 | continue |
---|
344 | entries = [entry for entry in currentlog |
---|
345 | if relrevision >= entry.revision and |
---|
346 | (prevrevision < entry.revision)] |
---|
347 | if not entries: |
---|
348 | #XXX probably a forced release, without commits in current/, |
---|
349 | # check if this is the right behavior |
---|
350 | sys.stderr.write("warning: skipping (possible) release " |
---|
351 | "%s-%s@%s, no commits since previous markrelease (r%r)\n" % |
---|
352 | (version, release, relrevision, prevrevision)) |
---|
353 | continue |
---|
354 | |
---|
355 | release = make_release(author=relentry.author, |
---|
356 | revision=relentry.revision, date=relentry.date, |
---|
357 | lines=relentry.lines, entries=entries, |
---|
358 | version=version, release=release) |
---|
359 | releases.append(release) |
---|
360 | prevrevision = relrevision |
---|
361 | |
---|
362 | # look for commits that have been not submited (released) yet |
---|
363 | # this is done by getting all log entries newer (revision larger) |
---|
364 | # than releaseslog[0] (in the case it exists) |
---|
365 | if releaseslog: |
---|
366 | latest_revision = releaseslog[0].revision |
---|
367 | else: |
---|
368 | latest_revision = 0 |
---|
369 | notsubmitted = [entry for entry in currentlog |
---|
370 | if entry.revision > latest_revision] |
---|
371 | if notsubmitted: |
---|
372 | # if they are not submitted yet, what we have to do is to add |
---|
373 | # a release/version number from getrelease() |
---|
374 | version, release = getrelease(pkgdirurl) |
---|
375 | toprelease = make_release(entries=notsubmitted, released=False, |
---|
376 | version=version, release=release) |
---|
377 | releases.append(toprelease) |
---|
378 | |
---|
379 | data = dump_file(releases[::-1], currentlog=currentlog, template=template) |
---|
380 | return data |
---|
381 | |
---|
382 | |
---|
383 | |
---|
384 | def specfile_svn2rpm(pkgdirurl, specfile, rev=None, size=None, |
---|
385 | submit=False, template=None): |
---|
386 | newlines = [] |
---|
387 | found = 0 |
---|
388 | |
---|
389 | # Strip old changelogs |
---|
390 | for line in open(specfile): |
---|
391 | if line.startswith("%changelog"): |
---|
392 | found = 1 |
---|
393 | elif not found: |
---|
394 | newlines.append(line) |
---|
395 | elif line.startswith("%"): |
---|
396 | found = 0 |
---|
397 | newlines.append(line) |
---|
398 | |
---|
399 | # Create new changelog |
---|
400 | newlines.append("\n\n%changelog\n") |
---|
401 | newlines.append(svn2rpm(pkgdirurl, rev=rev, size=size, submit=submit, |
---|
402 | template=template)) |
---|
403 | |
---|
404 | # Merge old changelog, if available |
---|
405 | oldurl = config.get("log", "oldurl") |
---|
406 | if oldurl: |
---|
407 | svn = SVN(baseurl=pkgdirurl) |
---|
408 | tmpdir = tempfile.mktemp() |
---|
409 | try: |
---|
410 | pkgname = RepSysTree.pkgname(pkgdirurl) |
---|
411 | pkgoldurl = os.path.join(oldurl, pkgname) |
---|
412 | if svn.ls(pkgoldurl, noerror=1): |
---|
413 | svn.export(pkgoldurl, tmpdir, rev=rev) |
---|
414 | logfile = os.path.join(tmpdir, "log") |
---|
415 | if os.path.isfile(logfile): |
---|
416 | file = open(logfile) |
---|
417 | newlines.append("\n") |
---|
418 | newlines.append(file.read()) |
---|
419 | file.close() |
---|
420 | finally: |
---|
421 | if os.path.isdir(tmpdir): |
---|
422 | shutil.rmtree(tmpdir) |
---|
423 | |
---|
424 | # Write new specfile |
---|
425 | file = open(specfile, "w") |
---|
426 | file.write("".join(newlines)) |
---|
427 | file.close() |
---|
428 | |
---|
429 | |
---|
430 | if __name__ == "__main__": |
---|
431 | l = svn2rpm(sys.argv[1]) |
---|
432 | print l |
---|
433 | |
---|
434 | # vim:et:ts=4:sw=4 |
---|