##// END OF EJS Templates
diffs: add translation string
ergo -
r384:7507e2a3 default
parent child Browse files
Show More
@@ -1,872 +1,874 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2016 RhodeCode GmbH
3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 Set of diffing helpers, previously part of vcs
23 Set of diffing helpers, previously part of vcs
24 """
24 """
25
25
26 import collections
26 import collections
27 import re
27 import re
28 import difflib
28 import difflib
29 import logging
29 import logging
30
30
31 from itertools import tee, imap
31 from itertools import tee, imap
32
32
33 from pylons.i18n.translation import _
33 from pylons.i18n.translation import _
34
34
35 from rhodecode.lib.vcs.exceptions import VCSError
35 from rhodecode.lib.vcs.exceptions import VCSError
36 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
36 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
37 from rhodecode.lib.vcs.backends.base import EmptyCommit
37 from rhodecode.lib.vcs.backends.base import EmptyCommit
38 from rhodecode.lib.helpers import escape
38 from rhodecode.lib.helpers import escape
39 from rhodecode.lib.utils2 import safe_unicode
39 from rhodecode.lib.utils2 import safe_unicode
40
40
41 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
42
42
43
43
44 class OPS(object):
44 class OPS(object):
45 ADD = 'A'
45 ADD = 'A'
46 MOD = 'M'
46 MOD = 'M'
47 DEL = 'D'
47 DEL = 'D'
48
48
49 def wrap_to_table(str_):
49 def wrap_to_table(str_):
50 return '''<table class="code-difftable">
50 return '''<table class="code-difftable">
51 <tr class="line no-comment">
51 <tr class="line no-comment">
52 <td class="add-comment-line tooltip" title="Click to comment"><span class="add-comment-content"></span></td>
52 <td class="add-comment-line tooltip" title="%s"><span class="add-comment-content"></span></td>
53 <td class="lineno new"></td>
53 <td class="lineno new"></td>
54 <td class="code no-comment"><pre>%s</pre></td>
54 <td class="code no-comment"><pre>%s</pre></td>
55 </tr>
55 </tr>
56 </table>''' % str_
56 </table>''' % (_('Click to comment'), str_)
57
57
58
58
59 def wrapped_diff(filenode_old, filenode_new, diff_limit=None, file_limit=None,
59 def wrapped_diff(filenode_old, filenode_new, diff_limit=None, file_limit=None,
60 show_full_diff=False, ignore_whitespace=True, line_context=3,
60 show_full_diff=False, ignore_whitespace=True, line_context=3,
61 enable_comments=False):
61 enable_comments=False):
62 """
62 """
63 returns a wrapped diff into a table, checks for cut_off_limit for file and
63 returns a wrapped diff into a table, checks for cut_off_limit for file and
64 whole diff and presents proper message
64 whole diff and presents proper message
65 """
65 """
66
66
67 if filenode_old is None:
67 if filenode_old is None:
68 filenode_old = FileNode(filenode_new.path, '', EmptyCommit())
68 filenode_old = FileNode(filenode_new.path, '', EmptyCommit())
69
69
70 if filenode_old.is_binary or filenode_new.is_binary:
70 if filenode_old.is_binary or filenode_new.is_binary:
71 diff = wrap_to_table(_('Binary file'))
71 diff = wrap_to_table(_('Binary file'))
72 stats = None
72 stats = None
73 size = 0
73 size = 0
74 data = None
74 data = None
75
75
76 elif diff_limit != -1 and (diff_limit is None or
76 elif diff_limit != -1 and (diff_limit is None or
77 (filenode_old.size < diff_limit and filenode_new.size < diff_limit)):
77 (filenode_old.size < diff_limit and filenode_new.size < diff_limit)):
78
78
79 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
79 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
80 ignore_whitespace=ignore_whitespace,
80 ignore_whitespace=ignore_whitespace,
81 context=line_context)
81 context=line_context)
82 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff', diff_limit=diff_limit,
82 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff', diff_limit=diff_limit,
83 file_limit=file_limit, show_full_diff=show_full_diff)
83 file_limit=file_limit, show_full_diff=show_full_diff)
84 _parsed = diff_processor.prepare()
84 _parsed = diff_processor.prepare()
85
85
86 diff = diff_processor.as_html(enable_comments=enable_comments)
86 diff = diff_processor.as_html(enable_comments=enable_comments)
87 stats = _parsed[0]['stats'] if _parsed else None
87 stats = _parsed[0]['stats'] if _parsed else None
88 size = len(diff or '')
88 size = len(diff or '')
89 data = _parsed[0] if _parsed else None
89 data = _parsed[0] if _parsed else None
90 else:
90 else:
91 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
91 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
92 'diff menu to display this diff'))
92 'diff menu to display this diff'))
93 stats = None
93 stats = None
94 size = 0
94 size = 0
95 data = None
95 data = None
96 if not diff:
96 if not diff:
97 submodules = filter(lambda o: isinstance(o, SubModuleNode),
97 submodules = filter(lambda o: isinstance(o, SubModuleNode),
98 [filenode_new, filenode_old])
98 [filenode_new, filenode_old])
99 if submodules:
99 if submodules:
100 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
100 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
101 else:
101 else:
102 diff = wrap_to_table(_('No changes detected'))
102 diff = wrap_to_table(_('No changes detected'))
103
103
104 cs1 = filenode_old.commit.raw_id
104 cs1 = filenode_old.commit.raw_id
105 cs2 = filenode_new.commit.raw_id
105 cs2 = filenode_new.commit.raw_id
106
106
107 return size, cs1, cs2, diff, stats, data
107 return size, cs1, cs2, diff, stats, data
108
108
109
109
110 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
110 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
111 """
111 """
112 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
112 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
113
113
114 :param ignore_whitespace: ignore whitespaces in diff
114 :param ignore_whitespace: ignore whitespaces in diff
115 """
115 """
116 # make sure we pass in default context
116 # make sure we pass in default context
117 context = context or 3
117 context = context or 3
118 submodules = filter(lambda o: isinstance(o, SubModuleNode),
118 submodules = filter(lambda o: isinstance(o, SubModuleNode),
119 [filenode_new, filenode_old])
119 [filenode_new, filenode_old])
120 if submodules:
120 if submodules:
121 return ''
121 return ''
122
122
123 for filenode in (filenode_old, filenode_new):
123 for filenode in (filenode_old, filenode_new):
124 if not isinstance(filenode, FileNode):
124 if not isinstance(filenode, FileNode):
125 raise VCSError(
125 raise VCSError(
126 "Given object should be FileNode object, not %s"
126 "Given object should be FileNode object, not %s"
127 % filenode.__class__)
127 % filenode.__class__)
128
128
129 repo = filenode_new.commit.repository
129 repo = filenode_new.commit.repository
130 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
130 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
131 new_commit = filenode_new.commit
131 new_commit = filenode_new.commit
132
132
133 vcs_gitdiff = repo.get_diff(
133 vcs_gitdiff = repo.get_diff(
134 old_commit, new_commit, filenode_new.path,
134 old_commit, new_commit, filenode_new.path,
135 ignore_whitespace, context, path1=filenode_old.path)
135 ignore_whitespace, context, path1=filenode_old.path)
136 return vcs_gitdiff
136 return vcs_gitdiff
137
137
138 NEW_FILENODE = 1
138 NEW_FILENODE = 1
139 DEL_FILENODE = 2
139 DEL_FILENODE = 2
140 MOD_FILENODE = 3
140 MOD_FILENODE = 3
141 RENAMED_FILENODE = 4
141 RENAMED_FILENODE = 4
142 COPIED_FILENODE = 5
142 COPIED_FILENODE = 5
143 CHMOD_FILENODE = 6
143 CHMOD_FILENODE = 6
144 BIN_FILENODE = 7
144 BIN_FILENODE = 7
145
145
146
146
147 class LimitedDiffContainer(object):
147 class LimitedDiffContainer(object):
148
148
149 def __init__(self, diff_limit, cur_diff_size, diff):
149 def __init__(self, diff_limit, cur_diff_size, diff):
150 self.diff = diff
150 self.diff = diff
151 self.diff_limit = diff_limit
151 self.diff_limit = diff_limit
152 self.cur_diff_size = cur_diff_size
152 self.cur_diff_size = cur_diff_size
153
153
154 def __getitem__(self, key):
154 def __getitem__(self, key):
155 return self.diff.__getitem__(key)
155 return self.diff.__getitem__(key)
156
156
157 def __iter__(self):
157 def __iter__(self):
158 for l in self.diff:
158 for l in self.diff:
159 yield l
159 yield l
160
160
161
161
162 class Action(object):
162 class Action(object):
163 """
163 """
164 Contains constants for the action value of the lines in a parsed diff.
164 Contains constants for the action value of the lines in a parsed diff.
165 """
165 """
166
166
167 ADD = 'add'
167 ADD = 'add'
168 DELETE = 'del'
168 DELETE = 'del'
169 UNMODIFIED = 'unmod'
169 UNMODIFIED = 'unmod'
170
170
171 CONTEXT = 'context'
171 CONTEXT = 'context'
172
172
173
173
174 class DiffProcessor(object):
174 class DiffProcessor(object):
175 """
175 """
176 Give it a unified or git diff and it returns a list of the files that were
176 Give it a unified or git diff and it returns a list of the files that were
177 mentioned in the diff together with a dict of meta information that
177 mentioned in the diff together with a dict of meta information that
178 can be used to render it in a HTML template.
178 can be used to render it in a HTML template.
179
179
180 .. note:: Unicode handling
180 .. note:: Unicode handling
181
181
182 The original diffs are a byte sequence and can contain filenames
182 The original diffs are a byte sequence and can contain filenames
183 in mixed encodings. This class generally returns `unicode` objects
183 in mixed encodings. This class generally returns `unicode` objects
184 since the result is intended for presentation to the user.
184 since the result is intended for presentation to the user.
185
185
186 """
186 """
187 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
187 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
188 _newline_marker = re.compile(r'^\\ No newline at end of file')
188 _newline_marker = re.compile(r'^\\ No newline at end of file')
189
189
190 # used for inline highlighter word split
190 # used for inline highlighter word split
191 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
191 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
192
192
193 def __init__(self, diff, format='gitdiff', diff_limit=None, file_limit=None, show_full_diff=True):
193 def __init__(self, diff, format='gitdiff', diff_limit=None, file_limit=None, show_full_diff=True):
194 """
194 """
195 :param diff: A `Diff` object representing a diff from a vcs backend
195 :param diff: A `Diff` object representing a diff from a vcs backend
196 :param format: format of diff passed, `udiff` or `gitdiff`
196 :param format: format of diff passed, `udiff` or `gitdiff`
197 :param diff_limit: define the size of diff that is considered "big"
197 :param diff_limit: define the size of diff that is considered "big"
198 based on that parameter cut off will be triggered, set to None
198 based on that parameter cut off will be triggered, set to None
199 to show full diff
199 to show full diff
200 """
200 """
201 self._diff = diff
201 self._diff = diff
202 self._format = format
202 self._format = format
203 self.adds = 0
203 self.adds = 0
204 self.removes = 0
204 self.removes = 0
205 # calculate diff size
205 # calculate diff size
206 self.diff_limit = diff_limit
206 self.diff_limit = diff_limit
207 self.file_limit = file_limit
207 self.file_limit = file_limit
208 self.show_full_diff = show_full_diff
208 self.show_full_diff = show_full_diff
209 self.cur_diff_size = 0
209 self.cur_diff_size = 0
210 self.parsed = False
210 self.parsed = False
211 self.parsed_diff = []
211 self.parsed_diff = []
212
212
213 if format == 'gitdiff':
213 if format == 'gitdiff':
214 self.differ = self._highlight_line_difflib
214 self.differ = self._highlight_line_difflib
215 self._parser = self._parse_gitdiff
215 self._parser = self._parse_gitdiff
216 else:
216 else:
217 self.differ = self._highlight_line_udiff
217 self.differ = self._highlight_line_udiff
218 self._parser = self._parse_udiff
218 self._parser = self._parse_udiff
219
219
220 def _copy_iterator(self):
220 def _copy_iterator(self):
221 """
221 """
222 make a fresh copy of generator, we should not iterate thru
222 make a fresh copy of generator, we should not iterate thru
223 an original as it's needed for repeating operations on
223 an original as it's needed for repeating operations on
224 this instance of DiffProcessor
224 this instance of DiffProcessor
225 """
225 """
226 self.__udiff, iterator_copy = tee(self.__udiff)
226 self.__udiff, iterator_copy = tee(self.__udiff)
227 return iterator_copy
227 return iterator_copy
228
228
229 def _escaper(self, string):
229 def _escaper(self, string):
230 """
230 """
231 Escaper for diff escapes special chars and checks the diff limit
231 Escaper for diff escapes special chars and checks the diff limit
232
232
233 :param string:
233 :param string:
234 """
234 """
235
235
236 self.cur_diff_size += len(string)
236 self.cur_diff_size += len(string)
237
237
238 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
238 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
239 raise DiffLimitExceeded('Diff Limit Exceeded')
239 raise DiffLimitExceeded('Diff Limit Exceeded')
240
240
241 return safe_unicode(string)\
241 return safe_unicode(string)\
242 .replace('&', '&amp;')\
242 .replace('&', '&amp;')\
243 .replace('<', '&lt;')\
243 .replace('<', '&lt;')\
244 .replace('>', '&gt;')
244 .replace('>', '&gt;')
245
245
246 def _line_counter(self, l):
246 def _line_counter(self, l):
247 """
247 """
248 Checks each line and bumps total adds/removes for this diff
248 Checks each line and bumps total adds/removes for this diff
249
249
250 :param l:
250 :param l:
251 """
251 """
252 if l.startswith('+') and not l.startswith('+++'):
252 if l.startswith('+') and not l.startswith('+++'):
253 self.adds += 1
253 self.adds += 1
254 elif l.startswith('-') and not l.startswith('---'):
254 elif l.startswith('-') and not l.startswith('---'):
255 self.removes += 1
255 self.removes += 1
256 return safe_unicode(l)
256 return safe_unicode(l)
257
257
258 def _highlight_line_difflib(self, line, next_):
258 def _highlight_line_difflib(self, line, next_):
259 """
259 """
260 Highlight inline changes in both lines.
260 Highlight inline changes in both lines.
261 """
261 """
262
262
263 if line['action'] == Action.DELETE:
263 if line['action'] == Action.DELETE:
264 old, new = line, next_
264 old, new = line, next_
265 else:
265 else:
266 old, new = next_, line
266 old, new = next_, line
267
267
268 oldwords = self._token_re.split(old['line'])
268 oldwords = self._token_re.split(old['line'])
269 newwords = self._token_re.split(new['line'])
269 newwords = self._token_re.split(new['line'])
270 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
270 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
271
271
272 oldfragments, newfragments = [], []
272 oldfragments, newfragments = [], []
273 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
273 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
274 oldfrag = ''.join(oldwords[i1:i2])
274 oldfrag = ''.join(oldwords[i1:i2])
275 newfrag = ''.join(newwords[j1:j2])
275 newfrag = ''.join(newwords[j1:j2])
276 if tag != 'equal':
276 if tag != 'equal':
277 if oldfrag:
277 if oldfrag:
278 oldfrag = '<del>%s</del>' % oldfrag
278 oldfrag = '<del>%s</del>' % oldfrag
279 if newfrag:
279 if newfrag:
280 newfrag = '<ins>%s</ins>' % newfrag
280 newfrag = '<ins>%s</ins>' % newfrag
281 oldfragments.append(oldfrag)
281 oldfragments.append(oldfrag)
282 newfragments.append(newfrag)
282 newfragments.append(newfrag)
283
283
284 old['line'] = "".join(oldfragments)
284 old['line'] = "".join(oldfragments)
285 new['line'] = "".join(newfragments)
285 new['line'] = "".join(newfragments)
286
286
287 def _highlight_line_udiff(self, line, next_):
287 def _highlight_line_udiff(self, line, next_):
288 """
288 """
289 Highlight inline changes in both lines.
289 Highlight inline changes in both lines.
290 """
290 """
291 start = 0
291 start = 0
292 limit = min(len(line['line']), len(next_['line']))
292 limit = min(len(line['line']), len(next_['line']))
293 while start < limit and line['line'][start] == next_['line'][start]:
293 while start < limit and line['line'][start] == next_['line'][start]:
294 start += 1
294 start += 1
295 end = -1
295 end = -1
296 limit -= start
296 limit -= start
297 while -end <= limit and line['line'][end] == next_['line'][end]:
297 while -end <= limit and line['line'][end] == next_['line'][end]:
298 end -= 1
298 end -= 1
299 end += 1
299 end += 1
300 if start or end:
300 if start or end:
301 def do(l):
301 def do(l):
302 last = end + len(l['line'])
302 last = end + len(l['line'])
303 if l['action'] == Action.ADD:
303 if l['action'] == Action.ADD:
304 tag = 'ins'
304 tag = 'ins'
305 else:
305 else:
306 tag = 'del'
306 tag = 'del'
307 l['line'] = '%s<%s>%s</%s>%s' % (
307 l['line'] = '%s<%s>%s</%s>%s' % (
308 l['line'][:start],
308 l['line'][:start],
309 tag,
309 tag,
310 l['line'][start:last],
310 l['line'][start:last],
311 tag,
311 tag,
312 l['line'][last:]
312 l['line'][last:]
313 )
313 )
314 do(line)
314 do(line)
315 do(next_)
315 do(next_)
316
316
317 def _clean_line(self, line, command):
317 def _clean_line(self, line, command):
318 if command in ['+', '-', ' ']:
318 if command in ['+', '-', ' ']:
319 # only modify the line if it's actually a diff thing
319 # only modify the line if it's actually a diff thing
320 line = line[1:]
320 line = line[1:]
321 return line
321 return line
322
322
323 def _parse_gitdiff(self, inline_diff=True):
323 def _parse_gitdiff(self, inline_diff=True):
324 _files = []
324 _files = []
325 diff_container = lambda arg: arg
325 diff_container = lambda arg: arg
326
326
327 for chunk in self._diff.chunks():
327 for chunk in self._diff.chunks():
328 head = chunk.header
328 head = chunk.header
329
329
330 diff = imap(self._escaper, chunk.diff.splitlines(1))
330 diff = imap(self._escaper, chunk.diff.splitlines(1))
331 raw_diff = chunk.raw
331 raw_diff = chunk.raw
332 limited_diff = False
332 limited_diff = False
333 exceeds_limit = False
333 exceeds_limit = False
334
334
335 op = None
335 op = None
336 stats = {
336 stats = {
337 'added': 0,
337 'added': 0,
338 'deleted': 0,
338 'deleted': 0,
339 'binary': False,
339 'binary': False,
340 'ops': {},
340 'ops': {},
341 }
341 }
342
342
343 if head['deleted_file_mode']:
343 if head['deleted_file_mode']:
344 op = OPS.DEL
344 op = OPS.DEL
345 stats['binary'] = True
345 stats['binary'] = True
346 stats['ops'][DEL_FILENODE] = 'deleted file'
346 stats['ops'][DEL_FILENODE] = 'deleted file'
347
347
348 elif head['new_file_mode']:
348 elif head['new_file_mode']:
349 op = OPS.ADD
349 op = OPS.ADD
350 stats['binary'] = True
350 stats['binary'] = True
351 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
351 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
352 else: # modify operation, can be copy, rename or chmod
352 else: # modify operation, can be copy, rename or chmod
353
353
354 # CHMOD
354 # CHMOD
355 if head['new_mode'] and head['old_mode']:
355 if head['new_mode'] and head['old_mode']:
356 op = OPS.MOD
356 op = OPS.MOD
357 stats['binary'] = True
357 stats['binary'] = True
358 stats['ops'][CHMOD_FILENODE] = (
358 stats['ops'][CHMOD_FILENODE] = (
359 'modified file chmod %s => %s' % (
359 'modified file chmod %s => %s' % (
360 head['old_mode'], head['new_mode']))
360 head['old_mode'], head['new_mode']))
361 # RENAME
361 # RENAME
362 if head['rename_from'] != head['rename_to']:
362 if head['rename_from'] != head['rename_to']:
363 op = OPS.MOD
363 op = OPS.MOD
364 stats['binary'] = True
364 stats['binary'] = True
365 stats['ops'][RENAMED_FILENODE] = (
365 stats['ops'][RENAMED_FILENODE] = (
366 'file renamed from %s to %s' % (
366 'file renamed from %s to %s' % (
367 head['rename_from'], head['rename_to']))
367 head['rename_from'], head['rename_to']))
368 # COPY
368 # COPY
369 if head.get('copy_from') and head.get('copy_to'):
369 if head.get('copy_from') and head.get('copy_to'):
370 op = OPS.MOD
370 op = OPS.MOD
371 stats['binary'] = True
371 stats['binary'] = True
372 stats['ops'][COPIED_FILENODE] = (
372 stats['ops'][COPIED_FILENODE] = (
373 'file copied from %s to %s' % (
373 'file copied from %s to %s' % (
374 head['copy_from'], head['copy_to']))
374 head['copy_from'], head['copy_to']))
375
375
376 # If our new parsed headers didn't match anything fallback to
376 # If our new parsed headers didn't match anything fallback to
377 # old style detection
377 # old style detection
378 if op is None:
378 if op is None:
379 if not head['a_file'] and head['b_file']:
379 if not head['a_file'] and head['b_file']:
380 op = OPS.ADD
380 op = OPS.ADD
381 stats['binary'] = True
381 stats['binary'] = True
382 stats['ops'][NEW_FILENODE] = 'new file'
382 stats['ops'][NEW_FILENODE] = 'new file'
383
383
384 elif head['a_file'] and not head['b_file']:
384 elif head['a_file'] and not head['b_file']:
385 op = OPS.DEL
385 op = OPS.DEL
386 stats['binary'] = True
386 stats['binary'] = True
387 stats['ops'][DEL_FILENODE] = 'deleted file'
387 stats['ops'][DEL_FILENODE] = 'deleted file'
388
388
389 # it's not ADD not DELETE
389 # it's not ADD not DELETE
390 if op is None:
390 if op is None:
391 op = OPS.MOD
391 op = OPS.MOD
392 stats['binary'] = True
392 stats['binary'] = True
393 stats['ops'][MOD_FILENODE] = 'modified file'
393 stats['ops'][MOD_FILENODE] = 'modified file'
394
394
395 # a real non-binary diff
395 # a real non-binary diff
396 if head['a_file'] or head['b_file']:
396 if head['a_file'] or head['b_file']:
397 try:
397 try:
398 raw_diff, chunks, _stats = self._parse_lines(diff)
398 raw_diff, chunks, _stats = self._parse_lines(diff)
399 stats['binary'] = False
399 stats['binary'] = False
400 stats['added'] = _stats[0]
400 stats['added'] = _stats[0]
401 stats['deleted'] = _stats[1]
401 stats['deleted'] = _stats[1]
402 # explicit mark that it's a modified file
402 # explicit mark that it's a modified file
403 if op == OPS.MOD:
403 if op == OPS.MOD:
404 stats['ops'][MOD_FILENODE] = 'modified file'
404 stats['ops'][MOD_FILENODE] = 'modified file'
405 exceeds_limit = len(raw_diff) > self.file_limit
405 exceeds_limit = len(raw_diff) > self.file_limit
406
406
407 # changed from _escaper function so we validate size of
407 # changed from _escaper function so we validate size of
408 # each file instead of the whole diff
408 # each file instead of the whole diff
409 # diff will hide big files but still show small ones
409 # diff will hide big files but still show small ones
410 # from my tests, big files are fairly safe to be parsed
410 # from my tests, big files are fairly safe to be parsed
411 # but the browser is the bottleneck
411 # but the browser is the bottleneck
412 if not self.show_full_diff and exceeds_limit:
412 if not self.show_full_diff and exceeds_limit:
413 raise DiffLimitExceeded('File Limit Exceeded')
413 raise DiffLimitExceeded('File Limit Exceeded')
414
414
415 except DiffLimitExceeded:
415 except DiffLimitExceeded:
416 diff_container = lambda _diff: \
416 diff_container = lambda _diff: \
417 LimitedDiffContainer(
417 LimitedDiffContainer(
418 self.diff_limit, self.cur_diff_size, _diff)
418 self.diff_limit, self.cur_diff_size, _diff)
419
419
420 exceeds_limit = len(raw_diff) > self.file_limit
420 exceeds_limit = len(raw_diff) > self.file_limit
421 limited_diff = True
421 limited_diff = True
422 chunks = []
422 chunks = []
423
423
424 else: # GIT format binary patch, or possibly empty diff
424 else: # GIT format binary patch, or possibly empty diff
425 if head['bin_patch']:
425 if head['bin_patch']:
426 # we have operation already extracted, but we mark simply
426 # we have operation already extracted, but we mark simply
427 # it's a diff we wont show for binary files
427 # it's a diff we wont show for binary files
428 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
428 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
429 chunks = []
429 chunks = []
430
430
431 if chunks and not self.show_full_diff and op == OPS.DEL:
431 if chunks and not self.show_full_diff and op == OPS.DEL:
432 # if not full diff mode show deleted file contents
432 # if not full diff mode show deleted file contents
433 # TODO: anderson: if the view is not too big, there is no way
433 # TODO: anderson: if the view is not too big, there is no way
434 # to see the content of the file
434 # to see the content of the file
435 chunks = []
435 chunks = []
436
436
437 chunks.insert(0, [{
437 chunks.insert(0, [{
438 'old_lineno': '',
438 'old_lineno': '',
439 'new_lineno': '',
439 'new_lineno': '',
440 'action': Action.CONTEXT,
440 'action': Action.CONTEXT,
441 'line': msg,
441 'line': msg,
442 } for _op, msg in stats['ops'].iteritems()
442 } for _op, msg in stats['ops'].iteritems()
443 if _op not in [MOD_FILENODE]])
443 if _op not in [MOD_FILENODE]])
444
444
445 _files.append({
445 _files.append({
446 'filename': safe_unicode(head['b_path']),
446 'filename': safe_unicode(head['b_path']),
447 'old_revision': head['a_blob_id'],
447 'old_revision': head['a_blob_id'],
448 'new_revision': head['b_blob_id'],
448 'new_revision': head['b_blob_id'],
449 'chunks': chunks,
449 'chunks': chunks,
450 'raw_diff': safe_unicode(raw_diff),
450 'raw_diff': safe_unicode(raw_diff),
451 'operation': op,
451 'operation': op,
452 'stats': stats,
452 'stats': stats,
453 'exceeds_limit': exceeds_limit,
453 'exceeds_limit': exceeds_limit,
454 'is_limited_diff': limited_diff,
454 'is_limited_diff': limited_diff,
455 })
455 })
456
456
457 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
457 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
458 OPS.DEL: 2}.get(info['operation'])
458 OPS.DEL: 2}.get(info['operation'])
459
459
460 if not inline_diff:
460 if not inline_diff:
461 return diff_container(sorted(_files, key=sorter))
461 return diff_container(sorted(_files, key=sorter))
462
462
463 # highlight inline changes
463 # highlight inline changes
464 for diff_data in _files:
464 for diff_data in _files:
465 for chunk in diff_data['chunks']:
465 for chunk in diff_data['chunks']:
466 lineiter = iter(chunk)
466 lineiter = iter(chunk)
467 try:
467 try:
468 while 1:
468 while 1:
469 line = lineiter.next()
469 line = lineiter.next()
470 if line['action'] not in (
470 if line['action'] not in (
471 Action.UNMODIFIED, Action.CONTEXT):
471 Action.UNMODIFIED, Action.CONTEXT):
472 nextline = lineiter.next()
472 nextline = lineiter.next()
473 if nextline['action'] in ['unmod', 'context'] or \
473 if nextline['action'] in ['unmod', 'context'] or \
474 nextline['action'] == line['action']:
474 nextline['action'] == line['action']:
475 continue
475 continue
476 self.differ(line, nextline)
476 self.differ(line, nextline)
477 except StopIteration:
477 except StopIteration:
478 pass
478 pass
479
479
480 return diff_container(sorted(_files, key=sorter))
480 return diff_container(sorted(_files, key=sorter))
481
481
482 def _parse_udiff(self, inline_diff=True):
482 def _parse_udiff(self, inline_diff=True):
483 raise NotImplementedError()
483 raise NotImplementedError()
484
484
485 def _parse_lines(self, diff):
485 def _parse_lines(self, diff):
486 """
486 """
487 Parse the diff an return data for the template.
487 Parse the diff an return data for the template.
488 """
488 """
489
489
490 lineiter = iter(diff)
490 lineiter = iter(diff)
491 stats = [0, 0]
491 stats = [0, 0]
492 chunks = []
492 chunks = []
493 raw_diff = []
493 raw_diff = []
494
494
495 try:
495 try:
496 line = lineiter.next()
496 line = lineiter.next()
497
497
498 while line:
498 while line:
499 raw_diff.append(line)
499 raw_diff.append(line)
500 lines = []
500 lines = []
501 chunks.append(lines)
501 chunks.append(lines)
502
502
503 match = self._chunk_re.match(line)
503 match = self._chunk_re.match(line)
504
504
505 if not match:
505 if not match:
506 break
506 break
507
507
508 gr = match.groups()
508 gr = match.groups()
509 (old_line, old_end,
509 (old_line, old_end,
510 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
510 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
511 old_line -= 1
511 old_line -= 1
512 new_line -= 1
512 new_line -= 1
513
513
514 context = len(gr) == 5
514 context = len(gr) == 5
515 old_end += old_line
515 old_end += old_line
516 new_end += new_line
516 new_end += new_line
517
517
518 if context:
518 if context:
519 # skip context only if it's first line
519 # skip context only if it's first line
520 if int(gr[0]) > 1:
520 if int(gr[0]) > 1:
521 lines.append({
521 lines.append({
522 'old_lineno': '...',
522 'old_lineno': '...',
523 'new_lineno': '...',
523 'new_lineno': '...',
524 'action': Action.CONTEXT,
524 'action': Action.CONTEXT,
525 'line': line,
525 'line': line,
526 })
526 })
527
527
528 line = lineiter.next()
528 line = lineiter.next()
529
529
530 while old_line < old_end or new_line < new_end:
530 while old_line < old_end or new_line < new_end:
531 command = ' '
531 command = ' '
532 if line:
532 if line:
533 command = line[0]
533 command = line[0]
534
534
535 affects_old = affects_new = False
535 affects_old = affects_new = False
536
536
537 # ignore those if we don't expect them
537 # ignore those if we don't expect them
538 if command in '#@':
538 if command in '#@':
539 continue
539 continue
540 elif command == '+':
540 elif command == '+':
541 affects_new = True
541 affects_new = True
542 action = Action.ADD
542 action = Action.ADD
543 stats[0] += 1
543 stats[0] += 1
544 elif command == '-':
544 elif command == '-':
545 affects_old = True
545 affects_old = True
546 action = Action.DELETE
546 action = Action.DELETE
547 stats[1] += 1
547 stats[1] += 1
548 else:
548 else:
549 affects_old = affects_new = True
549 affects_old = affects_new = True
550 action = Action.UNMODIFIED
550 action = Action.UNMODIFIED
551
551
552 if not self._newline_marker.match(line):
552 if not self._newline_marker.match(line):
553 old_line += affects_old
553 old_line += affects_old
554 new_line += affects_new
554 new_line += affects_new
555 lines.append({
555 lines.append({
556 'old_lineno': affects_old and old_line or '',
556 'old_lineno': affects_old and old_line or '',
557 'new_lineno': affects_new and new_line or '',
557 'new_lineno': affects_new and new_line or '',
558 'action': action,
558 'action': action,
559 'line': self._clean_line(line, command)
559 'line': self._clean_line(line, command)
560 })
560 })
561 raw_diff.append(line)
561 raw_diff.append(line)
562
562
563 line = lineiter.next()
563 line = lineiter.next()
564
564
565 if self._newline_marker.match(line):
565 if self._newline_marker.match(line):
566 # we need to append to lines, since this is not
566 # we need to append to lines, since this is not
567 # counted in the line specs of diff
567 # counted in the line specs of diff
568 lines.append({
568 lines.append({
569 'old_lineno': '...',
569 'old_lineno': '...',
570 'new_lineno': '...',
570 'new_lineno': '...',
571 'action': Action.CONTEXT,
571 'action': Action.CONTEXT,
572 'line': self._clean_line(line, command)
572 'line': self._clean_line(line, command)
573 })
573 })
574
574
575 except StopIteration:
575 except StopIteration:
576 pass
576 pass
577 return ''.join(raw_diff), chunks, stats
577 return ''.join(raw_diff), chunks, stats
578
578
579 def _safe_id(self, idstring):
579 def _safe_id(self, idstring):
580 """Make a string safe for including in an id attribute.
580 """Make a string safe for including in an id attribute.
581
581
582 The HTML spec says that id attributes 'must begin with
582 The HTML spec says that id attributes 'must begin with
583 a letter ([A-Za-z]) and may be followed by any number
583 a letter ([A-Za-z]) and may be followed by any number
584 of letters, digits ([0-9]), hyphens ("-"), underscores
584 of letters, digits ([0-9]), hyphens ("-"), underscores
585 ("_"), colons (":"), and periods (".")'. These regexps
585 ("_"), colons (":"), and periods (".")'. These regexps
586 are slightly over-zealous, in that they remove colons
586 are slightly over-zealous, in that they remove colons
587 and periods unnecessarily.
587 and periods unnecessarily.
588
588
589 Whitespace is transformed into underscores, and then
589 Whitespace is transformed into underscores, and then
590 anything which is not a hyphen or a character that
590 anything which is not a hyphen or a character that
591 matches \w (alphanumerics and underscore) is removed.
591 matches \w (alphanumerics and underscore) is removed.
592
592
593 """
593 """
594 # Transform all whitespace to underscore
594 # Transform all whitespace to underscore
595 idstring = re.sub(r'\s', "_", '%s' % idstring)
595 idstring = re.sub(r'\s', "_", '%s' % idstring)
596 # Remove everything that is not a hyphen or a member of \w
596 # Remove everything that is not a hyphen or a member of \w
597 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
597 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
598 return idstring
598 return idstring
599
599
600 def prepare(self, inline_diff=True):
600 def prepare(self, inline_diff=True):
601 """
601 """
602 Prepare the passed udiff for HTML rendering.
602 Prepare the passed udiff for HTML rendering.
603
603
604 :return: A list of dicts with diff information.
604 :return: A list of dicts with diff information.
605 """
605 """
606 parsed = self._parser(inline_diff=inline_diff)
606 parsed = self._parser(inline_diff=inline_diff)
607 self.parsed = True
607 self.parsed = True
608 self.parsed_diff = parsed
608 self.parsed_diff = parsed
609 return parsed
609 return parsed
610
610
611 def as_raw(self, diff_lines=None):
611 def as_raw(self, diff_lines=None):
612 """
612 """
613 Returns raw diff as a byte string
613 Returns raw diff as a byte string
614 """
614 """
615 return self._diff.raw
615 return self._diff.raw
616
616
617 def as_html(self, table_class='code-difftable', line_class='line',
617 def as_html(self, table_class='code-difftable', line_class='line',
618 old_lineno_class='lineno old', new_lineno_class='lineno new',
618 old_lineno_class='lineno old', new_lineno_class='lineno new',
619 code_class='code', enable_comments=False, parsed_lines=None):
619 code_class='code', enable_comments=False, parsed_lines=None):
620 """
620 """
621 Return given diff as html table with customized css classes
621 Return given diff as html table with customized css classes
622 """
622 """
623 def _link_to_if(condition, label, url):
623 def _link_to_if(condition, label, url):
624 """
624 """
625 Generates a link if condition is meet or just the label if not.
625 Generates a link if condition is meet or just the label if not.
626 """
626 """
627
627
628 if condition:
628 if condition:
629 return '''<a href="%(url)s" class="tooltip" title="Click to select line">%(label)s</a>''' % {
629 return '''<a href="%(url)s" class="tooltip"
630 title="%(title)s">%(label)s</a>''' % {
631 'title': _('Click to select line'),
630 'url': url,
632 'url': url,
631 'label': label
633 'label': label
632 }
634 }
633 else:
635 else:
634 return label
636 return label
635 if not self.parsed:
637 if not self.parsed:
636 self.prepare()
638 self.prepare()
637
639
638 diff_lines = self.parsed_diff
640 diff_lines = self.parsed_diff
639 if parsed_lines:
641 if parsed_lines:
640 diff_lines = parsed_lines
642 diff_lines = parsed_lines
641
643
642 _html_empty = True
644 _html_empty = True
643 _html = []
645 _html = []
644 _html.append('''<table class="%(table_class)s">\n''' % {
646 _html.append('''<table class="%(table_class)s">\n''' % {
645 'table_class': table_class
647 'table_class': table_class
646 })
648 })
647
649
648 for diff in diff_lines:
650 for diff in diff_lines:
649 for line in diff['chunks']:
651 for line in diff['chunks']:
650 _html_empty = False
652 _html_empty = False
651 for change in line:
653 for change in line:
652 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
654 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
653 'lc': line_class,
655 'lc': line_class,
654 'action': change['action']
656 'action': change['action']
655 })
657 })
656 anchor_old_id = ''
658 anchor_old_id = ''
657 anchor_new_id = ''
659 anchor_new_id = ''
658 anchor_old = "%(filename)s_o%(oldline_no)s" % {
660 anchor_old = "%(filename)s_o%(oldline_no)s" % {
659 'filename': self._safe_id(diff['filename']),
661 'filename': self._safe_id(diff['filename']),
660 'oldline_no': change['old_lineno']
662 'oldline_no': change['old_lineno']
661 }
663 }
662 anchor_new = "%(filename)s_n%(oldline_no)s" % {
664 anchor_new = "%(filename)s_n%(oldline_no)s" % {
663 'filename': self._safe_id(diff['filename']),
665 'filename': self._safe_id(diff['filename']),
664 'oldline_no': change['new_lineno']
666 'oldline_no': change['new_lineno']
665 }
667 }
666 cond_old = (change['old_lineno'] != '...' and
668 cond_old = (change['old_lineno'] != '...' and
667 change['old_lineno'])
669 change['old_lineno'])
668 cond_new = (change['new_lineno'] != '...' and
670 cond_new = (change['new_lineno'] != '...' and
669 change['new_lineno'])
671 change['new_lineno'])
670 if cond_old:
672 if cond_old:
671 anchor_old_id = 'id="%s"' % anchor_old
673 anchor_old_id = 'id="%s"' % anchor_old
672 if cond_new:
674 if cond_new:
673 anchor_new_id = 'id="%s"' % anchor_new
675 anchor_new_id = 'id="%s"' % anchor_new
674
676
675 if change['action'] != Action.CONTEXT:
677 if change['action'] != Action.CONTEXT:
676 anchor_link = True
678 anchor_link = True
677 else:
679 else:
678 anchor_link = False
680 anchor_link = False
679
681
680 ###########################################################
682 ###########################################################
681 # COMMENT ICON
683 # COMMENT ICON
682 ###########################################################
684 ###########################################################
683 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
685 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
684
686
685 if enable_comments and change['action'] != Action.CONTEXT:
687 if enable_comments and change['action'] != Action.CONTEXT:
686 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
688 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
687
689
688 _html.append('''</span></td>\n''')
690 _html.append('''</span></td>\n''')
689
691
690 ###########################################################
692 ###########################################################
691 # OLD LINE NUMBER
693 # OLD LINE NUMBER
692 ###########################################################
694 ###########################################################
693 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
695 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
694 'a_id': anchor_old_id,
696 'a_id': anchor_old_id,
695 'olc': old_lineno_class
697 'olc': old_lineno_class
696 })
698 })
697
699
698 _html.append('''%(link)s''' % {
700 _html.append('''%(link)s''' % {
699 'link': _link_to_if(anchor_link, change['old_lineno'],
701 'link': _link_to_if(anchor_link, change['old_lineno'],
700 '#%s' % anchor_old)
702 '#%s' % anchor_old)
701 })
703 })
702 _html.append('''</td>\n''')
704 _html.append('''</td>\n''')
703 ###########################################################
705 ###########################################################
704 # NEW LINE NUMBER
706 # NEW LINE NUMBER
705 ###########################################################
707 ###########################################################
706
708
707 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
709 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
708 'a_id': anchor_new_id,
710 'a_id': anchor_new_id,
709 'nlc': new_lineno_class
711 'nlc': new_lineno_class
710 })
712 })
711
713
712 _html.append('''%(link)s''' % {
714 _html.append('''%(link)s''' % {
713 'link': _link_to_if(anchor_link, change['new_lineno'],
715 'link': _link_to_if(anchor_link, change['new_lineno'],
714 '#%s' % anchor_new)
716 '#%s' % anchor_new)
715 })
717 })
716 _html.append('''</td>\n''')
718 _html.append('''</td>\n''')
717 ###########################################################
719 ###########################################################
718 # CODE
720 # CODE
719 ###########################################################
721 ###########################################################
720 code_classes = [code_class]
722 code_classes = [code_class]
721 if (not enable_comments or
723 if (not enable_comments or
722 change['action'] == Action.CONTEXT):
724 change['action'] == Action.CONTEXT):
723 code_classes.append('no-comment')
725 code_classes.append('no-comment')
724 _html.append('\t<td class="%s">' % ' '.join(code_classes))
726 _html.append('\t<td class="%s">' % ' '.join(code_classes))
725 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
727 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
726 'code': change['line']
728 'code': change['line']
727 })
729 })
728
730
729 _html.append('''\t</td>''')
731 _html.append('''\t</td>''')
730 _html.append('''\n</tr>\n''')
732 _html.append('''\n</tr>\n''')
731 _html.append('''</table>''')
733 _html.append('''</table>''')
732 if _html_empty:
734 if _html_empty:
733 return None
735 return None
734 return ''.join(_html)
736 return ''.join(_html)
735
737
736 def stat(self):
738 def stat(self):
737 """
739 """
738 Returns tuple of added, and removed lines for this instance
740 Returns tuple of added, and removed lines for this instance
739 """
741 """
740 return self.adds, self.removes
742 return self.adds, self.removes
741
743
742 def get_context_of_line(
744 def get_context_of_line(
743 self, path, diff_line=None, context_before=3, context_after=3):
745 self, path, diff_line=None, context_before=3, context_after=3):
744 """
746 """
745 Returns the context lines for the specified diff line.
747 Returns the context lines for the specified diff line.
746
748
747 :type diff_line: :class:`DiffLineNumber`
749 :type diff_line: :class:`DiffLineNumber`
748 """
750 """
749 assert self.parsed, "DiffProcessor is not initialized."
751 assert self.parsed, "DiffProcessor is not initialized."
750
752
751 if None not in diff_line:
753 if None not in diff_line:
752 raise ValueError(
754 raise ValueError(
753 "Cannot specify both line numbers: {}".format(diff_line))
755 "Cannot specify both line numbers: {}".format(diff_line))
754
756
755 file_diff = self._get_file_diff(path)
757 file_diff = self._get_file_diff(path)
756 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
758 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
757
759
758 first_line_to_include = max(idx - context_before, 0)
760 first_line_to_include = max(idx - context_before, 0)
759 first_line_after_context = idx + context_after + 1
761 first_line_after_context = idx + context_after + 1
760 context_lines = chunk[first_line_to_include:first_line_after_context]
762 context_lines = chunk[first_line_to_include:first_line_after_context]
761
763
762 line_contents = [
764 line_contents = [
763 _context_line(line) for line in context_lines
765 _context_line(line) for line in context_lines
764 if _is_diff_content(line)]
766 if _is_diff_content(line)]
765 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
767 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
766 # Once they are fixed, we can drop this line here.
768 # Once they are fixed, we can drop this line here.
767 if line_contents:
769 if line_contents:
768 line_contents[-1] = (
770 line_contents[-1] = (
769 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
771 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
770 return line_contents
772 return line_contents
771
773
772 def find_context(self, path, context, offset=0):
774 def find_context(self, path, context, offset=0):
773 """
775 """
774 Finds the given `context` inside of the diff.
776 Finds the given `context` inside of the diff.
775
777
776 Use the parameter `offset` to specify which offset the target line has
778 Use the parameter `offset` to specify which offset the target line has
777 inside of the given `context`. This way the correct diff line will be
779 inside of the given `context`. This way the correct diff line will be
778 returned.
780 returned.
779
781
780 :param offset: Shall be used to specify the offset of the main line
782 :param offset: Shall be used to specify the offset of the main line
781 within the given `context`.
783 within the given `context`.
782 """
784 """
783 if offset < 0 or offset >= len(context):
785 if offset < 0 or offset >= len(context):
784 raise ValueError(
786 raise ValueError(
785 "Only positive values up to the length of the context "
787 "Only positive values up to the length of the context "
786 "minus one are allowed.")
788 "minus one are allowed.")
787
789
788 matches = []
790 matches = []
789 file_diff = self._get_file_diff(path)
791 file_diff = self._get_file_diff(path)
790
792
791 for chunk in file_diff['chunks']:
793 for chunk in file_diff['chunks']:
792 context_iter = iter(context)
794 context_iter = iter(context)
793 for line_idx, line in enumerate(chunk):
795 for line_idx, line in enumerate(chunk):
794 try:
796 try:
795 if _context_line(line) == context_iter.next():
797 if _context_line(line) == context_iter.next():
796 continue
798 continue
797 except StopIteration:
799 except StopIteration:
798 matches.append((line_idx, chunk))
800 matches.append((line_idx, chunk))
799 context_iter = iter(context)
801 context_iter = iter(context)
800
802
801 # Increment position and triger StopIteration
803 # Increment position and triger StopIteration
802 # if we had a match at the end
804 # if we had a match at the end
803 line_idx += 1
805 line_idx += 1
804 try:
806 try:
805 context_iter.next()
807 context_iter.next()
806 except StopIteration:
808 except StopIteration:
807 matches.append((line_idx, chunk))
809 matches.append((line_idx, chunk))
808
810
809 effective_offset = len(context) - offset
811 effective_offset = len(context) - offset
810 found_at_diff_lines = [
812 found_at_diff_lines = [
811 _line_to_diff_line_number(chunk[idx - effective_offset])
813 _line_to_diff_line_number(chunk[idx - effective_offset])
812 for idx, chunk in matches]
814 for idx, chunk in matches]
813
815
814 return found_at_diff_lines
816 return found_at_diff_lines
815
817
816 def _get_file_diff(self, path):
818 def _get_file_diff(self, path):
817 for file_diff in self.parsed_diff:
819 for file_diff in self.parsed_diff:
818 if file_diff['filename'] == path:
820 if file_diff['filename'] == path:
819 break
821 break
820 else:
822 else:
821 raise FileNotInDiffException("File {} not in diff".format(path))
823 raise FileNotInDiffException("File {} not in diff".format(path))
822 return file_diff
824 return file_diff
823
825
824 def _find_chunk_line_index(self, file_diff, diff_line):
826 def _find_chunk_line_index(self, file_diff, diff_line):
825 for chunk in file_diff['chunks']:
827 for chunk in file_diff['chunks']:
826 for idx, line in enumerate(chunk):
828 for idx, line in enumerate(chunk):
827 if line['old_lineno'] == diff_line.old:
829 if line['old_lineno'] == diff_line.old:
828 return chunk, idx
830 return chunk, idx
829 if line['new_lineno'] == diff_line.new:
831 if line['new_lineno'] == diff_line.new:
830 return chunk, idx
832 return chunk, idx
831 raise LineNotInDiffException(
833 raise LineNotInDiffException(
832 "The line {} is not part of the diff.".format(diff_line))
834 "The line {} is not part of the diff.".format(diff_line))
833
835
834
836
835 def _is_diff_content(line):
837 def _is_diff_content(line):
836 return line['action'] in (
838 return line['action'] in (
837 Action.UNMODIFIED, Action.ADD, Action.DELETE)
839 Action.UNMODIFIED, Action.ADD, Action.DELETE)
838
840
839
841
840 def _context_line(line):
842 def _context_line(line):
841 return (line['action'], line['line'])
843 return (line['action'], line['line'])
842
844
843
845
844 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
846 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
845
847
846
848
847 def _line_to_diff_line_number(line):
849 def _line_to_diff_line_number(line):
848 new_line_no = line['new_lineno'] or None
850 new_line_no = line['new_lineno'] or None
849 old_line_no = line['old_lineno'] or None
851 old_line_no = line['old_lineno'] or None
850 return DiffLineNumber(old=old_line_no, new=new_line_no)
852 return DiffLineNumber(old=old_line_no, new=new_line_no)
851
853
852
854
853 class FileNotInDiffException(Exception):
855 class FileNotInDiffException(Exception):
854 """
856 """
855 Raised when the context for a missing file is requested.
857 Raised when the context for a missing file is requested.
856
858
857 If you request the context for a line in a file which is not part of the
859 If you request the context for a line in a file which is not part of the
858 given diff, then this exception is raised.
860 given diff, then this exception is raised.
859 """
861 """
860
862
861
863
862 class LineNotInDiffException(Exception):
864 class LineNotInDiffException(Exception):
863 """
865 """
864 Raised when the context for a missing line is requested.
866 Raised when the context for a missing line is requested.
865
867
866 If you request the context for a line in a file and this line is not
868 If you request the context for a line in a file and this line is not
867 part of the given diff, then this exception is raised.
869 part of the given diff, then this exception is raised.
868 """
870 """
869
871
870
872
871 class DiffLimitExceeded(Exception):
873 class DiffLimitExceeded(Exception):
872 pass
874 pass
General Comments 0
You need to be logged in to leave comments. Login now