##// END OF EJS Templates
flake8: fix E125 continuation line with same indent as next logical line
Mads Kiilerich -
r7733:f73a1103 default
parent child Browse files
Show More
@@ -1,207 +1,208 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.annotate
15 kallithea.lib.annotate
16 ~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Annotation library for usage in Kallithea, previously part of vcs
18 Annotation library for usage in Kallithea, previously part of vcs
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Dec 4, 2011
22 :created_on: Dec 4, 2011
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import StringIO
28 import StringIO
29
29
30 from pygments import highlight
30 from pygments import highlight
31 from pygments.formatters import HtmlFormatter
31 from pygments.formatters import HtmlFormatter
32
32
33 from kallithea.lib.vcs.exceptions import VCSError
33 from kallithea.lib.vcs.exceptions import VCSError
34 from kallithea.lib.vcs.nodes import FileNode
34 from kallithea.lib.vcs.nodes import FileNode
35
35
36
36
37 def annotate_highlight(filenode, annotate_from_changeset_func=None,
37 def annotate_highlight(filenode, annotate_from_changeset_func=None,
38 order=None, headers=None, **options):
38 order=None, headers=None, **options):
39 """
39 """
40 Returns html portion containing annotated table with 3 columns: line
40 Returns html portion containing annotated table with 3 columns: line
41 numbers, changeset information and pygmentized line of code.
41 numbers, changeset information and pygmentized line of code.
42
42
43 :param filenode: FileNode object
43 :param filenode: FileNode object
44 :param annotate_from_changeset_func: function taking changeset and
44 :param annotate_from_changeset_func: function taking changeset and
45 returning single annotate cell; needs break line at the end
45 returning single annotate cell; needs break line at the end
46 :param order: ordered sequence of ``ls`` (line numbers column),
46 :param order: ordered sequence of ``ls`` (line numbers column),
47 ``annotate`` (annotate column), ``code`` (code column); Default is
47 ``annotate`` (annotate column), ``code`` (code column); Default is
48 ``['ls', 'annotate', 'code']``
48 ``['ls', 'annotate', 'code']``
49 :param headers: dictionary with headers (keys are whats in ``order``
49 :param headers: dictionary with headers (keys are whats in ``order``
50 parameter)
50 parameter)
51 """
51 """
52 from kallithea.lib.pygmentsutils import get_custom_lexer
52 from kallithea.lib.pygmentsutils import get_custom_lexer
53 options['linenos'] = True
53 options['linenos'] = True
54 formatter = AnnotateHtmlFormatter(filenode=filenode, order=order,
54 formatter = AnnotateHtmlFormatter(filenode=filenode, order=order,
55 headers=headers,
55 headers=headers,
56 annotate_from_changeset_func=annotate_from_changeset_func, **options)
56 annotate_from_changeset_func=annotate_from_changeset_func, **options)
57 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
57 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
58 highlighted = highlight(filenode.content, lexer, formatter)
58 highlighted = highlight(filenode.content, lexer, formatter)
59 return highlighted
59 return highlighted
60
60
61
61
62 class AnnotateHtmlFormatter(HtmlFormatter):
62 class AnnotateHtmlFormatter(HtmlFormatter):
63
63
64 def __init__(self, filenode, annotate_from_changeset_func=None,
64 def __init__(self, filenode, annotate_from_changeset_func=None,
65 order=None, **options):
65 order=None, **options):
66 """
66 """
67 If ``annotate_from_changeset_func`` is passed it should be a function
67 If ``annotate_from_changeset_func`` is passed it should be a function
68 which returns string from the given changeset. For example, we may pass
68 which returns string from the given changeset. For example, we may pass
69 following function as ``annotate_from_changeset_func``::
69 following function as ``annotate_from_changeset_func``::
70
70
71 def changeset_to_anchor(changeset):
71 def changeset_to_anchor(changeset):
72 return '<a href="/changesets/%s/">%s</a>\n' % \
72 return '<a href="/changesets/%s/">%s</a>\n' % \
73 (changeset.id, changeset.id)
73 (changeset.id, changeset.id)
74
74
75 :param annotate_from_changeset_func: see above
75 :param annotate_from_changeset_func: see above
76 :param order: (default: ``['ls', 'annotate', 'code']``); order of
76 :param order: (default: ``['ls', 'annotate', 'code']``); order of
77 columns;
77 columns;
78 :param options: standard pygment's HtmlFormatter options, there is
78 :param options: standard pygment's HtmlFormatter options, there is
79 extra option tough, ``headers``. For instance we can pass::
79 extra option tough, ``headers``. For instance we can pass::
80
80
81 formatter = AnnotateHtmlFormatter(filenode, headers={
81 formatter = AnnotateHtmlFormatter(filenode, headers={
82 'ls': '#',
82 'ls': '#',
83 'annotate': 'Annotate',
83 'annotate': 'Annotate',
84 'code': 'Code',
84 'code': 'Code',
85 })
85 })
86
86
87 """
87 """
88 super(AnnotateHtmlFormatter, self).__init__(**options)
88 super(AnnotateHtmlFormatter, self).__init__(**options)
89 self.annotate_from_changeset_func = annotate_from_changeset_func
89 self.annotate_from_changeset_func = annotate_from_changeset_func
90 self.order = order or ('ls', 'annotate', 'code')
90 self.order = order or ('ls', 'annotate', 'code')
91 headers = options.pop('headers', None)
91 headers = options.pop('headers', None)
92 if headers and not ('ls' in headers and 'annotate' in headers and
92 if headers and not ('ls' in headers and 'annotate' in headers and
93 'code' in headers):
93 'code' in headers
94 ):
94 raise ValueError("If headers option dict is specified it must "
95 raise ValueError("If headers option dict is specified it must "
95 "all 'ls', 'annotate' and 'code' keys")
96 "all 'ls', 'annotate' and 'code' keys")
96 self.headers = headers
97 self.headers = headers
97 if isinstance(filenode, FileNode):
98 if isinstance(filenode, FileNode):
98 self.filenode = filenode
99 self.filenode = filenode
99 else:
100 else:
100 raise VCSError("This formatter expect FileNode parameter, not %r"
101 raise VCSError("This formatter expect FileNode parameter, not %r"
101 % type(filenode))
102 % type(filenode))
102
103
103 def annotate_from_changeset(self, changeset):
104 def annotate_from_changeset(self, changeset):
104 """
105 """
105 Returns full html line for single changeset per annotated line.
106 Returns full html line for single changeset per annotated line.
106 """
107 """
107 if self.annotate_from_changeset_func:
108 if self.annotate_from_changeset_func:
108 return self.annotate_from_changeset_func(changeset)
109 return self.annotate_from_changeset_func(changeset)
109 else:
110 else:
110 return ''.join((changeset.id, '\n'))
111 return ''.join((changeset.id, '\n'))
111
112
112 def _wrap_tablelinenos(self, inner):
113 def _wrap_tablelinenos(self, inner):
113 dummyoutfile = StringIO.StringIO()
114 dummyoutfile = StringIO.StringIO()
114 lncount = 0
115 lncount = 0
115 for t, line in inner:
116 for t, line in inner:
116 if t:
117 if t:
117 lncount += 1
118 lncount += 1
118 dummyoutfile.write(line)
119 dummyoutfile.write(line)
119
120
120 fl = self.linenostart
121 fl = self.linenostart
121 mw = len(str(lncount + fl - 1))
122 mw = len(str(lncount + fl - 1))
122 sp = self.linenospecial
123 sp = self.linenospecial
123 st = self.linenostep
124 st = self.linenostep
124 la = self.lineanchors
125 la = self.lineanchors
125 aln = self.anchorlinenos
126 aln = self.anchorlinenos
126 if sp:
127 if sp:
127 lines = []
128 lines = []
128
129
129 for i in range(fl, fl + lncount):
130 for i in range(fl, fl + lncount):
130 if i % st == 0:
131 if i % st == 0:
131 if i % sp == 0:
132 if i % sp == 0:
132 if aln:
133 if aln:
133 lines.append('<a href="#%s-%d" class="special">'
134 lines.append('<a href="#%s-%d" class="special">'
134 '%*d</a>' %
135 '%*d</a>' %
135 (la, i, mw, i))
136 (la, i, mw, i))
136 else:
137 else:
137 lines.append('<span class="special">'
138 lines.append('<span class="special">'
138 '%*d</span>' % (mw, i))
139 '%*d</span>' % (mw, i))
139 else:
140 else:
140 if aln:
141 if aln:
141 lines.append('<a href="#%s-%d">'
142 lines.append('<a href="#%s-%d">'
142 '%*d</a>' % (la, i, mw, i))
143 '%*d</a>' % (la, i, mw, i))
143 else:
144 else:
144 lines.append('%*d' % (mw, i))
145 lines.append('%*d' % (mw, i))
145 else:
146 else:
146 lines.append('')
147 lines.append('')
147 ls = '\n'.join(lines)
148 ls = '\n'.join(lines)
148 else:
149 else:
149 lines = []
150 lines = []
150 for i in range(fl, fl + lncount):
151 for i in range(fl, fl + lncount):
151 if i % st == 0:
152 if i % st == 0:
152 if aln:
153 if aln:
153 lines.append('<a href="#%s-%d">%*d</a>'
154 lines.append('<a href="#%s-%d">%*d</a>'
154 % (la, i, mw, i))
155 % (la, i, mw, i))
155 else:
156 else:
156 lines.append('%*d' % (mw, i))
157 lines.append('%*d' % (mw, i))
157 else:
158 else:
158 lines.append('')
159 lines.append('')
159 ls = '\n'.join(lines)
160 ls = '\n'.join(lines)
160
161
161 # annotate_changesets = [tup[1] for tup in self.filenode.annotate]
162 # annotate_changesets = [tup[1] for tup in self.filenode.annotate]
162 # # TODO: not sure what that fixes
163 # # TODO: not sure what that fixes
163 # # If pygments cropped last lines break we need do that too
164 # # If pygments cropped last lines break we need do that too
164 # ln_cs = len(annotate_changesets)
165 # ln_cs = len(annotate_changesets)
165 # ln_ = len(ls.splitlines())
166 # ln_ = len(ls.splitlines())
166 # if ln_cs > ln_:
167 # if ln_cs > ln_:
167 # annotate_changesets = annotate_changesets[:ln_ - ln_cs]
168 # annotate_changesets = annotate_changesets[:ln_ - ln_cs]
168 annotate = ''.join((self.annotate_from_changeset(el[2]())
169 annotate = ''.join((self.annotate_from_changeset(el[2]())
169 for el in self.filenode.annotate))
170 for el in self.filenode.annotate))
170 # in case you wonder about the seemingly redundant <div> here:
171 # in case you wonder about the seemingly redundant <div> here:
171 # since the content in the other cell also is wrapped in a div,
172 # since the content in the other cell also is wrapped in a div,
172 # some browsers in some configurations seem to mess up the formatting.
173 # some browsers in some configurations seem to mess up the formatting.
173 '''
174 '''
174 yield 0, ('<table class="%stable">' % self.cssclass +
175 yield 0, ('<table class="%stable">' % self.cssclass +
175 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
176 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
176 ls + '</pre></div></td>' +
177 ls + '</pre></div></td>' +
177 '<td class="code">')
178 '<td class="code">')
178 yield 0, dummyoutfile.getvalue()
179 yield 0, dummyoutfile.getvalue()
179 yield 0, '</td></tr></table>'
180 yield 0, '</td></tr></table>'
180
181
181 '''
182 '''
182 headers_row = []
183 headers_row = []
183 if self.headers:
184 if self.headers:
184 headers_row = ['<tr class="annotate-header">']
185 headers_row = ['<tr class="annotate-header">']
185 for key in self.order:
186 for key in self.order:
186 td = ''.join(('<td>', self.headers[key], '</td>'))
187 td = ''.join(('<td>', self.headers[key], '</td>'))
187 headers_row.append(td)
188 headers_row.append(td)
188 headers_row.append('</tr>')
189 headers_row.append('</tr>')
189
190
190 body_row_start = ['<tr>']
191 body_row_start = ['<tr>']
191 for key in self.order:
192 for key in self.order:
192 if key == 'ls':
193 if key == 'ls':
193 body_row_start.append(
194 body_row_start.append(
194 '<td class="linenos"><div class="linenodiv"><pre>' +
195 '<td class="linenos"><div class="linenodiv"><pre>' +
195 ls + '</pre></div></td>')
196 ls + '</pre></div></td>')
196 elif key == 'annotate':
197 elif key == 'annotate':
197 body_row_start.append(
198 body_row_start.append(
198 '<td class="annotate"><div class="annotatediv"><pre>' +
199 '<td class="annotate"><div class="annotatediv"><pre>' +
199 annotate + '</pre></div></td>')
200 annotate + '</pre></div></td>')
200 elif key == 'code':
201 elif key == 'code':
201 body_row_start.append('<td class="code">')
202 body_row_start.append('<td class="code">')
202 yield 0, ('<table class="%stable">' % self.cssclass +
203 yield 0, ('<table class="%stable">' % self.cssclass +
203 ''.join(headers_row) +
204 ''.join(headers_row) +
204 ''.join(body_row_start)
205 ''.join(body_row_start)
205 )
206 )
206 yield 0, dummyoutfile.getvalue()
207 yield 0, dummyoutfile.getvalue()
207 yield 0, '</td></tr></table>'
208 yield 0, '</td></tr></table>'
@@ -1,254 +1,255 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 Custom paging classes
15 Custom paging classes
16 """
16 """
17 import logging
17 import logging
18 import math
18 import math
19 import re
19 import re
20
20
21 from webhelpers2.html import HTML, literal
21 from webhelpers2.html import HTML, literal
22 from webhelpers.paginate import Page as _Page
22 from webhelpers.paginate import Page as _Page
23
23
24 from kallithea.config.routing import url
24 from kallithea.config.routing import url
25
25
26
26
27 log = logging.getLogger(__name__)
27 log = logging.getLogger(__name__)
28
28
29
29
30 class Page(_Page):
30 class Page(_Page):
31 """
31 """
32 Custom pager emitting Bootstrap paginators
32 Custom pager emitting Bootstrap paginators
33 """
33 """
34
34
35 def __init__(self, *args, **kwargs):
35 def __init__(self, *args, **kwargs):
36 kwargs.setdefault('url', url.current)
36 kwargs.setdefault('url', url.current)
37 _Page.__init__(self, *args, **kwargs)
37 _Page.__init__(self, *args, **kwargs)
38
38
39 def _get_pos(self, cur_page, max_page, items):
39 def _get_pos(self, cur_page, max_page, items):
40 edge = (items / 2) + 1
40 edge = (items / 2) + 1
41 if (cur_page <= edge):
41 if (cur_page <= edge):
42 radius = max(items / 2, items - cur_page)
42 radius = max(items / 2, items - cur_page)
43 elif (max_page - cur_page) < edge:
43 elif (max_page - cur_page) < edge:
44 radius = (items - 1) - (max_page - cur_page)
44 radius = (items - 1) - (max_page - cur_page)
45 else:
45 else:
46 radius = items / 2
46 radius = items / 2
47
47
48 left = max(1, (cur_page - (radius)))
48 left = max(1, (cur_page - (radius)))
49 right = min(max_page, cur_page + (radius))
49 right = min(max_page, cur_page + (radius))
50 return left, cur_page, right
50 return left, cur_page, right
51
51
52 def _range(self, regexp_match):
52 def _range(self, regexp_match):
53 """
53 """
54 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
54 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
55
55
56 Arguments:
56 Arguments:
57
57
58 regexp_match
58 regexp_match
59 A "re" (regular expressions) match object containing the
59 A "re" (regular expressions) match object containing the
60 radius of linked pages around the current page in
60 radius of linked pages around the current page in
61 regexp_match.group(1) as a string
61 regexp_match.group(1) as a string
62
62
63 This function is supposed to be called as a callable in
63 This function is supposed to be called as a callable in
64 re.sub.
64 re.sub.
65
65
66 """
66 """
67 radius = int(regexp_match.group(1))
67 radius = int(regexp_match.group(1))
68
68
69 # Compute the first and last page number within the radius
69 # Compute the first and last page number within the radius
70 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
70 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
71 # -> leftmost_page = 5
71 # -> leftmost_page = 5
72 # -> rightmost_page = 9
72 # -> rightmost_page = 9
73 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
73 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
74 self.last_page,
74 self.last_page,
75 (radius * 2) + 1)
75 (radius * 2) + 1)
76 nav_items = []
76 nav_items = []
77
77
78 # Create a link to the first page (unless we are on the first page
78 # Create a link to the first page (unless we are on the first page
79 # or there would be no need to insert '..' spacers)
79 # or there would be no need to insert '..' spacers)
80 if self.page != self.first_page and self.first_page < leftmost_page:
80 if self.page != self.first_page and self.first_page < leftmost_page:
81 nav_items.append(HTML.li(self._pagerlink(self.first_page, self.first_page)))
81 nav_items.append(HTML.li(self._pagerlink(self.first_page, self.first_page)))
82
82
83 # Insert dots if there are pages between the first page
83 # Insert dots if there are pages between the first page
84 # and the currently displayed page range
84 # and the currently displayed page range
85 if leftmost_page - self.first_page > 1:
85 if leftmost_page - self.first_page > 1:
86 # Wrap in a SPAN tag if nolink_attr is set
86 # Wrap in a SPAN tag if nolink_attr is set
87 text_ = '..'
87 text_ = '..'
88 if self.dotdot_attr:
88 if self.dotdot_attr:
89 text_ = HTML.span(c=text_, **self.dotdot_attr)
89 text_ = HTML.span(c=text_, **self.dotdot_attr)
90 nav_items.append(HTML.li(text_))
90 nav_items.append(HTML.li(text_))
91
91
92 for thispage in xrange(leftmost_page, rightmost_page + 1):
92 for thispage in xrange(leftmost_page, rightmost_page + 1):
93 # Highlight the current page number and do not use a link
93 # Highlight the current page number and do not use a link
94 text_ = str(thispage)
94 text_ = str(thispage)
95 if thispage == self.page:
95 if thispage == self.page:
96 # Wrap in a SPAN tag if nolink_attr is set
96 # Wrap in a SPAN tag if nolink_attr is set
97 if self.curpage_attr:
97 if self.curpage_attr:
98 text_ = HTML.li(HTML.span(c=text_), **self.curpage_attr)
98 text_ = HTML.li(HTML.span(c=text_), **self.curpage_attr)
99 nav_items.append(text_)
99 nav_items.append(text_)
100 # Otherwise create just a link to that page
100 # Otherwise create just a link to that page
101 else:
101 else:
102 nav_items.append(HTML.li(self._pagerlink(thispage, text_)))
102 nav_items.append(HTML.li(self._pagerlink(thispage, text_)))
103
103
104 # Insert dots if there are pages between the displayed
104 # Insert dots if there are pages between the displayed
105 # page numbers and the end of the page range
105 # page numbers and the end of the page range
106 if self.last_page - rightmost_page > 1:
106 if self.last_page - rightmost_page > 1:
107 text_ = '..'
107 text_ = '..'
108 # Wrap in a SPAN tag if nolink_attr is set
108 # Wrap in a SPAN tag if nolink_attr is set
109 if self.dotdot_attr:
109 if self.dotdot_attr:
110 text_ = HTML.span(c=text_, **self.dotdot_attr)
110 text_ = HTML.span(c=text_, **self.dotdot_attr)
111 nav_items.append(HTML.li(text_))
111 nav_items.append(HTML.li(text_))
112
112
113 # Create a link to the very last page (unless we are on the last
113 # Create a link to the very last page (unless we are on the last
114 # page or there would be no need to insert '..' spacers)
114 # page or there would be no need to insert '..' spacers)
115 if self.page != self.last_page and rightmost_page < self.last_page:
115 if self.page != self.last_page and rightmost_page < self.last_page:
116 nav_items.append(HTML.li(self._pagerlink(self.last_page, self.last_page)))
116 nav_items.append(HTML.li(self._pagerlink(self.last_page, self.last_page)))
117
117
118 #_page_link = url.current()
118 #_page_link = url.current()
119 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
119 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
120 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
120 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
121 return self.separator.join(nav_items)
121 return self.separator.join(nav_items)
122
122
123 def pager(self, format='<ul class="pagination">$link_previous ~2~ $link_next</ul>', page_param='page', partial_param='partial',
123 def pager(self, format='<ul class="pagination">$link_previous ~2~ $link_next</ul>', page_param='page', partial_param='partial',
124 show_if_single_page=False, separator=' ', onclick=None,
124 show_if_single_page=False, separator=' ', onclick=None,
125 symbol_first='<<', symbol_last='>>',
125 symbol_first='<<', symbol_last='>>',
126 symbol_previous='<', symbol_next='>',
126 symbol_previous='<', symbol_next='>',
127 link_attr=None,
127 link_attr=None,
128 curpage_attr=None,
128 curpage_attr=None,
129 dotdot_attr=None, **kwargs):
129 dotdot_attr=None, **kwargs
130 ):
130 self.curpage_attr = curpage_attr or {'class': 'active'}
131 self.curpage_attr = curpage_attr or {'class': 'active'}
131 self.separator = separator
132 self.separator = separator
132 self.pager_kwargs = kwargs
133 self.pager_kwargs = kwargs
133 self.page_param = page_param
134 self.page_param = page_param
134 self.partial_param = partial_param
135 self.partial_param = partial_param
135 self.onclick = onclick
136 self.onclick = onclick
136 self.link_attr = link_attr or {'class': 'pager_link', 'rel': 'prerender'}
137 self.link_attr = link_attr or {'class': 'pager_link', 'rel': 'prerender'}
137 self.dotdot_attr = dotdot_attr or {'class': 'pager_dotdot'}
138 self.dotdot_attr = dotdot_attr or {'class': 'pager_dotdot'}
138
139
139 # Don't show navigator if there is no more than one page
140 # Don't show navigator if there is no more than one page
140 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
141 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
141 return ''
142 return ''
142
143
143 from string import Template
144 from string import Template
144 # Replace ~...~ in token format by range of pages
145 # Replace ~...~ in token format by range of pages
145 result = re.sub(r'~(\d+)~', self._range, format)
146 result = re.sub(r'~(\d+)~', self._range, format)
146
147
147 # Interpolate '%' variables
148 # Interpolate '%' variables
148 result = Template(result).safe_substitute({
149 result = Template(result).safe_substitute({
149 'first_page': self.first_page,
150 'first_page': self.first_page,
150 'last_page': self.last_page,
151 'last_page': self.last_page,
151 'page': self.page,
152 'page': self.page,
152 'page_count': self.page_count,
153 'page_count': self.page_count,
153 'items_per_page': self.items_per_page,
154 'items_per_page': self.items_per_page,
154 'first_item': self.first_item,
155 'first_item': self.first_item,
155 'last_item': self.last_item,
156 'last_item': self.last_item,
156 'item_count': self.item_count,
157 'item_count': self.item_count,
157 'link_first': self.page > self.first_page and
158 'link_first': self.page > self.first_page and
158 self._pagerlink(self.first_page, symbol_first) or '',
159 self._pagerlink(self.first_page, symbol_first) or '',
159 'link_last': self.page < self.last_page and
160 'link_last': self.page < self.last_page and
160 self._pagerlink(self.last_page, symbol_last) or '',
161 self._pagerlink(self.last_page, symbol_last) or '',
161 'link_previous': HTML.li(self.previous_page and
162 'link_previous': HTML.li(self.previous_page and
162 self._pagerlink(self.previous_page, symbol_previous)
163 self._pagerlink(self.previous_page, symbol_previous)
163 or HTML.a(symbol_previous)),
164 or HTML.a(symbol_previous)),
164 'link_next': HTML.li(self.next_page and
165 'link_next': HTML.li(self.next_page and
165 self._pagerlink(self.next_page, symbol_next)
166 self._pagerlink(self.next_page, symbol_next)
166 or HTML.a(symbol_next))
167 or HTML.a(symbol_next))
167 })
168 })
168
169
169 return literal(result)
170 return literal(result)
170
171
171
172
172 class RepoPage(Page):
173 class RepoPage(Page):
173
174
174 def __init__(self, collection, page=1, items_per_page=20,
175 def __init__(self, collection, page=1, items_per_page=20,
175 item_count=None, **kwargs):
176 item_count=None, **kwargs):
176
177
177 """Create a "RepoPage" instance. special pager for paging
178 """Create a "RepoPage" instance. special pager for paging
178 repository
179 repository
179 """
180 """
180 # TODO: call baseclass __init__
181 # TODO: call baseclass __init__
181 self._url_generator = kwargs.pop('url', url.current)
182 self._url_generator = kwargs.pop('url', url.current)
182
183
183 # Safe the kwargs class-wide so they can be used in the pager() method
184 # Safe the kwargs class-wide so they can be used in the pager() method
184 self.kwargs = kwargs
185 self.kwargs = kwargs
185
186
186 # Save a reference to the collection
187 # Save a reference to the collection
187 self.original_collection = collection
188 self.original_collection = collection
188
189
189 self.collection = collection
190 self.collection = collection
190
191
191 # The self.page is the number of the current page.
192 # The self.page is the number of the current page.
192 # The first page has the number 1!
193 # The first page has the number 1!
193 try:
194 try:
194 self.page = int(page) # make it int() if we get it as a string
195 self.page = int(page) # make it int() if we get it as a string
195 except (ValueError, TypeError):
196 except (ValueError, TypeError):
196 log.error("Invalid page value: %r", page)
197 log.error("Invalid page value: %r", page)
197 self.page = 1
198 self.page = 1
198
199
199 self.items_per_page = items_per_page
200 self.items_per_page = items_per_page
200
201
201 # Unless the user tells us how many items the collections has
202 # Unless the user tells us how many items the collections has
202 # we calculate that ourselves.
203 # we calculate that ourselves.
203 if item_count is not None:
204 if item_count is not None:
204 self.item_count = item_count
205 self.item_count = item_count
205 else:
206 else:
206 self.item_count = len(self.collection)
207 self.item_count = len(self.collection)
207
208
208 # Compute the number of the first and last available page
209 # Compute the number of the first and last available page
209 if self.item_count > 0:
210 if self.item_count > 0:
210 self.first_page = 1
211 self.first_page = 1
211 self.page_count = int(math.ceil(float(self.item_count) /
212 self.page_count = int(math.ceil(float(self.item_count) /
212 self.items_per_page))
213 self.items_per_page))
213 self.last_page = self.first_page + self.page_count - 1
214 self.last_page = self.first_page + self.page_count - 1
214
215
215 # Make sure that the requested page number is the range of
216 # Make sure that the requested page number is the range of
216 # valid pages
217 # valid pages
217 if self.page > self.last_page:
218 if self.page > self.last_page:
218 self.page = self.last_page
219 self.page = self.last_page
219 elif self.page < self.first_page:
220 elif self.page < self.first_page:
220 self.page = self.first_page
221 self.page = self.first_page
221
222
222 # Note: the number of items on this page can be less than
223 # Note: the number of items on this page can be less than
223 # items_per_page if the last page is not full
224 # items_per_page if the last page is not full
224 self.first_item = max(0, (self.item_count) - (self.page *
225 self.first_item = max(0, (self.item_count) - (self.page *
225 items_per_page))
226 items_per_page))
226 self.last_item = ((self.item_count - 1) - items_per_page *
227 self.last_item = ((self.item_count - 1) - items_per_page *
227 (self.page - 1))
228 (self.page - 1))
228
229
229 self.items = list(self.collection[self.first_item:self.last_item + 1])
230 self.items = list(self.collection[self.first_item:self.last_item + 1])
230
231
231 # Links to previous and next page
232 # Links to previous and next page
232 if self.page > self.first_page:
233 if self.page > self.first_page:
233 self.previous_page = self.page - 1
234 self.previous_page = self.page - 1
234 else:
235 else:
235 self.previous_page = None
236 self.previous_page = None
236
237
237 if self.page < self.last_page:
238 if self.page < self.last_page:
238 self.next_page = self.page + 1
239 self.next_page = self.page + 1
239 else:
240 else:
240 self.next_page = None
241 self.next_page = None
241
242
242 # No items available
243 # No items available
243 else:
244 else:
244 self.first_page = None
245 self.first_page = None
245 self.page_count = 0
246 self.page_count = 0
246 self.last_page = None
247 self.last_page = None
247 self.first_item = None
248 self.first_item = None
248 self.last_item = None
249 self.last_item = None
249 self.previous_page = None
250 self.previous_page = None
250 self.next_page = None
251 self.next_page = None
251 self.items = []
252 self.items = []
252
253
253 # This is a subclass of the 'list' type. Initialise the list now.
254 # This is a subclass of the 'list' type. Initialise the list now.
254 list.__init__(self, reversed(self.items))
255 list.__init__(self, reversed(self.items))
@@ -1,699 +1,700 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.utils2
15 kallithea.lib.utils2
16 ~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~
17
17
18 Some simple helper functions.
18 Some simple helper functions.
19 Note: all these functions should be independent of Kallithea classes, i.e.
19 Note: all these functions should be independent of Kallithea classes, i.e.
20 models, controllers, etc. to prevent import cycles.
20 models, controllers, etc. to prevent import cycles.
21
21
22 This file was forked by the Kallithea project in July 2014.
22 This file was forked by the Kallithea project in July 2014.
23 Original author and date, and relevant copyright and licensing information is below:
23 Original author and date, and relevant copyright and licensing information is below:
24 :created_on: Jan 5, 2011
24 :created_on: Jan 5, 2011
25 :author: marcink
25 :author: marcink
26 :copyright: (c) 2013 RhodeCode GmbH, and others.
26 :copyright: (c) 2013 RhodeCode GmbH, and others.
27 :license: GPLv3, see LICENSE.md for more details.
27 :license: GPLv3, see LICENSE.md for more details.
28 """
28 """
29
29
30
30
31 import binascii
31 import binascii
32 import datetime
32 import datetime
33 import os
33 import os
34 import pwd
34 import pwd
35 import re
35 import re
36 import time
36 import time
37 import urllib
37 import urllib
38
38
39 import urlobject
39 import urlobject
40 from tg.i18n import ugettext as _
40 from tg.i18n import ugettext as _
41 from tg.i18n import ungettext
41 from tg.i18n import ungettext
42 from webhelpers2.text import collapse, remove_formatting, strip_tags
42 from webhelpers2.text import collapse, remove_formatting, strip_tags
43
43
44 from kallithea.lib.compat import json
44 from kallithea.lib.compat import json
45 from kallithea.lib.vcs.utils.lazy import LazyProperty
45 from kallithea.lib.vcs.utils.lazy import LazyProperty
46
46
47
47
48 def str2bool(_str):
48 def str2bool(_str):
49 """
49 """
50 returns True/False value from given string, it tries to translate the
50 returns True/False value from given string, it tries to translate the
51 string into boolean
51 string into boolean
52
52
53 :param _str: string value to translate into boolean
53 :param _str: string value to translate into boolean
54 :rtype: boolean
54 :rtype: boolean
55 :returns: boolean from given string
55 :returns: boolean from given string
56 """
56 """
57 if _str is None:
57 if _str is None:
58 return False
58 return False
59 if _str in (True, False):
59 if _str in (True, False):
60 return _str
60 return _str
61 _str = str(_str).strip().lower()
61 _str = str(_str).strip().lower()
62 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
62 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
63
63
64
64
65 def aslist(obj, sep=None, strip=True):
65 def aslist(obj, sep=None, strip=True):
66 """
66 """
67 Returns given string separated by sep as list
67 Returns given string separated by sep as list
68
68
69 :param obj:
69 :param obj:
70 :param sep:
70 :param sep:
71 :param strip:
71 :param strip:
72 """
72 """
73 if isinstance(obj, (basestring)):
73 if isinstance(obj, (basestring)):
74 lst = obj.split(sep)
74 lst = obj.split(sep)
75 if strip:
75 if strip:
76 lst = [v.strip() for v in lst]
76 lst = [v.strip() for v in lst]
77 return lst
77 return lst
78 elif isinstance(obj, (list, tuple)):
78 elif isinstance(obj, (list, tuple)):
79 return obj
79 return obj
80 elif obj is None:
80 elif obj is None:
81 return []
81 return []
82 else:
82 else:
83 return [obj]
83 return [obj]
84
84
85
85
86 def convert_line_endings(line, mode):
86 def convert_line_endings(line, mode):
87 """
87 """
88 Converts a given line "line end" according to given mode
88 Converts a given line "line end" according to given mode
89
89
90 Available modes are::
90 Available modes are::
91 0 - Unix
91 0 - Unix
92 1 - Mac
92 1 - Mac
93 2 - DOS
93 2 - DOS
94
94
95 :param line: given line to convert
95 :param line: given line to convert
96 :param mode: mode to convert to
96 :param mode: mode to convert to
97 :rtype: str
97 :rtype: str
98 :return: converted line according to mode
98 :return: converted line according to mode
99 """
99 """
100 from string import replace
100 from string import replace
101
101
102 if mode == 0:
102 if mode == 0:
103 line = replace(line, '\r\n', '\n')
103 line = replace(line, '\r\n', '\n')
104 line = replace(line, '\r', '\n')
104 line = replace(line, '\r', '\n')
105 elif mode == 1:
105 elif mode == 1:
106 line = replace(line, '\r\n', '\r')
106 line = replace(line, '\r\n', '\r')
107 line = replace(line, '\n', '\r')
107 line = replace(line, '\n', '\r')
108 elif mode == 2:
108 elif mode == 2:
109 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
109 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
110 return line
110 return line
111
111
112
112
113 def detect_mode(line, default):
113 def detect_mode(line, default):
114 """
114 """
115 Detects line break for given line, if line break couldn't be found
115 Detects line break for given line, if line break couldn't be found
116 given default value is returned
116 given default value is returned
117
117
118 :param line: str line
118 :param line: str line
119 :param default: default
119 :param default: default
120 :rtype: int
120 :rtype: int
121 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
121 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
122 """
122 """
123 if line.endswith('\r\n'):
123 if line.endswith('\r\n'):
124 return 2
124 return 2
125 elif line.endswith('\n'):
125 elif line.endswith('\n'):
126 return 0
126 return 0
127 elif line.endswith('\r'):
127 elif line.endswith('\r'):
128 return 1
128 return 1
129 else:
129 else:
130 return default
130 return default
131
131
132
132
133 def generate_api_key():
133 def generate_api_key():
134 """
134 """
135 Generates a random (presumably unique) API key.
135 Generates a random (presumably unique) API key.
136
136
137 This value is used in URLs and "Bearer" HTTP Authorization headers,
137 This value is used in URLs and "Bearer" HTTP Authorization headers,
138 which in practice means it should only contain URL-safe characters
138 which in practice means it should only contain URL-safe characters
139 (RFC 3986):
139 (RFC 3986):
140
140
141 unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
141 unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
142 """
142 """
143 # Hexadecimal certainly qualifies as URL-safe.
143 # Hexadecimal certainly qualifies as URL-safe.
144 return binascii.hexlify(os.urandom(20))
144 return binascii.hexlify(os.urandom(20))
145
145
146
146
147 def safe_int(val, default=None):
147 def safe_int(val, default=None):
148 """
148 """
149 Returns int() of val if val is not convertable to int use default
149 Returns int() of val if val is not convertable to int use default
150 instead
150 instead
151
151
152 :param val:
152 :param val:
153 :param default:
153 :param default:
154 """
154 """
155
155
156 try:
156 try:
157 val = int(val)
157 val = int(val)
158 except (ValueError, TypeError):
158 except (ValueError, TypeError):
159 val = default
159 val = default
160
160
161 return val
161 return val
162
162
163
163
164 def safe_unicode(str_, from_encoding=None):
164 def safe_unicode(str_, from_encoding=None):
165 """
165 """
166 safe unicode function. Does few trick to turn str_ into unicode
166 safe unicode function. Does few trick to turn str_ into unicode
167
167
168 In case of UnicodeDecode error we try to return it with encoding detected
168 In case of UnicodeDecode error we try to return it with encoding detected
169 by chardet library if it fails fallback to unicode with errors replaced
169 by chardet library if it fails fallback to unicode with errors replaced
170
170
171 :param str_: string to decode
171 :param str_: string to decode
172 :rtype: unicode
172 :rtype: unicode
173 :returns: unicode object
173 :returns: unicode object
174 """
174 """
175 if isinstance(str_, unicode):
175 if isinstance(str_, unicode):
176 return str_
176 return str_
177
177
178 if not from_encoding:
178 if not from_encoding:
179 import kallithea
179 import kallithea
180 DEFAULT_ENCODINGS = aslist(kallithea.CONFIG.get('default_encoding',
180 DEFAULT_ENCODINGS = aslist(kallithea.CONFIG.get('default_encoding',
181 'utf-8'), sep=',')
181 'utf-8'), sep=',')
182 from_encoding = DEFAULT_ENCODINGS
182 from_encoding = DEFAULT_ENCODINGS
183
183
184 if not isinstance(from_encoding, (list, tuple)):
184 if not isinstance(from_encoding, (list, tuple)):
185 from_encoding = [from_encoding]
185 from_encoding = [from_encoding]
186
186
187 try:
187 try:
188 return unicode(str_)
188 return unicode(str_)
189 except UnicodeDecodeError:
189 except UnicodeDecodeError:
190 pass
190 pass
191
191
192 for enc in from_encoding:
192 for enc in from_encoding:
193 try:
193 try:
194 return unicode(str_, enc)
194 return unicode(str_, enc)
195 except UnicodeDecodeError:
195 except UnicodeDecodeError:
196 pass
196 pass
197
197
198 try:
198 try:
199 import chardet
199 import chardet
200 encoding = chardet.detect(str_)['encoding']
200 encoding = chardet.detect(str_)['encoding']
201 if encoding is None:
201 if encoding is None:
202 raise Exception()
202 raise Exception()
203 return str_.decode(encoding)
203 return str_.decode(encoding)
204 except (ImportError, UnicodeDecodeError, Exception):
204 except (ImportError, UnicodeDecodeError, Exception):
205 return unicode(str_, from_encoding[0], 'replace')
205 return unicode(str_, from_encoding[0], 'replace')
206
206
207
207
208 def safe_str(unicode_, to_encoding=None):
208 def safe_str(unicode_, to_encoding=None):
209 """
209 """
210 safe str function. Does few trick to turn unicode_ into string
210 safe str function. Does few trick to turn unicode_ into string
211
211
212 In case of UnicodeEncodeError we try to return it with encoding detected
212 In case of UnicodeEncodeError we try to return it with encoding detected
213 by chardet library if it fails fallback to string with errors replaced
213 by chardet library if it fails fallback to string with errors replaced
214
214
215 :param unicode_: unicode to encode
215 :param unicode_: unicode to encode
216 :rtype: str
216 :rtype: str
217 :returns: str object
217 :returns: str object
218 """
218 """
219
219
220 # if it's not basestr cast to str
220 # if it's not basestr cast to str
221 if not isinstance(unicode_, basestring):
221 if not isinstance(unicode_, basestring):
222 return str(unicode_)
222 return str(unicode_)
223
223
224 if isinstance(unicode_, str):
224 if isinstance(unicode_, str):
225 return unicode_
225 return unicode_
226
226
227 if not to_encoding:
227 if not to_encoding:
228 import kallithea
228 import kallithea
229 DEFAULT_ENCODINGS = aslist(kallithea.CONFIG.get('default_encoding',
229 DEFAULT_ENCODINGS = aslist(kallithea.CONFIG.get('default_encoding',
230 'utf-8'), sep=',')
230 'utf-8'), sep=',')
231 to_encoding = DEFAULT_ENCODINGS
231 to_encoding = DEFAULT_ENCODINGS
232
232
233 if not isinstance(to_encoding, (list, tuple)):
233 if not isinstance(to_encoding, (list, tuple)):
234 to_encoding = [to_encoding]
234 to_encoding = [to_encoding]
235
235
236 for enc in to_encoding:
236 for enc in to_encoding:
237 try:
237 try:
238 return unicode_.encode(enc)
238 return unicode_.encode(enc)
239 except UnicodeEncodeError:
239 except UnicodeEncodeError:
240 pass
240 pass
241
241
242 try:
242 try:
243 import chardet
243 import chardet
244 encoding = chardet.detect(unicode_)['encoding']
244 encoding = chardet.detect(unicode_)['encoding']
245 if encoding is None:
245 if encoding is None:
246 raise UnicodeEncodeError()
246 raise UnicodeEncodeError()
247
247
248 return unicode_.encode(encoding)
248 return unicode_.encode(encoding)
249 except (ImportError, UnicodeEncodeError):
249 except (ImportError, UnicodeEncodeError):
250 return unicode_.encode(to_encoding[0], 'replace')
250 return unicode_.encode(to_encoding[0], 'replace')
251
251
252
252
253 def remove_suffix(s, suffix):
253 def remove_suffix(s, suffix):
254 if s.endswith(suffix):
254 if s.endswith(suffix):
255 s = s[:-1 * len(suffix)]
255 s = s[:-1 * len(suffix)]
256 return s
256 return s
257
257
258
258
259 def remove_prefix(s, prefix):
259 def remove_prefix(s, prefix):
260 if s.startswith(prefix):
260 if s.startswith(prefix):
261 s = s[len(prefix):]
261 s = s[len(prefix):]
262 return s
262 return s
263
263
264
264
265 def age(prevdate, show_short_version=False, now=None):
265 def age(prevdate, show_short_version=False, now=None):
266 """
266 """
267 turns a datetime into an age string.
267 turns a datetime into an age string.
268 If show_short_version is True, then it will generate a not so accurate but shorter string,
268 If show_short_version is True, then it will generate a not so accurate but shorter string,
269 example: 2days ago, instead of 2 days and 23 hours ago.
269 example: 2days ago, instead of 2 days and 23 hours ago.
270
270
271 :param prevdate: datetime object
271 :param prevdate: datetime object
272 :param show_short_version: if it should approximate the date and return a shorter string
272 :param show_short_version: if it should approximate the date and return a shorter string
273 :rtype: unicode
273 :rtype: unicode
274 :returns: unicode words describing age
274 :returns: unicode words describing age
275 """
275 """
276 now = now or datetime.datetime.now()
276 now = now or datetime.datetime.now()
277 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
277 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
278 deltas = {}
278 deltas = {}
279 future = False
279 future = False
280
280
281 if prevdate > now:
281 if prevdate > now:
282 now, prevdate = prevdate, now
282 now, prevdate = prevdate, now
283 future = True
283 future = True
284 if future:
284 if future:
285 prevdate = prevdate.replace(microsecond=0)
285 prevdate = prevdate.replace(microsecond=0)
286 # Get date parts deltas
286 # Get date parts deltas
287 from dateutil import relativedelta
287 from dateutil import relativedelta
288 for part in order:
288 for part in order:
289 d = relativedelta.relativedelta(now, prevdate)
289 d = relativedelta.relativedelta(now, prevdate)
290 deltas[part] = getattr(d, part + 's')
290 deltas[part] = getattr(d, part + 's')
291
291
292 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
292 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
293 # not 1 hour, -59 minutes and -59 seconds)
293 # not 1 hour, -59 minutes and -59 seconds)
294 for num, length in [(5, 60), (4, 60), (3, 24)]: # seconds, minutes, hours
294 for num, length in [(5, 60), (4, 60), (3, 24)]: # seconds, minutes, hours
295 part = order[num]
295 part = order[num]
296 carry_part = order[num - 1]
296 carry_part = order[num - 1]
297
297
298 if deltas[part] < 0:
298 if deltas[part] < 0:
299 deltas[part] += length
299 deltas[part] += length
300 deltas[carry_part] -= 1
300 deltas[carry_part] -= 1
301
301
302 # Same thing for days except that the increment depends on the (variable)
302 # Same thing for days except that the increment depends on the (variable)
303 # number of days in the month
303 # number of days in the month
304 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
304 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
305 if deltas['day'] < 0:
305 if deltas['day'] < 0:
306 if prevdate.month == 2 and (prevdate.year % 4 == 0 and
306 if prevdate.month == 2 and (prevdate.year % 4 == 0 and
307 (prevdate.year % 100 != 0 or prevdate.year % 400 == 0)):
307 (prevdate.year % 100 != 0 or prevdate.year % 400 == 0)
308 ):
308 deltas['day'] += 29
309 deltas['day'] += 29
309 else:
310 else:
310 deltas['day'] += month_lengths[prevdate.month - 1]
311 deltas['day'] += month_lengths[prevdate.month - 1]
311
312
312 deltas['month'] -= 1
313 deltas['month'] -= 1
313
314
314 if deltas['month'] < 0:
315 if deltas['month'] < 0:
315 deltas['month'] += 12
316 deltas['month'] += 12
316 deltas['year'] -= 1
317 deltas['year'] -= 1
317
318
318 # In short version, we want nicer handling of ages of more than a year
319 # In short version, we want nicer handling of ages of more than a year
319 if show_short_version:
320 if show_short_version:
320 if deltas['year'] == 1:
321 if deltas['year'] == 1:
321 # ages between 1 and 2 years: show as months
322 # ages between 1 and 2 years: show as months
322 deltas['month'] += 12
323 deltas['month'] += 12
323 deltas['year'] = 0
324 deltas['year'] = 0
324 if deltas['year'] >= 2:
325 if deltas['year'] >= 2:
325 # ages 2+ years: round
326 # ages 2+ years: round
326 if deltas['month'] > 6:
327 if deltas['month'] > 6:
327 deltas['year'] += 1
328 deltas['year'] += 1
328 deltas['month'] = 0
329 deltas['month'] = 0
329
330
330 # Format the result
331 # Format the result
331 fmt_funcs = {
332 fmt_funcs = {
332 'year': lambda d: ungettext(u'%d year', '%d years', d) % d,
333 'year': lambda d: ungettext(u'%d year', '%d years', d) % d,
333 'month': lambda d: ungettext(u'%d month', '%d months', d) % d,
334 'month': lambda d: ungettext(u'%d month', '%d months', d) % d,
334 'day': lambda d: ungettext(u'%d day', '%d days', d) % d,
335 'day': lambda d: ungettext(u'%d day', '%d days', d) % d,
335 'hour': lambda d: ungettext(u'%d hour', '%d hours', d) % d,
336 'hour': lambda d: ungettext(u'%d hour', '%d hours', d) % d,
336 'minute': lambda d: ungettext(u'%d minute', '%d minutes', d) % d,
337 'minute': lambda d: ungettext(u'%d minute', '%d minutes', d) % d,
337 'second': lambda d: ungettext(u'%d second', '%d seconds', d) % d,
338 'second': lambda d: ungettext(u'%d second', '%d seconds', d) % d,
338 }
339 }
339
340
340 for i, part in enumerate(order):
341 for i, part in enumerate(order):
341 value = deltas[part]
342 value = deltas[part]
342 if value == 0:
343 if value == 0:
343 continue
344 continue
344
345
345 if i < 5:
346 if i < 5:
346 sub_part = order[i + 1]
347 sub_part = order[i + 1]
347 sub_value = deltas[sub_part]
348 sub_value = deltas[sub_part]
348 else:
349 else:
349 sub_value = 0
350 sub_value = 0
350
351
351 if sub_value == 0 or show_short_version:
352 if sub_value == 0 or show_short_version:
352 if future:
353 if future:
353 return _('in %s') % fmt_funcs[part](value)
354 return _('in %s') % fmt_funcs[part](value)
354 else:
355 else:
355 return _('%s ago') % fmt_funcs[part](value)
356 return _('%s ago') % fmt_funcs[part](value)
356 if future:
357 if future:
357 return _('in %s and %s') % (fmt_funcs[part](value),
358 return _('in %s and %s') % (fmt_funcs[part](value),
358 fmt_funcs[sub_part](sub_value))
359 fmt_funcs[sub_part](sub_value))
359 else:
360 else:
360 return _('%s and %s ago') % (fmt_funcs[part](value),
361 return _('%s and %s ago') % (fmt_funcs[part](value),
361 fmt_funcs[sub_part](sub_value))
362 fmt_funcs[sub_part](sub_value))
362
363
363 return _('just now')
364 return _('just now')
364
365
365
366
366 def uri_filter(uri):
367 def uri_filter(uri):
367 """
368 """
368 Removes user:password from given url string
369 Removes user:password from given url string
369
370
370 :param uri:
371 :param uri:
371 :rtype: unicode
372 :rtype: unicode
372 :returns: filtered list of strings
373 :returns: filtered list of strings
373 """
374 """
374 if not uri:
375 if not uri:
375 return ''
376 return ''
376
377
377 proto = ''
378 proto = ''
378
379
379 for pat in ('https://', 'http://', 'git://'):
380 for pat in ('https://', 'http://', 'git://'):
380 if uri.startswith(pat):
381 if uri.startswith(pat):
381 uri = uri[len(pat):]
382 uri = uri[len(pat):]
382 proto = pat
383 proto = pat
383 break
384 break
384
385
385 # remove passwords and username
386 # remove passwords and username
386 uri = uri[uri.find('@') + 1:]
387 uri = uri[uri.find('@') + 1:]
387
388
388 # get the port
389 # get the port
389 cred_pos = uri.find(':')
390 cred_pos = uri.find(':')
390 if cred_pos == -1:
391 if cred_pos == -1:
391 host, port = uri, None
392 host, port = uri, None
392 else:
393 else:
393 host, port = uri[:cred_pos], uri[cred_pos + 1:]
394 host, port = uri[:cred_pos], uri[cred_pos + 1:]
394
395
395 return filter(None, [proto, host, port])
396 return filter(None, [proto, host, port])
396
397
397
398
398 def credentials_filter(uri):
399 def credentials_filter(uri):
399 """
400 """
400 Returns a url with removed credentials
401 Returns a url with removed credentials
401
402
402 :param uri:
403 :param uri:
403 """
404 """
404
405
405 uri = uri_filter(uri)
406 uri = uri_filter(uri)
406 # check if we have port
407 # check if we have port
407 if len(uri) > 2 and uri[2]:
408 if len(uri) > 2 and uri[2]:
408 uri[2] = ':' + uri[2]
409 uri[2] = ':' + uri[2]
409
410
410 return ''.join(uri)
411 return ''.join(uri)
411
412
412
413
413 def get_clone_url(clone_uri_tmpl, prefix_url, repo_name, repo_id, username=None):
414 def get_clone_url(clone_uri_tmpl, prefix_url, repo_name, repo_id, username=None):
414 parsed_url = urlobject.URLObject(prefix_url)
415 parsed_url = urlobject.URLObject(prefix_url)
415 prefix = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
416 prefix = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
416 try:
417 try:
417 system_user = pwd.getpwuid(os.getuid()).pw_name
418 system_user = pwd.getpwuid(os.getuid()).pw_name
418 except Exception: # TODO: support all systems - especially Windows
419 except Exception: # TODO: support all systems - especially Windows
419 system_user = 'kallithea' # hardcoded default value ...
420 system_user = 'kallithea' # hardcoded default value ...
420 args = {
421 args = {
421 'scheme': parsed_url.scheme,
422 'scheme': parsed_url.scheme,
422 'user': safe_unicode(urllib.quote(safe_str(username or ''))),
423 'user': safe_unicode(urllib.quote(safe_str(username or ''))),
423 'netloc': parsed_url.netloc + prefix, # like "hostname:port/prefix" (with optional ":port" and "/prefix")
424 'netloc': parsed_url.netloc + prefix, # like "hostname:port/prefix" (with optional ":port" and "/prefix")
424 'prefix': prefix, # undocumented, empty or starting with /
425 'prefix': prefix, # undocumented, empty or starting with /
425 'repo': repo_name,
426 'repo': repo_name,
426 'repoid': str(repo_id),
427 'repoid': str(repo_id),
427 'system_user': safe_unicode(system_user),
428 'system_user': safe_unicode(system_user),
428 'hostname': parsed_url.hostname,
429 'hostname': parsed_url.hostname,
429 }
430 }
430 url = re.sub('{([^{}]+)}', lambda m: args.get(m.group(1), m.group(0)), clone_uri_tmpl)
431 url = re.sub('{([^{}]+)}', lambda m: args.get(m.group(1), m.group(0)), clone_uri_tmpl)
431
432
432 # remove leading @ sign if it's present. Case of empty user
433 # remove leading @ sign if it's present. Case of empty user
433 url_obj = urlobject.URLObject(url)
434 url_obj = urlobject.URLObject(url)
434 if not url_obj.username:
435 if not url_obj.username:
435 url_obj = url_obj.with_username(None)
436 url_obj = url_obj.with_username(None)
436
437
437 return safe_unicode(url_obj)
438 return safe_unicode(url_obj)
438
439
439
440
440 def get_changeset_safe(repo, rev):
441 def get_changeset_safe(repo, rev):
441 """
442 """
442 Safe version of get_changeset if this changeset doesn't exists for a
443 Safe version of get_changeset if this changeset doesn't exists for a
443 repo it returns a Dummy one instead
444 repo it returns a Dummy one instead
444
445
445 :param repo:
446 :param repo:
446 :param rev:
447 :param rev:
447 """
448 """
448 from kallithea.lib.vcs.backends.base import BaseRepository
449 from kallithea.lib.vcs.backends.base import BaseRepository
449 from kallithea.lib.vcs.exceptions import RepositoryError
450 from kallithea.lib.vcs.exceptions import RepositoryError
450 from kallithea.lib.vcs.backends.base import EmptyChangeset
451 from kallithea.lib.vcs.backends.base import EmptyChangeset
451 if not isinstance(repo, BaseRepository):
452 if not isinstance(repo, BaseRepository):
452 raise Exception('You must pass an Repository '
453 raise Exception('You must pass an Repository '
453 'object as first argument got %s', type(repo))
454 'object as first argument got %s', type(repo))
454
455
455 try:
456 try:
456 cs = repo.get_changeset(rev)
457 cs = repo.get_changeset(rev)
457 except (RepositoryError, LookupError):
458 except (RepositoryError, LookupError):
458 cs = EmptyChangeset(requested_revision=rev)
459 cs = EmptyChangeset(requested_revision=rev)
459 return cs
460 return cs
460
461
461
462
462 def datetime_to_time(dt):
463 def datetime_to_time(dt):
463 if dt:
464 if dt:
464 return time.mktime(dt.timetuple())
465 return time.mktime(dt.timetuple())
465
466
466
467
467 def time_to_datetime(tm):
468 def time_to_datetime(tm):
468 if tm:
469 if tm:
469 if isinstance(tm, basestring):
470 if isinstance(tm, basestring):
470 try:
471 try:
471 tm = float(tm)
472 tm = float(tm)
472 except ValueError:
473 except ValueError:
473 return
474 return
474 return datetime.datetime.fromtimestamp(tm)
475 return datetime.datetime.fromtimestamp(tm)
475
476
476
477
477 # Must match regexp in kallithea/public/js/base.js MentionsAutoComplete()
478 # Must match regexp in kallithea/public/js/base.js MentionsAutoComplete()
478 # Check char before @ - it must not look like we are in an email addresses.
479 # Check char before @ - it must not look like we are in an email addresses.
479 # Matching is greedy so we don't have to look beyond the end.
480 # Matching is greedy so we don't have to look beyond the end.
480 MENTIONS_REGEX = re.compile(r'(?:^|(?<=[^a-zA-Z0-9]))@([a-zA-Z0-9][-_.a-zA-Z0-9]*[a-zA-Z0-9])')
481 MENTIONS_REGEX = re.compile(r'(?:^|(?<=[^a-zA-Z0-9]))@([a-zA-Z0-9][-_.a-zA-Z0-9]*[a-zA-Z0-9])')
481
482
482
483
483 def extract_mentioned_usernames(text):
484 def extract_mentioned_usernames(text):
484 r"""
485 r"""
485 Returns list of (possible) usernames @mentioned in given text.
486 Returns list of (possible) usernames @mentioned in given text.
486
487
487 >>> extract_mentioned_usernames('@1-2.a_X,@1234 not@not @ddd@not @n @ee @ff @gg, @gg;@hh @n\n@zz,')
488 >>> extract_mentioned_usernames('@1-2.a_X,@1234 not@not @ddd@not @n @ee @ff @gg, @gg;@hh @n\n@zz,')
488 ['1-2.a_X', '1234', 'ddd', 'ee', 'ff', 'gg', 'gg', 'hh', 'zz']
489 ['1-2.a_X', '1234', 'ddd', 'ee', 'ff', 'gg', 'gg', 'hh', 'zz']
489 """
490 """
490 return MENTIONS_REGEX.findall(text)
491 return MENTIONS_REGEX.findall(text)
491
492
492
493
493 def extract_mentioned_users(text):
494 def extract_mentioned_users(text):
494 """ Returns set of actual database Users @mentioned in given text. """
495 """ Returns set of actual database Users @mentioned in given text. """
495 from kallithea.model.db import User
496 from kallithea.model.db import User
496 result = set()
497 result = set()
497 for name in extract_mentioned_usernames(text):
498 for name in extract_mentioned_usernames(text):
498 user = User.get_by_username(name, case_insensitive=True)
499 user = User.get_by_username(name, case_insensitive=True)
499 if user is not None and not user.is_default_user:
500 if user is not None and not user.is_default_user:
500 result.add(user)
501 result.add(user)
501 return result
502 return result
502
503
503
504
504 class AttributeDict(dict):
505 class AttributeDict(dict):
505 def __getattr__(self, attr):
506 def __getattr__(self, attr):
506 return self.get(attr, None)
507 return self.get(attr, None)
507 __setattr__ = dict.__setitem__
508 __setattr__ = dict.__setitem__
508 __delattr__ = dict.__delitem__
509 __delattr__ = dict.__delitem__
509
510
510
511
511 def obfuscate_url_pw(engine):
512 def obfuscate_url_pw(engine):
512 from sqlalchemy.engine import url as sa_url
513 from sqlalchemy.engine import url as sa_url
513 from sqlalchemy.exc import ArgumentError
514 from sqlalchemy.exc import ArgumentError
514 try:
515 try:
515 _url = sa_url.make_url(engine or '')
516 _url = sa_url.make_url(engine or '')
516 except ArgumentError:
517 except ArgumentError:
517 return engine
518 return engine
518 if _url.password:
519 if _url.password:
519 _url.password = 'XXXXX'
520 _url.password = 'XXXXX'
520 return str(_url)
521 return str(_url)
521
522
522
523
523 def get_hook_environment():
524 def get_hook_environment():
524 """
525 """
525 Get hook context by deserializing the global KALLITHEA_EXTRAS environment
526 Get hook context by deserializing the global KALLITHEA_EXTRAS environment
526 variable.
527 variable.
527
528
528 Called early in Git out-of-process hooks to get .ini config path so the
529 Called early in Git out-of-process hooks to get .ini config path so the
529 basic environment can be configured properly. Also used in all hooks to get
530 basic environment can be configured properly. Also used in all hooks to get
530 information about the action that triggered it.
531 information about the action that triggered it.
531 """
532 """
532
533
533 try:
534 try:
534 extras = json.loads(os.environ['KALLITHEA_EXTRAS'])
535 extras = json.loads(os.environ['KALLITHEA_EXTRAS'])
535 except KeyError:
536 except KeyError:
536 raise Exception("Environment variable KALLITHEA_EXTRAS not found")
537 raise Exception("Environment variable KALLITHEA_EXTRAS not found")
537
538
538 try:
539 try:
539 for k in ['username', 'repository', 'scm', 'action', 'ip']:
540 for k in ['username', 'repository', 'scm', 'action', 'ip']:
540 extras[k]
541 extras[k]
541 except KeyError:
542 except KeyError:
542 raise Exception('Missing key %s in KALLITHEA_EXTRAS %s' % (k, extras))
543 raise Exception('Missing key %s in KALLITHEA_EXTRAS %s' % (k, extras))
543
544
544 return AttributeDict(extras)
545 return AttributeDict(extras)
545
546
546
547
547 def set_hook_environment(username, ip_addr, repo_name, repo_alias, action=None):
548 def set_hook_environment(username, ip_addr, repo_name, repo_alias, action=None):
548 """Prepare global context for running hooks by serializing data in the
549 """Prepare global context for running hooks by serializing data in the
549 global KALLITHEA_EXTRAS environment variable.
550 global KALLITHEA_EXTRAS environment variable.
550
551
551 Most importantly, this allow Git hooks to do proper logging and updating of
552 Most importantly, this allow Git hooks to do proper logging and updating of
552 caches after pushes.
553 caches after pushes.
553
554
554 Must always be called before anything with hooks are invoked.
555 Must always be called before anything with hooks are invoked.
555 """
556 """
556 from kallithea import CONFIG
557 from kallithea import CONFIG
557 extras = {
558 extras = {
558 'ip': ip_addr, # used in log_push/pull_action action_logger
559 'ip': ip_addr, # used in log_push/pull_action action_logger
559 'username': username,
560 'username': username,
560 'action': action or 'push_local', # used in log_push_action_raw_ids action_logger
561 'action': action or 'push_local', # used in log_push_action_raw_ids action_logger
561 'repository': repo_name,
562 'repository': repo_name,
562 'scm': repo_alias, # used to pick hack in log_push_action_raw_ids
563 'scm': repo_alias, # used to pick hack in log_push_action_raw_ids
563 'config': CONFIG['__file__'], # used by git hook to read config
564 'config': CONFIG['__file__'], # used by git hook to read config
564 }
565 }
565 os.environ['KALLITHEA_EXTRAS'] = json.dumps(extras)
566 os.environ['KALLITHEA_EXTRAS'] = json.dumps(extras)
566
567
567
568
568 def get_current_authuser():
569 def get_current_authuser():
569 """
570 """
570 Gets kallithea user from threadlocal tmpl_context variable if it's
571 Gets kallithea user from threadlocal tmpl_context variable if it's
571 defined, else returns None.
572 defined, else returns None.
572 """
573 """
573 from tg import tmpl_context
574 from tg import tmpl_context
574 if hasattr(tmpl_context, 'authuser'):
575 if hasattr(tmpl_context, 'authuser'):
575 return tmpl_context.authuser
576 return tmpl_context.authuser
576
577
577 return None
578 return None
578
579
579
580
580 class OptionalAttr(object):
581 class OptionalAttr(object):
581 """
582 """
582 Special Optional Option that defines other attribute. Example::
583 Special Optional Option that defines other attribute. Example::
583
584
584 def test(apiuser, userid=Optional(OAttr('apiuser')):
585 def test(apiuser, userid=Optional(OAttr('apiuser')):
585 user = Optional.extract(userid)
586 user = Optional.extract(userid)
586 # calls
587 # calls
587
588
588 """
589 """
589
590
590 def __init__(self, attr_name):
591 def __init__(self, attr_name):
591 self.attr_name = attr_name
592 self.attr_name = attr_name
592
593
593 def __repr__(self):
594 def __repr__(self):
594 return '<OptionalAttr:%s>' % self.attr_name
595 return '<OptionalAttr:%s>' % self.attr_name
595
596
596 def __call__(self):
597 def __call__(self):
597 return self
598 return self
598
599
599
600
600 # alias
601 # alias
601 OAttr = OptionalAttr
602 OAttr = OptionalAttr
602
603
603
604
604 class Optional(object):
605 class Optional(object):
605 """
606 """
606 Defines an optional parameter::
607 Defines an optional parameter::
607
608
608 param = param.getval() if isinstance(param, Optional) else param
609 param = param.getval() if isinstance(param, Optional) else param
609 param = param() if isinstance(param, Optional) else param
610 param = param() if isinstance(param, Optional) else param
610
611
611 is equivalent of::
612 is equivalent of::
612
613
613 param = Optional.extract(param)
614 param = Optional.extract(param)
614
615
615 """
616 """
616
617
617 def __init__(self, type_):
618 def __init__(self, type_):
618 self.type_ = type_
619 self.type_ = type_
619
620
620 def __repr__(self):
621 def __repr__(self):
621 return '<Optional:%s>' % self.type_.__repr__()
622 return '<Optional:%s>' % self.type_.__repr__()
622
623
623 def __call__(self):
624 def __call__(self):
624 return self.getval()
625 return self.getval()
625
626
626 def getval(self):
627 def getval(self):
627 """
628 """
628 returns value from this Optional instance
629 returns value from this Optional instance
629 """
630 """
630 if isinstance(self.type_, OAttr):
631 if isinstance(self.type_, OAttr):
631 # use params name
632 # use params name
632 return self.type_.attr_name
633 return self.type_.attr_name
633 return self.type_
634 return self.type_
634
635
635 @classmethod
636 @classmethod
636 def extract(cls, val):
637 def extract(cls, val):
637 """
638 """
638 Extracts value from Optional() instance
639 Extracts value from Optional() instance
639
640
640 :param val:
641 :param val:
641 :return: original value if it's not Optional instance else
642 :return: original value if it's not Optional instance else
642 value of instance
643 value of instance
643 """
644 """
644 if isinstance(val, cls):
645 if isinstance(val, cls):
645 return val.getval()
646 return val.getval()
646 return val
647 return val
647
648
648
649
649 def urlreadable(s, _cleanstringsub=re.compile('[^-a-zA-Z0-9./]+').sub):
650 def urlreadable(s, _cleanstringsub=re.compile('[^-a-zA-Z0-9./]+').sub):
650 return _cleanstringsub('_', safe_str(s)).rstrip('_')
651 return _cleanstringsub('_', safe_str(s)).rstrip('_')
651
652
652
653
653 def recursive_replace(str_, replace=' '):
654 def recursive_replace(str_, replace=' '):
654 """
655 """
655 Recursive replace of given sign to just one instance
656 Recursive replace of given sign to just one instance
656
657
657 :param str_: given string
658 :param str_: given string
658 :param replace: char to find and replace multiple instances
659 :param replace: char to find and replace multiple instances
659
660
660 Examples::
661 Examples::
661 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
662 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
662 'Mighty-Mighty-Bo-sstones'
663 'Mighty-Mighty-Bo-sstones'
663 """
664 """
664
665
665 if str_.find(replace * 2) == -1:
666 if str_.find(replace * 2) == -1:
666 return str_
667 return str_
667 else:
668 else:
668 str_ = str_.replace(replace * 2, replace)
669 str_ = str_.replace(replace * 2, replace)
669 return recursive_replace(str_, replace)
670 return recursive_replace(str_, replace)
670
671
671
672
672 def repo_name_slug(value):
673 def repo_name_slug(value):
673 """
674 """
674 Return slug of name of repository
675 Return slug of name of repository
675 This function is called on each creation/modification
676 This function is called on each creation/modification
676 of repository to prevent bad names in repo
677 of repository to prevent bad names in repo
677 """
678 """
678
679
679 slug = remove_formatting(value)
680 slug = remove_formatting(value)
680 slug = strip_tags(slug)
681 slug = strip_tags(slug)
681
682
682 for c in r"""`?=[]\;'"<>,/~!@#$%^&*()+{}|: """:
683 for c in r"""`?=[]\;'"<>,/~!@#$%^&*()+{}|: """:
683 slug = slug.replace(c, '-')
684 slug = slug.replace(c, '-')
684 slug = recursive_replace(slug, '-')
685 slug = recursive_replace(slug, '-')
685 slug = collapse(slug, '-')
686 slug = collapse(slug, '-')
686 return slug
687 return slug
687
688
688
689
689 def ask_ok(prompt, retries=4, complaint='Yes or no please!'):
690 def ask_ok(prompt, retries=4, complaint='Yes or no please!'):
690 while True:
691 while True:
691 ok = raw_input(prompt)
692 ok = raw_input(prompt)
692 if ok in ('y', 'ye', 'yes'):
693 if ok in ('y', 'ye', 'yes'):
693 return True
694 return True
694 if ok in ('n', 'no', 'nop', 'nope'):
695 if ok in ('n', 'no', 'nop', 'nope'):
695 return False
696 return False
696 retries = retries - 1
697 retries = retries - 1
697 if retries < 0:
698 if retries < 0:
698 raise IOError
699 raise IOError
699 print complaint
700 print complaint
@@ -1,200 +1,201 b''
1 import datetime
1 import datetime
2 import posixpath
2 import posixpath
3 import stat
3 import stat
4 import time
4 import time
5
5
6 from dulwich import objects
6 from dulwich import objects
7
7
8 from kallithea.lib.vcs.backends.base import BaseInMemoryChangeset
8 from kallithea.lib.vcs.backends.base import BaseInMemoryChangeset
9 from kallithea.lib.vcs.exceptions import RepositoryError
9 from kallithea.lib.vcs.exceptions import RepositoryError
10 from kallithea.lib.vcs.utils import safe_str
10 from kallithea.lib.vcs.utils import safe_str
11
11
12
12
13 class GitInMemoryChangeset(BaseInMemoryChangeset):
13 class GitInMemoryChangeset(BaseInMemoryChangeset):
14
14
15 def commit(self, message, author, parents=None, branch=None, date=None,
15 def commit(self, message, author, parents=None, branch=None, date=None,
16 **kwargs):
16 **kwargs):
17 """
17 """
18 Performs in-memory commit (doesn't check workdir in any way) and
18 Performs in-memory commit (doesn't check workdir in any way) and
19 returns newly created ``Changeset``. Updates repository's
19 returns newly created ``Changeset``. Updates repository's
20 ``revisions``.
20 ``revisions``.
21
21
22 :param message: message of the commit
22 :param message: message of the commit
23 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
23 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
24 :param parents: single parent or sequence of parents from which commit
24 :param parents: single parent or sequence of parents from which commit
25 would be derived
25 would be derived
26 :param date: ``datetime.datetime`` instance. Defaults to
26 :param date: ``datetime.datetime`` instance. Defaults to
27 ``datetime.datetime.now()``.
27 ``datetime.datetime.now()``.
28 :param branch: branch name, as string. If none given, default backend's
28 :param branch: branch name, as string. If none given, default backend's
29 branch would be used.
29 branch would be used.
30
30
31 :raises ``CommitError``: if any error occurs while committing
31 :raises ``CommitError``: if any error occurs while committing
32 """
32 """
33 self.check_integrity(parents)
33 self.check_integrity(parents)
34
34
35 from .repository import GitRepository
35 from .repository import GitRepository
36 if branch is None:
36 if branch is None:
37 branch = GitRepository.DEFAULT_BRANCH_NAME
37 branch = GitRepository.DEFAULT_BRANCH_NAME
38
38
39 repo = self.repository._repo
39 repo = self.repository._repo
40 object_store = repo.object_store
40 object_store = repo.object_store
41
41
42 ENCODING = "UTF-8"
42 ENCODING = "UTF-8"
43
43
44 # Create tree and populates it with blobs
44 # Create tree and populates it with blobs
45 commit_tree = self.parents[0] and repo[self.parents[0]._commit.tree] or \
45 commit_tree = self.parents[0] and repo[self.parents[0]._commit.tree] or \
46 objects.Tree()
46 objects.Tree()
47 for node in self.added + self.changed:
47 for node in self.added + self.changed:
48 # Compute subdirs if needed
48 # Compute subdirs if needed
49 dirpath, nodename = posixpath.split(node.path)
49 dirpath, nodename = posixpath.split(node.path)
50 dirnames = map(safe_str, dirpath and dirpath.split('/') or [])
50 dirnames = map(safe_str, dirpath and dirpath.split('/') or [])
51 parent = commit_tree
51 parent = commit_tree
52 ancestors = [('', parent)]
52 ancestors = [('', parent)]
53
53
54 # Tries to dig for the deepest existing tree
54 # Tries to dig for the deepest existing tree
55 while dirnames:
55 while dirnames:
56 curdir = dirnames.pop(0)
56 curdir = dirnames.pop(0)
57 try:
57 try:
58 dir_id = parent[curdir][1]
58 dir_id = parent[curdir][1]
59 except KeyError:
59 except KeyError:
60 # put curdir back into dirnames and stops
60 # put curdir back into dirnames and stops
61 dirnames.insert(0, curdir)
61 dirnames.insert(0, curdir)
62 break
62 break
63 else:
63 else:
64 # If found, updates parent
64 # If found, updates parent
65 parent = self.repository._repo[dir_id]
65 parent = self.repository._repo[dir_id]
66 ancestors.append((curdir, parent))
66 ancestors.append((curdir, parent))
67 # Now parent is deepest existing tree and we need to create subtrees
67 # Now parent is deepest existing tree and we need to create subtrees
68 # for dirnames (in reverse order) [this only applies for nodes from added]
68 # for dirnames (in reverse order) [this only applies for nodes from added]
69 new_trees = []
69 new_trees = []
70
70
71 if not node.is_binary:
71 if not node.is_binary:
72 content = node.content.encode(ENCODING)
72 content = node.content.encode(ENCODING)
73 else:
73 else:
74 content = node.content
74 content = node.content
75 blob = objects.Blob.from_string(content)
75 blob = objects.Blob.from_string(content)
76
76
77 node_path = node.name.encode(ENCODING)
77 node_path = node.name.encode(ENCODING)
78 if dirnames:
78 if dirnames:
79 # If there are trees which should be created we need to build
79 # If there are trees which should be created we need to build
80 # them now (in reverse order)
80 # them now (in reverse order)
81 reversed_dirnames = list(reversed(dirnames))
81 reversed_dirnames = list(reversed(dirnames))
82 curtree = objects.Tree()
82 curtree = objects.Tree()
83 curtree[node_path] = node.mode, blob.id
83 curtree[node_path] = node.mode, blob.id
84 new_trees.append(curtree)
84 new_trees.append(curtree)
85 for dirname in reversed_dirnames[:-1]:
85 for dirname in reversed_dirnames[:-1]:
86 newtree = objects.Tree()
86 newtree = objects.Tree()
87 #newtree.add(stat.S_IFDIR, dirname, curtree.id)
87 #newtree.add(stat.S_IFDIR, dirname, curtree.id)
88 newtree[dirname] = stat.S_IFDIR, curtree.id
88 newtree[dirname] = stat.S_IFDIR, curtree.id
89 new_trees.append(newtree)
89 new_trees.append(newtree)
90 curtree = newtree
90 curtree = newtree
91 parent[reversed_dirnames[-1]] = stat.S_IFDIR, curtree.id
91 parent[reversed_dirnames[-1]] = stat.S_IFDIR, curtree.id
92 else:
92 else:
93 parent.add(name=node_path, mode=node.mode, hexsha=blob.id)
93 parent.add(name=node_path, mode=node.mode, hexsha=blob.id)
94
94
95 new_trees.append(parent)
95 new_trees.append(parent)
96 # Update ancestors
96 # Update ancestors
97 for parent, tree, path in reversed([(a[1], b[1], b[0]) for a, b in
97 for parent, tree, path in reversed([(a[1], b[1], b[0]) for a, b in
98 zip(ancestors, ancestors[1:])]):
98 zip(ancestors, ancestors[1:])]
99 ):
99 parent[path] = stat.S_IFDIR, tree.id
100 parent[path] = stat.S_IFDIR, tree.id
100 object_store.add_object(tree)
101 object_store.add_object(tree)
101
102
102 object_store.add_object(blob)
103 object_store.add_object(blob)
103 for tree in new_trees:
104 for tree in new_trees:
104 object_store.add_object(tree)
105 object_store.add_object(tree)
105 for node in self.removed:
106 for node in self.removed:
106 paths = node.path.split('/')
107 paths = node.path.split('/')
107 tree = commit_tree
108 tree = commit_tree
108 trees = [tree]
109 trees = [tree]
109 # Traverse deep into the forest...
110 # Traverse deep into the forest...
110 for path in paths:
111 for path in paths:
111 try:
112 try:
112 obj = self.repository._repo[tree[path][1]]
113 obj = self.repository._repo[tree[path][1]]
113 if isinstance(obj, objects.Tree):
114 if isinstance(obj, objects.Tree):
114 trees.append(obj)
115 trees.append(obj)
115 tree = obj
116 tree = obj
116 except KeyError:
117 except KeyError:
117 break
118 break
118 # Cut down the blob and all rotten trees on the way back...
119 # Cut down the blob and all rotten trees on the way back...
119 for path, tree in reversed(zip(paths, trees)):
120 for path, tree in reversed(zip(paths, trees)):
120 del tree[path]
121 del tree[path]
121 if tree:
122 if tree:
122 # This tree still has elements - don't remove it or any
123 # This tree still has elements - don't remove it or any
123 # of it's parents
124 # of it's parents
124 break
125 break
125
126
126 object_store.add_object(commit_tree)
127 object_store.add_object(commit_tree)
127
128
128 # Create commit
129 # Create commit
129 commit = objects.Commit()
130 commit = objects.Commit()
130 commit.tree = commit_tree.id
131 commit.tree = commit_tree.id
131 commit.parents = [p._commit.id for p in self.parents if p]
132 commit.parents = [p._commit.id for p in self.parents if p]
132 commit.author = commit.committer = safe_str(author)
133 commit.author = commit.committer = safe_str(author)
133 commit.encoding = ENCODING
134 commit.encoding = ENCODING
134 commit.message = safe_str(message)
135 commit.message = safe_str(message)
135
136
136 # Compute date
137 # Compute date
137 if date is None:
138 if date is None:
138 date = time.time()
139 date = time.time()
139 elif isinstance(date, datetime.datetime):
140 elif isinstance(date, datetime.datetime):
140 date = time.mktime(date.timetuple())
141 date = time.mktime(date.timetuple())
141
142
142 author_time = kwargs.pop('author_time', date)
143 author_time = kwargs.pop('author_time', date)
143 commit.commit_time = int(date)
144 commit.commit_time = int(date)
144 commit.author_time = int(author_time)
145 commit.author_time = int(author_time)
145 tz = time.timezone
146 tz = time.timezone
146 author_tz = kwargs.pop('author_timezone', tz)
147 author_tz = kwargs.pop('author_timezone', tz)
147 commit.commit_timezone = tz
148 commit.commit_timezone = tz
148 commit.author_timezone = author_tz
149 commit.author_timezone = author_tz
149
150
150 object_store.add_object(commit)
151 object_store.add_object(commit)
151
152
152 ref = 'refs/heads/%s' % branch
153 ref = 'refs/heads/%s' % branch
153 repo.refs[ref] = commit.id
154 repo.refs[ref] = commit.id
154
155
155 # Update vcs repository object & recreate dulwich repo
156 # Update vcs repository object & recreate dulwich repo
156 self.repository.revisions.append(commit.id)
157 self.repository.revisions.append(commit.id)
157 # invalidate parsed refs after commit
158 # invalidate parsed refs after commit
158 self.repository._parsed_refs = self.repository._get_parsed_refs()
159 self.repository._parsed_refs = self.repository._get_parsed_refs()
159 tip = self.repository.get_changeset()
160 tip = self.repository.get_changeset()
160 self.reset()
161 self.reset()
161 return tip
162 return tip
162
163
163 def _get_missing_trees(self, path, root_tree):
164 def _get_missing_trees(self, path, root_tree):
164 """
165 """
165 Creates missing ``Tree`` objects for the given path.
166 Creates missing ``Tree`` objects for the given path.
166
167
167 :param path: path given as a string. It may be a path to a file node
168 :param path: path given as a string. It may be a path to a file node
168 (i.e. ``foo/bar/baz.txt``) or directory path - in that case it must
169 (i.e. ``foo/bar/baz.txt``) or directory path - in that case it must
169 end with slash (i.e. ``foo/bar/``).
170 end with slash (i.e. ``foo/bar/``).
170 :param root_tree: ``dulwich.objects.Tree`` object from which we start
171 :param root_tree: ``dulwich.objects.Tree`` object from which we start
171 traversing (should be commit's root tree)
172 traversing (should be commit's root tree)
172 """
173 """
173 dirpath = posixpath.split(path)[0]
174 dirpath = posixpath.split(path)[0]
174 dirs = dirpath.split('/')
175 dirs = dirpath.split('/')
175 if not dirs or dirs == ['']:
176 if not dirs or dirs == ['']:
176 return []
177 return []
177
178
178 def get_tree_for_dir(tree, dirname):
179 def get_tree_for_dir(tree, dirname):
179 for name, mode, id in tree.iteritems():
180 for name, mode, id in tree.iteritems():
180 if name == dirname:
181 if name == dirname:
181 obj = self.repository._repo[id]
182 obj = self.repository._repo[id]
182 if isinstance(obj, objects.Tree):
183 if isinstance(obj, objects.Tree):
183 return obj
184 return obj
184 else:
185 else:
185 raise RepositoryError("Cannot create directory %s "
186 raise RepositoryError("Cannot create directory %s "
186 "at tree %s as path is occupied and is not a "
187 "at tree %s as path is occupied and is not a "
187 "Tree" % (dirname, tree))
188 "Tree" % (dirname, tree))
188 return None
189 return None
189
190
190 trees = []
191 trees = []
191 parent = root_tree
192 parent = root_tree
192 for dirname in dirs:
193 for dirname in dirs:
193 tree = get_tree_for_dir(parent, dirname)
194 tree = get_tree_for_dir(parent, dirname)
194 if tree is None:
195 if tree is None:
195 tree = objects.Tree()
196 tree = objects.Tree()
196 parent.add(stat.S_IFDIR, dirname, tree.id)
197 parent.add(stat.S_IFDIR, dirname, tree.id)
197 parent = tree
198 parent = tree
198 # Always append tree
199 # Always append tree
199 trees.append(tree)
200 trees.append(tree)
200 return trees
201 return trees
@@ -1,178 +1,179 b''
1 import StringIO
1 import StringIO
2
2
3 from pygments import highlight
3 from pygments import highlight
4 from pygments.formatters import HtmlFormatter
4 from pygments.formatters import HtmlFormatter
5
5
6 from kallithea.lib.vcs.exceptions import VCSError
6 from kallithea.lib.vcs.exceptions import VCSError
7 from kallithea.lib.vcs.nodes import FileNode
7 from kallithea.lib.vcs.nodes import FileNode
8
8
9
9
10 def annotate_highlight(filenode, annotate_from_changeset_func=None,
10 def annotate_highlight(filenode, annotate_from_changeset_func=None,
11 order=None, headers=None, **options):
11 order=None, headers=None, **options):
12 """
12 """
13 Returns html portion containing annotated table with 3 columns: line
13 Returns html portion containing annotated table with 3 columns: line
14 numbers, changeset information and pygmentized line of code.
14 numbers, changeset information and pygmentized line of code.
15
15
16 :param filenode: FileNode object
16 :param filenode: FileNode object
17 :param annotate_from_changeset_func: function taking changeset and
17 :param annotate_from_changeset_func: function taking changeset and
18 returning single annotate cell; needs break line at the end
18 returning single annotate cell; needs break line at the end
19 :param order: ordered sequence of ``ls`` (line numbers column),
19 :param order: ordered sequence of ``ls`` (line numbers column),
20 ``annotate`` (annotate column), ``code`` (code column); Default is
20 ``annotate`` (annotate column), ``code`` (code column); Default is
21 ``['ls', 'annotate', 'code']``
21 ``['ls', 'annotate', 'code']``
22 :param headers: dictionary with headers (keys are whats in ``order``
22 :param headers: dictionary with headers (keys are whats in ``order``
23 parameter)
23 parameter)
24 """
24 """
25 options['linenos'] = True
25 options['linenos'] = True
26 formatter = AnnotateHtmlFormatter(filenode=filenode, order=order,
26 formatter = AnnotateHtmlFormatter(filenode=filenode, order=order,
27 headers=headers,
27 headers=headers,
28 annotate_from_changeset_func=annotate_from_changeset_func, **options)
28 annotate_from_changeset_func=annotate_from_changeset_func, **options)
29 lexer = filenode.lexer
29 lexer = filenode.lexer
30 highlighted = highlight(filenode.content, lexer, formatter)
30 highlighted = highlight(filenode.content, lexer, formatter)
31 return highlighted
31 return highlighted
32
32
33
33
34 class AnnotateHtmlFormatter(HtmlFormatter):
34 class AnnotateHtmlFormatter(HtmlFormatter):
35
35
36 def __init__(self, filenode, annotate_from_changeset_func=None,
36 def __init__(self, filenode, annotate_from_changeset_func=None,
37 order=None, **options):
37 order=None, **options):
38 """
38 """
39 If ``annotate_from_changeset_func`` is passed it should be a function
39 If ``annotate_from_changeset_func`` is passed it should be a function
40 which returns string from the given changeset. For example, we may pass
40 which returns string from the given changeset. For example, we may pass
41 following function as ``annotate_from_changeset_func``::
41 following function as ``annotate_from_changeset_func``::
42
42
43 def changeset_to_anchor(changeset):
43 def changeset_to_anchor(changeset):
44 return '<a href="/changesets/%s/">%s</a>\n' % \
44 return '<a href="/changesets/%s/">%s</a>\n' % \
45 (changeset.id, changeset.id)
45 (changeset.id, changeset.id)
46
46
47 :param annotate_from_changeset_func: see above
47 :param annotate_from_changeset_func: see above
48 :param order: (default: ``['ls', 'annotate', 'code']``); order of
48 :param order: (default: ``['ls', 'annotate', 'code']``); order of
49 columns;
49 columns;
50 :param options: standard pygment's HtmlFormatter options, there is
50 :param options: standard pygment's HtmlFormatter options, there is
51 extra option tough, ``headers``. For instance we can pass::
51 extra option tough, ``headers``. For instance we can pass::
52
52
53 formatter = AnnotateHtmlFormatter(filenode, headers={
53 formatter = AnnotateHtmlFormatter(filenode, headers={
54 'ls': '#',
54 'ls': '#',
55 'annotate': 'Annotate',
55 'annotate': 'Annotate',
56 'code': 'Code',
56 'code': 'Code',
57 })
57 })
58
58
59 """
59 """
60 super(AnnotateHtmlFormatter, self).__init__(**options)
60 super(AnnotateHtmlFormatter, self).__init__(**options)
61 self.annotate_from_changeset_func = annotate_from_changeset_func
61 self.annotate_from_changeset_func = annotate_from_changeset_func
62 self.order = order or ('ls', 'annotate', 'code')
62 self.order = order or ('ls', 'annotate', 'code')
63 headers = options.pop('headers', None)
63 headers = options.pop('headers', None)
64 if headers and not ('ls' in headers and 'annotate' in headers and
64 if headers and not ('ls' in headers and 'annotate' in headers and
65 'code' in headers):
65 'code' in headers
66 ):
66 raise ValueError("If headers option dict is specified it must "
67 raise ValueError("If headers option dict is specified it must "
67 "all 'ls', 'annotate' and 'code' keys")
68 "all 'ls', 'annotate' and 'code' keys")
68 self.headers = headers
69 self.headers = headers
69 if isinstance(filenode, FileNode):
70 if isinstance(filenode, FileNode):
70 self.filenode = filenode
71 self.filenode = filenode
71 else:
72 else:
72 raise VCSError("This formatter expect FileNode parameter, not %r"
73 raise VCSError("This formatter expect FileNode parameter, not %r"
73 % type(filenode))
74 % type(filenode))
74
75
75 def annotate_from_changeset(self, changeset):
76 def annotate_from_changeset(self, changeset):
76 """
77 """
77 Returns full html line for single changeset per annotated line.
78 Returns full html line for single changeset per annotated line.
78 """
79 """
79 if self.annotate_from_changeset_func:
80 if self.annotate_from_changeset_func:
80 return self.annotate_from_changeset_func(changeset)
81 return self.annotate_from_changeset_func(changeset)
81 else:
82 else:
82 return ''.join((changeset.id, '\n'))
83 return ''.join((changeset.id, '\n'))
83
84
84 def _wrap_tablelinenos(self, inner):
85 def _wrap_tablelinenos(self, inner):
85 dummyoutfile = StringIO.StringIO()
86 dummyoutfile = StringIO.StringIO()
86 lncount = 0
87 lncount = 0
87 for t, line in inner:
88 for t, line in inner:
88 if t:
89 if t:
89 lncount += 1
90 lncount += 1
90 dummyoutfile.write(line)
91 dummyoutfile.write(line)
91
92
92 fl = self.linenostart
93 fl = self.linenostart
93 mw = len(str(lncount + fl - 1))
94 mw = len(str(lncount + fl - 1))
94 sp = self.linenospecial
95 sp = self.linenospecial
95 st = self.linenostep
96 st = self.linenostep
96 la = self.lineanchors
97 la = self.lineanchors
97 aln = self.anchorlinenos
98 aln = self.anchorlinenos
98 if sp:
99 if sp:
99 lines = []
100 lines = []
100
101
101 for i in range(fl, fl + lncount):
102 for i in range(fl, fl + lncount):
102 if i % st == 0:
103 if i % st == 0:
103 if i % sp == 0:
104 if i % sp == 0:
104 if aln:
105 if aln:
105 lines.append('<a href="#%s-%d" class="special">'
106 lines.append('<a href="#%s-%d" class="special">'
106 '%*d</a>' %
107 '%*d</a>' %
107 (la, i, mw, i))
108 (la, i, mw, i))
108 else:
109 else:
109 lines.append('<span class="special">'
110 lines.append('<span class="special">'
110 '%*d</span>' % (mw, i))
111 '%*d</span>' % (mw, i))
111 else:
112 else:
112 if aln:
113 if aln:
113 lines.append('<a href="#%s-%d">'
114 lines.append('<a href="#%s-%d">'
114 '%*d</a>' % (la, i, mw, i))
115 '%*d</a>' % (la, i, mw, i))
115 else:
116 else:
116 lines.append('%*d' % (mw, i))
117 lines.append('%*d' % (mw, i))
117 else:
118 else:
118 lines.append('')
119 lines.append('')
119 ls = '\n'.join(lines)
120 ls = '\n'.join(lines)
120 else:
121 else:
121 lines = []
122 lines = []
122 for i in range(fl, fl + lncount):
123 for i in range(fl, fl + lncount):
123 if i % st == 0:
124 if i % st == 0:
124 if aln:
125 if aln:
125 lines.append('<a href="#%s-%d">%*d</a>'
126 lines.append('<a href="#%s-%d">%*d</a>'
126 % (la, i, mw, i))
127 % (la, i, mw, i))
127 else:
128 else:
128 lines.append('%*d' % (mw, i))
129 lines.append('%*d' % (mw, i))
129 else:
130 else:
130 lines.append('')
131 lines.append('')
131 ls = '\n'.join(lines)
132 ls = '\n'.join(lines)
132
133
133 annotate_changesets = [tup[1] for tup in self.filenode.annotate]
134 annotate_changesets = [tup[1] for tup in self.filenode.annotate]
134 # If pygments cropped last lines break we need do that too
135 # If pygments cropped last lines break we need do that too
135 ln_cs = len(annotate_changesets)
136 ln_cs = len(annotate_changesets)
136 ln_ = len(ls.splitlines())
137 ln_ = len(ls.splitlines())
137 if ln_cs > ln_:
138 if ln_cs > ln_:
138 annotate_changesets = annotate_changesets[:ln_ - ln_cs]
139 annotate_changesets = annotate_changesets[:ln_ - ln_cs]
139 annotate = ''.join((self.annotate_from_changeset(changeset)
140 annotate = ''.join((self.annotate_from_changeset(changeset)
140 for changeset in annotate_changesets))
141 for changeset in annotate_changesets))
141 # in case you wonder about the seemingly redundant <div> here:
142 # in case you wonder about the seemingly redundant <div> here:
142 # since the content in the other cell also is wrapped in a div,
143 # since the content in the other cell also is wrapped in a div,
143 # some browsers in some configurations seem to mess up the formatting.
144 # some browsers in some configurations seem to mess up the formatting.
144 '''
145 '''
145 yield 0, ('<table class="%stable">' % self.cssclass +
146 yield 0, ('<table class="%stable">' % self.cssclass +
146 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
147 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
147 ls + '</pre></div></td>' +
148 ls + '</pre></div></td>' +
148 '<td class="code">')
149 '<td class="code">')
149 yield 0, dummyoutfile.getvalue()
150 yield 0, dummyoutfile.getvalue()
150 yield 0, '</td></tr></table>'
151 yield 0, '</td></tr></table>'
151
152
152 '''
153 '''
153 headers_row = []
154 headers_row = []
154 if self.headers:
155 if self.headers:
155 headers_row = ['<tr class="annotate-header">']
156 headers_row = ['<tr class="annotate-header">']
156 for key in self.order:
157 for key in self.order:
157 td = ''.join(('<td>', self.headers[key], '</td>'))
158 td = ''.join(('<td>', self.headers[key], '</td>'))
158 headers_row.append(td)
159 headers_row.append(td)
159 headers_row.append('</tr>')
160 headers_row.append('</tr>')
160
161
161 body_row_start = ['<tr>']
162 body_row_start = ['<tr>']
162 for key in self.order:
163 for key in self.order:
163 if key == 'ls':
164 if key == 'ls':
164 body_row_start.append(
165 body_row_start.append(
165 '<td class="linenos"><div class="linenodiv"><pre>' +
166 '<td class="linenos"><div class="linenodiv"><pre>' +
166 ls + '</pre></div></td>')
167 ls + '</pre></div></td>')
167 elif key == 'annotate':
168 elif key == 'annotate':
168 body_row_start.append(
169 body_row_start.append(
169 '<td class="annotate"><div class="annotatediv"><pre>' +
170 '<td class="annotate"><div class="annotatediv"><pre>' +
170 annotate + '</pre></div></td>')
171 annotate + '</pre></div></td>')
171 elif key == 'code':
172 elif key == 'code':
172 body_row_start.append('<td class="code">')
173 body_row_start.append('<td class="code">')
173 yield 0, ('<table class="%stable">' % self.cssclass +
174 yield 0, ('<table class="%stable">' % self.cssclass +
174 ''.join(headers_row) +
175 ''.join(headers_row) +
175 ''.join(body_row_start)
176 ''.join(body_row_start)
176 )
177 )
177 yield 0, dummyoutfile.getvalue()
178 yield 0, dummyoutfile.getvalue()
178 yield 0, '</td></tr></table>'
179 yield 0, '</td></tr></table>'
General Comments 0
You need to be logged in to leave comments. Login now