##// END OF EJS Templates
Added handling of copied files diff parsing
marcink -
r3997:156cb1cd default
parent child Browse files
Show More
@@ -0,0 +1,3 b''
1 diff --git a/file1 b/file2
2 copy from file1
3 copy to file2
@@ -1,711 +1,719 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.diffs
3 rhodecode.lib.diffs
4 ~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~
5
5
6 Set of diffing helpers, previously part of vcs
6 Set of diffing helpers, previously part of vcs
7
7
8
8
9 :created_on: Dec 4, 2011
9 :created_on: Dec 4, 2011
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :original copyright: 2007-2008 by Armin Ronacher
12 :original copyright: 2007-2008 by Armin Ronacher
13 :license: GPLv3, see COPYING for more details.
13 :license: GPLv3, see COPYING for more details.
14 """
14 """
15 # This program is free software: you can redistribute it and/or modify
15 # This program is free software: you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License as published by
16 # it under the terms of the GNU General Public License as published by
17 # the Free Software Foundation, either version 3 of the License, or
17 # the Free Software Foundation, either version 3 of the License, or
18 # (at your option) any later version.
18 # (at your option) any later version.
19 #
19 #
20 # This program is distributed in the hope that it will be useful,
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
23 # GNU General Public License for more details.
24 #
24 #
25 # You should have received a copy of the GNU General Public License
25 # You should have received a copy of the GNU General Public License
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
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 rhodecode.lib.vcs.exceptions import VCSError
36 from rhodecode.lib.vcs.exceptions import VCSError
37 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
37 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
38 from rhodecode.lib.vcs.backends.base import EmptyChangeset
38 from rhodecode.lib.vcs.backends.base import EmptyChangeset
39 from rhodecode.lib.helpers import escape
39 from rhodecode.lib.helpers import escape
40 from rhodecode.lib.utils2 import safe_unicode, safe_str
40 from rhodecode.lib.utils2 import safe_unicode, safe_str
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 CHMOD_FILENODE = 5
130 COPIED_FILENODE = 5
131 BIN_FILENODE = 6
131 CHMOD_FILENODE = 6
132 BIN_FILENODE = 7
132
133
133
134
134 class DiffLimitExceeded(Exception):
135 class DiffLimitExceeded(Exception):
135 pass
136 pass
136
137
137
138
138 class LimitedDiffContainer(object):
139 class LimitedDiffContainer(object):
139
140
140 def __init__(self, diff_limit, cur_diff_size, diff):
141 def __init__(self, diff_limit, cur_diff_size, diff):
141 self.diff = diff
142 self.diff = diff
142 self.diff_limit = diff_limit
143 self.diff_limit = diff_limit
143 self.cur_diff_size = cur_diff_size
144 self.cur_diff_size = cur_diff_size
144
145
145 def __iter__(self):
146 def __iter__(self):
146 for l in self.diff:
147 for l in self.diff:
147 yield l
148 yield l
148
149
149
150
150 class DiffProcessor(object):
151 class DiffProcessor(object):
151 """
152 """
152 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
153 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
154 can be used to render it in a HTML template.
155 can be used to render it in a HTML template.
155 """
156 """
156 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
157 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
157 _newline_marker = re.compile(r'^\\ No newline at end of file')
158 _newline_marker = re.compile(r'^\\ No newline at end of file')
158 _git_header_re = re.compile(r"""
159 _git_header_re = re.compile(r"""
159 #^diff[ ]--git
160 #^diff[ ]--git
160 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
161 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
161 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n
162 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n
162 ^rename[ ]from[ ](?P<rename_from>\S+)\n
163 ^rename[ ]from[ ](?P<rename_from>\S+)\n
163 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
164 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
164 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
165 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
165 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
166 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
166 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
167 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
167 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
168 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
168 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
169 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
169 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
170 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
170 (?:^(?P<bin_patch>GIT[ ]binary[ ]patch)(?:\n|$))?
171 (?:^(?P<bin_patch>GIT[ ]binary[ ]patch)(?:\n|$))?
171 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
172 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
172 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
173 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
173 """, re.VERBOSE | re.MULTILINE)
174 """, re.VERBOSE | re.MULTILINE)
174 _hg_header_re = re.compile(r"""
175 _hg_header_re = re.compile(r"""
175 #^diff[ ]--git
176 #^diff[ ]--git
176 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
177 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
177 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
178 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
178 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
179 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
179 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))?
180 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))?
180 (?:^rename[ ]from[ ](?P<rename_from>\S+)\n
181 (?:^rename[ ]from[ ](?P<rename_from>\S+)\n
181 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
182 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
183 (?:^copy[ ]from[ ](?P<copy_from>\S+)\n
184 ^copy[ ]to[ ](?P<copy_to>\S+)(?:\n|$))?
182 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
185 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
183 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
186 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
184 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
187 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
185 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
188 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
186 (?:^(?P<bin_patch>GIT[ ]binary[ ]patch)(?:\n|$))?
189 (?:^(?P<bin_patch>GIT[ ]binary[ ]patch)(?:\n|$))?
187 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
190 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
188 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
191 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
189 """, re.VERBOSE | re.MULTILINE)
192 """, re.VERBOSE | re.MULTILINE)
190
193
191 #used for inline highlighter word split
194 #used for inline highlighter word split
192 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
195 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
193
196
194 def __init__(self, diff, vcs='hg', format='gitdiff', diff_limit=None):
197 def __init__(self, diff, vcs='hg', format='gitdiff', diff_limit=None):
195 """
198 """
196 :param diff: a text in diff format
199 :param diff: a text in diff format
197 :param vcs: type of version controll hg or git
200 :param vcs: type of version controll hg or git
198 :param format: format of diff passed, `udiff` or `gitdiff`
201 :param format: format of diff passed, `udiff` or `gitdiff`
199 :param diff_limit: define the size of diff that is considered "big"
202 :param diff_limit: define the size of diff that is considered "big"
200 based on that parameter cut off will be triggered, set to None
203 based on that parameter cut off will be triggered, set to None
201 to show full diff
204 to show full diff
202 """
205 """
203 if not isinstance(diff, basestring):
206 if not isinstance(diff, basestring):
204 raise Exception('Diff must be a basestring got %s instead' % type(diff))
207 raise Exception('Diff must be a basestring got %s instead' % type(diff))
205
208
206 self._diff = diff
209 self._diff = diff
207 self._format = format
210 self._format = format
208 self.adds = 0
211 self.adds = 0
209 self.removes = 0
212 self.removes = 0
210 # calculate diff size
213 # calculate diff size
211 self.diff_size = len(diff)
214 self.diff_size = len(diff)
212 self.diff_limit = diff_limit
215 self.diff_limit = diff_limit
213 self.cur_diff_size = 0
216 self.cur_diff_size = 0
214 self.parsed = False
217 self.parsed = False
215 self.parsed_diff = []
218 self.parsed_diff = []
216 self.vcs = vcs
219 self.vcs = vcs
217
220
218 if format == 'gitdiff':
221 if format == 'gitdiff':
219 self.differ = self._highlight_line_difflib
222 self.differ = self._highlight_line_difflib
220 self._parser = self._parse_gitdiff
223 self._parser = self._parse_gitdiff
221 else:
224 else:
222 self.differ = self._highlight_line_udiff
225 self.differ = self._highlight_line_udiff
223 self._parser = self._parse_udiff
226 self._parser = self._parse_udiff
224
227
225 def _copy_iterator(self):
228 def _copy_iterator(self):
226 """
229 """
227 make a fresh copy of generator, we should not iterate thru
230 make a fresh copy of generator, we should not iterate thru
228 an original as it's needed for repeating operations on
231 an original as it's needed for repeating operations on
229 this instance of DiffProcessor
232 this instance of DiffProcessor
230 """
233 """
231 self.__udiff, iterator_copy = tee(self.__udiff)
234 self.__udiff, iterator_copy = tee(self.__udiff)
232 return iterator_copy
235 return iterator_copy
233
236
234 def _escaper(self, string):
237 def _escaper(self, string):
235 """
238 """
236 Escaper for diff escapes special chars and checks the diff limit
239 Escaper for diff escapes special chars and checks the diff limit
237
240
238 :param string:
241 :param string:
239 """
242 """
240
243
241 self.cur_diff_size += len(string)
244 self.cur_diff_size += len(string)
242
245
243 # escaper get's iterated on each .next() call and it checks if each
246 # escaper get's iterated on each .next() call and it checks if each
244 # parsed line doesn't exceed the diff limit
247 # parsed line doesn't exceed the diff limit
245 if self.diff_limit is not None and self.cur_diff_size > self.diff_limit:
248 if self.diff_limit is not None and self.cur_diff_size > self.diff_limit:
246 raise DiffLimitExceeded('Diff Limit Exceeded')
249 raise DiffLimitExceeded('Diff Limit Exceeded')
247
250
248 return safe_unicode(string).replace('&', '&amp;')\
251 return safe_unicode(string).replace('&', '&amp;')\
249 .replace('<', '&lt;')\
252 .replace('<', '&lt;')\
250 .replace('>', '&gt;')
253 .replace('>', '&gt;')
251
254
252 def _line_counter(self, l):
255 def _line_counter(self, l):
253 """
256 """
254 Checks each line and bumps total adds/removes for this diff
257 Checks each line and bumps total adds/removes for this diff
255
258
256 :param l:
259 :param l:
257 """
260 """
258 if l.startswith('+') and not l.startswith('+++'):
261 if l.startswith('+') and not l.startswith('+++'):
259 self.adds += 1
262 self.adds += 1
260 elif l.startswith('-') and not l.startswith('---'):
263 elif l.startswith('-') and not l.startswith('---'):
261 self.removes += 1
264 self.removes += 1
262 return safe_unicode(l)
265 return safe_unicode(l)
263
266
264 def _highlight_line_difflib(self, line, next_):
267 def _highlight_line_difflib(self, line, next_):
265 """
268 """
266 Highlight inline changes in both lines.
269 Highlight inline changes in both lines.
267 """
270 """
268
271
269 if line['action'] == 'del':
272 if line['action'] == 'del':
270 old, new = line, next_
273 old, new = line, next_
271 else:
274 else:
272 old, new = next_, line
275 old, new = next_, line
273
276
274 oldwords = self._token_re.split(old['line'])
277 oldwords = self._token_re.split(old['line'])
275 newwords = self._token_re.split(new['line'])
278 newwords = self._token_re.split(new['line'])
276 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
279 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
277
280
278 oldfragments, newfragments = [], []
281 oldfragments, newfragments = [], []
279 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
282 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
280 oldfrag = ''.join(oldwords[i1:i2])
283 oldfrag = ''.join(oldwords[i1:i2])
281 newfrag = ''.join(newwords[j1:j2])
284 newfrag = ''.join(newwords[j1:j2])
282 if tag != 'equal':
285 if tag != 'equal':
283 if oldfrag:
286 if oldfrag:
284 oldfrag = '<del>%s</del>' % oldfrag
287 oldfrag = '<del>%s</del>' % oldfrag
285 if newfrag:
288 if newfrag:
286 newfrag = '<ins>%s</ins>' % newfrag
289 newfrag = '<ins>%s</ins>' % newfrag
287 oldfragments.append(oldfrag)
290 oldfragments.append(oldfrag)
288 newfragments.append(newfrag)
291 newfragments.append(newfrag)
289
292
290 old['line'] = "".join(oldfragments)
293 old['line'] = "".join(oldfragments)
291 new['line'] = "".join(newfragments)
294 new['line'] = "".join(newfragments)
292
295
293 def _highlight_line_udiff(self, line, next_):
296 def _highlight_line_udiff(self, line, next_):
294 """
297 """
295 Highlight inline changes in both lines.
298 Highlight inline changes in both lines.
296 """
299 """
297 start = 0
300 start = 0
298 limit = min(len(line['line']), len(next_['line']))
301 limit = min(len(line['line']), len(next_['line']))
299 while start < limit and line['line'][start] == next_['line'][start]:
302 while start < limit and line['line'][start] == next_['line'][start]:
300 start += 1
303 start += 1
301 end = -1
304 end = -1
302 limit -= start
305 limit -= start
303 while -end <= limit and line['line'][end] == next_['line'][end]:
306 while -end <= limit and line['line'][end] == next_['line'][end]:
304 end -= 1
307 end -= 1
305 end += 1
308 end += 1
306 if start or end:
309 if start or end:
307 def do(l):
310 def do(l):
308 last = end + len(l['line'])
311 last = end + len(l['line'])
309 if l['action'] == 'add':
312 if l['action'] == 'add':
310 tag = 'ins'
313 tag = 'ins'
311 else:
314 else:
312 tag = 'del'
315 tag = 'del'
313 l['line'] = '%s<%s>%s</%s>%s' % (
316 l['line'] = '%s<%s>%s</%s>%s' % (
314 l['line'][:start],
317 l['line'][:start],
315 tag,
318 tag,
316 l['line'][start:last],
319 l['line'][start:last],
317 tag,
320 tag,
318 l['line'][last:]
321 l['line'][last:]
319 )
322 )
320 do(line)
323 do(line)
321 do(next_)
324 do(next_)
322
325
323 def _get_header(self, diff_chunk):
326 def _get_header(self, diff_chunk):
324 """
327 """
325 parses the diff header, and returns parts, and leftover diff
328 parses the diff header, and returns parts, and leftover diff
326 parts consists of 14 elements::
329 parts consists of 14 elements::
327
330
328 a_path, b_path, similarity_index, rename_from, rename_to,
331 a_path, b_path, similarity_index, rename_from, rename_to,
329 old_mode, new_mode, new_file_mode, deleted_file_mode,
332 old_mode, new_mode, new_file_mode, deleted_file_mode,
330 a_blob_id, b_blob_id, b_mode, a_file, b_file
333 a_blob_id, b_blob_id, b_mode, a_file, b_file
331
334
332 :param diff_chunk:
335 :param diff_chunk:
333 """
336 """
334
337
335 if self.vcs == 'git':
338 if self.vcs == 'git':
336 match = self._git_header_re.match(diff_chunk)
339 match = self._git_header_re.match(diff_chunk)
337 diff = diff_chunk[match.end():]
340 diff = diff_chunk[match.end():]
338 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
341 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
339 elif self.vcs == 'hg':
342 elif self.vcs == 'hg':
340 match = self._hg_header_re.match(diff_chunk)
343 match = self._hg_header_re.match(diff_chunk)
341 diff = diff_chunk[match.end():]
344 diff = diff_chunk[match.end():]
342 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
345 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
343 else:
346 else:
344 raise Exception('VCS type %s is not supported' % self.vcs)
347 raise Exception('VCS type %s is not supported' % self.vcs)
345
348
346 def _clean_line(self, line, command):
349 def _clean_line(self, line, command):
347 if command in ['+', '-', ' ']:
350 if command in ['+', '-', ' ']:
348 #only modify the line if it's actually a diff thing
351 #only modify the line if it's actually a diff thing
349 line = line[1:]
352 line = line[1:]
350 return line
353 return line
351
354
352 def _parse_gitdiff(self, inline_diff=True):
355 def _parse_gitdiff(self, inline_diff=True):
353 _files = []
356 _files = []
354 diff_container = lambda arg: arg
357 diff_container = lambda arg: arg
355
358
356 ##split the diff in chunks of separate --git a/file b/file chunks
359 ##split the diff in chunks of separate --git a/file b/file chunks
357 for raw_diff in ('\n' + self._diff).split('\ndiff --git')[1:]:
360 for raw_diff in ('\n' + self._diff).split('\ndiff --git')[1:]:
358 head, diff = self._get_header(raw_diff)
361 head, diff = self._get_header(raw_diff)
359
362
360 op = None
363 op = None
361 stats = {
364 stats = {
362 'added': 0,
365 'added': 0,
363 'deleted': 0,
366 'deleted': 0,
364 'binary': False,
367 'binary': False,
365 'ops': {},
368 'ops': {},
366 }
369 }
367
370
368 if head['deleted_file_mode']:
371 if head['deleted_file_mode']:
369 op = 'D'
372 op = 'D'
370 stats['binary'] = True
373 stats['binary'] = True
371 stats['ops'][DEL_FILENODE] = 'deleted file'
374 stats['ops'][DEL_FILENODE] = 'deleted file'
372
375
373 elif head['new_file_mode']:
376 elif head['new_file_mode']:
374 op = 'A'
377 op = 'A'
375 stats['binary'] = True
378 stats['binary'] = True
376 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
379 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
377 else: # modify operation, can be cp, rename, chmod
380 else: # modify operation, can be cp, rename, chmod
378 # CHMOD
381 # CHMOD
379 if head['new_mode'] and head['old_mode']:
382 if head['new_mode'] and head['old_mode']:
380 op = 'M'
383 op = 'M'
381 stats['binary'] = True
384 stats['binary'] = True
382 stats['ops'][CHMOD_FILENODE] = ('modified file chmod %s => %s'
385 stats['ops'][CHMOD_FILENODE] = ('modified file chmod %s => %s'
383 % (head['old_mode'], head['new_mode']))
386 % (head['old_mode'], head['new_mode']))
384 # RENAME
387 # RENAME
385 if (head['rename_from'] and head['rename_to']
388 if (head['rename_from'] and head['rename_to']
386 and head['rename_from'] != head['rename_to']):
389 and head['rename_from'] != head['rename_to']):
387 op = 'M'
390 op = 'M'
388 stats['binary'] = True
391 stats['binary'] = True
389 stats['ops'][RENAMED_FILENODE] = ('file renamed from %s to %s'
392 stats['ops'][RENAMED_FILENODE] = ('file renamed from %s to %s'
390 % (head['rename_from'], head['rename_to']))
393 % (head['rename_from'], head['rename_to']))
391
394 # COPY
395 if head['copy_from'] and head['copy_to']:
396 op = 'M'
397 stats['binary'] = True
398 stats['ops'][COPIED_FILENODE] = ('file copied from %s to %s'
399 % (head['copy_from'], head['copy_to']))
392 # FALL BACK: detect missed old style add or remove
400 # FALL BACK: detect missed old style add or remove
393 if op is None:
401 if op is None:
394 if not head['a_file'] and head['b_file']:
402 if not head['a_file'] and head['b_file']:
395 op = 'A'
403 op = 'A'
396 stats['binary'] = True
404 stats['binary'] = True
397 stats['ops'][NEW_FILENODE] = 'new file'
405 stats['ops'][NEW_FILENODE] = 'new file'
398
406
399 elif head['a_file'] and not head['b_file']:
407 elif head['a_file'] and not head['b_file']:
400 op = 'D'
408 op = 'D'
401 stats['binary'] = True
409 stats['binary'] = True
402 stats['ops'][DEL_FILENODE] = 'deleted file'
410 stats['ops'][DEL_FILENODE] = 'deleted file'
403
411
404 # it's not ADD not DELETE
412 # it's not ADD not DELETE
405 if op is None:
413 if op is None:
406 op = 'M'
414 op = 'M'
407 stats['binary'] = True
415 stats['binary'] = True
408 stats['ops'][MOD_FILENODE] = 'modified file'
416 stats['ops'][MOD_FILENODE] = 'modified file'
409
417
410 # a real non-binary diff
418 # a real non-binary diff
411 if head['a_file'] or head['b_file']:
419 if head['a_file'] or head['b_file']:
412 try:
420 try:
413 chunks, _stats = self._parse_lines(diff)
421 chunks, _stats = self._parse_lines(diff)
414 stats['binary'] = False
422 stats['binary'] = False
415 stats['added'] = _stats[0]
423 stats['added'] = _stats[0]
416 stats['deleted'] = _stats[1]
424 stats['deleted'] = _stats[1]
417 # explicit mark that it's a modified file
425 # explicit mark that it's a modified file
418 if op == 'M':
426 if op == 'M':
419 stats['ops'][MOD_FILENODE] = 'modified file'
427 stats['ops'][MOD_FILENODE] = 'modified file'
420
428
421 except DiffLimitExceeded:
429 except DiffLimitExceeded:
422 diff_container = lambda _diff: \
430 diff_container = lambda _diff: \
423 LimitedDiffContainer(self.diff_limit,
431 LimitedDiffContainer(self.diff_limit,
424 self.cur_diff_size, _diff)
432 self.cur_diff_size, _diff)
425 break
433 break
426 else: # GIT binary patch (or empty diff)
434 else: # GIT binary patch (or empty diff)
427 # GIT Binary patch
435 # GIT Binary patch
428 if head['bin_patch']:
436 if head['bin_patch']:
429 stats['ops'][BIN_FILENODE] = 'binary diff not shown'
437 stats['ops'][BIN_FILENODE] = 'binary diff not shown'
430 chunks = []
438 chunks = []
431
439
432 chunks.insert(0, [{
440 chunks.insert(0, [{
433 'old_lineno': '',
441 'old_lineno': '',
434 'new_lineno': '',
442 'new_lineno': '',
435 'action': 'context',
443 'action': 'context',
436 'line': msg,
444 'line': msg,
437 } for _op, msg in stats['ops'].iteritems()
445 } for _op, msg in stats['ops'].iteritems()
438 if _op not in [MOD_FILENODE]])
446 if _op not in [MOD_FILENODE]])
439
447
440 _files.append({
448 _files.append({
441 'filename': head['b_path'],
449 'filename': head['b_path'],
442 'old_revision': head['a_blob_id'],
450 'old_revision': head['a_blob_id'],
443 'new_revision': head['b_blob_id'],
451 'new_revision': head['b_blob_id'],
444 'chunks': chunks,
452 'chunks': chunks,
445 'operation': op,
453 'operation': op,
446 'stats': stats,
454 'stats': stats,
447 })
455 })
448
456
449 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
457 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
450
458
451 if not inline_diff:
459 if not inline_diff:
452 return diff_container(sorted(_files, key=sorter))
460 return diff_container(sorted(_files, key=sorter))
453
461
454 # highlight inline changes
462 # highlight inline changes
455 for diff_data in _files:
463 for diff_data in _files:
456 for chunk in diff_data['chunks']:
464 for chunk in diff_data['chunks']:
457 lineiter = iter(chunk)
465 lineiter = iter(chunk)
458 try:
466 try:
459 while 1:
467 while 1:
460 line = lineiter.next()
468 line = lineiter.next()
461 if line['action'] not in ['unmod', 'context']:
469 if line['action'] not in ['unmod', 'context']:
462 nextline = lineiter.next()
470 nextline = lineiter.next()
463 if nextline['action'] in ['unmod', 'context'] or \
471 if nextline['action'] in ['unmod', 'context'] or \
464 nextline['action'] == line['action']:
472 nextline['action'] == line['action']:
465 continue
473 continue
466 self.differ(line, nextline)
474 self.differ(line, nextline)
467 except StopIteration:
475 except StopIteration:
468 pass
476 pass
469
477
470 return diff_container(sorted(_files, key=sorter))
478 return diff_container(sorted(_files, key=sorter))
471
479
472 def _parse_udiff(self, inline_diff=True):
480 def _parse_udiff(self, inline_diff=True):
473 raise NotImplementedError()
481 raise NotImplementedError()
474
482
475 def _parse_lines(self, diff):
483 def _parse_lines(self, diff):
476 """
484 """
477 Parse the diff an return data for the template.
485 Parse the diff an return data for the template.
478 """
486 """
479
487
480 lineiter = iter(diff)
488 lineiter = iter(diff)
481 stats = [0, 0]
489 stats = [0, 0]
482
490
483 try:
491 try:
484 chunks = []
492 chunks = []
485 line = lineiter.next()
493 line = lineiter.next()
486
494
487 while line:
495 while line:
488 lines = []
496 lines = []
489 chunks.append(lines)
497 chunks.append(lines)
490
498
491 match = self._chunk_re.match(line)
499 match = self._chunk_re.match(line)
492
500
493 if not match:
501 if not match:
494 break
502 break
495
503
496 gr = match.groups()
504 gr = match.groups()
497 (old_line, old_end,
505 (old_line, old_end,
498 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
506 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
499 old_line -= 1
507 old_line -= 1
500 new_line -= 1
508 new_line -= 1
501
509
502 context = len(gr) == 5
510 context = len(gr) == 5
503 old_end += old_line
511 old_end += old_line
504 new_end += new_line
512 new_end += new_line
505
513
506 if context:
514 if context:
507 # skip context only if it's first line
515 # skip context only if it's first line
508 if int(gr[0]) > 1:
516 if int(gr[0]) > 1:
509 lines.append({
517 lines.append({
510 'old_lineno': '...',
518 'old_lineno': '...',
511 'new_lineno': '...',
519 'new_lineno': '...',
512 'action': 'context',
520 'action': 'context',
513 'line': line,
521 'line': line,
514 })
522 })
515
523
516 line = lineiter.next()
524 line = lineiter.next()
517
525
518 while old_line < old_end or new_line < new_end:
526 while old_line < old_end or new_line < new_end:
519 command = ' '
527 command = ' '
520 if line:
528 if line:
521 command = line[0]
529 command = line[0]
522
530
523 affects_old = affects_new = False
531 affects_old = affects_new = False
524
532
525 # ignore those if we don't expect them
533 # ignore those if we don't expect them
526 if command in '#@':
534 if command in '#@':
527 continue
535 continue
528 elif command == '+':
536 elif command == '+':
529 affects_new = True
537 affects_new = True
530 action = 'add'
538 action = 'add'
531 stats[0] += 1
539 stats[0] += 1
532 elif command == '-':
540 elif command == '-':
533 affects_old = True
541 affects_old = True
534 action = 'del'
542 action = 'del'
535 stats[1] += 1
543 stats[1] += 1
536 else:
544 else:
537 affects_old = affects_new = True
545 affects_old = affects_new = True
538 action = 'unmod'
546 action = 'unmod'
539
547
540 if not self._newline_marker.match(line):
548 if not self._newline_marker.match(line):
541 old_line += affects_old
549 old_line += affects_old
542 new_line += affects_new
550 new_line += affects_new
543 lines.append({
551 lines.append({
544 'old_lineno': affects_old and old_line or '',
552 'old_lineno': affects_old and old_line or '',
545 'new_lineno': affects_new and new_line or '',
553 'new_lineno': affects_new and new_line or '',
546 'action': action,
554 'action': action,
547 'line': self._clean_line(line, command)
555 'line': self._clean_line(line, command)
548 })
556 })
549
557
550 line = lineiter.next()
558 line = lineiter.next()
551
559
552 if self._newline_marker.match(line):
560 if self._newline_marker.match(line):
553 # we need to append to lines, since this is not
561 # we need to append to lines, since this is not
554 # counted in the line specs of diff
562 # counted in the line specs of diff
555 lines.append({
563 lines.append({
556 'old_lineno': '...',
564 'old_lineno': '...',
557 'new_lineno': '...',
565 'new_lineno': '...',
558 'action': 'context',
566 'action': 'context',
559 'line': self._clean_line(line, command)
567 'line': self._clean_line(line, command)
560 })
568 })
561
569
562 except StopIteration:
570 except StopIteration:
563 pass
571 pass
564 return chunks, stats
572 return chunks, stats
565
573
566 def _safe_id(self, idstring):
574 def _safe_id(self, idstring):
567 """Make a string safe for including in an id attribute.
575 """Make a string safe for including in an id attribute.
568
576
569 The HTML spec says that id attributes 'must begin with
577 The HTML spec says that id attributes 'must begin with
570 a letter ([A-Za-z]) and may be followed by any number
578 a letter ([A-Za-z]) and may be followed by any number
571 of letters, digits ([0-9]), hyphens ("-"), underscores
579 of letters, digits ([0-9]), hyphens ("-"), underscores
572 ("_"), colons (":"), and periods (".")'. These regexps
580 ("_"), colons (":"), and periods (".")'. These regexps
573 are slightly over-zealous, in that they remove colons
581 are slightly over-zealous, in that they remove colons
574 and periods unnecessarily.
582 and periods unnecessarily.
575
583
576 Whitespace is transformed into underscores, and then
584 Whitespace is transformed into underscores, and then
577 anything which is not a hyphen or a character that
585 anything which is not a hyphen or a character that
578 matches \w (alphanumerics and underscore) is removed.
586 matches \w (alphanumerics and underscore) is removed.
579
587
580 """
588 """
581 # Transform all whitespace to underscore
589 # Transform all whitespace to underscore
582 idstring = re.sub(r'\s', "_", '%s' % idstring)
590 idstring = re.sub(r'\s', "_", '%s' % idstring)
583 # Remove everything that is not a hyphen or a member of \w
591 # Remove everything that is not a hyphen or a member of \w
584 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
592 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
585 return idstring
593 return idstring
586
594
587 def prepare(self, inline_diff=True):
595 def prepare(self, inline_diff=True):
588 """
596 """
589 Prepare the passed udiff for HTML rendering. It'l return a list
597 Prepare the passed udiff for HTML rendering. It'l return a list
590 of dicts with diff information
598 of dicts with diff information
591 """
599 """
592 parsed = self._parser(inline_diff=inline_diff)
600 parsed = self._parser(inline_diff=inline_diff)
593 self.parsed = True
601 self.parsed = True
594 self.parsed_diff = parsed
602 self.parsed_diff = parsed
595 return parsed
603 return parsed
596
604
597 def as_raw(self, diff_lines=None):
605 def as_raw(self, diff_lines=None):
598 """
606 """
599 Returns raw string diff
607 Returns raw string diff
600 """
608 """
601 return self._diff
609 return self._diff
602 #return u''.join(imap(self._line_counter, self._diff.splitlines(1)))
610 #return u''.join(imap(self._line_counter, self._diff.splitlines(1)))
603
611
604 def as_html(self, table_class='code-difftable', line_class='line',
612 def as_html(self, table_class='code-difftable', line_class='line',
605 old_lineno_class='lineno old', new_lineno_class='lineno new',
613 old_lineno_class='lineno old', new_lineno_class='lineno new',
606 code_class='code', enable_comments=False, parsed_lines=None):
614 code_class='code', enable_comments=False, parsed_lines=None):
607 """
615 """
608 Return given diff as html table with customized css classes
616 Return given diff as html table with customized css classes
609 """
617 """
610 def _link_to_if(condition, label, url):
618 def _link_to_if(condition, label, url):
611 """
619 """
612 Generates a link if condition is meet or just the label if not.
620 Generates a link if condition is meet or just the label if not.
613 """
621 """
614
622
615 if condition:
623 if condition:
616 return '''<a href="%(url)s">%(label)s</a>''' % {
624 return '''<a href="%(url)s">%(label)s</a>''' % {
617 'url': url,
625 'url': url,
618 'label': label
626 'label': label
619 }
627 }
620 else:
628 else:
621 return label
629 return label
622 if not self.parsed:
630 if not self.parsed:
623 self.prepare()
631 self.prepare()
624
632
625 diff_lines = self.parsed_diff
633 diff_lines = self.parsed_diff
626 if parsed_lines:
634 if parsed_lines:
627 diff_lines = parsed_lines
635 diff_lines = parsed_lines
628
636
629 _html_empty = True
637 _html_empty = True
630 _html = []
638 _html = []
631 _html.append('''<table class="%(table_class)s">\n''' % {
639 _html.append('''<table class="%(table_class)s">\n''' % {
632 'table_class': table_class
640 'table_class': table_class
633 })
641 })
634
642
635 for diff in diff_lines:
643 for diff in diff_lines:
636 for line in diff['chunks']:
644 for line in diff['chunks']:
637 _html_empty = False
645 _html_empty = False
638 for change in line:
646 for change in line:
639 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
647 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
640 'lc': line_class,
648 'lc': line_class,
641 'action': change['action']
649 'action': change['action']
642 })
650 })
643 anchor_old_id = ''
651 anchor_old_id = ''
644 anchor_new_id = ''
652 anchor_new_id = ''
645 anchor_old = "%(filename)s_o%(oldline_no)s" % {
653 anchor_old = "%(filename)s_o%(oldline_no)s" % {
646 'filename': self._safe_id(diff['filename']),
654 'filename': self._safe_id(diff['filename']),
647 'oldline_no': change['old_lineno']
655 'oldline_no': change['old_lineno']
648 }
656 }
649 anchor_new = "%(filename)s_n%(oldline_no)s" % {
657 anchor_new = "%(filename)s_n%(oldline_no)s" % {
650 'filename': self._safe_id(diff['filename']),
658 'filename': self._safe_id(diff['filename']),
651 'oldline_no': change['new_lineno']
659 'oldline_no': change['new_lineno']
652 }
660 }
653 cond_old = (change['old_lineno'] != '...' and
661 cond_old = (change['old_lineno'] != '...' and
654 change['old_lineno'])
662 change['old_lineno'])
655 cond_new = (change['new_lineno'] != '...' and
663 cond_new = (change['new_lineno'] != '...' and
656 change['new_lineno'])
664 change['new_lineno'])
657 if cond_old:
665 if cond_old:
658 anchor_old_id = 'id="%s"' % anchor_old
666 anchor_old_id = 'id="%s"' % anchor_old
659 if cond_new:
667 if cond_new:
660 anchor_new_id = 'id="%s"' % anchor_new
668 anchor_new_id = 'id="%s"' % anchor_new
661 ###########################################################
669 ###########################################################
662 # OLD LINE NUMBER
670 # OLD LINE NUMBER
663 ###########################################################
671 ###########################################################
664 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
672 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
665 'a_id': anchor_old_id,
673 'a_id': anchor_old_id,
666 'olc': old_lineno_class
674 'olc': old_lineno_class
667 })
675 })
668
676
669 _html.append('''%(link)s''' % {
677 _html.append('''%(link)s''' % {
670 'link': _link_to_if(True, change['old_lineno'],
678 'link': _link_to_if(True, change['old_lineno'],
671 '#%s' % anchor_old)
679 '#%s' % anchor_old)
672 })
680 })
673 _html.append('''</td>\n''')
681 _html.append('''</td>\n''')
674 ###########################################################
682 ###########################################################
675 # NEW LINE NUMBER
683 # NEW LINE NUMBER
676 ###########################################################
684 ###########################################################
677
685
678 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
686 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
679 'a_id': anchor_new_id,
687 'a_id': anchor_new_id,
680 'nlc': new_lineno_class
688 'nlc': new_lineno_class
681 })
689 })
682
690
683 _html.append('''%(link)s''' % {
691 _html.append('''%(link)s''' % {
684 'link': _link_to_if(True, change['new_lineno'],
692 'link': _link_to_if(True, change['new_lineno'],
685 '#%s' % anchor_new)
693 '#%s' % anchor_new)
686 })
694 })
687 _html.append('''</td>\n''')
695 _html.append('''</td>\n''')
688 ###########################################################
696 ###########################################################
689 # CODE
697 # CODE
690 ###########################################################
698 ###########################################################
691 comments = '' if enable_comments else 'no-comment'
699 comments = '' if enable_comments else 'no-comment'
692 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
700 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
693 'cc': code_class,
701 'cc': code_class,
694 'inc': comments
702 'inc': comments
695 })
703 })
696 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
704 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
697 'code': change['line']
705 'code': change['line']
698 })
706 })
699
707
700 _html.append('''\t</td>''')
708 _html.append('''\t</td>''')
701 _html.append('''\n</tr>\n''')
709 _html.append('''\n</tr>\n''')
702 _html.append('''</table>''')
710 _html.append('''</table>''')
703 if _html_empty:
711 if _html_empty:
704 return None
712 return None
705 return ''.join(_html)
713 return ''.join(_html)
706
714
707 def stat(self):
715 def stat(self):
708 """
716 """
709 Returns tuple of added, and removed lines for this instance
717 Returns tuple of added, and removed lines for this instance
710 """
718 """
711 return self.adds, self.removes
719 return self.adds, self.removes
@@ -1,250 +1,257 b''
1 from __future__ import with_statement
1 from __future__ import with_statement
2 import os
2 import os
3 from rhodecode.tests import *
3 from rhodecode.tests import *
4 from rhodecode.lib.diffs import DiffProcessor, NEW_FILENODE, DEL_FILENODE, \
4 from rhodecode.lib.diffs import DiffProcessor, NEW_FILENODE, DEL_FILENODE, \
5 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
5 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
6
6
7 dn = os.path.dirname
7 dn = os.path.dirname
8 FIXTURES = os.path.join(dn(dn(os.path.abspath(__file__))), 'fixtures')
8 FIXTURES = os.path.join(dn(dn(os.path.abspath(__file__))), 'fixtures')
9
9
10 DIFF_FIXTURES = {
10 DIFF_FIXTURES = {
11 'hg_diff_add_single_binary_file.diff': [
11 'hg_diff_add_single_binary_file.diff': [
12 ('US Warszawa.jpg', 'A',
12 ('US Warszawa.jpg', 'A',
13 {'added': 0,
13 {'added': 0,
14 'deleted': 0,
14 'deleted': 0,
15 'binary': True,
15 'binary': True,
16 'ops': {NEW_FILENODE: 'new file 100755',
16 'ops': {NEW_FILENODE: 'new file 100755',
17 BIN_FILENODE: 'binary diff not shown'}}),
17 BIN_FILENODE: 'binary diff not shown'}}),
18 ],
18 ],
19 'hg_diff_mod_single_binary_file.diff': [
19 'hg_diff_mod_single_binary_file.diff': [
20 ('US Warszawa.jpg', 'M',
20 ('US Warszawa.jpg', 'M',
21 {'added': 0,
21 {'added': 0,
22 'deleted': 0,
22 'deleted': 0,
23 'binary': True,
23 'binary': True,
24 'ops': {MOD_FILENODE: 'modified file',
24 'ops': {MOD_FILENODE: 'modified file',
25 BIN_FILENODE: 'binary diff not shown'}}),
25 BIN_FILENODE: 'binary diff not shown'}}),
26 ],
26 ],
27
27
28 'hg_diff_mod_single_file_and_rename_and_chmod.diff': [
28 'hg_diff_mod_single_file_and_rename_and_chmod.diff': [
29 ('README', 'M',
29 ('README', 'M',
30 {'added': 3,
30 {'added': 3,
31 'deleted': 0,
31 'deleted': 0,
32 'binary': False,
32 'binary': False,
33 'ops': {MOD_FILENODE: 'modified file',
33 'ops': {MOD_FILENODE: 'modified file',
34 RENAMED_FILENODE: 'file renamed from README.rst to README',
34 RENAMED_FILENODE: 'file renamed from README.rst to README',
35 CHMOD_FILENODE: 'modified file chmod 100755 => 100644'}}),
35 CHMOD_FILENODE: 'modified file chmod 100755 => 100644'}}),
36 ],
36 ],
37 'hg_diff_mod_file_and_rename.diff': [
37 'hg_diff_mod_file_and_rename.diff': [
38 ('README.rst', 'M',
38 ('README.rst', 'M',
39 {'added': 3,
39 {'added': 3,
40 'deleted': 0,
40 'deleted': 0,
41 'binary': False,
41 'binary': False,
42 'ops': {MOD_FILENODE: 'modified file',
42 'ops': {MOD_FILENODE: 'modified file',
43 RENAMED_FILENODE: 'file renamed from README to README.rst'}}),
43 RENAMED_FILENODE: 'file renamed from README to README.rst'}}),
44 ],
44 ],
45 'hg_diff_del_single_binary_file.diff': [
45 'hg_diff_del_single_binary_file.diff': [
46 ('US Warszawa.jpg', 'D',
46 ('US Warszawa.jpg', 'D',
47 {'added': 0,
47 {'added': 0,
48 'deleted': 0,
48 'deleted': 0,
49 'binary': True,
49 'binary': True,
50 'ops': {DEL_FILENODE: 'deleted file',
50 'ops': {DEL_FILENODE: 'deleted file',
51 BIN_FILENODE: 'binary diff not shown'}}),
51 BIN_FILENODE: 'binary diff not shown'}}),
52 ],
52 ],
53 'hg_diff_chmod_and_mod_single_binary_file.diff': [
53 'hg_diff_chmod_and_mod_single_binary_file.diff': [
54 ('gravatar.png', 'M',
54 ('gravatar.png', 'M',
55 {'added': 0,
55 {'added': 0,
56 'deleted': 0,
56 'deleted': 0,
57 'binary': True,
57 'binary': True,
58 'ops': {CHMOD_FILENODE: 'modified file chmod 100644 => 100755',
58 'ops': {CHMOD_FILENODE: 'modified file chmod 100644 => 100755',
59 BIN_FILENODE: 'binary diff not shown'}}),
59 BIN_FILENODE: 'binary diff not shown'}}),
60 ],
60 ],
61 'hg_diff_chmod.diff': [
61 'hg_diff_chmod.diff': [
62 ('file', 'M',
62 ('file', 'M',
63 {'added': 0,
63 {'added': 0,
64 'deleted': 0,
64 'deleted': 0,
65 'binary': True,
65 'binary': True,
66 'ops': {CHMOD_FILENODE: 'modified file chmod 100755 => 100644'}}),
66 'ops': {CHMOD_FILENODE: 'modified file chmod 100755 => 100644'}}),
67 ],
67 ],
68 'hg_diff_rename_file.diff': [
68 'hg_diff_rename_file.diff': [
69 ('file_renamed', 'M',
69 ('file_renamed', 'M',
70 {'added': 0,
70 {'added': 0,
71 'deleted': 0,
71 'deleted': 0,
72 'binary': True,
72 'binary': True,
73 'ops': {RENAMED_FILENODE: 'file renamed from file to file_renamed'}}),
73 'ops': {RENAMED_FILENODE: 'file renamed from file to file_renamed'}}),
74 ],
74 ],
75 'hg_diff_rename_and_chmod_file.diff': [
75 'hg_diff_rename_and_chmod_file.diff': [
76 ('README', 'M',
76 ('README', 'M',
77 {'added': 0,
77 {'added': 0,
78 'deleted': 0,
78 'deleted': 0,
79 'binary': True,
79 'binary': True,
80 'ops': {CHMOD_FILENODE: 'modified file chmod 100644 => 100755',
80 'ops': {CHMOD_FILENODE: 'modified file chmod 100644 => 100755',
81 RENAMED_FILENODE: 'file renamed from README.rst to README'}}),
81 RENAMED_FILENODE: 'file renamed from README.rst to README'}}),
82 ],
82 ],
83 'hg_diff_binary_and_normal.diff': [
83 'hg_diff_binary_and_normal.diff': [
84 ('img/baseline-10px.png', 'A',
84 ('img/baseline-10px.png', 'A',
85 {'added': 0,
85 {'added': 0,
86 'deleted': 0,
86 'deleted': 0,
87 'binary': True,
87 'binary': True,
88 'ops': {NEW_FILENODE: 'new file 100644',
88 'ops': {NEW_FILENODE: 'new file 100644',
89 BIN_FILENODE: 'binary diff not shown'}}),
89 BIN_FILENODE: 'binary diff not shown'}}),
90 ('js/jquery/hashgrid.js', 'A',
90 ('js/jquery/hashgrid.js', 'A',
91 {'added': 340,
91 {'added': 340,
92 'deleted': 0,
92 'deleted': 0,
93 'binary': False,
93 'binary': False,
94 'ops': {NEW_FILENODE: 'new file 100755'}}),
94 'ops': {NEW_FILENODE: 'new file 100755'}}),
95 ('index.html', 'M',
95 ('index.html', 'M',
96 {'added': 3,
96 {'added': 3,
97 'deleted': 2,
97 'deleted': 2,
98 'binary': False,
98 'binary': False,
99 'ops': {MOD_FILENODE: 'modified file'}}),
99 'ops': {MOD_FILENODE: 'modified file'}}),
100 ('less/docs.less', 'M',
100 ('less/docs.less', 'M',
101 {'added': 34,
101 {'added': 34,
102 'deleted': 0,
102 'deleted': 0,
103 'binary': False,
103 'binary': False,
104 'ops': {MOD_FILENODE: 'modified file'}}),
104 'ops': {MOD_FILENODE: 'modified file'}}),
105 ('less/scaffolding.less', 'M',
105 ('less/scaffolding.less', 'M',
106 {'added': 1,
106 {'added': 1,
107 'deleted': 3,
107 'deleted': 3,
108 'binary': False,
108 'binary': False,
109 'ops': {MOD_FILENODE: 'modified file'}}),
109 'ops': {MOD_FILENODE: 'modified file'}}),
110 ('readme.markdown', 'M',
110 ('readme.markdown', 'M',
111 {'added': 1,
111 {'added': 1,
112 'deleted': 10,
112 'deleted': 10,
113 'binary': False,
113 'binary': False,
114 'ops': {MOD_FILENODE: 'modified file'}}),
114 'ops': {MOD_FILENODE: 'modified file'}}),
115 ('img/baseline-20px.png', 'D',
115 ('img/baseline-20px.png', 'D',
116 {'added': 0,
116 {'added': 0,
117 'deleted': 0,
117 'deleted': 0,
118 'binary': True,
118 'binary': True,
119 'ops': {DEL_FILENODE: 'deleted file',
119 'ops': {DEL_FILENODE: 'deleted file',
120 BIN_FILENODE: 'binary diff not shown'}}),
120 BIN_FILENODE: 'binary diff not shown'}}),
121 ('js/global.js', 'D',
121 ('js/global.js', 'D',
122 {'added': 0,
122 {'added': 0,
123 'deleted': 75,
123 'deleted': 75,
124 'binary': False,
124 'binary': False,
125 'ops': {DEL_FILENODE: 'deleted file'}})
125 'ops': {DEL_FILENODE: 'deleted file'}})
126 ],
126 ],
127 'git_diff_chmod.diff': [
127 'git_diff_chmod.diff': [
128 ('work-horus.xls', 'M',
128 ('work-horus.xls', 'M',
129 {'added': 0,
129 {'added': 0,
130 'deleted': 0,
130 'deleted': 0,
131 'binary': True,
131 'binary': True,
132 'ops': {CHMOD_FILENODE: 'modified file chmod 100644 => 100755'}})
132 'ops': {CHMOD_FILENODE: 'modified file chmod 100644 => 100755'}})
133 ],
133 ],
134 'git_diff_rename_file.diff': [
134 'git_diff_rename_file.diff': [
135 ('file.xls', 'M',
135 ('file.xls', 'M',
136 {'added': 0,
136 {'added': 0,
137 'deleted': 0,
137 'deleted': 0,
138 'binary': True,
138 'binary': True,
139 'ops': {RENAMED_FILENODE: 'file renamed from work-horus.xls to file.xls'}})
139 'ops': {RENAMED_FILENODE: 'file renamed from work-horus.xls to file.xls'}})
140 ],
140 ],
141 'git_diff_mod_single_binary_file.diff': [
141 'git_diff_mod_single_binary_file.diff': [
142 ('US Warszawa.jpg', 'M',
142 ('US Warszawa.jpg', 'M',
143 {'added': 0,
143 {'added': 0,
144 'deleted': 0,
144 'deleted': 0,
145 'binary': True,
145 'binary': True,
146 'ops': {MOD_FILENODE: 'modified file',
146 'ops': {MOD_FILENODE: 'modified file',
147 BIN_FILENODE: 'binary diff not shown'}})
147 BIN_FILENODE: 'binary diff not shown'}})
148 ],
148 ],
149 'git_diff_binary_and_normal.diff': [
149 'git_diff_binary_and_normal.diff': [
150 ('img/baseline-10px.png', 'A',
150 ('img/baseline-10px.png', 'A',
151 {'added': 0,
151 {'added': 0,
152 'deleted': 0,
152 'deleted': 0,
153 'binary': True,
153 'binary': True,
154 'ops': {NEW_FILENODE: 'new file 100644',
154 'ops': {NEW_FILENODE: 'new file 100644',
155 BIN_FILENODE: 'binary diff not shown'}}),
155 BIN_FILENODE: 'binary diff not shown'}}),
156 ('js/jquery/hashgrid.js', 'A',
156 ('js/jquery/hashgrid.js', 'A',
157 {'added': 340,
157 {'added': 340,
158 'deleted': 0,
158 'deleted': 0,
159 'binary': False,
159 'binary': False,
160 'ops': {NEW_FILENODE: 'new file 100755'}}),
160 'ops': {NEW_FILENODE: 'new file 100755'}}),
161 ('index.html', 'M',
161 ('index.html', 'M',
162 {'added': 3,
162 {'added': 3,
163 'deleted': 2,
163 'deleted': 2,
164 'binary': False,
164 'binary': False,
165 'ops': {MOD_FILENODE: 'modified file'}}),
165 'ops': {MOD_FILENODE: 'modified file'}}),
166 ('less/docs.less', 'M',
166 ('less/docs.less', 'M',
167 {'added': 34,
167 {'added': 34,
168 'deleted': 0,
168 'deleted': 0,
169 'binary': False,
169 'binary': False,
170 'ops': {MOD_FILENODE: 'modified file'}}),
170 'ops': {MOD_FILENODE: 'modified file'}}),
171 ('less/scaffolding.less', 'M',
171 ('less/scaffolding.less', 'M',
172 {'added': 1,
172 {'added': 1,
173 'deleted': 3,
173 'deleted': 3,
174 'binary': False,
174 'binary': False,
175 'ops': {MOD_FILENODE: 'modified file'}}),
175 'ops': {MOD_FILENODE: 'modified file'}}),
176 ('readme.markdown', 'M',
176 ('readme.markdown', 'M',
177 {'added': 1,
177 {'added': 1,
178 'deleted': 10,
178 'deleted': 10,
179 'binary': False,
179 'binary': False,
180 'ops': {MOD_FILENODE: 'modified file'}}),
180 'ops': {MOD_FILENODE: 'modified file'}}),
181 ('img/baseline-20px.png', 'D',
181 ('img/baseline-20px.png', 'D',
182 {'added': 0,
182 {'added': 0,
183 'deleted': 0,
183 'deleted': 0,
184 'binary': True,
184 'binary': True,
185 'ops': {DEL_FILENODE: 'deleted file',
185 'ops': {DEL_FILENODE: 'deleted file',
186 BIN_FILENODE: 'binary diff not shown'}}),
186 BIN_FILENODE: 'binary diff not shown'}}),
187 ('js/global.js', 'D',
187 ('js/global.js', 'D',
188 {'added': 0,
188 {'added': 0,
189 'deleted': 75,
189 'deleted': 75,
190 'binary': False,
190 'binary': False,
191 'ops': {DEL_FILENODE: 'deleted file'}}),
191 'ops': {DEL_FILENODE: 'deleted file'}}),
192 ],
192 ],
193 'diff_with_diff_data.diff': [
193 'diff_with_diff_data.diff': [
194 ('vcs/backends/base.py', 'M',
194 ('vcs/backends/base.py', 'M',
195 {'added': 18,
195 {'added': 18,
196 'deleted': 2,
196 'deleted': 2,
197 'binary': False,
197 'binary': False,
198 'ops': {MOD_FILENODE: 'modified file'}}),
198 'ops': {MOD_FILENODE: 'modified file'}}),
199 ('vcs/backends/git/repository.py', 'M',
199 ('vcs/backends/git/repository.py', 'M',
200 {'added': 46,
200 {'added': 46,
201 'deleted': 15,
201 'deleted': 15,
202 'binary': False,
202 'binary': False,
203 'ops': {MOD_FILENODE: 'modified file'}}),
203 'ops': {MOD_FILENODE: 'modified file'}}),
204 ('vcs/backends/hg.py', 'M',
204 ('vcs/backends/hg.py', 'M',
205 {'added': 22,
205 {'added': 22,
206 'deleted': 3,
206 'deleted': 3,
207 'binary': False,
207 'binary': False,
208 'ops': {MOD_FILENODE: 'modified file'}}),
208 'ops': {MOD_FILENODE: 'modified file'}}),
209 ('vcs/tests/test_git.py', 'M',
209 ('vcs/tests/test_git.py', 'M',
210 {'added': 5,
210 {'added': 5,
211 'deleted': 5,
211 'deleted': 5,
212 'binary': False,
212 'binary': False,
213 'ops': {MOD_FILENODE: 'modified file'}}),
213 'ops': {MOD_FILENODE: 'modified file'}}),
214 ('vcs/tests/test_repository.py', 'M',
214 ('vcs/tests/test_repository.py', 'M',
215 {'added': 174,
215 {'added': 174,
216 'deleted': 2,
216 'deleted': 2,
217 'binary': False,
217 'binary': False,
218 'ops': {MOD_FILENODE: 'modified file'}}),
218 'ops': {MOD_FILENODE: 'modified file'}}),
219 ],
219 ],
220 'hg_diff_copy_file.diff': [
221 ('file2', 'M',
222 {'added': 0,
223 'deleted': 0,
224 'binary': True,
225 'ops': {COPIED_FILENODE: 'file copied from file1 to file2'}}),
226 ]
220 # 'large_diff.diff': [
227 # 'large_diff.diff': [
221 # ('.hgignore', 'A', {'deleted': 0, 'binary': False, 'added': 3, 'ops': {1: 'new file 100644'}}),
228 # ('.hgignore', 'A', {'deleted': 0, 'binary': False, 'added': 3, 'ops': {1: 'new file 100644'}}),
222 # ('MANIFEST.in', 'A', {'deleted': 0, 'binary': False, 'added': 3, 'ops': {1: 'new file 100644'}}),
229 # ('MANIFEST.in', 'A', {'deleted': 0, 'binary': False, 'added': 3, 'ops': {1: 'new file 100644'}}),
223 # ('README.txt', 'A', {'deleted': 0, 'binary': False, 'added': 19, 'ops': {1: 'new file 100644'}}),
230 # ('README.txt', 'A', {'deleted': 0, 'binary': False, 'added': 19, 'ops': {1: 'new file 100644'}}),
224 # ('development.ini', 'A', {'deleted': 0, 'binary': False, 'added': 116, 'ops': {1: 'new file 100644'}}),
231 # ('development.ini', 'A', {'deleted': 0, 'binary': False, 'added': 116, 'ops': {1: 'new file 100644'}}),
225 # ('docs/index.txt', 'A', {'deleted': 0, 'binary': False, 'added': 19, 'ops': {1: 'new file 100644'}}),
232 # ('docs/index.txt', 'A', {'deleted': 0, 'binary': False, 'added': 19, 'ops': {1: 'new file 100644'}}),
226 # ('ez_setup.py', 'A', {'deleted': 0, 'binary': False, 'added': 276, 'ops': {1: 'new file 100644'}}),
233 # ('ez_setup.py', 'A', {'deleted': 0, 'binary': False, 'added': 276, 'ops': {1: 'new file 100644'}}),
227 # ('hgapp.py', 'A', {'deleted': 0, 'binary': False, 'added': 26, 'ops': {1: 'new file 100644'}}),
234 # ('hgapp.py', 'A', {'deleted': 0, 'binary': False, 'added': 26, 'ops': {1: 'new file 100644'}}),
228 # ('hgwebdir.config', 'A', {'deleted': 0, 'binary': False, 'added': 21, 'ops': {1: 'new file 100644'}}),
235 # ('hgwebdir.config', 'A', {'deleted': 0, 'binary': False, 'added': 21, 'ops': {1: 'new file 100644'}}),
229 # ('pylons_app.egg-info/PKG-INFO', 'A', {'deleted': 0, 'binary': False, 'added': 10, 'ops': {1: 'new file 100644'}}),
236 # ('pylons_app.egg-info/PKG-INFO', 'A', {'deleted': 0, 'binary': False, 'added': 10, 'ops': {1: 'new file 100644'}}),
230 # ('pylons_app.egg-info/SOURCES.txt', 'A', {'deleted': 0, 'binary': False, 'added': 33, 'ops': {1: 'new file 100644'}}),
237 # ('pylons_app.egg-info/SOURCES.txt', 'A', {'deleted': 0, 'binary': False, 'added': 33, 'ops': {1: 'new file 100644'}}),
231 # ('pylons_app.egg-info/dependency_links.txt', 'A', {'deleted': 0, 'binary': False, 'added': 1, 'ops': {1: 'new file 100644'}}),
238 # ('pylons_app.egg-info/dependency_links.txt', 'A', {'deleted': 0, 'binary': False, 'added': 1, 'ops': {1: 'new file 100644'}}),
232 # #TODO:
239 # #TODO:
233 # ],
240 # ],
234
241
235 }
242 }
236
243
237
244
238 class DiffLibTest(BaseTestCase):
245 class DiffLibTest(BaseTestCase):
239
246
240 @parameterized.expand([(x,) for x in DIFF_FIXTURES])
247 @parameterized.expand([(x,) for x in DIFF_FIXTURES])
241 def test_diff(self, diff_fixture):
248 def test_diff(self, diff_fixture):
242
249
243 with open(os.path.join(FIXTURES, diff_fixture)) as f:
250 with open(os.path.join(FIXTURES, diff_fixture)) as f:
244 diff = f.read()
251 diff = f.read()
245
252
246 diff_proc = DiffProcessor(diff)
253 diff_proc = DiffProcessor(diff)
247 diff_proc_d = diff_proc.prepare()
254 diff_proc_d = diff_proc.prepare()
248 data = [(x['filename'], x['operation'], x['stats']) for x in diff_proc_d]
255 data = [(x['filename'], x['operation'], x['stats']) for x in diff_proc_d]
249 expected_data = DIFF_FIXTURES[diff_fixture]
256 expected_data = DIFF_FIXTURES[diff_fixture]
250 self.assertListEqual(expected_data, data)
257 self.assertListEqual(expected_data, data)
General Comments 0
You need to be logged in to leave comments. Login now