##// END OF EJS Templates
spelling: use correct Git capitalisation where appropriate
Andrew Shadura -
r4937:326a9336 default
parent child Browse files
Show More
@@ -1,49 +1,49 b''
1 .. _git_support:
1 .. _git_support:
2
2
3 ===========
3 ===========
4 GIT support
4 Git support
5 ===========
5 ===========
6
6
7
7
8 Kallithea Git support is enabled by default. You just need a git
8 Kallithea Git support is enabled by default. You just need a git
9 command line client installed on the server to make Git work fully.
9 command line client installed on the server to make Git work fully.
10
10
11 Web server with chunked encoding
11 Web server with chunked encoding
12 --------------------------------
12 --------------------------------
13
13
14 Large Git pushes do however require a http server with support for chunked encoding for POST.
14 Large Git pushes do however require a http server with support for chunked encoding for POST.
15
15
16 The Python web servers waitress_ and gunicorn_ (linux only) can be used.
16 The Python web servers waitress_ and gunicorn_ (linux only) can be used.
17 By default, Kallithea uses waitress_ for `paster serve` instead of the built-in `paste` WSGI server.
17 By default, Kallithea uses waitress_ for `paster serve` instead of the built-in `paste` WSGI server.
18
18
19 The default paste server is controlled in the .ini file::
19 The default paste server is controlled in the .ini file::
20
20
21 use = egg:waitress#main
21 use = egg:waitress#main
22
22
23 or::
23 or::
24
24
25 use = egg:gunicorn#main
25 use = egg:gunicorn#main
26
26
27
27
28 Also make sure to comment out the following options::
28 Also make sure to comment out the following options::
29
29
30 threadpool_workers =
30 threadpool_workers =
31 threadpool_max_requests =
31 threadpool_max_requests =
32 use_threadpool =
32 use_threadpool =
33
33
34
34
35 Disabling Git
35 Disabling Git
36 -------------
36 -------------
37
37
38 You can always disable git/hg support by editing a
38 You can always disable git/hg support by editing a
39 file **kallithea/__init__.py** and commenting out the backend.
39 file **kallithea/__init__.py** and commenting out the backend.
40
40
41 .. code-block:: python
41 .. code-block:: python
42
42
43 BACKENDS = {
43 BACKENDS = {
44 'hg': 'Mercurial repository',
44 'hg': 'Mercurial repository',
45 #'git': 'Git repository',
45 #'git': 'Git repository',
46 }
46 }
47
47
48 .. _waitress: http://pypi.python.org/pypi/waitress
48 .. _waitress: http://pypi.python.org/pypi/waitress
49 .. _gunicorn: http://pypi.python.org/pypi/gunicorn
49 .. _gunicorn: http://pypi.python.org/pypi/gunicorn
@@ -1,750 +1,750 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.diffs
15 kallithea.lib.diffs
16 ~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~
17
17
18 Set of diffing helpers, previously part of vcs
18 Set of diffing helpers, previously part of vcs
19
19
20
20
21 This file was forked by the Kallithea project in July 2014.
21 This file was forked by the Kallithea project in July 2014.
22 Original author and date, and relevant copyright and licensing information is below:
22 Original author and date, and relevant copyright and licensing information is below:
23 :created_on: Dec 4, 2011
23 :created_on: Dec 4, 2011
24 :author: marcink
24 :author: marcink
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
26 :license: GPLv3, see LICENSE.md for more details.
26 :license: GPLv3, see LICENSE.md for more details.
27 """
27 """
28 import re
28 import re
29 import difflib
29 import difflib
30 import logging
30 import logging
31
31
32 from itertools import tee, imap
32 from itertools import tee, imap
33
33
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35
35
36 from kallithea.lib.vcs.exceptions import VCSError
36 from kallithea.lib.vcs.exceptions import VCSError
37 from kallithea.lib.vcs.nodes import FileNode, SubModuleNode
37 from kallithea.lib.vcs.nodes import FileNode, SubModuleNode
38 from kallithea.lib.vcs.backends.base import EmptyChangeset
38 from kallithea.lib.vcs.backends.base import EmptyChangeset
39 from kallithea.lib.helpers import escape
39 from kallithea.lib.helpers import escape
40 from kallithea.lib.utils2 import safe_unicode
40 from kallithea.lib.utils2 import safe_unicode
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 def wrap_to_table(str_):
45 def wrap_to_table(str_):
46 return '''<table class="code-difftable">
46 return '''<table class="code-difftable">
47 <tr class="line no-comment">
47 <tr class="line no-comment">
48 <td class="lineno new"></td>
48 <td class="lineno new"></td>
49 <td class="code no-comment"><pre>%s</pre></td>
49 <td class="code no-comment"><pre>%s</pre></td>
50 </tr>
50 </tr>
51 </table>''' % str_
51 </table>''' % str_
52
52
53
53
54 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
54 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
55 ignore_whitespace=True, line_context=3,
55 ignore_whitespace=True, line_context=3,
56 enable_comments=False):
56 enable_comments=False):
57 """
57 """
58 returns a wrapped diff into a table, checks for cut_off_limit and presents
58 returns a wrapped diff into a table, checks for cut_off_limit and presents
59 proper message
59 proper message
60 """
60 """
61
61
62 if filenode_old is None:
62 if filenode_old is None:
63 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
63 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
64
64
65 if filenode_old.is_binary or filenode_new.is_binary:
65 if filenode_old.is_binary or filenode_new.is_binary:
66 diff = wrap_to_table(_('Binary file'))
66 diff = wrap_to_table(_('Binary file'))
67 stats = (0, 0)
67 stats = (0, 0)
68 size = 0
68 size = 0
69
69
70 elif cut_off_limit != -1 and (cut_off_limit is None or
70 elif cut_off_limit != -1 and (cut_off_limit is None or
71 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
71 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
72
72
73 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
73 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
74 ignore_whitespace=ignore_whitespace,
74 ignore_whitespace=ignore_whitespace,
75 context=line_context)
75 context=line_context)
76 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
76 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
77
77
78 diff = diff_processor.as_html(enable_comments=enable_comments)
78 diff = diff_processor.as_html(enable_comments=enable_comments)
79 stats = diff_processor.stat()
79 stats = diff_processor.stat()
80 size = len(diff or '')
80 size = len(diff or '')
81 else:
81 else:
82 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
82 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
83 'diff menu to display this diff'))
83 'diff menu to display this diff'))
84 stats = (0, 0)
84 stats = (0, 0)
85 size = 0
85 size = 0
86 if not diff:
86 if not diff:
87 submodules = filter(lambda o: isinstance(o, SubModuleNode),
87 submodules = filter(lambda o: isinstance(o, SubModuleNode),
88 [filenode_new, filenode_old])
88 [filenode_new, filenode_old])
89 if submodules:
89 if submodules:
90 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
90 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
91 else:
91 else:
92 diff = wrap_to_table(_('No changes detected'))
92 diff = wrap_to_table(_('No changes detected'))
93
93
94 cs1 = filenode_old.changeset.raw_id
94 cs1 = filenode_old.changeset.raw_id
95 cs2 = filenode_new.changeset.raw_id
95 cs2 = filenode_new.changeset.raw_id
96
96
97 return size, cs1, cs2, diff, stats
97 return size, cs1, cs2, diff, stats
98
98
99
99
100 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
100 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
101 """
101 """
102 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
102 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
103
103
104 :param ignore_whitespace: ignore whitespaces in diff
104 :param ignore_whitespace: ignore whitespaces in diff
105 """
105 """
106 # make sure we pass in default context
106 # make sure we pass in default context
107 context = context or 3
107 context = context or 3
108 submodules = filter(lambda o: isinstance(o, SubModuleNode),
108 submodules = filter(lambda o: isinstance(o, SubModuleNode),
109 [filenode_new, filenode_old])
109 [filenode_new, filenode_old])
110 if submodules:
110 if submodules:
111 return ''
111 return ''
112
112
113 for filenode in (filenode_old, filenode_new):
113 for filenode in (filenode_old, filenode_new):
114 if not isinstance(filenode, FileNode):
114 if not isinstance(filenode, FileNode):
115 raise VCSError("Given object should be FileNode object, not %s"
115 raise VCSError("Given object should be FileNode object, not %s"
116 % filenode.__class__)
116 % filenode.__class__)
117
117
118 repo = filenode_new.changeset.repository
118 repo = filenode_new.changeset.repository
119 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
119 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
120 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
120 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
121
121
122 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
122 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
123 ignore_whitespace, context)
123 ignore_whitespace, context)
124 return vcs_gitdiff
124 return vcs_gitdiff
125
125
126 NEW_FILENODE = 1
126 NEW_FILENODE = 1
127 DEL_FILENODE = 2
127 DEL_FILENODE = 2
128 MOD_FILENODE = 3
128 MOD_FILENODE = 3
129 RENAMED_FILENODE = 4
129 RENAMED_FILENODE = 4
130 COPIED_FILENODE = 5
130 COPIED_FILENODE = 5
131 CHMOD_FILENODE = 6
131 CHMOD_FILENODE = 6
132 BIN_FILENODE = 7
132 BIN_FILENODE = 7
133
133
134
134
135 class DiffLimitExceeded(Exception):
135 class DiffLimitExceeded(Exception):
136 pass
136 pass
137
137
138
138
139 class LimitedDiffContainer(object):
139 class LimitedDiffContainer(object):
140
140
141 def __init__(self, diff_limit, cur_diff_size, diff):
141 def __init__(self, diff_limit, cur_diff_size, diff):
142 self.diff = diff
142 self.diff = diff
143 self.diff_limit = diff_limit
143 self.diff_limit = diff_limit
144 self.cur_diff_size = cur_diff_size
144 self.cur_diff_size = cur_diff_size
145
145
146 def __iter__(self):
146 def __iter__(self):
147 for l in self.diff:
147 for l in self.diff:
148 yield l
148 yield l
149
149
150
150
151 class DiffProcessor(object):
151 class DiffProcessor(object):
152 """
152 """
153 Give it a unified or git diff and it returns a list of the files that were
153 Give it a unified or git diff and it returns a list of the files that were
154 mentioned in the diff together with a dict of meta information that
154 mentioned in the diff together with a dict of meta information that
155 can be used to render it in a HTML template.
155 can be used to render it in a HTML template.
156 """
156 """
157 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
157 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
158 _newline_marker = re.compile(r'^\\ No newline at end of file')
158 _newline_marker = re.compile(r'^\\ No newline at end of file')
159 _git_header_re = re.compile(r"""
159 _git_header_re = re.compile(r"""
160 # has already been split on this:
160 # has already been split on this:
161 # ^diff[ ]--git
161 # ^diff[ ]--git
162 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
162 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
163 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n
163 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n
164 ^rename[ ]from[ ](?P<rename_from>.+)\n
164 ^rename[ ]from[ ](?P<rename_from>.+)\n
165 ^rename[ ]to[ ](?P<rename_to>.+)(?:\n|$))?
165 ^rename[ ]to[ ](?P<rename_to>.+)(?:\n|$))?
166 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
166 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
167 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
167 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
168 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
168 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
169 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
169 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
170 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
170 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
171 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
171 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
172 (?:^(?P<bin_patch>GIT[ ]binary[ ]patch)(?:\n|$))?
172 (?:^(?P<bin_patch>GIT[ ]binary[ ]patch)(?:\n|$))?
173 (?:^---[ ](a/(?P<a_file>.+?)|/dev/null)\t?(?:\n|$))?
173 (?:^---[ ](a/(?P<a_file>.+?)|/dev/null)\t?(?:\n|$))?
174 (?:^\+\+\+[ ](b/(?P<b_file>.+?)|/dev/null)\t?(?:\n|$))?
174 (?:^\+\+\+[ ](b/(?P<b_file>.+?)|/dev/null)\t?(?:\n|$))?
175 """, re.VERBOSE | re.MULTILINE)
175 """, re.VERBOSE | re.MULTILINE)
176 _hg_header_re = re.compile(r"""
176 _hg_header_re = re.compile(r"""
177 # has already been split on this:
177 # has already been split on this:
178 # ^diff[ ]--git
178 # ^diff[ ]--git
179 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
179 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
180 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
180 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
181 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
181 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
182 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))?
182 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))?
183 (?:^rename[ ]from[ ](?P<rename_from>.+)\n
183 (?:^rename[ ]from[ ](?P<rename_from>.+)\n
184 ^rename[ ]to[ ](?P<rename_to>.+)(?:\n|$))?
184 ^rename[ ]to[ ](?P<rename_to>.+)(?:\n|$))?
185 (?:^copy[ ]from[ ](?P<copy_from>.+)\n
185 (?:^copy[ ]from[ ](?P<copy_from>.+)\n
186 ^copy[ ]to[ ](?P<copy_to>.+)(?:\n|$))?
186 ^copy[ ]to[ ](?P<copy_to>.+)(?:\n|$))?
187 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
187 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
188 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
188 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
189 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
189 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
190 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
190 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
191 (?:^(?P<bin_patch>GIT[ ]binary[ ]patch)(?:\n|$))?
191 (?:^(?P<bin_patch>GIT[ ]binary[ ]patch)(?:\n|$))?
192 (?:^---[ ](a/(?P<a_file>.+?)|/dev/null)\t?(?:\n|$))?
192 (?:^---[ ](a/(?P<a_file>.+?)|/dev/null)\t?(?:\n|$))?
193 (?:^\+\+\+[ ](b/(?P<b_file>.+?)|/dev/null)\t?(?:\n|$))?
193 (?:^\+\+\+[ ](b/(?P<b_file>.+?)|/dev/null)\t?(?:\n|$))?
194 """, re.VERBOSE | re.MULTILINE)
194 """, re.VERBOSE | re.MULTILINE)
195
195
196 #used for inline highlighter word split
196 #used for inline highlighter word split
197 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|<u>\t</u>| <i></i>|\W+?)')
197 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|<u>\t</u>| <i></i>|\W+?)')
198
198
199 _escape_re = re.compile(r'(&)|(<)|(>)|(\t)|(\r)|(?<=.)( \n| $)')
199 _escape_re = re.compile(r'(&)|(<)|(>)|(\t)|(\r)|(?<=.)( \n| $)')
200
200
201
201
202 def __init__(self, diff, vcs='hg', format='gitdiff', diff_limit=None):
202 def __init__(self, diff, vcs='hg', format='gitdiff', diff_limit=None):
203 """
203 """
204 :param diff: a text in diff format
204 :param diff: a text in diff format
205 :param vcs: type of version control hg or git
205 :param vcs: type of version control hg or git
206 :param format: format of diff passed, `udiff` or `gitdiff`
206 :param format: format of diff passed, `udiff` or `gitdiff`
207 :param diff_limit: define the size of diff that is considered "big"
207 :param diff_limit: define the size of diff that is considered "big"
208 based on that parameter cut off will be triggered, set to None
208 based on that parameter cut off will be triggered, set to None
209 to show full diff
209 to show full diff
210 """
210 """
211 if not isinstance(diff, basestring):
211 if not isinstance(diff, basestring):
212 raise Exception('Diff must be a basestring got %s instead' % type(diff))
212 raise Exception('Diff must be a basestring got %s instead' % type(diff))
213
213
214 self._diff = diff
214 self._diff = diff
215 self._format = format
215 self._format = format
216 self.adds = 0
216 self.adds = 0
217 self.removes = 0
217 self.removes = 0
218 # calculate diff size
218 # calculate diff size
219 self.diff_size = len(diff)
219 self.diff_size = len(diff)
220 self.diff_limit = diff_limit
220 self.diff_limit = diff_limit
221 self.cur_diff_size = 0
221 self.cur_diff_size = 0
222 self.parsed = False
222 self.parsed = False
223 self.parsed_diff = []
223 self.parsed_diff = []
224 self.vcs = vcs
224 self.vcs = vcs
225
225
226 if format == 'gitdiff':
226 if format == 'gitdiff':
227 self.differ = self._highlight_line_difflib
227 self.differ = self._highlight_line_difflib
228 self._parser = self._parse_gitdiff
228 self._parser = self._parse_gitdiff
229 else:
229 else:
230 self.differ = self._highlight_line_udiff
230 self.differ = self._highlight_line_udiff
231 self._parser = self._parse_udiff
231 self._parser = self._parse_udiff
232
232
233 def _copy_iterator(self):
233 def _copy_iterator(self):
234 """
234 """
235 make a fresh copy of generator, we should not iterate thru
235 make a fresh copy of generator, we should not iterate thru
236 an original as it's needed for repeating operations on
236 an original as it's needed for repeating operations on
237 this instance of DiffProcessor
237 this instance of DiffProcessor
238 """
238 """
239 self.__udiff, iterator_copy = tee(self.__udiff)
239 self.__udiff, iterator_copy = tee(self.__udiff)
240 return iterator_copy
240 return iterator_copy
241
241
242 def _escaper(self, string):
242 def _escaper(self, string):
243 """
243 """
244 Escaper for diff escapes special chars and checks the diff limit
244 Escaper for diff escapes special chars and checks the diff limit
245
245
246 :param string:
246 :param string:
247 """
247 """
248
248
249 self.cur_diff_size += len(string)
249 self.cur_diff_size += len(string)
250
250
251 # escaper gets iterated on each .next() call and it checks if each
251 # escaper gets iterated on each .next() call and it checks if each
252 # parsed line doesn't exceed the diff limit
252 # parsed line doesn't exceed the diff limit
253 if self.diff_limit is not None and self.cur_diff_size > self.diff_limit:
253 if self.diff_limit is not None and self.cur_diff_size > self.diff_limit:
254 raise DiffLimitExceeded('Diff Limit Exceeded')
254 raise DiffLimitExceeded('Diff Limit Exceeded')
255
255
256 def substitute(m):
256 def substitute(m):
257 groups = m.groups()
257 groups = m.groups()
258 if groups[0]:
258 if groups[0]:
259 return '&amp;'
259 return '&amp;'
260 if groups[1]:
260 if groups[1]:
261 return '&lt;'
261 return '&lt;'
262 if groups[2]:
262 if groups[2]:
263 return '&gt;'
263 return '&gt;'
264 if groups[3]:
264 if groups[3]:
265 return '<u>\t</u>'
265 return '<u>\t</u>'
266 if groups[4]:
266 if groups[4]:
267 return '<u class="cr"></u>'
267 return '<u class="cr"></u>'
268 if groups[5]:
268 if groups[5]:
269 return ' <i></i>'
269 return ' <i></i>'
270 assert False
270 assert False
271
271
272 return self._escape_re.sub(substitute, safe_unicode(string))
272 return self._escape_re.sub(substitute, safe_unicode(string))
273
273
274 def _line_counter(self, l):
274 def _line_counter(self, l):
275 """
275 """
276 Checks each line and bumps total adds/removes for this diff
276 Checks each line and bumps total adds/removes for this diff
277
277
278 :param l:
278 :param l:
279 """
279 """
280 if l.startswith('+') and not l.startswith('+++'):
280 if l.startswith('+') and not l.startswith('+++'):
281 self.adds += 1
281 self.adds += 1
282 elif l.startswith('-') and not l.startswith('---'):
282 elif l.startswith('-') and not l.startswith('---'):
283 self.removes += 1
283 self.removes += 1
284 return safe_unicode(l)
284 return safe_unicode(l)
285
285
286 def _highlight_line_difflib(self, line, next_):
286 def _highlight_line_difflib(self, line, next_):
287 """
287 """
288 Highlight inline changes in both lines.
288 Highlight inline changes in both lines.
289 """
289 """
290
290
291 if line['action'] == 'del':
291 if line['action'] == 'del':
292 old, new = line, next_
292 old, new = line, next_
293 else:
293 else:
294 old, new = next_, line
294 old, new = next_, line
295
295
296 oldwords = self._token_re.split(old['line'])
296 oldwords = self._token_re.split(old['line'])
297 newwords = self._token_re.split(new['line'])
297 newwords = self._token_re.split(new['line'])
298 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
298 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
299
299
300 oldfragments, newfragments = [], []
300 oldfragments, newfragments = [], []
301 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
301 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
302 oldfrag = ''.join(oldwords[i1:i2])
302 oldfrag = ''.join(oldwords[i1:i2])
303 newfrag = ''.join(newwords[j1:j2])
303 newfrag = ''.join(newwords[j1:j2])
304 if tag != 'equal':
304 if tag != 'equal':
305 if oldfrag:
305 if oldfrag:
306 oldfrag = '<del>%s</del>' % oldfrag
306 oldfrag = '<del>%s</del>' % oldfrag
307 if newfrag:
307 if newfrag:
308 newfrag = '<ins>%s</ins>' % newfrag
308 newfrag = '<ins>%s</ins>' % newfrag
309 oldfragments.append(oldfrag)
309 oldfragments.append(oldfrag)
310 newfragments.append(newfrag)
310 newfragments.append(newfrag)
311
311
312 old['line'] = "".join(oldfragments)
312 old['line'] = "".join(oldfragments)
313 new['line'] = "".join(newfragments)
313 new['line'] = "".join(newfragments)
314
314
315 def _highlight_line_udiff(self, line, next_):
315 def _highlight_line_udiff(self, line, next_):
316 """
316 """
317 Highlight inline changes in both lines.
317 Highlight inline changes in both lines.
318 """
318 """
319 start = 0
319 start = 0
320 limit = min(len(line['line']), len(next_['line']))
320 limit = min(len(line['line']), len(next_['line']))
321 while start < limit and line['line'][start] == next_['line'][start]:
321 while start < limit and line['line'][start] == next_['line'][start]:
322 start += 1
322 start += 1
323 end = -1
323 end = -1
324 limit -= start
324 limit -= start
325 while -end <= limit and line['line'][end] == next_['line'][end]:
325 while -end <= limit and line['line'][end] == next_['line'][end]:
326 end -= 1
326 end -= 1
327 end += 1
327 end += 1
328 if start or end:
328 if start or end:
329 def do(l):
329 def do(l):
330 last = end + len(l['line'])
330 last = end + len(l['line'])
331 if l['action'] == 'add':
331 if l['action'] == 'add':
332 tag = 'ins'
332 tag = 'ins'
333 else:
333 else:
334 tag = 'del'
334 tag = 'del'
335 l['line'] = '%s<%s>%s</%s>%s' % (
335 l['line'] = '%s<%s>%s</%s>%s' % (
336 l['line'][:start],
336 l['line'][:start],
337 tag,
337 tag,
338 l['line'][start:last],
338 l['line'][start:last],
339 tag,
339 tag,
340 l['line'][last:]
340 l['line'][last:]
341 )
341 )
342 do(line)
342 do(line)
343 do(next_)
343 do(next_)
344
344
345 def _get_header(self, diff_chunk):
345 def _get_header(self, diff_chunk):
346 """
346 """
347 parses the diff header, and returns parts, and leftover diff
347 parses the diff header, and returns parts, and leftover diff
348 parts consists of 14 elements::
348 parts consists of 14 elements::
349
349
350 a_path, b_path, similarity_index, rename_from, rename_to,
350 a_path, b_path, similarity_index, rename_from, rename_to,
351 old_mode, new_mode, new_file_mode, deleted_file_mode,
351 old_mode, new_mode, new_file_mode, deleted_file_mode,
352 a_blob_id, b_blob_id, b_mode, a_file, b_file
352 a_blob_id, b_blob_id, b_mode, a_file, b_file
353
353
354 :param diff_chunk:
354 :param diff_chunk:
355 """
355 """
356
356
357 match = None
357 match = None
358 if self.vcs == 'git':
358 if self.vcs == 'git':
359 match = self._git_header_re.match(diff_chunk)
359 match = self._git_header_re.match(diff_chunk)
360 elif self.vcs == 'hg':
360 elif self.vcs == 'hg':
361 match = self._hg_header_re.match(diff_chunk)
361 match = self._hg_header_re.match(diff_chunk)
362 if match is None:
362 if match is None:
363 raise Exception('diff not recognized as valid %s diff' % self.vcs)
363 raise Exception('diff not recognized as valid %s diff' % self.vcs)
364 groups = match.groupdict()
364 groups = match.groupdict()
365 rest = diff_chunk[match.end():]
365 rest = diff_chunk[match.end():]
366 if rest and not rest.startswith('@') and not rest.startswith('literal ') and not rest.startswith('delta '):
366 if rest and not rest.startswith('@') and not rest.startswith('literal ') and not rest.startswith('delta '):
367 raise Exception('cannot parse diff header: %r followed by %r' % (diff_chunk[:match.end()], rest[:1000]))
367 raise Exception('cannot parse diff header: %r followed by %r' % (diff_chunk[:match.end()], rest[:1000]))
368 difflines = imap(self._escaper, re.findall(r'.*\n|.+$', rest)) # don't split on \r as str.splitlines do
368 difflines = imap(self._escaper, re.findall(r'.*\n|.+$', rest)) # don't split on \r as str.splitlines do
369 return groups, difflines
369 return groups, difflines
370
370
371 def _clean_line(self, line, command):
371 def _clean_line(self, line, command):
372 if command in ['+', '-', ' ']:
372 if command in ['+', '-', ' ']:
373 #only modify the line if it's actually a diff thing
373 #only modify the line if it's actually a diff thing
374 line = line[1:]
374 line = line[1:]
375 return line
375 return line
376
376
377 def _parse_gitdiff(self, inline_diff=True):
377 def _parse_gitdiff(self, inline_diff=True):
378 _files = []
378 _files = []
379 diff_container = lambda arg: arg
379 diff_container = lambda arg: arg
380
380
381 ##split the diff in chunks of separate --git a/file b/file chunks
381 ##split the diff in chunks of separate --git a/file b/file chunks
382 for raw_diff in ('\n' + self._diff).split('\ndiff --git')[1:]:
382 for raw_diff in ('\n' + self._diff).split('\ndiff --git')[1:]:
383 head, diff = self._get_header(raw_diff)
383 head, diff = self._get_header(raw_diff)
384
384
385 op = None
385 op = None
386 stats = {
386 stats = {
387 'added': 0,
387 'added': 0,
388 'deleted': 0,
388 'deleted': 0,
389 'binary': False,
389 'binary': False,
390 'ops': {},
390 'ops': {},
391 }
391 }
392
392
393 if head['deleted_file_mode']:
393 if head['deleted_file_mode']:
394 op = 'D'
394 op = 'D'
395 stats['binary'] = True
395 stats['binary'] = True
396 stats['ops'][DEL_FILENODE] = 'deleted file'
396 stats['ops'][DEL_FILENODE] = 'deleted file'
397
397
398 elif head['new_file_mode']:
398 elif head['new_file_mode']:
399 op = 'A'
399 op = 'A'
400 stats['binary'] = True
400 stats['binary'] = True
401 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
401 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
402 else: # modify operation, can be cp, rename, chmod
402 else: # modify operation, can be cp, rename, chmod
403 # CHMOD
403 # CHMOD
404 if head['new_mode'] and head['old_mode']:
404 if head['new_mode'] and head['old_mode']:
405 op = 'M'
405 op = 'M'
406 stats['binary'] = True
406 stats['binary'] = True
407 stats['ops'][CHMOD_FILENODE] = ('modified file chmod %s => %s'
407 stats['ops'][CHMOD_FILENODE] = ('modified file chmod %s => %s'
408 % (head['old_mode'], head['new_mode']))
408 % (head['old_mode'], head['new_mode']))
409 # RENAME
409 # RENAME
410 if (head['rename_from'] and head['rename_to']
410 if (head['rename_from'] and head['rename_to']
411 and head['rename_from'] != head['rename_to']):
411 and head['rename_from'] != head['rename_to']):
412 op = 'R'
412 op = 'R'
413 stats['binary'] = True
413 stats['binary'] = True
414 stats['ops'][RENAMED_FILENODE] = ('file renamed from %s to %s'
414 stats['ops'][RENAMED_FILENODE] = ('file renamed from %s to %s'
415 % (head['rename_from'], head['rename_to']))
415 % (head['rename_from'], head['rename_to']))
416 # COPY
416 # COPY
417 if head.get('copy_from') and head.get('copy_to'):
417 if head.get('copy_from') and head.get('copy_to'):
418 op = 'M'
418 op = 'M'
419 stats['binary'] = True
419 stats['binary'] = True
420 stats['ops'][COPIED_FILENODE] = ('file copied from %s to %s'
420 stats['ops'][COPIED_FILENODE] = ('file copied from %s to %s'
421 % (head['copy_from'], head['copy_to']))
421 % (head['copy_from'], head['copy_to']))
422 # FALL BACK: detect missed old style add or remove
422 # FALL BACK: detect missed old style add or remove
423 if op is None:
423 if op is None:
424 if not head['a_file'] and head['b_file']:
424 if not head['a_file'] and head['b_file']:
425 op = 'A'
425 op = 'A'
426 stats['binary'] = True
426 stats['binary'] = True
427 stats['ops'][NEW_FILENODE] = 'new file'
427 stats['ops'][NEW_FILENODE] = 'new file'
428
428
429 elif head['a_file'] and not head['b_file']:
429 elif head['a_file'] and not head['b_file']:
430 op = 'D'
430 op = 'D'
431 stats['binary'] = True
431 stats['binary'] = True
432 stats['ops'][DEL_FILENODE] = 'deleted file'
432 stats['ops'][DEL_FILENODE] = 'deleted file'
433
433
434 # it's not ADD not DELETE
434 # it's not ADD not DELETE
435 if op is None:
435 if op is None:
436 op = 'M'
436 op = 'M'
437 stats['binary'] = True
437 stats['binary'] = True
438 stats['ops'][MOD_FILENODE] = 'modified file'
438 stats['ops'][MOD_FILENODE] = 'modified file'
439
439
440 # a real non-binary diff
440 # a real non-binary diff
441 if head['a_file'] or head['b_file']:
441 if head['a_file'] or head['b_file']:
442 try:
442 try:
443 chunks, _stats = self._parse_lines(diff)
443 chunks, _stats = self._parse_lines(diff)
444 stats['binary'] = False
444 stats['binary'] = False
445 stats['added'] = _stats[0]
445 stats['added'] = _stats[0]
446 stats['deleted'] = _stats[1]
446 stats['deleted'] = _stats[1]
447 # explicit mark that it's a modified file
447 # explicit mark that it's a modified file
448 if op == 'M':
448 if op == 'M':
449 stats['ops'][MOD_FILENODE] = 'modified file'
449 stats['ops'][MOD_FILENODE] = 'modified file'
450
450
451 except DiffLimitExceeded:
451 except DiffLimitExceeded:
452 diff_container = lambda _diff: \
452 diff_container = lambda _diff: \
453 LimitedDiffContainer(self.diff_limit,
453 LimitedDiffContainer(self.diff_limit,
454 self.cur_diff_size, _diff)
454 self.cur_diff_size, _diff)
455 break
455 break
456 else: # GIT binary patch (or empty diff)
456 else: # Git binary patch (or empty diff)
457 # GIT Binary patch
457 # Git binary patch
458 if head['bin_patch']:
458 if head['bin_patch']:
459 stats['ops'][BIN_FILENODE] = 'binary diff not shown'
459 stats['ops'][BIN_FILENODE] = 'binary diff not shown'
460 chunks = []
460 chunks = []
461
461
462 if op == 'D' and chunks:
462 if op == 'D' and chunks:
463 # a way of seeing deleted content could perhaps be nice - but
463 # a way of seeing deleted content could perhaps be nice - but
464 # not with the current UI
464 # not with the current UI
465 chunks = []
465 chunks = []
466
466
467 chunks.insert(0, [{
467 chunks.insert(0, [{
468 'old_lineno': '',
468 'old_lineno': '',
469 'new_lineno': '',
469 'new_lineno': '',
470 'action': 'context',
470 'action': 'context',
471 'line': msg,
471 'line': msg,
472 } for _op, msg in stats['ops'].iteritems()
472 } for _op, msg in stats['ops'].iteritems()
473 if _op not in [MOD_FILENODE]])
473 if _op not in [MOD_FILENODE]])
474
474
475 _files.append({
475 _files.append({
476 'filename': head['b_path'],
476 'filename': head['b_path'],
477 'old_revision': head['a_blob_id'],
477 'old_revision': head['a_blob_id'],
478 'new_revision': head['b_blob_id'],
478 'new_revision': head['b_blob_id'],
479 'chunks': chunks,
479 'chunks': chunks,
480 'operation': op,
480 'operation': op,
481 'stats': stats,
481 'stats': stats,
482 })
482 })
483
483
484 if not inline_diff:
484 if not inline_diff:
485 return diff_container(_files)
485 return diff_container(_files)
486
486
487 # highlight inline changes
487 # highlight inline changes
488 for diff_data in _files:
488 for diff_data in _files:
489 for chunk in diff_data['chunks']:
489 for chunk in diff_data['chunks']:
490 lineiter = iter(chunk)
490 lineiter = iter(chunk)
491 try:
491 try:
492 while 1:
492 while 1:
493 line = lineiter.next()
493 line = lineiter.next()
494 if line['action'] not in ['unmod', 'context']:
494 if line['action'] not in ['unmod', 'context']:
495 nextline = lineiter.next()
495 nextline = lineiter.next()
496 if nextline['action'] in ['unmod', 'context'] or \
496 if nextline['action'] in ['unmod', 'context'] or \
497 nextline['action'] == line['action']:
497 nextline['action'] == line['action']:
498 continue
498 continue
499 self.differ(line, nextline)
499 self.differ(line, nextline)
500 except StopIteration:
500 except StopIteration:
501 pass
501 pass
502
502
503 return diff_container(_files)
503 return diff_container(_files)
504
504
505 def _parse_udiff(self, inline_diff=True):
505 def _parse_udiff(self, inline_diff=True):
506 raise NotImplementedError()
506 raise NotImplementedError()
507
507
508 def _parse_lines(self, diff):
508 def _parse_lines(self, diff):
509 """
509 """
510 Parse the diff and return data for the template.
510 Parse the diff and return data for the template.
511 """
511 """
512
512
513 stats = [0, 0]
513 stats = [0, 0]
514 (old_line, old_end, new_line, new_end) = (None, None, None, None)
514 (old_line, old_end, new_line, new_end) = (None, None, None, None)
515
515
516 try:
516 try:
517 chunks = []
517 chunks = []
518 line = diff.next()
518 line = diff.next()
519
519
520 while True:
520 while True:
521 lines = []
521 lines = []
522 chunks.append(lines)
522 chunks.append(lines)
523
523
524 match = self._chunk_re.match(line)
524 match = self._chunk_re.match(line)
525
525
526 if not match:
526 if not match:
527 raise Exception('error parsing diff @@ line %r' % line)
527 raise Exception('error parsing diff @@ line %r' % line)
528
528
529 gr = match.groups()
529 gr = match.groups()
530 (old_line, old_end,
530 (old_line, old_end,
531 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
531 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
532 old_line -= 1
532 old_line -= 1
533 new_line -= 1
533 new_line -= 1
534
534
535 context = len(gr) == 5
535 context = len(gr) == 5
536 old_end += old_line
536 old_end += old_line
537 new_end += new_line
537 new_end += new_line
538
538
539 if context:
539 if context:
540 # skip context only if it's first line
540 # skip context only if it's first line
541 if int(gr[0]) > 1:
541 if int(gr[0]) > 1:
542 lines.append({
542 lines.append({
543 'old_lineno': '...',
543 'old_lineno': '...',
544 'new_lineno': '...',
544 'new_lineno': '...',
545 'action': 'context',
545 'action': 'context',
546 'line': line,
546 'line': line,
547 })
547 })
548
548
549 line = diff.next()
549 line = diff.next()
550
550
551 while old_line < old_end or new_line < new_end:
551 while old_line < old_end or new_line < new_end:
552 if not line:
552 if not line:
553 raise Exception('error parsing diff - empty line at -%s+%s' % (old_line, new_line))
553 raise Exception('error parsing diff - empty line at -%s+%s' % (old_line, new_line))
554
554
555 affects_old = affects_new = False
555 affects_old = affects_new = False
556
556
557 command = line[0]
557 command = line[0]
558 if command == '+':
558 if command == '+':
559 affects_new = True
559 affects_new = True
560 action = 'add'
560 action = 'add'
561 stats[0] += 1
561 stats[0] += 1
562 elif command == '-':
562 elif command == '-':
563 affects_old = True
563 affects_old = True
564 action = 'del'
564 action = 'del'
565 stats[1] += 1
565 stats[1] += 1
566 elif command == ' ':
566 elif command == ' ':
567 affects_old = affects_new = True
567 affects_old = affects_new = True
568 action = 'unmod'
568 action = 'unmod'
569 else:
569 else:
570 raise Exception('error parsing diff - unknown command in line %r at -%s+%s' % (line, old_line, new_line))
570 raise Exception('error parsing diff - unknown command in line %r at -%s+%s' % (line, old_line, new_line))
571
571
572 if not self._newline_marker.match(line):
572 if not self._newline_marker.match(line):
573 old_line += affects_old
573 old_line += affects_old
574 new_line += affects_new
574 new_line += affects_new
575 lines.append({
575 lines.append({
576 'old_lineno': affects_old and old_line or '',
576 'old_lineno': affects_old and old_line or '',
577 'new_lineno': affects_new and new_line or '',
577 'new_lineno': affects_new and new_line or '',
578 'action': action,
578 'action': action,
579 'line': self._clean_line(line, command)
579 'line': self._clean_line(line, command)
580 })
580 })
581
581
582 line = diff.next()
582 line = diff.next()
583
583
584 if self._newline_marker.match(line):
584 if self._newline_marker.match(line):
585 # we need to append to lines, since this is not
585 # we need to append to lines, since this is not
586 # counted in the line specs of diff
586 # counted in the line specs of diff
587 lines.append({
587 lines.append({
588 'old_lineno': '...',
588 'old_lineno': '...',
589 'new_lineno': '...',
589 'new_lineno': '...',
590 'action': 'context',
590 'action': 'context',
591 'line': self._clean_line(line, command)
591 'line': self._clean_line(line, command)
592 })
592 })
593 line = diff.next()
593 line = diff.next()
594 if old_line > old_end:
594 if old_line > old_end:
595 raise Exception('error parsing diff - more than %s "-" lines at -%s+%s' % (old_end, old_line, new_line))
595 raise Exception('error parsing diff - more than %s "-" lines at -%s+%s' % (old_end, old_line, new_line))
596 if new_line > new_end:
596 if new_line > new_end:
597 raise Exception('error parsing diff - more than %s "+" lines at -%s+%s' % (new_end, old_line, new_line))
597 raise Exception('error parsing diff - more than %s "+" lines at -%s+%s' % (new_end, old_line, new_line))
598 except StopIteration:
598 except StopIteration:
599 pass
599 pass
600 if old_line != old_end or new_line != new_end:
600 if old_line != old_end or new_line != new_end:
601 raise Exception('diff processing broken when old %s<>%s or new %s<>%s line %r' % (old_line, old_end, new_line, new_end, line))
601 raise Exception('diff processing broken when old %s<>%s or new %s<>%s line %r' % (old_line, old_end, new_line, new_end, line))
602
602
603 return chunks, stats
603 return chunks, stats
604
604
605 def _safe_id(self, idstring):
605 def _safe_id(self, idstring):
606 """Make a string safe for including in an id attribute.
606 """Make a string safe for including in an id attribute.
607
607
608 The HTML spec says that id attributes 'must begin with
608 The HTML spec says that id attributes 'must begin with
609 a letter ([A-Za-z]) and may be followed by any number
609 a letter ([A-Za-z]) and may be followed by any number
610 of letters, digits ([0-9]), hyphens ("-"), underscores
610 of letters, digits ([0-9]), hyphens ("-"), underscores
611 ("_"), colons (":"), and periods (".")'. These regexps
611 ("_"), colons (":"), and periods (".")'. These regexps
612 are slightly over-zealous, in that they remove colons
612 are slightly over-zealous, in that they remove colons
613 and periods unnecessarily.
613 and periods unnecessarily.
614
614
615 Whitespace is transformed into underscores, and then
615 Whitespace is transformed into underscores, and then
616 anything which is not a hyphen or a character that
616 anything which is not a hyphen or a character that
617 matches \w (alphanumerics and underscore) is removed.
617 matches \w (alphanumerics and underscore) is removed.
618
618
619 """
619 """
620 # Transform all whitespace to underscore
620 # Transform all whitespace to underscore
621 idstring = re.sub(r'\s', "_", '%s' % idstring)
621 idstring = re.sub(r'\s', "_", '%s' % idstring)
622 # Remove everything that is not a hyphen or a member of \w
622 # Remove everything that is not a hyphen or a member of \w
623 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
623 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
624 return idstring
624 return idstring
625
625
626 def prepare(self, inline_diff=True):
626 def prepare(self, inline_diff=True):
627 """
627 """
628 Prepare the passed udiff for HTML rendering. It'l return a list
628 Prepare the passed udiff for HTML rendering. It'l return a list
629 of dicts with diff information
629 of dicts with diff information
630 """
630 """
631 parsed = self._parser(inline_diff=inline_diff)
631 parsed = self._parser(inline_diff=inline_diff)
632 self.parsed = True
632 self.parsed = True
633 self.parsed_diff = parsed
633 self.parsed_diff = parsed
634 return parsed
634 return parsed
635
635
636 def as_raw(self, diff_lines=None):
636 def as_raw(self, diff_lines=None):
637 """
637 """
638 Returns raw string diff
638 Returns raw string diff
639 """
639 """
640 return self._diff
640 return self._diff
641 #return u''.join(imap(self._line_counter, self._diff.splitlines(1)))
641 #return u''.join(imap(self._line_counter, self._diff.splitlines(1)))
642
642
643 def as_html(self, table_class='code-difftable', line_class='line',
643 def as_html(self, table_class='code-difftable', line_class='line',
644 old_lineno_class='lineno old', new_lineno_class='lineno new',
644 old_lineno_class='lineno old', new_lineno_class='lineno new',
645 code_class='code', enable_comments=False, parsed_lines=None):
645 code_class='code', enable_comments=False, parsed_lines=None):
646 """
646 """
647 Return given diff as html table with customized css classes
647 Return given diff as html table with customized css classes
648 """
648 """
649 def _link_to_if(condition, label, url):
649 def _link_to_if(condition, label, url):
650 """
650 """
651 Generates a link if condition is meet or just the label if not.
651 Generates a link if condition is meet or just the label if not.
652 """
652 """
653
653
654 if condition:
654 if condition:
655 return '''<a href="%(url)s">%(label)s</a>''' % {
655 return '''<a href="%(url)s">%(label)s</a>''' % {
656 'url': url,
656 'url': url,
657 'label': label
657 'label': label
658 }
658 }
659 else:
659 else:
660 return label
660 return label
661 if not self.parsed:
661 if not self.parsed:
662 self.prepare()
662 self.prepare()
663
663
664 diff_lines = self.parsed_diff
664 diff_lines = self.parsed_diff
665 if parsed_lines:
665 if parsed_lines:
666 diff_lines = parsed_lines
666 diff_lines = parsed_lines
667
667
668 _html_empty = True
668 _html_empty = True
669 _html = []
669 _html = []
670 _html.append('''<table class="%(table_class)s">\n''' % {
670 _html.append('''<table class="%(table_class)s">\n''' % {
671 'table_class': table_class
671 'table_class': table_class
672 })
672 })
673
673
674 for diff in diff_lines:
674 for diff in diff_lines:
675 for line in diff['chunks']:
675 for line in diff['chunks']:
676 _html_empty = False
676 _html_empty = False
677 for change in line:
677 for change in line:
678 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
678 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
679 'lc': line_class,
679 'lc': line_class,
680 'action': change['action']
680 'action': change['action']
681 })
681 })
682 anchor_old_id = ''
682 anchor_old_id = ''
683 anchor_new_id = ''
683 anchor_new_id = ''
684 anchor_old = "%(filename)s_o%(oldline_no)s" % {
684 anchor_old = "%(filename)s_o%(oldline_no)s" % {
685 'filename': self._safe_id(diff['filename']),
685 'filename': self._safe_id(diff['filename']),
686 'oldline_no': change['old_lineno']
686 'oldline_no': change['old_lineno']
687 }
687 }
688 anchor_new = "%(filename)s_n%(oldline_no)s" % {
688 anchor_new = "%(filename)s_n%(oldline_no)s" % {
689 'filename': self._safe_id(diff['filename']),
689 'filename': self._safe_id(diff['filename']),
690 'oldline_no': change['new_lineno']
690 'oldline_no': change['new_lineno']
691 }
691 }
692 cond_old = (change['old_lineno'] != '...' and
692 cond_old = (change['old_lineno'] != '...' and
693 change['old_lineno'])
693 change['old_lineno'])
694 cond_new = (change['new_lineno'] != '...' and
694 cond_new = (change['new_lineno'] != '...' and
695 change['new_lineno'])
695 change['new_lineno'])
696 if cond_old:
696 if cond_old:
697 anchor_old_id = 'id="%s"' % anchor_old
697 anchor_old_id = 'id="%s"' % anchor_old
698 if cond_new:
698 if cond_new:
699 anchor_new_id = 'id="%s"' % anchor_new
699 anchor_new_id = 'id="%s"' % anchor_new
700 ###########################################################
700 ###########################################################
701 # OLD LINE NUMBER
701 # OLD LINE NUMBER
702 ###########################################################
702 ###########################################################
703 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
703 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
704 'a_id': anchor_old_id,
704 'a_id': anchor_old_id,
705 'olc': old_lineno_class
705 'olc': old_lineno_class
706 })
706 })
707
707
708 _html.append('''%(link)s''' % {
708 _html.append('''%(link)s''' % {
709 'link': _link_to_if(True, change['old_lineno'],
709 'link': _link_to_if(True, change['old_lineno'],
710 '#%s' % anchor_old)
710 '#%s' % anchor_old)
711 })
711 })
712 _html.append('''</td>\n''')
712 _html.append('''</td>\n''')
713 ###########################################################
713 ###########################################################
714 # NEW LINE NUMBER
714 # NEW LINE NUMBER
715 ###########################################################
715 ###########################################################
716
716
717 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
717 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
718 'a_id': anchor_new_id,
718 'a_id': anchor_new_id,
719 'nlc': new_lineno_class
719 'nlc': new_lineno_class
720 })
720 })
721
721
722 _html.append('''%(link)s''' % {
722 _html.append('''%(link)s''' % {
723 'link': _link_to_if(True, change['new_lineno'],
723 'link': _link_to_if(True, change['new_lineno'],
724 '#%s' % anchor_new)
724 '#%s' % anchor_new)
725 })
725 })
726 _html.append('''</td>\n''')
726 _html.append('''</td>\n''')
727 ###########################################################
727 ###########################################################
728 # CODE
728 # CODE
729 ###########################################################
729 ###########################################################
730 comments = '' if enable_comments else 'no-comment'
730 comments = '' if enable_comments else 'no-comment'
731 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
731 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
732 'cc': code_class,
732 'cc': code_class,
733 'inc': comments
733 'inc': comments
734 })
734 })
735 _html.append('''\n\t\t<div class="add-bubble"><div>&nbsp;</div></div><pre>%(code)s</pre>\n''' % {
735 _html.append('''\n\t\t<div class="add-bubble"><div>&nbsp;</div></div><pre>%(code)s</pre>\n''' % {
736 'code': change['line']
736 'code': change['line']
737 })
737 })
738
738
739 _html.append('''\t</td>''')
739 _html.append('''\t</td>''')
740 _html.append('''\n</tr>\n''')
740 _html.append('''\n</tr>\n''')
741 _html.append('''</table>''')
741 _html.append('''</table>''')
742 if _html_empty:
742 if _html_empty:
743 return None
743 return None
744 return ''.join(_html)
744 return ''.join(_html)
745
745
746 def stat(self):
746 def stat(self):
747 """
747 """
748 Returns tuple of added, and removed lines for this instance
748 Returns tuple of added, and removed lines for this instance
749 """
749 """
750 return self.adds, self.removes
750 return self.adds, self.removes
@@ -1,184 +1,184 b''
1 #!/usr/bin/env python2
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
2 # -*- coding: utf-8 -*-
3 import os
3 import os
4 import sys
4 import sys
5 import platform
5 import platform
6
6
7 if sys.version_info < (2, 6):
7 if sys.version_info < (2, 6):
8 raise Exception('Kallithea requires python 2.6 or 2.7')
8 raise Exception('Kallithea requires python 2.6 or 2.7')
9
9
10
10
11 here = os.path.abspath(os.path.dirname(__file__))
11 here = os.path.abspath(os.path.dirname(__file__))
12
12
13
13
14 def _get_meta_var(name, data, callback_handler=None):
14 def _get_meta_var(name, data, callback_handler=None):
15 import re
15 import re
16 matches = re.compile(r'(?:%s)\s*=\s*(.*)' % name).search(data)
16 matches = re.compile(r'(?:%s)\s*=\s*(.*)' % name).search(data)
17 if matches:
17 if matches:
18 if not callable(callback_handler):
18 if not callable(callback_handler):
19 callback_handler = lambda v: v
19 callback_handler = lambda v: v
20
20
21 return callback_handler(eval(matches.groups()[0]))
21 return callback_handler(eval(matches.groups()[0]))
22
22
23 _meta = open(os.path.join(here, 'kallithea', '__init__.py'), 'rb')
23 _meta = open(os.path.join(here, 'kallithea', '__init__.py'), 'rb')
24 _metadata = _meta.read()
24 _metadata = _meta.read()
25 _meta.close()
25 _meta.close()
26
26
27 callback = lambda V: ('.'.join(map(str, V[:3])) + '.'.join(V[3:]))
27 callback = lambda V: ('.'.join(map(str, V[:3])) + '.'.join(V[3:]))
28 __version__ = _get_meta_var('VERSION', _metadata, callback)
28 __version__ = _get_meta_var('VERSION', _metadata, callback)
29 __license__ = _get_meta_var('__license__', _metadata)
29 __license__ = _get_meta_var('__license__', _metadata)
30 __author__ = _get_meta_var('__author__', _metadata)
30 __author__ = _get_meta_var('__author__', _metadata)
31 __url__ = _get_meta_var('__url__', _metadata)
31 __url__ = _get_meta_var('__url__', _metadata)
32 # defines current platform
32 # defines current platform
33 __platform__ = platform.system()
33 __platform__ = platform.system()
34
34
35 is_windows = __platform__ in ['Windows']
35 is_windows = __platform__ in ['Windows']
36
36
37 requirements = [
37 requirements = [
38 "waitress==0.8.8",
38 "waitress==0.8.8",
39 "webob==1.0.8",
39 "webob==1.0.8",
40 "webtest==1.4.3",
40 "webtest==1.4.3",
41 "Pylons==1.0.0",
41 "Pylons==1.0.0",
42 "Beaker==1.6.4",
42 "Beaker==1.6.4",
43 "WebHelpers==1.3",
43 "WebHelpers==1.3",
44 "formencode>=1.2.4,<=1.2.6",
44 "formencode>=1.2.4,<=1.2.6",
45 "SQLAlchemy==0.7.10",
45 "SQLAlchemy==0.7.10",
46 "Mako>=0.9.0,<=1.0.0",
46 "Mako>=0.9.0,<=1.0.0",
47 "pygments>=1.5",
47 "pygments>=1.5",
48 "whoosh>=2.4.0,<=2.5.7",
48 "whoosh>=2.4.0,<=2.5.7",
49 "celery>=2.2.5,<2.3",
49 "celery>=2.2.5,<2.3",
50 "babel>=0.9.6,<=1.3",
50 "babel>=0.9.6,<=1.3",
51 "python-dateutil>=1.5.0,<2.0.0",
51 "python-dateutil>=1.5.0,<2.0.0",
52 "markdown==2.2.1",
52 "markdown==2.2.1",
53 "docutils>=0.8.1,<=0.11",
53 "docutils>=0.8.1,<=0.11",
54 "simplejson==2.5.2",
54 "simplejson==2.5.2",
55 "mock",
55 "mock",
56 "pycrypto>=2.6.0,<=2.6.1",
56 "pycrypto>=2.6.0,<=2.6.1",
57 "URLObject==2.3.4",
57 "URLObject==2.3.4",
58 "Routes==1.13",
58 "Routes==1.13",
59 ]
59 ]
60
60
61 if sys.version_info < (2, 7):
61 if sys.version_info < (2, 7):
62 requirements.append("importlib==1.0.1")
62 requirements.append("importlib==1.0.1")
63 requirements.append("unittest2")
63 requirements.append("unittest2")
64 requirements.append("argparse")
64 requirements.append("argparse")
65
65
66 requirements.append("mercurial>=2.8.2,<3.4")
66 requirements.append("mercurial>=2.8.2,<3.4")
67 if not is_windows:
67 if not is_windows:
68 requirements.append("py-bcrypt>=0.3.0,<=0.4")
68 requirements.append("py-bcrypt>=0.3.0,<=0.4")
69
69
70 if sys.version_info < (2, 7):
70 if sys.version_info < (2, 7):
71 # Dulwich 0.9.6 and later do not support Python2.6.
71 # Dulwich 0.9.6 and later do not support Python2.6.
72 requirements.append("dulwich>=0.9.3,<=0.9.5")
72 requirements.append("dulwich>=0.9.3,<=0.9.5")
73 else:
73 else:
74 requirements.append("dulwich>=0.9.3,<=0.9.7")
74 requirements.append("dulwich>=0.9.3,<=0.9.7")
75
75
76 dependency_links = [
76 dependency_links = [
77 ]
77 ]
78
78
79 classifiers = [
79 classifiers = [
80 'Development Status :: 4 - Beta',
80 'Development Status :: 4 - Beta',
81 'Environment :: Web Environment',
81 'Environment :: Web Environment',
82 'Framework :: Pylons',
82 'Framework :: Pylons',
83 'Intended Audience :: Developers',
83 'Intended Audience :: Developers',
84 'License :: OSI Approved :: GNU General Public License (GPL)',
84 'License :: OSI Approved :: GNU General Public License (GPL)',
85 'Operating System :: OS Independent',
85 'Operating System :: OS Independent',
86 'Programming Language :: Python',
86 'Programming Language :: Python',
87 'Programming Language :: Python :: 2.6',
87 'Programming Language :: Python :: 2.6',
88 'Programming Language :: Python :: 2.7',
88 'Programming Language :: Python :: 2.7',
89 'Topic :: Software Development :: Version Control',
89 'Topic :: Software Development :: Version Control',
90 ]
90 ]
91
91
92
92
93 # additional files from project that goes somewhere in the filesystem
93 # additional files from project that goes somewhere in the filesystem
94 # relative to sys.prefix
94 # relative to sys.prefix
95 data_files = []
95 data_files = []
96
96
97 # additional files that goes into package itself
97 # additional files that goes into package itself
98 package_data = {'kallithea': ['i18n/*/LC_MESSAGES/*.mo', ], }
98 package_data = {'kallithea': ['i18n/*/LC_MESSAGES/*.mo', ], }
99
99
100 description = ('Kallithea is a fast and powerful management tool '
100 description = ('Kallithea is a fast and powerful management tool '
101 'for Mercurial and GIT with a built in push/pull server, '
101 'for Mercurial and Git with a built in push/pull server, '
102 'full text search and code-review.')
102 'full text search and code-review.')
103
103
104 keywords = ' '.join([
104 keywords = ' '.join([
105 'kallithea', 'mercurial', 'git', 'code review',
105 'kallithea', 'mercurial', 'git', 'code review',
106 'repo groups', 'ldap', 'repository management', 'hgweb replacement',
106 'repo groups', 'ldap', 'repository management', 'hgweb replacement',
107 'hgwebdir', 'gitweb replacement', 'serving hgweb',
107 'hgwebdir', 'gitweb replacement', 'serving hgweb',
108 ])
108 ])
109
109
110 # long description
110 # long description
111 README_FILE = 'README.rst'
111 README_FILE = 'README.rst'
112 CHANGELOG_FILE = 'docs/changelog.rst'
112 CHANGELOG_FILE = 'docs/changelog.rst'
113 try:
113 try:
114 long_description = open(README_FILE).read() + '\n\n' + \
114 long_description = open(README_FILE).read() + '\n\n' + \
115 open(CHANGELOG_FILE).read()
115 open(CHANGELOG_FILE).read()
116
116
117 except IOError, err:
117 except IOError, err:
118 sys.stderr.write(
118 sys.stderr.write(
119 "[WARNING] Cannot find file specified as long_description (%s)\n or "
119 "[WARNING] Cannot find file specified as long_description (%s)\n or "
120 "changelog (%s) skipping that file" % (README_FILE, CHANGELOG_FILE)
120 "changelog (%s) skipping that file" % (README_FILE, CHANGELOG_FILE)
121 )
121 )
122 long_description = description
122 long_description = description
123
123
124 try:
124 try:
125 from setuptools import setup, find_packages
125 from setuptools import setup, find_packages
126 except ImportError:
126 except ImportError:
127 from ez_setup import use_setuptools
127 from ez_setup import use_setuptools
128 use_setuptools()
128 use_setuptools()
129 from setuptools import setup, find_packages
129 from setuptools import setup, find_packages
130 # packages
130 # packages
131 packages = find_packages(exclude=['ez_setup'])
131 packages = find_packages(exclude=['ez_setup'])
132
132
133 setup(
133 setup(
134 name='Kallithea',
134 name='Kallithea',
135 version=__version__,
135 version=__version__,
136 description=description,
136 description=description,
137 long_description=long_description,
137 long_description=long_description,
138 keywords=keywords,
138 keywords=keywords,
139 license=__license__,
139 license=__license__,
140 author=__author__,
140 author=__author__,
141 author_email='kallithea@sfconservancy.org',
141 author_email='kallithea@sfconservancy.org',
142 dependency_links=dependency_links,
142 dependency_links=dependency_links,
143 url=__url__,
143 url=__url__,
144 install_requires=requirements,
144 install_requires=requirements,
145 classifiers=classifiers,
145 classifiers=classifiers,
146 setup_requires=["PasteScript>=1.6.3"],
146 setup_requires=["PasteScript>=1.6.3"],
147 data_files=data_files,
147 data_files=data_files,
148 packages=packages,
148 packages=packages,
149 include_package_data=True,
149 include_package_data=True,
150 test_suite='nose.collector',
150 test_suite='nose.collector',
151 package_data=package_data,
151 package_data=package_data,
152 message_extractors={'kallithea': [
152 message_extractors={'kallithea': [
153 ('**.py', 'python', None),
153 ('**.py', 'python', None),
154 ('templates/**.mako', 'mako', {'input_encoding': 'utf-8'}),
154 ('templates/**.mako', 'mako', {'input_encoding': 'utf-8'}),
155 ('templates/**.html', 'mako', {'input_encoding': 'utf-8'}),
155 ('templates/**.html', 'mako', {'input_encoding': 'utf-8'}),
156 ('public/**', 'ignore', None)]},
156 ('public/**', 'ignore', None)]},
157 zip_safe=False,
157 zip_safe=False,
158 paster_plugins=['PasteScript', 'Pylons'],
158 paster_plugins=['PasteScript', 'Pylons'],
159 entry_points="""
159 entry_points="""
160 [console_scripts]
160 [console_scripts]
161 kallithea-api = kallithea.bin.kallithea_api:main
161 kallithea-api = kallithea.bin.kallithea_api:main
162 kallithea-gist = kallithea.bin.kallithea_gist:main
162 kallithea-gist = kallithea.bin.kallithea_gist:main
163 kallithea-config = kallithea.bin.kallithea_config:main
163 kallithea-config = kallithea.bin.kallithea_config:main
164
164
165 [paste.app_factory]
165 [paste.app_factory]
166 main = kallithea.config.middleware:make_app
166 main = kallithea.config.middleware:make_app
167
167
168 [paste.app_install]
168 [paste.app_install]
169 main = pylons.util:PylonsInstaller
169 main = pylons.util:PylonsInstaller
170
170
171 [paste.global_paster_command]
171 [paste.global_paster_command]
172 setup-db=kallithea.lib.paster_commands.setup_db:Command
172 setup-db=kallithea.lib.paster_commands.setup_db:Command
173 cleanup-repos=kallithea.lib.paster_commands.cleanup:Command
173 cleanup-repos=kallithea.lib.paster_commands.cleanup:Command
174 update-repoinfo=kallithea.lib.paster_commands.update_repoinfo:Command
174 update-repoinfo=kallithea.lib.paster_commands.update_repoinfo:Command
175 make-rcext=kallithea.lib.paster_commands.make_rcextensions:Command
175 make-rcext=kallithea.lib.paster_commands.make_rcextensions:Command
176 repo-scan=kallithea.lib.paster_commands.repo_scan:Command
176 repo-scan=kallithea.lib.paster_commands.repo_scan:Command
177 cache-keys=kallithea.lib.paster_commands.cache_keys:Command
177 cache-keys=kallithea.lib.paster_commands.cache_keys:Command
178 ishell=kallithea.lib.paster_commands.ishell:Command
178 ishell=kallithea.lib.paster_commands.ishell:Command
179 make-index=kallithea.lib.paster_commands.make_index:Command
179 make-index=kallithea.lib.paster_commands.make_index:Command
180 upgrade-db=kallithea.lib.dbmigrate:UpgradeDb
180 upgrade-db=kallithea.lib.dbmigrate:UpgradeDb
181 celeryd=kallithea.lib.celerypylons.commands:CeleryDaemonCommand
181 celeryd=kallithea.lib.celerypylons.commands:CeleryDaemonCommand
182 install-iis=kallithea.lib.paster_commands.install_iis:Command
182 install-iis=kallithea.lib.paster_commands.install_iis:Command
183 """,
183 """,
184 )
184 )
General Comments 0
You need to be logged in to leave comments. Login now