source: soft/build_system/build_system/repsys/tags/V1_6_15/RepSys/log.py @ 1

Last change on this file since 1 was 1, checked in by fasma, 12 years ago

Initial Import from Mandriva's soft revision 224062 and package revision 45733

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