##// END OF EJS Templates
logging: fixed some log calls.
marcink -
r3841:fd62517f default
parent child Browse files
Show More
@@ -1,1254 +1,1254 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2019 RhodeCode GmbH
3 # Copyright (C) 2011-2019 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 os
26 import os
27 import re
27 import re
28 import bz2
28 import bz2
29 import time
29 import time
30
30
31 import collections
31 import collections
32 import difflib
32 import difflib
33 import logging
33 import logging
34 import cPickle as pickle
34 import cPickle as pickle
35 from itertools import tee, imap
35 from itertools import tee, imap
36
36
37 from rhodecode.lib.vcs.exceptions import VCSError
37 from rhodecode.lib.vcs.exceptions import VCSError
38 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
38 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
39 from rhodecode.lib.utils2 import safe_unicode, safe_str
39 from rhodecode.lib.utils2 import safe_unicode, safe_str
40
40
41 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
42
42
43 # define max context, a file with more than this numbers of lines is unusable
43 # define max context, a file with more than this numbers of lines is unusable
44 # in browser anyway
44 # in browser anyway
45 MAX_CONTEXT = 20 * 1024
45 MAX_CONTEXT = 20 * 1024
46 DEFAULT_CONTEXT = 3
46 DEFAULT_CONTEXT = 3
47
47
48
48
49 def get_diff_context(request):
49 def get_diff_context(request):
50 return MAX_CONTEXT if request.GET.get('fullcontext', '') == '1' else DEFAULT_CONTEXT
50 return MAX_CONTEXT if request.GET.get('fullcontext', '') == '1' else DEFAULT_CONTEXT
51
51
52
52
53 def get_diff_whitespace_flag(request):
53 def get_diff_whitespace_flag(request):
54 return request.GET.get('ignorews', '') == '1'
54 return request.GET.get('ignorews', '') == '1'
55
55
56
56
57 class OPS(object):
57 class OPS(object):
58 ADD = 'A'
58 ADD = 'A'
59 MOD = 'M'
59 MOD = 'M'
60 DEL = 'D'
60 DEL = 'D'
61
61
62
62
63 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
63 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
64 """
64 """
65 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
65 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
66
66
67 :param ignore_whitespace: ignore whitespaces in diff
67 :param ignore_whitespace: ignore whitespaces in diff
68 """
68 """
69 # make sure we pass in default context
69 # make sure we pass in default context
70 context = context or 3
70 context = context or 3
71 # protect against IntOverflow when passing HUGE context
71 # protect against IntOverflow when passing HUGE context
72 if context > MAX_CONTEXT:
72 if context > MAX_CONTEXT:
73 context = MAX_CONTEXT
73 context = MAX_CONTEXT
74
74
75 submodules = filter(lambda o: isinstance(o, SubModuleNode),
75 submodules = filter(lambda o: isinstance(o, SubModuleNode),
76 [filenode_new, filenode_old])
76 [filenode_new, filenode_old])
77 if submodules:
77 if submodules:
78 return ''
78 return ''
79
79
80 for filenode in (filenode_old, filenode_new):
80 for filenode in (filenode_old, filenode_new):
81 if not isinstance(filenode, FileNode):
81 if not isinstance(filenode, FileNode):
82 raise VCSError(
82 raise VCSError(
83 "Given object should be FileNode object, not %s"
83 "Given object should be FileNode object, not %s"
84 % filenode.__class__)
84 % filenode.__class__)
85
85
86 repo = filenode_new.commit.repository
86 repo = filenode_new.commit.repository
87 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
87 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
88 new_commit = filenode_new.commit
88 new_commit = filenode_new.commit
89
89
90 vcs_gitdiff = repo.get_diff(
90 vcs_gitdiff = repo.get_diff(
91 old_commit, new_commit, filenode_new.path,
91 old_commit, new_commit, filenode_new.path,
92 ignore_whitespace, context, path1=filenode_old.path)
92 ignore_whitespace, context, path1=filenode_old.path)
93 return vcs_gitdiff
93 return vcs_gitdiff
94
94
95 NEW_FILENODE = 1
95 NEW_FILENODE = 1
96 DEL_FILENODE = 2
96 DEL_FILENODE = 2
97 MOD_FILENODE = 3
97 MOD_FILENODE = 3
98 RENAMED_FILENODE = 4
98 RENAMED_FILENODE = 4
99 COPIED_FILENODE = 5
99 COPIED_FILENODE = 5
100 CHMOD_FILENODE = 6
100 CHMOD_FILENODE = 6
101 BIN_FILENODE = 7
101 BIN_FILENODE = 7
102
102
103
103
104 class LimitedDiffContainer(object):
104 class LimitedDiffContainer(object):
105
105
106 def __init__(self, diff_limit, cur_diff_size, diff):
106 def __init__(self, diff_limit, cur_diff_size, diff):
107 self.diff = diff
107 self.diff = diff
108 self.diff_limit = diff_limit
108 self.diff_limit = diff_limit
109 self.cur_diff_size = cur_diff_size
109 self.cur_diff_size = cur_diff_size
110
110
111 def __getitem__(self, key):
111 def __getitem__(self, key):
112 return self.diff.__getitem__(key)
112 return self.diff.__getitem__(key)
113
113
114 def __iter__(self):
114 def __iter__(self):
115 for l in self.diff:
115 for l in self.diff:
116 yield l
116 yield l
117
117
118
118
119 class Action(object):
119 class Action(object):
120 """
120 """
121 Contains constants for the action value of the lines in a parsed diff.
121 Contains constants for the action value of the lines in a parsed diff.
122 """
122 """
123
123
124 ADD = 'add'
124 ADD = 'add'
125 DELETE = 'del'
125 DELETE = 'del'
126 UNMODIFIED = 'unmod'
126 UNMODIFIED = 'unmod'
127
127
128 CONTEXT = 'context'
128 CONTEXT = 'context'
129 OLD_NO_NL = 'old-no-nl'
129 OLD_NO_NL = 'old-no-nl'
130 NEW_NO_NL = 'new-no-nl'
130 NEW_NO_NL = 'new-no-nl'
131
131
132
132
133 class DiffProcessor(object):
133 class DiffProcessor(object):
134 """
134 """
135 Give it a unified or git diff and it returns a list of the files that were
135 Give it a unified or git diff and it returns a list of the files that were
136 mentioned in the diff together with a dict of meta information that
136 mentioned in the diff together with a dict of meta information that
137 can be used to render it in a HTML template.
137 can be used to render it in a HTML template.
138
138
139 .. note:: Unicode handling
139 .. note:: Unicode handling
140
140
141 The original diffs are a byte sequence and can contain filenames
141 The original diffs are a byte sequence and can contain filenames
142 in mixed encodings. This class generally returns `unicode` objects
142 in mixed encodings. This class generally returns `unicode` objects
143 since the result is intended for presentation to the user.
143 since the result is intended for presentation to the user.
144
144
145 """
145 """
146 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
146 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
147 _newline_marker = re.compile(r'^\\ No newline at end of file')
147 _newline_marker = re.compile(r'^\\ No newline at end of file')
148
148
149 # used for inline highlighter word split
149 # used for inline highlighter word split
150 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
150 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
151
151
152 # collapse ranges of commits over given number
152 # collapse ranges of commits over given number
153 _collapse_commits_over = 5
153 _collapse_commits_over = 5
154
154
155 def __init__(self, diff, format='gitdiff', diff_limit=None,
155 def __init__(self, diff, format='gitdiff', diff_limit=None,
156 file_limit=None, show_full_diff=True):
156 file_limit=None, show_full_diff=True):
157 """
157 """
158 :param diff: A `Diff` object representing a diff from a vcs backend
158 :param diff: A `Diff` object representing a diff from a vcs backend
159 :param format: format of diff passed, `udiff` or `gitdiff`
159 :param format: format of diff passed, `udiff` or `gitdiff`
160 :param diff_limit: define the size of diff that is considered "big"
160 :param diff_limit: define the size of diff that is considered "big"
161 based on that parameter cut off will be triggered, set to None
161 based on that parameter cut off will be triggered, set to None
162 to show full diff
162 to show full diff
163 """
163 """
164 self._diff = diff
164 self._diff = diff
165 self._format = format
165 self._format = format
166 self.adds = 0
166 self.adds = 0
167 self.removes = 0
167 self.removes = 0
168 # calculate diff size
168 # calculate diff size
169 self.diff_limit = diff_limit
169 self.diff_limit = diff_limit
170 self.file_limit = file_limit
170 self.file_limit = file_limit
171 self.show_full_diff = show_full_diff
171 self.show_full_diff = show_full_diff
172 self.cur_diff_size = 0
172 self.cur_diff_size = 0
173 self.parsed = False
173 self.parsed = False
174 self.parsed_diff = []
174 self.parsed_diff = []
175
175
176 log.debug('Initialized DiffProcessor with %s mode', format)
176 log.debug('Initialized DiffProcessor with %s mode', format)
177 if format == 'gitdiff':
177 if format == 'gitdiff':
178 self.differ = self._highlight_line_difflib
178 self.differ = self._highlight_line_difflib
179 self._parser = self._parse_gitdiff
179 self._parser = self._parse_gitdiff
180 else:
180 else:
181 self.differ = self._highlight_line_udiff
181 self.differ = self._highlight_line_udiff
182 self._parser = self._new_parse_gitdiff
182 self._parser = self._new_parse_gitdiff
183
183
184 def _copy_iterator(self):
184 def _copy_iterator(self):
185 """
185 """
186 make a fresh copy of generator, we should not iterate thru
186 make a fresh copy of generator, we should not iterate thru
187 an original as it's needed for repeating operations on
187 an original as it's needed for repeating operations on
188 this instance of DiffProcessor
188 this instance of DiffProcessor
189 """
189 """
190 self.__udiff, iterator_copy = tee(self.__udiff)
190 self.__udiff, iterator_copy = tee(self.__udiff)
191 return iterator_copy
191 return iterator_copy
192
192
193 def _escaper(self, string):
193 def _escaper(self, string):
194 """
194 """
195 Escaper for diff escapes special chars and checks the diff limit
195 Escaper for diff escapes special chars and checks the diff limit
196
196
197 :param string:
197 :param string:
198 """
198 """
199 self.cur_diff_size += len(string)
199 self.cur_diff_size += len(string)
200
200
201 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
201 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
202 raise DiffLimitExceeded('Diff Limit Exceeded')
202 raise DiffLimitExceeded('Diff Limit Exceeded')
203
203
204 return string \
204 return string \
205 .replace('&', '&amp;')\
205 .replace('&', '&amp;')\
206 .replace('<', '&lt;')\
206 .replace('<', '&lt;')\
207 .replace('>', '&gt;')
207 .replace('>', '&gt;')
208
208
209 def _line_counter(self, l):
209 def _line_counter(self, l):
210 """
210 """
211 Checks each line and bumps total adds/removes for this diff
211 Checks each line and bumps total adds/removes for this diff
212
212
213 :param l:
213 :param l:
214 """
214 """
215 if l.startswith('+') and not l.startswith('+++'):
215 if l.startswith('+') and not l.startswith('+++'):
216 self.adds += 1
216 self.adds += 1
217 elif l.startswith('-') and not l.startswith('---'):
217 elif l.startswith('-') and not l.startswith('---'):
218 self.removes += 1
218 self.removes += 1
219 return safe_unicode(l)
219 return safe_unicode(l)
220
220
221 def _highlight_line_difflib(self, line, next_):
221 def _highlight_line_difflib(self, line, next_):
222 """
222 """
223 Highlight inline changes in both lines.
223 Highlight inline changes in both lines.
224 """
224 """
225
225
226 if line['action'] == Action.DELETE:
226 if line['action'] == Action.DELETE:
227 old, new = line, next_
227 old, new = line, next_
228 else:
228 else:
229 old, new = next_, line
229 old, new = next_, line
230
230
231 oldwords = self._token_re.split(old['line'])
231 oldwords = self._token_re.split(old['line'])
232 newwords = self._token_re.split(new['line'])
232 newwords = self._token_re.split(new['line'])
233 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
233 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
234
234
235 oldfragments, newfragments = [], []
235 oldfragments, newfragments = [], []
236 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
236 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
237 oldfrag = ''.join(oldwords[i1:i2])
237 oldfrag = ''.join(oldwords[i1:i2])
238 newfrag = ''.join(newwords[j1:j2])
238 newfrag = ''.join(newwords[j1:j2])
239 if tag != 'equal':
239 if tag != 'equal':
240 if oldfrag:
240 if oldfrag:
241 oldfrag = '<del>%s</del>' % oldfrag
241 oldfrag = '<del>%s</del>' % oldfrag
242 if newfrag:
242 if newfrag:
243 newfrag = '<ins>%s</ins>' % newfrag
243 newfrag = '<ins>%s</ins>' % newfrag
244 oldfragments.append(oldfrag)
244 oldfragments.append(oldfrag)
245 newfragments.append(newfrag)
245 newfragments.append(newfrag)
246
246
247 old['line'] = "".join(oldfragments)
247 old['line'] = "".join(oldfragments)
248 new['line'] = "".join(newfragments)
248 new['line'] = "".join(newfragments)
249
249
250 def _highlight_line_udiff(self, line, next_):
250 def _highlight_line_udiff(self, line, next_):
251 """
251 """
252 Highlight inline changes in both lines.
252 Highlight inline changes in both lines.
253 """
253 """
254 start = 0
254 start = 0
255 limit = min(len(line['line']), len(next_['line']))
255 limit = min(len(line['line']), len(next_['line']))
256 while start < limit and line['line'][start] == next_['line'][start]:
256 while start < limit and line['line'][start] == next_['line'][start]:
257 start += 1
257 start += 1
258 end = -1
258 end = -1
259 limit -= start
259 limit -= start
260 while -end <= limit and line['line'][end] == next_['line'][end]:
260 while -end <= limit and line['line'][end] == next_['line'][end]:
261 end -= 1
261 end -= 1
262 end += 1
262 end += 1
263 if start or end:
263 if start or end:
264 def do(l):
264 def do(l):
265 last = end + len(l['line'])
265 last = end + len(l['line'])
266 if l['action'] == Action.ADD:
266 if l['action'] == Action.ADD:
267 tag = 'ins'
267 tag = 'ins'
268 else:
268 else:
269 tag = 'del'
269 tag = 'del'
270 l['line'] = '%s<%s>%s</%s>%s' % (
270 l['line'] = '%s<%s>%s</%s>%s' % (
271 l['line'][:start],
271 l['line'][:start],
272 tag,
272 tag,
273 l['line'][start:last],
273 l['line'][start:last],
274 tag,
274 tag,
275 l['line'][last:]
275 l['line'][last:]
276 )
276 )
277 do(line)
277 do(line)
278 do(next_)
278 do(next_)
279
279
280 def _clean_line(self, line, command):
280 def _clean_line(self, line, command):
281 if command in ['+', '-', ' ']:
281 if command in ['+', '-', ' ']:
282 # only modify the line if it's actually a diff thing
282 # only modify the line if it's actually a diff thing
283 line = line[1:]
283 line = line[1:]
284 return line
284 return line
285
285
286 def _parse_gitdiff(self, inline_diff=True):
286 def _parse_gitdiff(self, inline_diff=True):
287 _files = []
287 _files = []
288 diff_container = lambda arg: arg
288 diff_container = lambda arg: arg
289
289
290 for chunk in self._diff.chunks():
290 for chunk in self._diff.chunks():
291 head = chunk.header
291 head = chunk.header
292
292
293 diff = imap(self._escaper, self.diff_splitter(chunk.diff))
293 diff = imap(self._escaper, self.diff_splitter(chunk.diff))
294 raw_diff = chunk.raw
294 raw_diff = chunk.raw
295 limited_diff = False
295 limited_diff = False
296 exceeds_limit = False
296 exceeds_limit = False
297
297
298 op = None
298 op = None
299 stats = {
299 stats = {
300 'added': 0,
300 'added': 0,
301 'deleted': 0,
301 'deleted': 0,
302 'binary': False,
302 'binary': False,
303 'ops': {},
303 'ops': {},
304 }
304 }
305
305
306 if head['deleted_file_mode']:
306 if head['deleted_file_mode']:
307 op = OPS.DEL
307 op = OPS.DEL
308 stats['binary'] = True
308 stats['binary'] = True
309 stats['ops'][DEL_FILENODE] = 'deleted file'
309 stats['ops'][DEL_FILENODE] = 'deleted file'
310
310
311 elif head['new_file_mode']:
311 elif head['new_file_mode']:
312 op = OPS.ADD
312 op = OPS.ADD
313 stats['binary'] = True
313 stats['binary'] = True
314 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
314 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
315 else: # modify operation, can be copy, rename or chmod
315 else: # modify operation, can be copy, rename or chmod
316
316
317 # CHMOD
317 # CHMOD
318 if head['new_mode'] and head['old_mode']:
318 if head['new_mode'] and head['old_mode']:
319 op = OPS.MOD
319 op = OPS.MOD
320 stats['binary'] = True
320 stats['binary'] = True
321 stats['ops'][CHMOD_FILENODE] = (
321 stats['ops'][CHMOD_FILENODE] = (
322 'modified file chmod %s => %s' % (
322 'modified file chmod %s => %s' % (
323 head['old_mode'], head['new_mode']))
323 head['old_mode'], head['new_mode']))
324 # RENAME
324 # RENAME
325 if head['rename_from'] != head['rename_to']:
325 if head['rename_from'] != head['rename_to']:
326 op = OPS.MOD
326 op = OPS.MOD
327 stats['binary'] = True
327 stats['binary'] = True
328 stats['ops'][RENAMED_FILENODE] = (
328 stats['ops'][RENAMED_FILENODE] = (
329 'file renamed from %s to %s' % (
329 'file renamed from %s to %s' % (
330 head['rename_from'], head['rename_to']))
330 head['rename_from'], head['rename_to']))
331 # COPY
331 # COPY
332 if head.get('copy_from') and head.get('copy_to'):
332 if head.get('copy_from') and head.get('copy_to'):
333 op = OPS.MOD
333 op = OPS.MOD
334 stats['binary'] = True
334 stats['binary'] = True
335 stats['ops'][COPIED_FILENODE] = (
335 stats['ops'][COPIED_FILENODE] = (
336 'file copied from %s to %s' % (
336 'file copied from %s to %s' % (
337 head['copy_from'], head['copy_to']))
337 head['copy_from'], head['copy_to']))
338
338
339 # If our new parsed headers didn't match anything fallback to
339 # If our new parsed headers didn't match anything fallback to
340 # old style detection
340 # old style detection
341 if op is None:
341 if op is None:
342 if not head['a_file'] and head['b_file']:
342 if not head['a_file'] and head['b_file']:
343 op = OPS.ADD
343 op = OPS.ADD
344 stats['binary'] = True
344 stats['binary'] = True
345 stats['ops'][NEW_FILENODE] = 'new file'
345 stats['ops'][NEW_FILENODE] = 'new file'
346
346
347 elif head['a_file'] and not head['b_file']:
347 elif head['a_file'] and not head['b_file']:
348 op = OPS.DEL
348 op = OPS.DEL
349 stats['binary'] = True
349 stats['binary'] = True
350 stats['ops'][DEL_FILENODE] = 'deleted file'
350 stats['ops'][DEL_FILENODE] = 'deleted file'
351
351
352 # it's not ADD not DELETE
352 # it's not ADD not DELETE
353 if op is None:
353 if op is None:
354 op = OPS.MOD
354 op = OPS.MOD
355 stats['binary'] = True
355 stats['binary'] = True
356 stats['ops'][MOD_FILENODE] = 'modified file'
356 stats['ops'][MOD_FILENODE] = 'modified file'
357
357
358 # a real non-binary diff
358 # a real non-binary diff
359 if head['a_file'] or head['b_file']:
359 if head['a_file'] or head['b_file']:
360 try:
360 try:
361 raw_diff, chunks, _stats = self._parse_lines(diff)
361 raw_diff, chunks, _stats = self._parse_lines(diff)
362 stats['binary'] = False
362 stats['binary'] = False
363 stats['added'] = _stats[0]
363 stats['added'] = _stats[0]
364 stats['deleted'] = _stats[1]
364 stats['deleted'] = _stats[1]
365 # explicit mark that it's a modified file
365 # explicit mark that it's a modified file
366 if op == OPS.MOD:
366 if op == OPS.MOD:
367 stats['ops'][MOD_FILENODE] = 'modified file'
367 stats['ops'][MOD_FILENODE] = 'modified file'
368 exceeds_limit = len(raw_diff) > self.file_limit
368 exceeds_limit = len(raw_diff) > self.file_limit
369
369
370 # changed from _escaper function so we validate size of
370 # changed from _escaper function so we validate size of
371 # each file instead of the whole diff
371 # each file instead of the whole diff
372 # diff will hide big files but still show small ones
372 # diff will hide big files but still show small ones
373 # from my tests, big files are fairly safe to be parsed
373 # from my tests, big files are fairly safe to be parsed
374 # but the browser is the bottleneck
374 # but the browser is the bottleneck
375 if not self.show_full_diff and exceeds_limit:
375 if not self.show_full_diff and exceeds_limit:
376 raise DiffLimitExceeded('File Limit Exceeded')
376 raise DiffLimitExceeded('File Limit Exceeded')
377
377
378 except DiffLimitExceeded:
378 except DiffLimitExceeded:
379 diff_container = lambda _diff: \
379 diff_container = lambda _diff: \
380 LimitedDiffContainer(
380 LimitedDiffContainer(
381 self.diff_limit, self.cur_diff_size, _diff)
381 self.diff_limit, self.cur_diff_size, _diff)
382
382
383 exceeds_limit = len(raw_diff) > self.file_limit
383 exceeds_limit = len(raw_diff) > self.file_limit
384 limited_diff = True
384 limited_diff = True
385 chunks = []
385 chunks = []
386
386
387 else: # GIT format binary patch, or possibly empty diff
387 else: # GIT format binary patch, or possibly empty diff
388 if head['bin_patch']:
388 if head['bin_patch']:
389 # we have operation already extracted, but we mark simply
389 # we have operation already extracted, but we mark simply
390 # it's a diff we wont show for binary files
390 # it's a diff we wont show for binary files
391 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
391 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
392 chunks = []
392 chunks = []
393
393
394 if chunks and not self.show_full_diff and op == OPS.DEL:
394 if chunks and not self.show_full_diff and op == OPS.DEL:
395 # if not full diff mode show deleted file contents
395 # if not full diff mode show deleted file contents
396 # TODO: anderson: if the view is not too big, there is no way
396 # TODO: anderson: if the view is not too big, there is no way
397 # to see the content of the file
397 # to see the content of the file
398 chunks = []
398 chunks = []
399
399
400 chunks.insert(0, [{
400 chunks.insert(0, [{
401 'old_lineno': '',
401 'old_lineno': '',
402 'new_lineno': '',
402 'new_lineno': '',
403 'action': Action.CONTEXT,
403 'action': Action.CONTEXT,
404 'line': msg,
404 'line': msg,
405 } for _op, msg in stats['ops'].iteritems()
405 } for _op, msg in stats['ops'].iteritems()
406 if _op not in [MOD_FILENODE]])
406 if _op not in [MOD_FILENODE]])
407
407
408 _files.append({
408 _files.append({
409 'filename': safe_unicode(head['b_path']),
409 'filename': safe_unicode(head['b_path']),
410 'old_revision': head['a_blob_id'],
410 'old_revision': head['a_blob_id'],
411 'new_revision': head['b_blob_id'],
411 'new_revision': head['b_blob_id'],
412 'chunks': chunks,
412 'chunks': chunks,
413 'raw_diff': safe_unicode(raw_diff),
413 'raw_diff': safe_unicode(raw_diff),
414 'operation': op,
414 'operation': op,
415 'stats': stats,
415 'stats': stats,
416 'exceeds_limit': exceeds_limit,
416 'exceeds_limit': exceeds_limit,
417 'is_limited_diff': limited_diff,
417 'is_limited_diff': limited_diff,
418 })
418 })
419
419
420 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
420 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
421 OPS.DEL: 2}.get(info['operation'])
421 OPS.DEL: 2}.get(info['operation'])
422
422
423 if not inline_diff:
423 if not inline_diff:
424 return diff_container(sorted(_files, key=sorter))
424 return diff_container(sorted(_files, key=sorter))
425
425
426 # highlight inline changes
426 # highlight inline changes
427 for diff_data in _files:
427 for diff_data in _files:
428 for chunk in diff_data['chunks']:
428 for chunk in diff_data['chunks']:
429 lineiter = iter(chunk)
429 lineiter = iter(chunk)
430 try:
430 try:
431 while 1:
431 while 1:
432 line = lineiter.next()
432 line = lineiter.next()
433 if line['action'] not in (
433 if line['action'] not in (
434 Action.UNMODIFIED, Action.CONTEXT):
434 Action.UNMODIFIED, Action.CONTEXT):
435 nextline = lineiter.next()
435 nextline = lineiter.next()
436 if nextline['action'] in ['unmod', 'context'] or \
436 if nextline['action'] in ['unmod', 'context'] or \
437 nextline['action'] == line['action']:
437 nextline['action'] == line['action']:
438 continue
438 continue
439 self.differ(line, nextline)
439 self.differ(line, nextline)
440 except StopIteration:
440 except StopIteration:
441 pass
441 pass
442
442
443 return diff_container(sorted(_files, key=sorter))
443 return diff_container(sorted(_files, key=sorter))
444
444
445 def _check_large_diff(self):
445 def _check_large_diff(self):
446 log.debug('Diff exceeds current diff_limit of %s', self.diff_limit)
446 log.debug('Diff exceeds current diff_limit of %s', self.diff_limit)
447 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
447 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
448 raise DiffLimitExceeded('Diff Limit `%s` Exceeded', self.diff_limit)
448 raise DiffLimitExceeded('Diff Limit `%s` Exceeded', self.diff_limit)
449
449
450 # FIXME: NEWDIFFS: dan: this replaces _parse_gitdiff
450 # FIXME: NEWDIFFS: dan: this replaces _parse_gitdiff
451 def _new_parse_gitdiff(self, inline_diff=True):
451 def _new_parse_gitdiff(self, inline_diff=True):
452 _files = []
452 _files = []
453
453
454 # this can be overriden later to a LimitedDiffContainer type
454 # this can be overriden later to a LimitedDiffContainer type
455 diff_container = lambda arg: arg
455 diff_container = lambda arg: arg
456
456
457 for chunk in self._diff.chunks():
457 for chunk in self._diff.chunks():
458 head = chunk.header
458 head = chunk.header
459 log.debug('parsing diff %r', head)
459 log.debug('parsing diff %r', head)
460
460
461 raw_diff = chunk.raw
461 raw_diff = chunk.raw
462 limited_diff = False
462 limited_diff = False
463 exceeds_limit = False
463 exceeds_limit = False
464
464
465 op = None
465 op = None
466 stats = {
466 stats = {
467 'added': 0,
467 'added': 0,
468 'deleted': 0,
468 'deleted': 0,
469 'binary': False,
469 'binary': False,
470 'old_mode': None,
470 'old_mode': None,
471 'new_mode': None,
471 'new_mode': None,
472 'ops': {},
472 'ops': {},
473 }
473 }
474 if head['old_mode']:
474 if head['old_mode']:
475 stats['old_mode'] = head['old_mode']
475 stats['old_mode'] = head['old_mode']
476 if head['new_mode']:
476 if head['new_mode']:
477 stats['new_mode'] = head['new_mode']
477 stats['new_mode'] = head['new_mode']
478 if head['b_mode']:
478 if head['b_mode']:
479 stats['new_mode'] = head['b_mode']
479 stats['new_mode'] = head['b_mode']
480
480
481 # delete file
481 # delete file
482 if head['deleted_file_mode']:
482 if head['deleted_file_mode']:
483 op = OPS.DEL
483 op = OPS.DEL
484 stats['binary'] = True
484 stats['binary'] = True
485 stats['ops'][DEL_FILENODE] = 'deleted file'
485 stats['ops'][DEL_FILENODE] = 'deleted file'
486
486
487 # new file
487 # new file
488 elif head['new_file_mode']:
488 elif head['new_file_mode']:
489 op = OPS.ADD
489 op = OPS.ADD
490 stats['binary'] = True
490 stats['binary'] = True
491 stats['old_mode'] = None
491 stats['old_mode'] = None
492 stats['new_mode'] = head['new_file_mode']
492 stats['new_mode'] = head['new_file_mode']
493 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
493 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
494
494
495 # modify operation, can be copy, rename or chmod
495 # modify operation, can be copy, rename or chmod
496 else:
496 else:
497 # CHMOD
497 # CHMOD
498 if head['new_mode'] and head['old_mode']:
498 if head['new_mode'] and head['old_mode']:
499 op = OPS.MOD
499 op = OPS.MOD
500 stats['binary'] = True
500 stats['binary'] = True
501 stats['ops'][CHMOD_FILENODE] = (
501 stats['ops'][CHMOD_FILENODE] = (
502 'modified file chmod %s => %s' % (
502 'modified file chmod %s => %s' % (
503 head['old_mode'], head['new_mode']))
503 head['old_mode'], head['new_mode']))
504
504
505 # RENAME
505 # RENAME
506 if head['rename_from'] != head['rename_to']:
506 if head['rename_from'] != head['rename_to']:
507 op = OPS.MOD
507 op = OPS.MOD
508 stats['binary'] = True
508 stats['binary'] = True
509 stats['renamed'] = (head['rename_from'], head['rename_to'])
509 stats['renamed'] = (head['rename_from'], head['rename_to'])
510 stats['ops'][RENAMED_FILENODE] = (
510 stats['ops'][RENAMED_FILENODE] = (
511 'file renamed from %s to %s' % (
511 'file renamed from %s to %s' % (
512 head['rename_from'], head['rename_to']))
512 head['rename_from'], head['rename_to']))
513 # COPY
513 # COPY
514 if head.get('copy_from') and head.get('copy_to'):
514 if head.get('copy_from') and head.get('copy_to'):
515 op = OPS.MOD
515 op = OPS.MOD
516 stats['binary'] = True
516 stats['binary'] = True
517 stats['copied'] = (head['copy_from'], head['copy_to'])
517 stats['copied'] = (head['copy_from'], head['copy_to'])
518 stats['ops'][COPIED_FILENODE] = (
518 stats['ops'][COPIED_FILENODE] = (
519 'file copied from %s to %s' % (
519 'file copied from %s to %s' % (
520 head['copy_from'], head['copy_to']))
520 head['copy_from'], head['copy_to']))
521
521
522 # If our new parsed headers didn't match anything fallback to
522 # If our new parsed headers didn't match anything fallback to
523 # old style detection
523 # old style detection
524 if op is None:
524 if op is None:
525 if not head['a_file'] and head['b_file']:
525 if not head['a_file'] and head['b_file']:
526 op = OPS.ADD
526 op = OPS.ADD
527 stats['binary'] = True
527 stats['binary'] = True
528 stats['new_file'] = True
528 stats['new_file'] = True
529 stats['ops'][NEW_FILENODE] = 'new file'
529 stats['ops'][NEW_FILENODE] = 'new file'
530
530
531 elif head['a_file'] and not head['b_file']:
531 elif head['a_file'] and not head['b_file']:
532 op = OPS.DEL
532 op = OPS.DEL
533 stats['binary'] = True
533 stats['binary'] = True
534 stats['ops'][DEL_FILENODE] = 'deleted file'
534 stats['ops'][DEL_FILENODE] = 'deleted file'
535
535
536 # it's not ADD not DELETE
536 # it's not ADD not DELETE
537 if op is None:
537 if op is None:
538 op = OPS.MOD
538 op = OPS.MOD
539 stats['binary'] = True
539 stats['binary'] = True
540 stats['ops'][MOD_FILENODE] = 'modified file'
540 stats['ops'][MOD_FILENODE] = 'modified file'
541
541
542 # a real non-binary diff
542 # a real non-binary diff
543 if head['a_file'] or head['b_file']:
543 if head['a_file'] or head['b_file']:
544 # simulate splitlines, so we keep the line end part
544 # simulate splitlines, so we keep the line end part
545 diff = self.diff_splitter(chunk.diff)
545 diff = self.diff_splitter(chunk.diff)
546
546
547 # append each file to the diff size
547 # append each file to the diff size
548 raw_chunk_size = len(raw_diff)
548 raw_chunk_size = len(raw_diff)
549
549
550 exceeds_limit = raw_chunk_size > self.file_limit
550 exceeds_limit = raw_chunk_size > self.file_limit
551 self.cur_diff_size += raw_chunk_size
551 self.cur_diff_size += raw_chunk_size
552
552
553 try:
553 try:
554 # Check each file instead of the whole diff.
554 # Check each file instead of the whole diff.
555 # Diff will hide big files but still show small ones.
555 # Diff will hide big files but still show small ones.
556 # From the tests big files are fairly safe to be parsed
556 # From the tests big files are fairly safe to be parsed
557 # but the browser is the bottleneck.
557 # but the browser is the bottleneck.
558 if not self.show_full_diff and exceeds_limit:
558 if not self.show_full_diff and exceeds_limit:
559 log.debug('File `%s` exceeds current file_limit of %s',
559 log.debug('File `%s` exceeds current file_limit of %s',
560 safe_unicode(head['b_path']), self.file_limit)
560 safe_unicode(head['b_path']), self.file_limit)
561 raise DiffLimitExceeded(
561 raise DiffLimitExceeded(
562 'File Limit %s Exceeded', self.file_limit)
562 'File Limit %s Exceeded', self.file_limit)
563
563
564 self._check_large_diff()
564 self._check_large_diff()
565
565
566 raw_diff, chunks, _stats = self._new_parse_lines(diff)
566 raw_diff, chunks, _stats = self._new_parse_lines(diff)
567 stats['binary'] = False
567 stats['binary'] = False
568 stats['added'] = _stats[0]
568 stats['added'] = _stats[0]
569 stats['deleted'] = _stats[1]
569 stats['deleted'] = _stats[1]
570 # explicit mark that it's a modified file
570 # explicit mark that it's a modified file
571 if op == OPS.MOD:
571 if op == OPS.MOD:
572 stats['ops'][MOD_FILENODE] = 'modified file'
572 stats['ops'][MOD_FILENODE] = 'modified file'
573
573
574 except DiffLimitExceeded:
574 except DiffLimitExceeded:
575 diff_container = lambda _diff: \
575 diff_container = lambda _diff: \
576 LimitedDiffContainer(
576 LimitedDiffContainer(
577 self.diff_limit, self.cur_diff_size, _diff)
577 self.diff_limit, self.cur_diff_size, _diff)
578
578
579 limited_diff = True
579 limited_diff = True
580 chunks = []
580 chunks = []
581
581
582 else: # GIT format binary patch, or possibly empty diff
582 else: # GIT format binary patch, or possibly empty diff
583 if head['bin_patch']:
583 if head['bin_patch']:
584 # we have operation already extracted, but we mark simply
584 # we have operation already extracted, but we mark simply
585 # it's a diff we wont show for binary files
585 # it's a diff we wont show for binary files
586 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
586 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
587 chunks = []
587 chunks = []
588
588
589 # Hide content of deleted node by setting empty chunks
589 # Hide content of deleted node by setting empty chunks
590 if chunks and not self.show_full_diff and op == OPS.DEL:
590 if chunks and not self.show_full_diff and op == OPS.DEL:
591 # if not full diff mode show deleted file contents
591 # if not full diff mode show deleted file contents
592 # TODO: anderson: if the view is not too big, there is no way
592 # TODO: anderson: if the view is not too big, there is no way
593 # to see the content of the file
593 # to see the content of the file
594 chunks = []
594 chunks = []
595
595
596 chunks.insert(
596 chunks.insert(
597 0, [{'old_lineno': '',
597 0, [{'old_lineno': '',
598 'new_lineno': '',
598 'new_lineno': '',
599 'action': Action.CONTEXT,
599 'action': Action.CONTEXT,
600 'line': msg,
600 'line': msg,
601 } for _op, msg in stats['ops'].iteritems()
601 } for _op, msg in stats['ops'].iteritems()
602 if _op not in [MOD_FILENODE]])
602 if _op not in [MOD_FILENODE]])
603
603
604 original_filename = safe_unicode(head['a_path'])
604 original_filename = safe_unicode(head['a_path'])
605 _files.append({
605 _files.append({
606 'original_filename': original_filename,
606 'original_filename': original_filename,
607 'filename': safe_unicode(head['b_path']),
607 'filename': safe_unicode(head['b_path']),
608 'old_revision': head['a_blob_id'],
608 'old_revision': head['a_blob_id'],
609 'new_revision': head['b_blob_id'],
609 'new_revision': head['b_blob_id'],
610 'chunks': chunks,
610 'chunks': chunks,
611 'raw_diff': safe_unicode(raw_diff),
611 'raw_diff': safe_unicode(raw_diff),
612 'operation': op,
612 'operation': op,
613 'stats': stats,
613 'stats': stats,
614 'exceeds_limit': exceeds_limit,
614 'exceeds_limit': exceeds_limit,
615 'is_limited_diff': limited_diff,
615 'is_limited_diff': limited_diff,
616 })
616 })
617
617
618 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
618 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
619 OPS.DEL: 2}.get(info['operation'])
619 OPS.DEL: 2}.get(info['operation'])
620
620
621 return diff_container(sorted(_files, key=sorter))
621 return diff_container(sorted(_files, key=sorter))
622
622
623 # FIXME: NEWDIFFS: dan: this gets replaced by _new_parse_lines
623 # FIXME: NEWDIFFS: dan: this gets replaced by _new_parse_lines
624 def _parse_lines(self, diff_iter):
624 def _parse_lines(self, diff_iter):
625 """
625 """
626 Parse the diff an return data for the template.
626 Parse the diff an return data for the template.
627 """
627 """
628
628
629 stats = [0, 0]
629 stats = [0, 0]
630 chunks = []
630 chunks = []
631 raw_diff = []
631 raw_diff = []
632
632
633 try:
633 try:
634 line = diff_iter.next()
634 line = diff_iter.next()
635
635
636 while line:
636 while line:
637 raw_diff.append(line)
637 raw_diff.append(line)
638 lines = []
638 lines = []
639 chunks.append(lines)
639 chunks.append(lines)
640
640
641 match = self._chunk_re.match(line)
641 match = self._chunk_re.match(line)
642
642
643 if not match:
643 if not match:
644 break
644 break
645
645
646 gr = match.groups()
646 gr = match.groups()
647 (old_line, old_end,
647 (old_line, old_end,
648 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
648 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
649 old_line -= 1
649 old_line -= 1
650 new_line -= 1
650 new_line -= 1
651
651
652 context = len(gr) == 5
652 context = len(gr) == 5
653 old_end += old_line
653 old_end += old_line
654 new_end += new_line
654 new_end += new_line
655
655
656 if context:
656 if context:
657 # skip context only if it's first line
657 # skip context only if it's first line
658 if int(gr[0]) > 1:
658 if int(gr[0]) > 1:
659 lines.append({
659 lines.append({
660 'old_lineno': '...',
660 'old_lineno': '...',
661 'new_lineno': '...',
661 'new_lineno': '...',
662 'action': Action.CONTEXT,
662 'action': Action.CONTEXT,
663 'line': line,
663 'line': line,
664 })
664 })
665
665
666 line = diff_iter.next()
666 line = diff_iter.next()
667
667
668 while old_line < old_end or new_line < new_end:
668 while old_line < old_end or new_line < new_end:
669 command = ' '
669 command = ' '
670 if line:
670 if line:
671 command = line[0]
671 command = line[0]
672
672
673 affects_old = affects_new = False
673 affects_old = affects_new = False
674
674
675 # ignore those if we don't expect them
675 # ignore those if we don't expect them
676 if command in '#@':
676 if command in '#@':
677 continue
677 continue
678 elif command == '+':
678 elif command == '+':
679 affects_new = True
679 affects_new = True
680 action = Action.ADD
680 action = Action.ADD
681 stats[0] += 1
681 stats[0] += 1
682 elif command == '-':
682 elif command == '-':
683 affects_old = True
683 affects_old = True
684 action = Action.DELETE
684 action = Action.DELETE
685 stats[1] += 1
685 stats[1] += 1
686 else:
686 else:
687 affects_old = affects_new = True
687 affects_old = affects_new = True
688 action = Action.UNMODIFIED
688 action = Action.UNMODIFIED
689
689
690 if not self._newline_marker.match(line):
690 if not self._newline_marker.match(line):
691 old_line += affects_old
691 old_line += affects_old
692 new_line += affects_new
692 new_line += affects_new
693 lines.append({
693 lines.append({
694 'old_lineno': affects_old and old_line or '',
694 'old_lineno': affects_old and old_line or '',
695 'new_lineno': affects_new and new_line or '',
695 'new_lineno': affects_new and new_line or '',
696 'action': action,
696 'action': action,
697 'line': self._clean_line(line, command)
697 'line': self._clean_line(line, command)
698 })
698 })
699 raw_diff.append(line)
699 raw_diff.append(line)
700
700
701 line = diff_iter.next()
701 line = diff_iter.next()
702
702
703 if self._newline_marker.match(line):
703 if self._newline_marker.match(line):
704 # we need to append to lines, since this is not
704 # we need to append to lines, since this is not
705 # counted in the line specs of diff
705 # counted in the line specs of diff
706 lines.append({
706 lines.append({
707 'old_lineno': '...',
707 'old_lineno': '...',
708 'new_lineno': '...',
708 'new_lineno': '...',
709 'action': Action.CONTEXT,
709 'action': Action.CONTEXT,
710 'line': self._clean_line(line, command)
710 'line': self._clean_line(line, command)
711 })
711 })
712
712
713 except StopIteration:
713 except StopIteration:
714 pass
714 pass
715 return ''.join(raw_diff), chunks, stats
715 return ''.join(raw_diff), chunks, stats
716
716
717 # FIXME: NEWDIFFS: dan: this replaces _parse_lines
717 # FIXME: NEWDIFFS: dan: this replaces _parse_lines
718 def _new_parse_lines(self, diff_iter):
718 def _new_parse_lines(self, diff_iter):
719 """
719 """
720 Parse the diff an return data for the template.
720 Parse the diff an return data for the template.
721 """
721 """
722
722
723 stats = [0, 0]
723 stats = [0, 0]
724 chunks = []
724 chunks = []
725 raw_diff = []
725 raw_diff = []
726
726
727 try:
727 try:
728 line = diff_iter.next()
728 line = diff_iter.next()
729
729
730 while line:
730 while line:
731 raw_diff.append(line)
731 raw_diff.append(line)
732 # match header e.g @@ -0,0 +1 @@\n'
732 # match header e.g @@ -0,0 +1 @@\n'
733 match = self._chunk_re.match(line)
733 match = self._chunk_re.match(line)
734
734
735 if not match:
735 if not match:
736 break
736 break
737
737
738 gr = match.groups()
738 gr = match.groups()
739 (old_line, old_end,
739 (old_line, old_end,
740 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
740 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
741
741
742 lines = []
742 lines = []
743 hunk = {
743 hunk = {
744 'section_header': gr[-1],
744 'section_header': gr[-1],
745 'source_start': old_line,
745 'source_start': old_line,
746 'source_length': old_end,
746 'source_length': old_end,
747 'target_start': new_line,
747 'target_start': new_line,
748 'target_length': new_end,
748 'target_length': new_end,
749 'lines': lines,
749 'lines': lines,
750 }
750 }
751 chunks.append(hunk)
751 chunks.append(hunk)
752
752
753 old_line -= 1
753 old_line -= 1
754 new_line -= 1
754 new_line -= 1
755
755
756 context = len(gr) == 5
756 context = len(gr) == 5
757 old_end += old_line
757 old_end += old_line
758 new_end += new_line
758 new_end += new_line
759
759
760 line = diff_iter.next()
760 line = diff_iter.next()
761
761
762 while old_line < old_end or new_line < new_end:
762 while old_line < old_end or new_line < new_end:
763 command = ' '
763 command = ' '
764 if line:
764 if line:
765 command = line[0]
765 command = line[0]
766
766
767 affects_old = affects_new = False
767 affects_old = affects_new = False
768
768
769 # ignore those if we don't expect them
769 # ignore those if we don't expect them
770 if command in '#@':
770 if command in '#@':
771 continue
771 continue
772 elif command == '+':
772 elif command == '+':
773 affects_new = True
773 affects_new = True
774 action = Action.ADD
774 action = Action.ADD
775 stats[0] += 1
775 stats[0] += 1
776 elif command == '-':
776 elif command == '-':
777 affects_old = True
777 affects_old = True
778 action = Action.DELETE
778 action = Action.DELETE
779 stats[1] += 1
779 stats[1] += 1
780 else:
780 else:
781 affects_old = affects_new = True
781 affects_old = affects_new = True
782 action = Action.UNMODIFIED
782 action = Action.UNMODIFIED
783
783
784 if not self._newline_marker.match(line):
784 if not self._newline_marker.match(line):
785 old_line += affects_old
785 old_line += affects_old
786 new_line += affects_new
786 new_line += affects_new
787 lines.append({
787 lines.append({
788 'old_lineno': affects_old and old_line or '',
788 'old_lineno': affects_old and old_line or '',
789 'new_lineno': affects_new and new_line or '',
789 'new_lineno': affects_new and new_line or '',
790 'action': action,
790 'action': action,
791 'line': self._clean_line(line, command)
791 'line': self._clean_line(line, command)
792 })
792 })
793 raw_diff.append(line)
793 raw_diff.append(line)
794
794
795 line = diff_iter.next()
795 line = diff_iter.next()
796
796
797 if self._newline_marker.match(line):
797 if self._newline_marker.match(line):
798 # we need to append to lines, since this is not
798 # we need to append to lines, since this is not
799 # counted in the line specs of diff
799 # counted in the line specs of diff
800 if affects_old:
800 if affects_old:
801 action = Action.OLD_NO_NL
801 action = Action.OLD_NO_NL
802 elif affects_new:
802 elif affects_new:
803 action = Action.NEW_NO_NL
803 action = Action.NEW_NO_NL
804 else:
804 else:
805 raise Exception('invalid context for no newline')
805 raise Exception('invalid context for no newline')
806
806
807 lines.append({
807 lines.append({
808 'old_lineno': None,
808 'old_lineno': None,
809 'new_lineno': None,
809 'new_lineno': None,
810 'action': action,
810 'action': action,
811 'line': self._clean_line(line, command)
811 'line': self._clean_line(line, command)
812 })
812 })
813
813
814 except StopIteration:
814 except StopIteration:
815 pass
815 pass
816
816
817 return ''.join(raw_diff), chunks, stats
817 return ''.join(raw_diff), chunks, stats
818
818
819 def _safe_id(self, idstring):
819 def _safe_id(self, idstring):
820 """Make a string safe for including in an id attribute.
820 """Make a string safe for including in an id attribute.
821
821
822 The HTML spec says that id attributes 'must begin with
822 The HTML spec says that id attributes 'must begin with
823 a letter ([A-Za-z]) and may be followed by any number
823 a letter ([A-Za-z]) and may be followed by any number
824 of letters, digits ([0-9]), hyphens ("-"), underscores
824 of letters, digits ([0-9]), hyphens ("-"), underscores
825 ("_"), colons (":"), and periods (".")'. These regexps
825 ("_"), colons (":"), and periods (".")'. These regexps
826 are slightly over-zealous, in that they remove colons
826 are slightly over-zealous, in that they remove colons
827 and periods unnecessarily.
827 and periods unnecessarily.
828
828
829 Whitespace is transformed into underscores, and then
829 Whitespace is transformed into underscores, and then
830 anything which is not a hyphen or a character that
830 anything which is not a hyphen or a character that
831 matches \w (alphanumerics and underscore) is removed.
831 matches \w (alphanumerics and underscore) is removed.
832
832
833 """
833 """
834 # Transform all whitespace to underscore
834 # Transform all whitespace to underscore
835 idstring = re.sub(r'\s', "_", '%s' % idstring)
835 idstring = re.sub(r'\s', "_", '%s' % idstring)
836 # Remove everything that is not a hyphen or a member of \w
836 # Remove everything that is not a hyphen or a member of \w
837 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
837 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
838 return idstring
838 return idstring
839
839
840 @classmethod
840 @classmethod
841 def diff_splitter(cls, string):
841 def diff_splitter(cls, string):
842 """
842 """
843 Diff split that emulates .splitlines() but works only on \n
843 Diff split that emulates .splitlines() but works only on \n
844 """
844 """
845 if not string:
845 if not string:
846 return
846 return
847 elif string == '\n':
847 elif string == '\n':
848 yield u'\n'
848 yield u'\n'
849 else:
849 else:
850
850
851 has_newline = string.endswith('\n')
851 has_newline = string.endswith('\n')
852 elements = string.split('\n')
852 elements = string.split('\n')
853 if has_newline:
853 if has_newline:
854 # skip last element as it's empty string from newlines
854 # skip last element as it's empty string from newlines
855 elements = elements[:-1]
855 elements = elements[:-1]
856
856
857 len_elements = len(elements)
857 len_elements = len(elements)
858
858
859 for cnt, line in enumerate(elements, start=1):
859 for cnt, line in enumerate(elements, start=1):
860 last_line = cnt == len_elements
860 last_line = cnt == len_elements
861 if last_line and not has_newline:
861 if last_line and not has_newline:
862 yield safe_unicode(line)
862 yield safe_unicode(line)
863 else:
863 else:
864 yield safe_unicode(line) + '\n'
864 yield safe_unicode(line) + '\n'
865
865
866 def prepare(self, inline_diff=True):
866 def prepare(self, inline_diff=True):
867 """
867 """
868 Prepare the passed udiff for HTML rendering.
868 Prepare the passed udiff for HTML rendering.
869
869
870 :return: A list of dicts with diff information.
870 :return: A list of dicts with diff information.
871 """
871 """
872 parsed = self._parser(inline_diff=inline_diff)
872 parsed = self._parser(inline_diff=inline_diff)
873 self.parsed = True
873 self.parsed = True
874 self.parsed_diff = parsed
874 self.parsed_diff = parsed
875 return parsed
875 return parsed
876
876
877 def as_raw(self, diff_lines=None):
877 def as_raw(self, diff_lines=None):
878 """
878 """
879 Returns raw diff as a byte string
879 Returns raw diff as a byte string
880 """
880 """
881 return self._diff.raw
881 return self._diff.raw
882
882
883 def as_html(self, table_class='code-difftable', line_class='line',
883 def as_html(self, table_class='code-difftable', line_class='line',
884 old_lineno_class='lineno old', new_lineno_class='lineno new',
884 old_lineno_class='lineno old', new_lineno_class='lineno new',
885 code_class='code', enable_comments=False, parsed_lines=None):
885 code_class='code', enable_comments=False, parsed_lines=None):
886 """
886 """
887 Return given diff as html table with customized css classes
887 Return given diff as html table with customized css classes
888 """
888 """
889 # TODO(marcink): not sure how to pass in translator
889 # TODO(marcink): not sure how to pass in translator
890 # here in an efficient way, leave the _ for proper gettext extraction
890 # here in an efficient way, leave the _ for proper gettext extraction
891 _ = lambda s: s
891 _ = lambda s: s
892
892
893 def _link_to_if(condition, label, url):
893 def _link_to_if(condition, label, url):
894 """
894 """
895 Generates a link if condition is meet or just the label if not.
895 Generates a link if condition is meet or just the label if not.
896 """
896 """
897
897
898 if condition:
898 if condition:
899 return '''<a href="%(url)s" class="tooltip"
899 return '''<a href="%(url)s" class="tooltip"
900 title="%(title)s">%(label)s</a>''' % {
900 title="%(title)s">%(label)s</a>''' % {
901 'title': _('Click to select line'),
901 'title': _('Click to select line'),
902 'url': url,
902 'url': url,
903 'label': label
903 'label': label
904 }
904 }
905 else:
905 else:
906 return label
906 return label
907 if not self.parsed:
907 if not self.parsed:
908 self.prepare()
908 self.prepare()
909
909
910 diff_lines = self.parsed_diff
910 diff_lines = self.parsed_diff
911 if parsed_lines:
911 if parsed_lines:
912 diff_lines = parsed_lines
912 diff_lines = parsed_lines
913
913
914 _html_empty = True
914 _html_empty = True
915 _html = []
915 _html = []
916 _html.append('''<table class="%(table_class)s">\n''' % {
916 _html.append('''<table class="%(table_class)s">\n''' % {
917 'table_class': table_class
917 'table_class': table_class
918 })
918 })
919
919
920 for diff in diff_lines:
920 for diff in diff_lines:
921 for line in diff['chunks']:
921 for line in diff['chunks']:
922 _html_empty = False
922 _html_empty = False
923 for change in line:
923 for change in line:
924 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
924 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
925 'lc': line_class,
925 'lc': line_class,
926 'action': change['action']
926 'action': change['action']
927 })
927 })
928 anchor_old_id = ''
928 anchor_old_id = ''
929 anchor_new_id = ''
929 anchor_new_id = ''
930 anchor_old = "%(filename)s_o%(oldline_no)s" % {
930 anchor_old = "%(filename)s_o%(oldline_no)s" % {
931 'filename': self._safe_id(diff['filename']),
931 'filename': self._safe_id(diff['filename']),
932 'oldline_no': change['old_lineno']
932 'oldline_no': change['old_lineno']
933 }
933 }
934 anchor_new = "%(filename)s_n%(oldline_no)s" % {
934 anchor_new = "%(filename)s_n%(oldline_no)s" % {
935 'filename': self._safe_id(diff['filename']),
935 'filename': self._safe_id(diff['filename']),
936 'oldline_no': change['new_lineno']
936 'oldline_no': change['new_lineno']
937 }
937 }
938 cond_old = (change['old_lineno'] != '...' and
938 cond_old = (change['old_lineno'] != '...' and
939 change['old_lineno'])
939 change['old_lineno'])
940 cond_new = (change['new_lineno'] != '...' and
940 cond_new = (change['new_lineno'] != '...' and
941 change['new_lineno'])
941 change['new_lineno'])
942 if cond_old:
942 if cond_old:
943 anchor_old_id = 'id="%s"' % anchor_old
943 anchor_old_id = 'id="%s"' % anchor_old
944 if cond_new:
944 if cond_new:
945 anchor_new_id = 'id="%s"' % anchor_new
945 anchor_new_id = 'id="%s"' % anchor_new
946
946
947 if change['action'] != Action.CONTEXT:
947 if change['action'] != Action.CONTEXT:
948 anchor_link = True
948 anchor_link = True
949 else:
949 else:
950 anchor_link = False
950 anchor_link = False
951
951
952 ###########################################################
952 ###########################################################
953 # COMMENT ICONS
953 # COMMENT ICONS
954 ###########################################################
954 ###########################################################
955 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
955 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
956
956
957 if enable_comments and change['action'] != Action.CONTEXT:
957 if enable_comments and change['action'] != Action.CONTEXT:
958 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
958 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
959
959
960 _html.append('''</span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>\n''')
960 _html.append('''</span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>\n''')
961
961
962 ###########################################################
962 ###########################################################
963 # OLD LINE NUMBER
963 # OLD LINE NUMBER
964 ###########################################################
964 ###########################################################
965 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
965 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
966 'a_id': anchor_old_id,
966 'a_id': anchor_old_id,
967 'olc': old_lineno_class
967 'olc': old_lineno_class
968 })
968 })
969
969
970 _html.append('''%(link)s''' % {
970 _html.append('''%(link)s''' % {
971 'link': _link_to_if(anchor_link, change['old_lineno'],
971 'link': _link_to_if(anchor_link, change['old_lineno'],
972 '#%s' % anchor_old)
972 '#%s' % anchor_old)
973 })
973 })
974 _html.append('''</td>\n''')
974 _html.append('''</td>\n''')
975 ###########################################################
975 ###########################################################
976 # NEW LINE NUMBER
976 # NEW LINE NUMBER
977 ###########################################################
977 ###########################################################
978
978
979 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
979 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
980 'a_id': anchor_new_id,
980 'a_id': anchor_new_id,
981 'nlc': new_lineno_class
981 'nlc': new_lineno_class
982 })
982 })
983
983
984 _html.append('''%(link)s''' % {
984 _html.append('''%(link)s''' % {
985 'link': _link_to_if(anchor_link, change['new_lineno'],
985 'link': _link_to_if(anchor_link, change['new_lineno'],
986 '#%s' % anchor_new)
986 '#%s' % anchor_new)
987 })
987 })
988 _html.append('''</td>\n''')
988 _html.append('''</td>\n''')
989 ###########################################################
989 ###########################################################
990 # CODE
990 # CODE
991 ###########################################################
991 ###########################################################
992 code_classes = [code_class]
992 code_classes = [code_class]
993 if (not enable_comments or
993 if (not enable_comments or
994 change['action'] == Action.CONTEXT):
994 change['action'] == Action.CONTEXT):
995 code_classes.append('no-comment')
995 code_classes.append('no-comment')
996 _html.append('\t<td class="%s">' % ' '.join(code_classes))
996 _html.append('\t<td class="%s">' % ' '.join(code_classes))
997 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
997 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
998 'code': change['line']
998 'code': change['line']
999 })
999 })
1000
1000
1001 _html.append('''\t</td>''')
1001 _html.append('''\t</td>''')
1002 _html.append('''\n</tr>\n''')
1002 _html.append('''\n</tr>\n''')
1003 _html.append('''</table>''')
1003 _html.append('''</table>''')
1004 if _html_empty:
1004 if _html_empty:
1005 return None
1005 return None
1006 return ''.join(_html)
1006 return ''.join(_html)
1007
1007
1008 def stat(self):
1008 def stat(self):
1009 """
1009 """
1010 Returns tuple of added, and removed lines for this instance
1010 Returns tuple of added, and removed lines for this instance
1011 """
1011 """
1012 return self.adds, self.removes
1012 return self.adds, self.removes
1013
1013
1014 def get_context_of_line(
1014 def get_context_of_line(
1015 self, path, diff_line=None, context_before=3, context_after=3):
1015 self, path, diff_line=None, context_before=3, context_after=3):
1016 """
1016 """
1017 Returns the context lines for the specified diff line.
1017 Returns the context lines for the specified diff line.
1018
1018
1019 :type diff_line: :class:`DiffLineNumber`
1019 :type diff_line: :class:`DiffLineNumber`
1020 """
1020 """
1021 assert self.parsed, "DiffProcessor is not initialized."
1021 assert self.parsed, "DiffProcessor is not initialized."
1022
1022
1023 if None not in diff_line:
1023 if None not in diff_line:
1024 raise ValueError(
1024 raise ValueError(
1025 "Cannot specify both line numbers: {}".format(diff_line))
1025 "Cannot specify both line numbers: {}".format(diff_line))
1026
1026
1027 file_diff = self._get_file_diff(path)
1027 file_diff = self._get_file_diff(path)
1028 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
1028 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
1029
1029
1030 first_line_to_include = max(idx - context_before, 0)
1030 first_line_to_include = max(idx - context_before, 0)
1031 first_line_after_context = idx + context_after + 1
1031 first_line_after_context = idx + context_after + 1
1032 context_lines = chunk[first_line_to_include:first_line_after_context]
1032 context_lines = chunk[first_line_to_include:first_line_after_context]
1033
1033
1034 line_contents = [
1034 line_contents = [
1035 _context_line(line) for line in context_lines
1035 _context_line(line) for line in context_lines
1036 if _is_diff_content(line)]
1036 if _is_diff_content(line)]
1037 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
1037 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
1038 # Once they are fixed, we can drop this line here.
1038 # Once they are fixed, we can drop this line here.
1039 if line_contents:
1039 if line_contents:
1040 line_contents[-1] = (
1040 line_contents[-1] = (
1041 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
1041 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
1042 return line_contents
1042 return line_contents
1043
1043
1044 def find_context(self, path, context, offset=0):
1044 def find_context(self, path, context, offset=0):
1045 """
1045 """
1046 Finds the given `context` inside of the diff.
1046 Finds the given `context` inside of the diff.
1047
1047
1048 Use the parameter `offset` to specify which offset the target line has
1048 Use the parameter `offset` to specify which offset the target line has
1049 inside of the given `context`. This way the correct diff line will be
1049 inside of the given `context`. This way the correct diff line will be
1050 returned.
1050 returned.
1051
1051
1052 :param offset: Shall be used to specify the offset of the main line
1052 :param offset: Shall be used to specify the offset of the main line
1053 within the given `context`.
1053 within the given `context`.
1054 """
1054 """
1055 if offset < 0 or offset >= len(context):
1055 if offset < 0 or offset >= len(context):
1056 raise ValueError(
1056 raise ValueError(
1057 "Only positive values up to the length of the context "
1057 "Only positive values up to the length of the context "
1058 "minus one are allowed.")
1058 "minus one are allowed.")
1059
1059
1060 matches = []
1060 matches = []
1061 file_diff = self._get_file_diff(path)
1061 file_diff = self._get_file_diff(path)
1062
1062
1063 for chunk in file_diff['chunks']:
1063 for chunk in file_diff['chunks']:
1064 context_iter = iter(context)
1064 context_iter = iter(context)
1065 for line_idx, line in enumerate(chunk):
1065 for line_idx, line in enumerate(chunk):
1066 try:
1066 try:
1067 if _context_line(line) == context_iter.next():
1067 if _context_line(line) == context_iter.next():
1068 continue
1068 continue
1069 except StopIteration:
1069 except StopIteration:
1070 matches.append((line_idx, chunk))
1070 matches.append((line_idx, chunk))
1071 context_iter = iter(context)
1071 context_iter = iter(context)
1072
1072
1073 # Increment position and triger StopIteration
1073 # Increment position and triger StopIteration
1074 # if we had a match at the end
1074 # if we had a match at the end
1075 line_idx += 1
1075 line_idx += 1
1076 try:
1076 try:
1077 context_iter.next()
1077 context_iter.next()
1078 except StopIteration:
1078 except StopIteration:
1079 matches.append((line_idx, chunk))
1079 matches.append((line_idx, chunk))
1080
1080
1081 effective_offset = len(context) - offset
1081 effective_offset = len(context) - offset
1082 found_at_diff_lines = [
1082 found_at_diff_lines = [
1083 _line_to_diff_line_number(chunk[idx - effective_offset])
1083 _line_to_diff_line_number(chunk[idx - effective_offset])
1084 for idx, chunk in matches]
1084 for idx, chunk in matches]
1085
1085
1086 return found_at_diff_lines
1086 return found_at_diff_lines
1087
1087
1088 def _get_file_diff(self, path):
1088 def _get_file_diff(self, path):
1089 for file_diff in self.parsed_diff:
1089 for file_diff in self.parsed_diff:
1090 if file_diff['filename'] == path:
1090 if file_diff['filename'] == path:
1091 break
1091 break
1092 else:
1092 else:
1093 raise FileNotInDiffException("File {} not in diff".format(path))
1093 raise FileNotInDiffException("File {} not in diff".format(path))
1094 return file_diff
1094 return file_diff
1095
1095
1096 def _find_chunk_line_index(self, file_diff, diff_line):
1096 def _find_chunk_line_index(self, file_diff, diff_line):
1097 for chunk in file_diff['chunks']:
1097 for chunk in file_diff['chunks']:
1098 for idx, line in enumerate(chunk):
1098 for idx, line in enumerate(chunk):
1099 if line['old_lineno'] == diff_line.old:
1099 if line['old_lineno'] == diff_line.old:
1100 return chunk, idx
1100 return chunk, idx
1101 if line['new_lineno'] == diff_line.new:
1101 if line['new_lineno'] == diff_line.new:
1102 return chunk, idx
1102 return chunk, idx
1103 raise LineNotInDiffException(
1103 raise LineNotInDiffException(
1104 "The line {} is not part of the diff.".format(diff_line))
1104 "The line {} is not part of the diff.".format(diff_line))
1105
1105
1106
1106
1107 def _is_diff_content(line):
1107 def _is_diff_content(line):
1108 return line['action'] in (
1108 return line['action'] in (
1109 Action.UNMODIFIED, Action.ADD, Action.DELETE)
1109 Action.UNMODIFIED, Action.ADD, Action.DELETE)
1110
1110
1111
1111
1112 def _context_line(line):
1112 def _context_line(line):
1113 return (line['action'], line['line'])
1113 return (line['action'], line['line'])
1114
1114
1115
1115
1116 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
1116 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
1117
1117
1118
1118
1119 def _line_to_diff_line_number(line):
1119 def _line_to_diff_line_number(line):
1120 new_line_no = line['new_lineno'] or None
1120 new_line_no = line['new_lineno'] or None
1121 old_line_no = line['old_lineno'] or None
1121 old_line_no = line['old_lineno'] or None
1122 return DiffLineNumber(old=old_line_no, new=new_line_no)
1122 return DiffLineNumber(old=old_line_no, new=new_line_no)
1123
1123
1124
1124
1125 class FileNotInDiffException(Exception):
1125 class FileNotInDiffException(Exception):
1126 """
1126 """
1127 Raised when the context for a missing file is requested.
1127 Raised when the context for a missing file is requested.
1128
1128
1129 If you request the context for a line in a file which is not part of the
1129 If you request the context for a line in a file which is not part of the
1130 given diff, then this exception is raised.
1130 given diff, then this exception is raised.
1131 """
1131 """
1132
1132
1133
1133
1134 class LineNotInDiffException(Exception):
1134 class LineNotInDiffException(Exception):
1135 """
1135 """
1136 Raised when the context for a missing line is requested.
1136 Raised when the context for a missing line is requested.
1137
1137
1138 If you request the context for a line in a file and this line is not
1138 If you request the context for a line in a file and this line is not
1139 part of the given diff, then this exception is raised.
1139 part of the given diff, then this exception is raised.
1140 """
1140 """
1141
1141
1142
1142
1143 class DiffLimitExceeded(Exception):
1143 class DiffLimitExceeded(Exception):
1144 pass
1144 pass
1145
1145
1146
1146
1147 # NOTE(marcink): if diffs.mako change, probably this
1147 # NOTE(marcink): if diffs.mako change, probably this
1148 # needs a bump to next version
1148 # needs a bump to next version
1149 CURRENT_DIFF_VERSION = 'v4'
1149 CURRENT_DIFF_VERSION = 'v4'
1150
1150
1151
1151
1152 def _cleanup_cache_file(cached_diff_file):
1152 def _cleanup_cache_file(cached_diff_file):
1153 # cleanup file to not store it "damaged"
1153 # cleanup file to not store it "damaged"
1154 try:
1154 try:
1155 os.remove(cached_diff_file)
1155 os.remove(cached_diff_file)
1156 except Exception:
1156 except Exception:
1157 log.exception('Failed to cleanup path %s', cached_diff_file)
1157 log.exception('Failed to cleanup path %s', cached_diff_file)
1158
1158
1159
1159
1160 def cache_diff(cached_diff_file, diff, commits):
1160 def cache_diff(cached_diff_file, diff, commits):
1161 mode = 'plain' if 'mode:plain' in cached_diff_file else ''
1161 mode = 'plain' if 'mode:plain' in cached_diff_file else ''
1162
1162
1163 struct = {
1163 struct = {
1164 'version': CURRENT_DIFF_VERSION,
1164 'version': CURRENT_DIFF_VERSION,
1165 'diff': diff,
1165 'diff': diff,
1166 'commits': commits
1166 'commits': commits
1167 }
1167 }
1168
1168
1169 start = time.time()
1169 start = time.time()
1170 try:
1170 try:
1171 if mode == 'plain':
1171 if mode == 'plain':
1172 with open(cached_diff_file, 'wb') as f:
1172 with open(cached_diff_file, 'wb') as f:
1173 pickle.dump(struct, f)
1173 pickle.dump(struct, f)
1174 else:
1174 else:
1175 with bz2.BZ2File(cached_diff_file, 'wb') as f:
1175 with bz2.BZ2File(cached_diff_file, 'wb') as f:
1176 pickle.dump(struct, f)
1176 pickle.dump(struct, f)
1177 except Exception:
1177 except Exception:
1178 log.warn('Failed to save cache', exc_info=True)
1178 log.warn('Failed to save cache', exc_info=True)
1179 _cleanup_cache_file(cached_diff_file)
1179 _cleanup_cache_file(cached_diff_file)
1180
1180
1181 log.debug('Saved diff cache under %s in %.3fs', cached_diff_file, time.time() - start)
1181 log.debug('Saved diff cache under %s in %.3fs', cached_diff_file, time.time() - start)
1182
1182
1183
1183
1184 def load_cached_diff(cached_diff_file):
1184 def load_cached_diff(cached_diff_file):
1185 mode = 'plain' if 'mode:plain' in cached_diff_file else ''
1185 mode = 'plain' if 'mode:plain' in cached_diff_file else ''
1186
1186
1187 default_struct = {
1187 default_struct = {
1188 'version': CURRENT_DIFF_VERSION,
1188 'version': CURRENT_DIFF_VERSION,
1189 'diff': None,
1189 'diff': None,
1190 'commits': None
1190 'commits': None
1191 }
1191 }
1192
1192
1193 has_cache = os.path.isfile(cached_diff_file)
1193 has_cache = os.path.isfile(cached_diff_file)
1194 if not has_cache:
1194 if not has_cache:
1195 log.debug('Reading diff cache file failed', cached_diff_file)
1195 log.debug('Reading diff cache file failed %s', cached_diff_file)
1196 return default_struct
1196 return default_struct
1197
1197
1198 data = None
1198 data = None
1199
1199
1200 start = time.time()
1200 start = time.time()
1201 try:
1201 try:
1202 if mode == 'plain':
1202 if mode == 'plain':
1203 with open(cached_diff_file, 'rb') as f:
1203 with open(cached_diff_file, 'rb') as f:
1204 data = pickle.load(f)
1204 data = pickle.load(f)
1205 else:
1205 else:
1206 with bz2.BZ2File(cached_diff_file, 'rb') as f:
1206 with bz2.BZ2File(cached_diff_file, 'rb') as f:
1207 data = pickle.load(f)
1207 data = pickle.load(f)
1208 except Exception:
1208 except Exception:
1209 log.warn('Failed to read diff cache file', exc_info=True)
1209 log.warn('Failed to read diff cache file', exc_info=True)
1210
1210
1211 if not data:
1211 if not data:
1212 data = default_struct
1212 data = default_struct
1213
1213
1214 if not isinstance(data, dict):
1214 if not isinstance(data, dict):
1215 # old version of data ?
1215 # old version of data ?
1216 data = default_struct
1216 data = default_struct
1217
1217
1218 # check version
1218 # check version
1219 if data.get('version') != CURRENT_DIFF_VERSION:
1219 if data.get('version') != CURRENT_DIFF_VERSION:
1220 # purge cache
1220 # purge cache
1221 _cleanup_cache_file(cached_diff_file)
1221 _cleanup_cache_file(cached_diff_file)
1222 return default_struct
1222 return default_struct
1223
1223
1224 log.debug('Loaded diff cache from %s in %.3fs', cached_diff_file, time.time() - start)
1224 log.debug('Loaded diff cache from %s in %.3fs', cached_diff_file, time.time() - start)
1225
1225
1226 return data
1226 return data
1227
1227
1228
1228
1229 def generate_diff_cache_key(*args):
1229 def generate_diff_cache_key(*args):
1230 """
1230 """
1231 Helper to generate a cache key using arguments
1231 Helper to generate a cache key using arguments
1232 """
1232 """
1233 def arg_mapper(input_param):
1233 def arg_mapper(input_param):
1234 input_param = safe_str(input_param)
1234 input_param = safe_str(input_param)
1235 # we cannot allow '/' in arguments since it would allow
1235 # we cannot allow '/' in arguments since it would allow
1236 # subdirectory usage
1236 # subdirectory usage
1237 input_param.replace('/', '_')
1237 input_param.replace('/', '_')
1238 return input_param or None # prevent empty string arguments
1238 return input_param or None # prevent empty string arguments
1239
1239
1240 return '_'.join([
1240 return '_'.join([
1241 '{}' for i in range(len(args))]).format(*map(arg_mapper, args))
1241 '{}' for i in range(len(args))]).format(*map(arg_mapper, args))
1242
1242
1243
1243
1244 def diff_cache_exist(cache_storage, *args):
1244 def diff_cache_exist(cache_storage, *args):
1245 """
1245 """
1246 Based on all generated arguments check and return a cache path
1246 Based on all generated arguments check and return a cache path
1247 """
1247 """
1248 cache_key = generate_diff_cache_key(*args)
1248 cache_key = generate_diff_cache_key(*args)
1249 cache_file_path = os.path.join(cache_storage, cache_key)
1249 cache_file_path = os.path.join(cache_storage, cache_key)
1250 # prevent path traversal attacks using some param that have e.g '../../'
1250 # prevent path traversal attacks using some param that have e.g '../../'
1251 if not os.path.abspath(cache_file_path).startswith(cache_storage):
1251 if not os.path.abspath(cache_file_path).startswith(cache_storage):
1252 raise ValueError('Final path must be within {}'.format(cache_storage))
1252 raise ValueError('Final path must be within {}'.format(cache_storage))
1253
1253
1254 return cache_file_path
1254 return cache_file_path
@@ -1,381 +1,380 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2019 RhodeCode GmbH
3 # Copyright (C) 2014-2019 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 HG commit module
22 HG commit module
23 """
23 """
24
24
25 import os
25 import os
26
26
27 from zope.cachedescriptors.property import Lazy as LazyProperty
27 from zope.cachedescriptors.property import Lazy as LazyProperty
28
28
29 from rhodecode.lib.datelib import utcdate_fromtimestamp
29 from rhodecode.lib.datelib import utcdate_fromtimestamp
30 from rhodecode.lib.utils import safe_str, safe_unicode
30 from rhodecode.lib.utils import safe_str, safe_unicode
31 from rhodecode.lib.vcs import path as vcspath
31 from rhodecode.lib.vcs import path as vcspath
32 from rhodecode.lib.vcs.backends import base
32 from rhodecode.lib.vcs.backends import base
33 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
33 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
34 from rhodecode.lib.vcs.exceptions import CommitError
34 from rhodecode.lib.vcs.exceptions import CommitError
35 from rhodecode.lib.vcs.nodes import (
35 from rhodecode.lib.vcs.nodes import (
36 AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode,
36 AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode,
37 NodeKind, RemovedFileNodesGenerator, RootNode, SubModuleNode,
37 NodeKind, RemovedFileNodesGenerator, RootNode, SubModuleNode,
38 LargeFileNode, LARGEFILE_PREFIX)
38 LargeFileNode, LARGEFILE_PREFIX)
39 from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
39 from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
40
40
41
41
42 class MercurialCommit(base.BaseCommit):
42 class MercurialCommit(base.BaseCommit):
43 """
43 """
44 Represents state of the repository at the single commit.
44 Represents state of the repository at the single commit.
45 """
45 """
46
46
47 _filter_pre_load = [
47 _filter_pre_load = [
48 # git specific property not supported here
48 # git specific property not supported here
49 "_commit",
49 "_commit",
50 ]
50 ]
51
51
52 def __init__(self, repository, raw_id, idx, pre_load=None):
52 def __init__(self, repository, raw_id, idx, pre_load=None):
53 raw_id = safe_str(raw_id)
53 raw_id = safe_str(raw_id)
54
54
55 self.repository = repository
55 self.repository = repository
56 self._remote = repository._remote
56 self._remote = repository._remote
57
57
58 self.raw_id = raw_id
58 self.raw_id = raw_id
59 self.idx = idx
59 self.idx = idx
60
60
61 self._set_bulk_properties(pre_load)
61 self._set_bulk_properties(pre_load)
62
62
63 # caches
63 # caches
64 self.nodes = {}
64 self.nodes = {}
65
65
66 def _set_bulk_properties(self, pre_load):
66 def _set_bulk_properties(self, pre_load):
67 if not pre_load:
67 if not pre_load:
68 return
68 return
69 pre_load = [entry for entry in pre_load
69 pre_load = [entry for entry in pre_load
70 if entry not in self._filter_pre_load]
70 if entry not in self._filter_pre_load]
71 if not pre_load:
71 if not pre_load:
72 return
72 return
73
73
74 result = self._remote.bulk_request(self.idx, pre_load)
74 result = self._remote.bulk_request(self.idx, pre_load)
75 for attr, value in result.items():
75 for attr, value in result.items():
76 if attr in ["author", "branch", "message"]:
76 if attr in ["author", "branch", "message"]:
77 value = safe_unicode(value)
77 value = safe_unicode(value)
78 elif attr == "affected_files":
78 elif attr == "affected_files":
79 value = map(safe_unicode, value)
79 value = map(safe_unicode, value)
80 elif attr == "date":
80 elif attr == "date":
81 value = utcdate_fromtimestamp(*value)
81 value = utcdate_fromtimestamp(*value)
82 elif attr in ["children", "parents"]:
82 elif attr in ["children", "parents"]:
83 value = self._make_commits(value)
83 value = self._make_commits(value)
84 elif attr in ["phase"]:
84 elif attr in ["phase"]:
85 value = self._get_phase_text(value)
85 value = self._get_phase_text(value)
86 self.__dict__[attr] = value
86 self.__dict__[attr] = value
87
87
88 @LazyProperty
88 @LazyProperty
89 def tags(self):
89 def tags(self):
90 tags = [name for name, commit_id in self.repository.tags.iteritems()
90 tags = [name for name, commit_id in self.repository.tags.iteritems()
91 if commit_id == self.raw_id]
91 if commit_id == self.raw_id]
92 return tags
92 return tags
93
93
94 @LazyProperty
94 @LazyProperty
95 def branch(self):
95 def branch(self):
96 return safe_unicode(self._remote.ctx_branch(self.idx))
96 return safe_unicode(self._remote.ctx_branch(self.idx))
97
97
98 @LazyProperty
98 @LazyProperty
99 def bookmarks(self):
99 def bookmarks(self):
100 bookmarks = [
100 bookmarks = [
101 name for name, commit_id in self.repository.bookmarks.iteritems()
101 name for name, commit_id in self.repository.bookmarks.iteritems()
102 if commit_id == self.raw_id]
102 if commit_id == self.raw_id]
103 return bookmarks
103 return bookmarks
104
104
105 @LazyProperty
105 @LazyProperty
106 def message(self):
106 def message(self):
107 return safe_unicode(self._remote.ctx_description(self.idx))
107 return safe_unicode(self._remote.ctx_description(self.idx))
108
108
109 @LazyProperty
109 @LazyProperty
110 def committer(self):
110 def committer(self):
111 return safe_unicode(self.author)
111 return safe_unicode(self.author)
112
112
113 @LazyProperty
113 @LazyProperty
114 def author(self):
114 def author(self):
115 return safe_unicode(self._remote.ctx_user(self.idx))
115 return safe_unicode(self._remote.ctx_user(self.idx))
116
116
117 @LazyProperty
117 @LazyProperty
118 def date(self):
118 def date(self):
119 return utcdate_fromtimestamp(*self._remote.ctx_date(self.idx))
119 return utcdate_fromtimestamp(*self._remote.ctx_date(self.idx))
120
120
121 @LazyProperty
121 @LazyProperty
122 def status(self):
122 def status(self):
123 """
123 """
124 Returns modified, added, removed, deleted files for current commit
124 Returns modified, added, removed, deleted files for current commit
125 """
125 """
126 return self._remote.ctx_status(self.idx)
126 return self._remote.ctx_status(self.idx)
127
127
128 @LazyProperty
128 @LazyProperty
129 def _file_paths(self):
129 def _file_paths(self):
130 return self._remote.ctx_list(self.idx)
130 return self._remote.ctx_list(self.idx)
131
131
132 @LazyProperty
132 @LazyProperty
133 def _dir_paths(self):
133 def _dir_paths(self):
134 p = list(set(get_dirs_for_path(*self._file_paths)))
134 p = list(set(get_dirs_for_path(*self._file_paths)))
135 p.insert(0, '')
135 p.insert(0, '')
136 return p
136 return p
137
137
138 @LazyProperty
138 @LazyProperty
139 def _paths(self):
139 def _paths(self):
140 return self._dir_paths + self._file_paths
140 return self._dir_paths + self._file_paths
141
141
142 @LazyProperty
142 @LazyProperty
143 def id(self):
143 def id(self):
144 if self.last:
144 if self.last:
145 return u'tip'
145 return u'tip'
146 return self.short_id
146 return self.short_id
147
147
148 @LazyProperty
148 @LazyProperty
149 def short_id(self):
149 def short_id(self):
150 return self.raw_id[:12]
150 return self.raw_id[:12]
151
151
152 def _make_commits(self, indexes, pre_load=None):
152 def _make_commits(self, indexes, pre_load=None):
153 return [self.repository.get_commit(commit_idx=idx, pre_load=pre_load)
153 return [self.repository.get_commit(commit_idx=idx, pre_load=pre_load)
154 for idx in indexes if idx >= 0]
154 for idx in indexes if idx >= 0]
155
155
156 @LazyProperty
156 @LazyProperty
157 def parents(self):
157 def parents(self):
158 """
158 """
159 Returns list of parent commits.
159 Returns list of parent commits.
160 """
160 """
161 parents = self._remote.ctx_parents(self.idx)
161 parents = self._remote.ctx_parents(self.idx)
162 return self._make_commits(parents)
162 return self._make_commits(parents)
163
163
164 def _get_phase_text(self, phase_id):
164 def _get_phase_text(self, phase_id):
165 return {
165 return {
166 0: 'public',
166 0: 'public',
167 1: 'draft',
167 1: 'draft',
168 2: 'secret',
168 2: 'secret',
169 }.get(phase_id) or ''
169 }.get(phase_id) or ''
170
170
171 @LazyProperty
171 @LazyProperty
172 def phase(self):
172 def phase(self):
173 phase_id = self._remote.ctx_phase(self.idx)
173 phase_id = self._remote.ctx_phase(self.idx)
174 phase_text = self._get_phase_text(phase_id)
174 phase_text = self._get_phase_text(phase_id)
175
175
176 return safe_unicode(phase_text)
176 return safe_unicode(phase_text)
177
177
178 @LazyProperty
178 @LazyProperty
179 def obsolete(self):
179 def obsolete(self):
180 obsolete = self._remote.ctx_obsolete(self.idx)
180 obsolete = self._remote.ctx_obsolete(self.idx)
181 return obsolete
181 return obsolete
182
182
183 @LazyProperty
183 @LazyProperty
184 def hidden(self):
184 def hidden(self):
185 hidden = self._remote.ctx_hidden(self.idx)
185 hidden = self._remote.ctx_hidden(self.idx)
186 return hidden
186 return hidden
187
187
188 @LazyProperty
188 @LazyProperty
189 def children(self):
189 def children(self):
190 """
190 """
191 Returns list of child commits.
191 Returns list of child commits.
192 """
192 """
193 children = self._remote.ctx_children(self.idx)
193 children = self._remote.ctx_children(self.idx)
194 return self._make_commits(children)
194 return self._make_commits(children)
195
195
196 def _fix_path(self, path):
196 def _fix_path(self, path):
197 """
197 """
198 Mercurial keeps filenodes as str so we need to encode from unicode
198 Mercurial keeps filenodes as str so we need to encode from unicode
199 to str.
199 to str.
200 """
200 """
201 return safe_str(super(MercurialCommit, self)._fix_path(path))
201 return safe_str(super(MercurialCommit, self)._fix_path(path))
202
202
203 def _get_kind(self, path):
203 def _get_kind(self, path):
204 path = self._fix_path(path)
204 path = self._fix_path(path)
205 if path in self._file_paths:
205 if path in self._file_paths:
206 return NodeKind.FILE
206 return NodeKind.FILE
207 elif path in self._dir_paths:
207 elif path in self._dir_paths:
208 return NodeKind.DIR
208 return NodeKind.DIR
209 else:
209 else:
210 raise CommitError(
210 raise CommitError(
211 "Node does not exist at the given path '%s'" % (path, ))
211 "Node does not exist at the given path '%s'" % (path, ))
212
212
213 def _get_filectx(self, path):
213 def _get_filectx(self, path):
214 path = self._fix_path(path)
214 path = self._fix_path(path)
215 if self._get_kind(path) != NodeKind.FILE:
215 if self._get_kind(path) != NodeKind.FILE:
216 raise CommitError(
216 raise CommitError(
217 "File does not exist for idx %s at '%s'" % (self.raw_id, path))
217 "File does not exist for idx %s at '%s'" % (self.raw_id, path))
218 return path
218 return path
219
219
220 def get_file_mode(self, path):
220 def get_file_mode(self, path):
221 """
221 """
222 Returns stat mode of the file at the given ``path``.
222 Returns stat mode of the file at the given ``path``.
223 """
223 """
224 path = self._get_filectx(path)
224 path = self._get_filectx(path)
225 if 'x' in self._remote.fctx_flags(self.idx, path):
225 if 'x' in self._remote.fctx_flags(self.idx, path):
226 return base.FILEMODE_EXECUTABLE
226 return base.FILEMODE_EXECUTABLE
227 else:
227 else:
228 return base.FILEMODE_DEFAULT
228 return base.FILEMODE_DEFAULT
229
229
230 def is_link(self, path):
230 def is_link(self, path):
231 path = self._get_filectx(path)
231 path = self._get_filectx(path)
232 return 'l' in self._remote.fctx_flags(self.idx, path)
232 return 'l' in self._remote.fctx_flags(self.idx, path)
233
233
234 def get_file_content(self, path):
234 def get_file_content(self, path):
235 """
235 """
236 Returns content of the file at given ``path``.
236 Returns content of the file at given ``path``.
237 """
237 """
238 path = self._get_filectx(path)
238 path = self._get_filectx(path)
239 return self._remote.fctx_data(self.idx, path)
239 return self._remote.fctx_data(self.idx, path)
240
240
241 def get_file_size(self, path):
241 def get_file_size(self, path):
242 """
242 """
243 Returns size of the file at given ``path``.
243 Returns size of the file at given ``path``.
244 """
244 """
245 path = self._get_filectx(path)
245 path = self._get_filectx(path)
246 return self._remote.fctx_size(self.idx, path)
246 return self._remote.fctx_size(self.idx, path)
247
247
248 def get_path_history(self, path, limit=None, pre_load=None):
248 def get_path_history(self, path, limit=None, pre_load=None):
249 """
249 """
250 Returns history of file as reversed list of `MercurialCommit` objects
250 Returns history of file as reversed list of `MercurialCommit` objects
251 for which file at given ``path`` has been modified.
251 for which file at given ``path`` has been modified.
252 """
252 """
253 path = self._get_filectx(path)
253 path = self._get_filectx(path)
254 hist = self._remote.node_history(self.idx, path, limit)
254 hist = self._remote.node_history(self.idx, path, limit)
255 return [
255 return [
256 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
256 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
257 for commit_id in hist]
257 for commit_id in hist]
258
258
259 def get_file_annotate(self, path, pre_load=None):
259 def get_file_annotate(self, path, pre_load=None):
260 """
260 """
261 Returns a generator of four element tuples with
261 Returns a generator of four element tuples with
262 lineno, commit_id, commit lazy loader and line
262 lineno, commit_id, commit lazy loader and line
263 """
263 """
264 result = self._remote.fctx_annotate(self.idx, path)
264 result = self._remote.fctx_annotate(self.idx, path)
265
265
266 for ln_no, commit_id, content in result:
266 for ln_no, commit_id, content in result:
267 yield (
267 yield (
268 ln_no, commit_id,
268 ln_no, commit_id,
269 lambda: self.repository.get_commit(commit_id=commit_id,
269 lambda: self.repository.get_commit(commit_id=commit_id,
270 pre_load=pre_load),
270 pre_load=pre_load),
271 content)
271 content)
272
272
273 def get_nodes(self, path):
273 def get_nodes(self, path):
274 """
274 """
275 Returns combined ``DirNode`` and ``FileNode`` objects list representing
275 Returns combined ``DirNode`` and ``FileNode`` objects list representing
276 state of commit at the given ``path``. If node at the given ``path``
276 state of commit at the given ``path``. If node at the given ``path``
277 is not instance of ``DirNode``, CommitError would be raised.
277 is not instance of ``DirNode``, CommitError would be raised.
278 """
278 """
279
279
280 if self._get_kind(path) != NodeKind.DIR:
280 if self._get_kind(path) != NodeKind.DIR:
281 raise CommitError(
281 raise CommitError(
282 "Directory does not exist for idx %s at '%s'" %
282 "Directory does not exist for idx %s at '%s'" % (self.raw_id, path))
283 (self.idx, path))
284 path = self._fix_path(path)
283 path = self._fix_path(path)
285
284
286 filenodes = [
285 filenodes = [
287 FileNode(f, commit=self) for f in self._file_paths
286 FileNode(f, commit=self) for f in self._file_paths
288 if os.path.dirname(f) == path]
287 if os.path.dirname(f) == path]
289 # TODO: johbo: Check if this can be done in a more obvious way
288 # TODO: johbo: Check if this can be done in a more obvious way
290 dirs = path == '' and '' or [
289 dirs = path == '' and '' or [
291 d for d in self._dir_paths
290 d for d in self._dir_paths
292 if d and vcspath.dirname(d) == path]
291 if d and vcspath.dirname(d) == path]
293 dirnodes = [
292 dirnodes = [
294 DirNode(d, commit=self) for d in dirs
293 DirNode(d, commit=self) for d in dirs
295 if os.path.dirname(d) == path]
294 if os.path.dirname(d) == path]
296
295
297 alias = self.repository.alias
296 alias = self.repository.alias
298 for k, vals in self._submodules.iteritems():
297 for k, vals in self._submodules.iteritems():
299 if vcspath.dirname(k) == path:
298 if vcspath.dirname(k) == path:
300 loc = vals[0]
299 loc = vals[0]
301 commit = vals[1]
300 commit = vals[1]
302 dirnodes.append(SubModuleNode(k, url=loc, commit=commit, alias=alias))
301 dirnodes.append(SubModuleNode(k, url=loc, commit=commit, alias=alias))
303 nodes = dirnodes + filenodes
302 nodes = dirnodes + filenodes
304 # cache nodes
303 # cache nodes
305 for node in nodes:
304 for node in nodes:
306 self.nodes[node.path] = node
305 self.nodes[node.path] = node
307 nodes.sort()
306 nodes.sort()
308
307
309 return nodes
308 return nodes
310
309
311 def get_node(self, path, pre_load=None):
310 def get_node(self, path, pre_load=None):
312 """
311 """
313 Returns `Node` object from the given `path`. If there is no node at
312 Returns `Node` object from the given `path`. If there is no node at
314 the given `path`, `NodeDoesNotExistError` would be raised.
313 the given `path`, `NodeDoesNotExistError` would be raised.
315 """
314 """
316 path = self._fix_path(path)
315 path = self._fix_path(path)
317
316
318 if path not in self.nodes:
317 if path not in self.nodes:
319 if path in self._file_paths:
318 if path in self._file_paths:
320 node = FileNode(path, commit=self, pre_load=pre_load)
319 node = FileNode(path, commit=self, pre_load=pre_load)
321 elif path in self._dir_paths:
320 elif path in self._dir_paths:
322 if path == '':
321 if path == '':
323 node = RootNode(commit=self)
322 node = RootNode(commit=self)
324 else:
323 else:
325 node = DirNode(path, commit=self)
324 node = DirNode(path, commit=self)
326 else:
325 else:
327 raise self.no_node_at_path(path)
326 raise self.no_node_at_path(path)
328
327
329 # cache node
328 # cache node
330 self.nodes[path] = node
329 self.nodes[path] = node
331 return self.nodes[path]
330 return self.nodes[path]
332
331
333 def get_largefile_node(self, path):
332 def get_largefile_node(self, path):
334
333
335 if self._remote.is_large_file(path):
334 if self._remote.is_large_file(path):
336 # content of that file regular FileNode is the hash of largefile
335 # content of that file regular FileNode is the hash of largefile
337 file_id = self.get_file_content(path).strip()
336 file_id = self.get_file_content(path).strip()
338
337
339 if self._remote.in_largefiles_store(file_id):
338 if self._remote.in_largefiles_store(file_id):
340 lf_path = self._remote.store_path(file_id)
339 lf_path = self._remote.store_path(file_id)
341 return LargeFileNode(lf_path, commit=self, org_path=path)
340 return LargeFileNode(lf_path, commit=self, org_path=path)
342 elif self._remote.in_user_cache(file_id):
341 elif self._remote.in_user_cache(file_id):
343 lf_path = self._remote.store_path(file_id)
342 lf_path = self._remote.store_path(file_id)
344 self._remote.link(file_id, path)
343 self._remote.link(file_id, path)
345 return LargeFileNode(lf_path, commit=self, org_path=path)
344 return LargeFileNode(lf_path, commit=self, org_path=path)
346
345
347 @LazyProperty
346 @LazyProperty
348 def _submodules(self):
347 def _submodules(self):
349 """
348 """
350 Returns a dictionary with submodule information from substate file
349 Returns a dictionary with submodule information from substate file
351 of hg repository.
350 of hg repository.
352 """
351 """
353 return self._remote.ctx_substate(self.idx)
352 return self._remote.ctx_substate(self.idx)
354
353
355 @LazyProperty
354 @LazyProperty
356 def affected_files(self):
355 def affected_files(self):
357 """
356 """
358 Gets a fast accessible file changes for given commit
357 Gets a fast accessible file changes for given commit
359 """
358 """
360 return self._remote.ctx_files(self.idx)
359 return self._remote.ctx_files(self.idx)
361
360
362 @property
361 @property
363 def added(self):
362 def added(self):
364 """
363 """
365 Returns list of added ``FileNode`` objects.
364 Returns list of added ``FileNode`` objects.
366 """
365 """
367 return AddedFileNodesGenerator([n for n in self.status[1]], self)
366 return AddedFileNodesGenerator([n for n in self.status[1]], self)
368
367
369 @property
368 @property
370 def changed(self):
369 def changed(self):
371 """
370 """
372 Returns list of modified ``FileNode`` objects.
371 Returns list of modified ``FileNode`` objects.
373 """
372 """
374 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
373 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
375
374
376 @property
375 @property
377 def removed(self):
376 def removed(self):
378 """
377 """
379 Returns list of removed ``FileNode`` objects.
378 Returns list of removed ``FileNode`` objects.
380 """
379 """
381 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
380 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
General Comments 0
You need to be logged in to leave comments. Login now