source: changelogmacro/trunk/changelog/ChangeLogMacro.py

Last change on this file was 18148, checked in by Cinc-th, 2 years ago

ChangeLogMacro: Python 3 fixes for !StringIO and string/bytes handling. Some minimal testing indicates the macro does work with Trac 1.5/Trac 1.6.

  • Property svn:keywords set to LastChangedRevision HeadURL
File size: 5.9 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006-2008 Alec Thomas
4# Copyright (C) 2010-2015 Ryan J Ollos <ryan.j.ollos@gmail.com>
5# All rights reserved.
6#
7# This software is licensed as described in the file COPYING, which
8# you should have received as part of this distribution.
9#
10
11import re
12try:
13    from StringIO import StringIO
14except ImportError:
15    # Python 3
16    from io import StringIO
17
18from trac.util.datefmt import format_datetime
19from trac.util.html import Markup, html
20from trac.util.text import to_unicode
21from trac.util.translation import _
22from trac.versioncontrol.api import NoSuchNode, RepositoryManager
23from trac.web.chrome import web_context
24from trac.wiki.formatter import format_to_html, format_to_oneliner, \
25    system_message
26from trac.wiki.macros import WikiMacroBase, parse_args
27
28
29class ChangeLogMacro(WikiMacroBase):
30    """Write repository change log to output.
31
32    The !ChangeLog macro writes a log of the last changes of a repository at a
33    given path. Following variants are possible to use:
34    {{{
35    1. [[ChangeLog(/path)]]
36    2. [[ChangeLog(/path@rev)]]
37    3. [[ChangeLog(/path@rev, limit)]]
38    4. [[ChangeLog(/path@from-to)]]
39    5. [[ChangeLog(/path, limit, rev)]]
40    }}}
41
42    1. Default repository is used if reponame is left out. To show the last
43       five changes of the default repository:
44       {{{
45       [[ChangeLog(/)]]
46       }}}
47       To show the last five changes of the trunk folder in a named otherrepo:
48       {{{
49       [[ChangeLog(/otherrepo/trunk)]]
50       }}}
51    2. The ending revision can be set.
52       To show the last five changes up to revision 99:
53       {{{
54       [[ChangeLog(/otherrepo/trunk@99)]]
55       }}}
56    3. The limit can be set by an optional parameter. To show the last
57       10 changes, up to revision 99:
58       {{{
59       [[ChangeLog(/otherrepo/trunk@99, 10)]]
60       }}}
61    4. A range of revisions can be logged.
62       {{{
63       [[ChangeLog(/otherrepo/trunk@90-99)]]
64       }}}
65       To lists all changes:
66       {{{
67       [[ChangeLog(/otherrepo/trunk@1-HEAD)]]
68       }}}
69       HEAD can be left out:
70       {{{
71       [[ChangeLog(/otherrepo/trunk@1-)]]
72       }}}
73    5. For backwards compatibility, revision can be stated as a third
74       parameter:
75       {{{
76       [[ChangeLog(/otherrepo/trunk, 10, 99)]]
77       }}}
78
79    limit and rev may be keyword arguments.
80    {{{
81    [[ChangeLog(/otherrepo/trunk, limit=10, rev=99)]]
82    }}}
83    """
84
85    def expand_macro(self, formatter, name, content):
86        req = formatter.req
87
88        if 'CHANGESET_VIEW' not in req.perm:
89            return Markup('<i>Changelog not available</i>')
90
91        context = web_context(req)
92        args, kwargs = parse_args(content)
93        if len(args) == 0:
94            return system_message(_("ChangeLog macro error"),
95                                  _("Repository path is required."))
96        args += [None, None]
97        raw_path, limit, rev = args[:3]
98        limit = kwargs.pop('limit', limit)
99        rev = kwargs.pop('rev', rev)
100        if '@' in raw_path:
101            raw_path, rev = raw_path.split('@', 2)
102        if ':' in raw_path:  # Compatibility with version 0.4
103            abs_path = '/'.join(raw_path.split(':', 2))
104        else:
105            abs_path = raw_path
106        reponame, repo, path = \
107            RepositoryManager(self.env).get_repository_by_path(abs_path)
108        log_href = req.href.log(abs_path, rev=rev)
109        path = repo.normalize_path(path)
110        revstart = 0
111        if rev is not None:
112            for d in [':', '-']:
113                if d in rev:
114                    revstart, revstop = rev.split(d, 2)
115                    if not revstop or revstop.lower() in ['head', '0']:
116                        revstart = int(revstart)
117                        rev = repo.get_youngest_rev()
118                        limit = rev - revstart + 1
119                    else:
120                        revstart, revstop = int(revstart), int(revstop)
121                        if revstart > revstop:
122                            revstart, revstop = revstop, revstart
123                        limit = revstop - revstart + 1
124                        rev = revstop or None
125                    break
126
127        if rev is None:
128            rev = repo.get_youngest_rev()
129        rev = repo.normalize_rev(rev)
130        if limit is None:
131            limit = 5
132        else:
133            limit = int(limit)
134        try:
135            node = repo.get_node(path, rev)
136        except NoSuchNode as e:
137            return system_message(_("ChangeLog macro failed"), e)
138        out = StringIO()
139        out.write('</p>')  # close surrounding paragraph
140        out.write('\n<div class="changelog">\n<dl class="wiki">')
141        for npath, nrev, nlog in node.get_history(limit):
142            if nrev < revstart:
143                break
144            change = repo.get_changeset(nrev)
145            datetime = format_datetime(change.date, '%Y-%m-%d %H:%M:%S',
146                                       req.tz)
147            drev = repo.display_rev(nrev)
148            if not reponame:
149                sargs = nrev, drev
150            else:
151                sargs = '%s/%s' % (nrev, reponame), '%s/%s' % (drev, reponame)
152            cset = '[changeset:%s %s]' % sargs
153            header = format_to_oneliner(
154                self.env, context,
155                "%s by %s on %s" % (cset, change.author, datetime))
156            out.write('\n<dt id="changelog-changeset-%s">\n%s\n</dt>' %
157                      (cset, header))
158            message = _remove_p(format_to_html(
159                self.env, context, change.message, escape_newlines=True))
160            out.write('\n<dd>\n%s\n</dd>' % message)
161        out.write(to_unicode(html.small(html.a(_("(more)"), href=log_href))))
162        out.write('\n</dl>\n</div>')
163        out.write('\n<p>')  # re-open surrounding paragraph
164        return out.getvalue()
165
166
167REMOVE_P = '^\s*<p>(.*?)</p>\s*$'
168REMOVE_P_RE = re.compile(REMOVE_P, re.DOTALL)
169
170
171def _remove_p(html):
172    f = REMOVE_P_RE.findall(html)
173    if f:
174        return f[0]
175    else:
176        return html
Note: See TracBrowser for help on using the repository browser.