1 | #!/usr/bin/python |
---|
2 | from RepSys import Error, config |
---|
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 = os.path.basename(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") % 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 | if line: |
---|
108 | line = perexpr.sub("\\1%%\\2", line) |
---|
109 | if first: |
---|
110 | first = 0 |
---|
111 | line = line.lstrip() |
---|
112 | if line[0] != "-": |
---|
113 | nextline = "- " + line |
---|
114 | else: |
---|
115 | nextline = line |
---|
116 | elif line[0] != " " and line[0] != "-": |
---|
117 | nextline = " " + line |
---|
118 | else: |
---|
119 | nextline = line |
---|
120 | if nextline not in entrylines: |
---|
121 | entrylines.append(nextline) |
---|
122 | return entrylines |
---|
123 | |
---|
124 | |
---|
125 | class _Author: |
---|
126 | name = None |
---|
127 | email = None |
---|
128 | revisions = None |
---|
129 | |
---|
130 | |
---|
131 | def group_releases_by_author(releases): |
---|
132 | allauthors = [] |
---|
133 | for release in releases: |
---|
134 | authors = {} |
---|
135 | for revision in release.revisions: |
---|
136 | authors.setdefault(revision.author, []).append(revision) |
---|
137 | |
---|
138 | # all the mess below is to sort by author and by revision number |
---|
139 | decorated = [] |
---|
140 | for authorname, revs in authors.iteritems(): |
---|
141 | author = _Author() |
---|
142 | author.name = revs[0].author_name |
---|
143 | author.email = revs[0].author_email |
---|
144 | revdeco = [(r.revision, r) for r in revs] |
---|
145 | revdeco.sort(reverse=1) |
---|
146 | author.revisions = [t[1] for t in revdeco] |
---|
147 | decorated.append((max(revdeco)[0], author)) |
---|
148 | |
---|
149 | decorated.sort(reverse=1) |
---|
150 | release.authors = [t[1] for t in decorated] |
---|
151 | # the difference between a released and a not released _Release is |
---|
152 | # the way the release numbers is obtained. So, when this is a |
---|
153 | # released, we already have it, but if we don't, we should get de |
---|
154 | # version/release string using getrelease and then get the first |
---|
155 | first, release.authors = release.authors[0], release.authors[1:] |
---|
156 | release.author_name = first.name |
---|
157 | release.author_email = first.email |
---|
158 | release.date = first.revisions[0].date |
---|
159 | release.raw_date = first.revisions[0].raw_date |
---|
160 | release.release_revisions = first.revisions |
---|
161 | release.revision = first.revisions[0].revision |
---|
162 | |
---|
163 | return releases |
---|
164 | |
---|
165 | |
---|
166 | emailpat = re.compile("(?P<name>.*?)\s*<(?P<email>.*?)>") |
---|
167 | |
---|
168 | |
---|
169 | def make_release(author=None, revision=None, date=None, lines=None, |
---|
170 | entries=[], released=True, version=None, release=None): |
---|
171 | rel = _Release() |
---|
172 | rel.author = author |
---|
173 | found = emailpat.match(config.get("users", author, author or "")) |
---|
174 | rel.author_name = (found and found.group("name")) or author |
---|
175 | rel.author_email = (found and found.group("email")) or author |
---|
176 | rel.revision = revision |
---|
177 | rel.version = version |
---|
178 | rel.release = release |
---|
179 | rel.date = (date and time.strftime("%a %b %d %Y", date)) or None |
---|
180 | rel.lines = lines |
---|
181 | rel.released = released |
---|
182 | for entry in entries: |
---|
183 | revision = _Revision() |
---|
184 | revision.revision = entry.revision |
---|
185 | revision.lines = format_lines(entry.lines) |
---|
186 | revision.date = time.strftime("%a %b %d %Y", entry.date) |
---|
187 | revision.raw_date = entry.date |
---|
188 | revision.author = entry.author |
---|
189 | found = emailpat.match(config.get("users", entry.author, entry.author)) |
---|
190 | revision.author_name = ((found and found.group("name")) or |
---|
191 | entry.author) |
---|
192 | revision.author_email = ((found and found.group("email")) or |
---|
193 | entry.author) |
---|
194 | rel.revisions.append(revision) |
---|
195 | return rel |
---|
196 | |
---|
197 | |
---|
198 | def dump_file(releases, template=None): |
---|
199 | |
---|
200 | templpath = template or config.get("template", "path", None) |
---|
201 | params = {} |
---|
202 | if templpath is None or not os.path.exists(templpath): |
---|
203 | params["source"] = default_template |
---|
204 | sys.stderr.write("warning: %s not found. using built-in template.\n"% |
---|
205 | templpath) |
---|
206 | else: |
---|
207 | params["file"] = templpath |
---|
208 | releases_author = group_releases_by_author(releases) |
---|
209 | params["searchList"] = [{"releases_by_author" : releases_author, |
---|
210 | "releases" : releases}] |
---|
211 | t = Template(**params) |
---|
212 | return repr(t) |
---|
213 | |
---|
214 | |
---|
215 | class InvalidEntryError(Exception): |
---|
216 | pass |
---|
217 | |
---|
218 | def parse_repsys_entry(revlog): |
---|
219 | # parse entries in the format: |
---|
220 | # %repsys <operation> |
---|
221 | # key: value |
---|
222 | # .. |
---|
223 | # <newline> |
---|
224 | # <comments> |
---|
225 | # |
---|
226 | if len(revlog.lines) == 0 or not revlog.lines[0].startswith("%repsys"): |
---|
227 | raise InvalidEntryError |
---|
228 | try: |
---|
229 | data = {"operation" : revlog.lines[0].split()[1]} |
---|
230 | except IndexError: |
---|
231 | raise InvalidEntryError |
---|
232 | for line in revlog.lines[1:]: |
---|
233 | if not line: |
---|
234 | break |
---|
235 | try: |
---|
236 | key, value = line.split(":", 1) |
---|
237 | except ValueError: |
---|
238 | raise InvalidEntryError |
---|
239 | data[key.strip().lower()] = value.strip() # ??? |
---|
240 | return data |
---|
241 | |
---|
242 | |
---|
243 | def get_revision_offset(): |
---|
244 | try: |
---|
245 | revoffset = config.getint("log", "revision-offset", 0) |
---|
246 | except (ValueError, TypeError): |
---|
247 | raise Error, ("Invalid revision-offset number in configuration " |
---|
248 | "file(s).") |
---|
249 | return revoffset or 0 |
---|
250 | |
---|
251 | |
---|
252 | def svn2rpm(pkgdirurl, rev=None, size=None, submit=False, template=None): |
---|
253 | concat = config.get("log", "concat", "").split() |
---|
254 | revoffset = get_revision_offset() |
---|
255 | svn = SVN(baseurl=pkgdirurl) |
---|
256 | pkgreleasesurl = os.path.join(pkgdirurl, "releases") |
---|
257 | pkgcurrenturl = os.path.join(pkgdirurl, "current") |
---|
258 | releaseslog = svn.log(pkgreleasesurl, noerror=1) |
---|
259 | currentlog = svn.log(pkgcurrenturl, limit=size, start=rev, |
---|
260 | end=revoffset) |
---|
261 | lastauthor = None |
---|
262 | previous_revision = 0 |
---|
263 | currelease = None |
---|
264 | releases = [] |
---|
265 | |
---|
266 | # for the emergency bug fixer: the [].sort() is done using the |
---|
267 | # decorate-sort-undecorate pattern |
---|
268 | releases_data = [] |
---|
269 | if releaseslog: |
---|
270 | for relentry in releaseslog[::-1]: |
---|
271 | try: |
---|
272 | revinfo = parse_repsys_entry(relentry) |
---|
273 | except InvalidEntryError: |
---|
274 | continue |
---|
275 | try: |
---|
276 | release_number = int(revinfo["revision"]) |
---|
277 | except (KeyError, ValueError): |
---|
278 | raise Error, "Error parsing data from log entry from r%s" % \ |
---|
279 | relentry.revision |
---|
280 | releases_data.append((release_number, relentry, revinfo)) |
---|
281 | releases_data.sort() |
---|
282 | |
---|
283 | for release_number, relentry, revinfo in releases_data: |
---|
284 | try: |
---|
285 | revinfo = parse_repsys_entry(relentry) |
---|
286 | except InvalidEntryError: |
---|
287 | continue |
---|
288 | |
---|
289 | try: |
---|
290 | release_revision = int(revinfo["revision"]) |
---|
291 | except (ValueError, KeyError): |
---|
292 | raise Error, "Error parsing data from log entry from r%s" % \ |
---|
293 | relentry.revision |
---|
294 | |
---|
295 | # get entries newer than 'previous' and older than 'relentry' |
---|
296 | entries = [entry for entry in currentlog |
---|
297 | if release_revision >= entry.revision and |
---|
298 | (previous_revision < entry.revision)] |
---|
299 | if not entries: |
---|
300 | #XXX probably a forced release, without commits in current/, |
---|
301 | # check if this is the right behavior and if some release is |
---|
302 | # not being lost. |
---|
303 | continue |
---|
304 | |
---|
305 | release = make_release(author=relentry.author, |
---|
306 | revision=relentry.revision, date=relentry.date, |
---|
307 | lines=relentry.lines, entries=entries, |
---|
308 | version=revinfo["version"], release=revinfo["release"]) |
---|
309 | releases.append(release) |
---|
310 | previous_revision = release_revision |
---|
311 | |
---|
312 | # look for commits that have been not submited (released) yet |
---|
313 | # this is done by getting all log entries newer (revision larger) |
---|
314 | # than releaseslog[0] |
---|
315 | if releaseslog: |
---|
316 | latest_revision = releaseslog[0].revision |
---|
317 | notsubmitted = [entry for entry in currentlog |
---|
318 | if entry.revision > latest_revision] |
---|
319 | if notsubmitted: |
---|
320 | # if they are not submitted yet, what we have to do is to add |
---|
321 | # a release/version number from getrelease() |
---|
322 | version, release = getrelease(pkgdirurl) |
---|
323 | toprelease = make_release(entries=notsubmitted, released=False, |
---|
324 | version=version, release=release) |
---|
325 | releases.append(toprelease) |
---|
326 | |
---|
327 | data = dump_file(releases[::-1], template=template) |
---|
328 | return data |
---|
329 | |
---|
330 | |
---|
331 | |
---|
332 | def specfile_svn2rpm(pkgdirurl, specfile, rev=None, size=None, |
---|
333 | submit=False, template=None): |
---|
334 | newlines = [] |
---|
335 | found = 0 |
---|
336 | |
---|
337 | # Strip old changelogs |
---|
338 | for line in open(specfile): |
---|
339 | if line.startswith("%changelog"): |
---|
340 | found = 1 |
---|
341 | elif not found: |
---|
342 | newlines.append(line) |
---|
343 | elif line.startswith("%"): |
---|
344 | found = 0 |
---|
345 | newlines.append(line) |
---|
346 | |
---|
347 | # Create new changelog |
---|
348 | newlines.append("\n\n%changelog\n") |
---|
349 | newlines.append(svn2rpm(pkgdirurl, rev=rev, size=size, submit=submit, |
---|
350 | template=template)) |
---|
351 | |
---|
352 | # Merge old changelog, if available |
---|
353 | oldurl = config.get("log", "oldurl") |
---|
354 | if oldurl: |
---|
355 | svn = SVN(baseurl=pkgdirurl) |
---|
356 | tmpdir = tempfile.mktemp() |
---|
357 | try: |
---|
358 | pkgname = os.path.basename(pkgdirurl) |
---|
359 | pkgoldurl = os.path.join(oldurl, pkgname) |
---|
360 | if svn.ls(pkgoldurl, noerror=1): |
---|
361 | svn.export(pkgoldurl, tmpdir, rev=rev) |
---|
362 | logfile = os.path.join(tmpdir, "log") |
---|
363 | if os.path.isfile(logfile): |
---|
364 | file = open(logfile) |
---|
365 | newlines.append("\n") |
---|
366 | newlines.append(file.read()) |
---|
367 | file.close() |
---|
368 | finally: |
---|
369 | if os.path.isdir(tmpdir): |
---|
370 | shutil.rmtree(tmpdir) |
---|
371 | |
---|
372 | # Write new specfile |
---|
373 | file = open(specfile, "w") |
---|
374 | file.write("".join(newlines)) |
---|
375 | file.close() |
---|
376 | |
---|
377 | |
---|
378 | if __name__ == "__main__": |
---|
379 | l = svn2rpm(sys.argv[1]) |
---|
380 | print l |
---|
381 | |
---|
382 | # vim:et:ts=4:sw=4 |
---|